OMP Workbook

Read the source. Follow the evidence.

Package Lab: one file to an embedded host

Theo maintains fictional field observations. His first notebook fits in one file. Later he wants reusable helpers, optional package features and an application that embeds OMP.

His obstacle is architectural: an imported helper, an extension entry and an installed plugin are not the same thing.

Package Lab’s complete instructions accompany the source bundle.

Milestone: one file is enough

Files:

Terminal shell:

Package Lab: one file to an embedded host · source excerpt 1; read surrounding instructions
omp --no-extensions -e "$EXAMPLES/package-lab/single/field-notes.ts"

Human OMP slash command:

Package Lab: one file to an embedded host · source excerpt 2; read surrounding instructions
/field-notes reed

Model tool arguments—call field_notes:

Package Lab: one file to an embedded host · source excerpt 3; read surrounding instructions
{"op":"discover"}
Package Lab: one file to an embedded host · source excerpt 4; read surrounding instructions
{"op":"inspect","value":"reed"}
Package Lab: one file to an embedded host · source excerpt 5; read surrounding instructions
{"op":"query","value":"marsh"}

Observed discovery details:

Package Lab: one file to an embedded host · source excerpt 6; read surrounding instructions
{
  "operations": ["discover", "inspect", "query"],
  "ids": ["reed", "fern"],
  "writes": false
}

Inspection returns the fictional reed beside the footbridge.

The tool lowercases and trims its search value. Query searches ID, habitat and text. The slash command is narrower: it lists all notes or matches an exact lowercased ID.

Unknown tool inspection IDs throw. Unknown human IDs produce a warning.

This entry does not declare approval or loadMode. Therefore the current defaults apply: an execution-tier approval declaration and discoverable presentation, despite the domain being read-only. Descriptions do not set approval policy.

The command uses notifications without a headless message fallback. Its machine tool remains useful without the notification.

Exercise: query woodland.

Checkpoint: the tool finds fern. Do not assume /field-notes woodland performs the same full-text query; it is an ID-oriented human command.

Milestone: helpers become ordinary imports

Theo adds a current selection. He wants both /notebook fern and a tool act to change the same selection, without duplicating lookup logic.

Files:

Terminal shell—after exiting the single-file launch:

Package Lab: one file to an embedded host · source excerpt 7; read surrounding instructions
omp --no-extensions -e "$EXAMPLES/package-lab/multi"

The directory resolves to index.ts. notebook.ts is imported as a helper; it is not independently bound as another extension.

Complete helper source—multi/notebook.ts:

Package Lab: one file to an embedded host · source excerpt 8; read surrounding instructions
export const notes = [
    { id: "reed", text: "Fictional reeds border the marsh trail." },
    { id: "fern", text: "Fictional ferns shade the woodland path." },
] as const;

export function inspectNote(id: string): (typeof notes)[number] {
    const note = notes.find(candidate => candidate.id === id);
    if (!note) throw new Error(`Unknown note id: ${id}. Discover ids before selecting.`);
    return note;
}

Model tool arguments—call notebook, one at a time:

Package Lab: one file to an embedded host · source excerpt 9; read surrounding instructions
{"op":"discover"}
Package Lab: one file to an embedded host · source excerpt 10; read surrounding instructions
{"op":"inspect","id":"reed"}
Package Lab: one file to an embedded host · source excerpt 11; read surrounding instructions
{"op":"act","id":"reed"}
Package Lab: one file to an embedded host · source excerpt 12; read surrounding instructions
{"op":"query"}

Human OMP slash commands:

Package Lab: one file to an embedded host · source excerpt 13; read surrounding instructions
/notebook fern
/notebook

Observed: command and tool shared the factory’s state. An invalid action left the earlier selection intact. Exact completed arguments returned no completion.

The lifetime correction

The selection is declared inside the factory:

Exact excerpt from multi/index.ts:

Package Lab: one file to an embedded host · source excerpt 14; read surrounding instructions
// Factory-local state belongs to one binding, not Bun's cached module.
let selected: string | null = null;

That prevents this variable from being shared merely because a cached module is rebound. It does not make it transcript-local.

Observed SDK lifecycle checks:

OperationTranscript identityFactory selection
Select reedUnchangedreed
newSession()ChangesStill reed
Select fern, then switchSession()Changes to a real target headerStill fern
Construct a separate SDK hostSeparate bindingStarts at null
Rebind prepared factoriesFresh runtime and extension objectsStarts at null

The corrected teaching language is factory-local or binding-local, not session-local.

The public helpers deliberately do not reset on session_switch and do not append selection entries.

Exercise: select fern, use /new, then run /notebook.

Checkpoint: within the reused binding, it remains selected. If you want transcript-local selection, add explicit lifecycle reset or branch reconstruction in a new exercise; do not claim the supplied stage already does that.

Milestone: a manifest declares several entries

Theo now wants a catalog and a reusable prompt. An optional summary tool should be available only when selected as a package feature.

Files:

Exact config file—manifest/package.json:

Package Lab: one file to an embedded host · source excerpt 15; read surrounding instructions
{
  "name": "omp-workbook-field-notes",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "description": "Fictional field notes with an optional plugin summary",
  "omp": {
    "extensions": ["./entries/catalog.ts", "./entries/resources.ts"],
    "features": {
      "summary": {
        "description": "Add the fictional habitat summary tool",
        "default": false,
        "extensions": ["./entries/summary.ts"]
      }
    }
  }
}

Terminal shell—load the base manifest entries:

Package Lab: one file to an embedded host · source excerpt 16; read surrounding instructions
omp --no-extensions -e "$EXAMPLES/package-lab/manifest"

The base catalog registers field_catalog and /field-catalog.

Model tool arguments—call field_catalog:

Package Lab: one file to an embedded host · source excerpt 17; read surrounding instructions
{"op":"discover"}
Package Lab: one file to an embedded host · source excerpt 18; read surrounding instructions
{"op":"query","habitat":"marsh"}

Explicit -e directory loading reads the base extensions list. It does not select plugin features.

To try the optional entry without installation:

Terminal shell:

Package Lab: one file to an embedded host · source excerpt 19; read surrounding instructions
omp --no-extensions -e "$EXAMPLES/package-lab/manifest" -e "$EXAMPLES/package-lab/manifest/entries/summary.ts"

Model tool arguments—call field_summary:

Package Lab: one file to an embedded host · source excerpt 20; read surrounding instructions
{}

Expected result details:

Package Lab: one file to an embedded host · source excerpt 21; read surrounding instructions
{"fictional":true,"habitats":["marsh","woodland"]}

field_summary has no op parameter.

Observed: default installed-plugin feature resolution selected two entries; enabling summary selected three. Explicit base-manifest directory discovery did not include the optional entry.

Resource registration is explained in Resources, event buses, MCP and Gemini manifests. Its runner-level proof does not establish automatic prompt-menu rendering.

Exercise: why not import summary.ts for its side effects from the base catalog?

Answer: that would undermine the feature-selection boundary. A feature is meaningful only if its code is not activated through another unconditional path.

Milestone: embed a factory in an SDK host

Theo’s final goal is to include the notebook in another application without installing a global plugin.

Files:

createFieldNotesExtension(name) returns an ExtensionFactory. The SDK’s extensions option takes functions, not file paths. File paths belong in additionalExtensionPaths.

Exact excerpt—session options in inline/session.ts; use the linked complete module:

Package Lab: one file to an embedded host · source excerpt 22; read surrounding instructions
const result = await createAgentSession({
    cwd: scratchDirectory,
    agentDir: path.join(scratchDirectory, "agent"),
    authStorage: auth,
    modelRegistry: new ModelRegistry(auth, path.join(scratchDirectory, "models.yml")),
    settings: Settings.isolated(),
    sessionManager: SessionManager.inMemory(scratchDirectory),
    disableExtensionDiscovery: true,
    extensions: [createFieldNotesExtension("Fictional field notebook")],
    enableMCP: false,
    enableLsp: false,
    skipPythonPreflight: true,
    preloadedCustomToolPaths: [],
    skills: [], rules: [], contextFiles: [], promptTemplates: [], slashCommands: [],
    toolNames: [],
});

The complete module constructs an in-memory auth store, closes auth on failure, and returns a close() function that disposes the session before closing auth.

Install the workbook-compatible SDK package into the environment that runs the script. Package Lab marks it as an optional peer; the example is private and is not presented as a registry-published package.

Terminal shell—with that runtime dependency available:

Package Lab: one file to an embedded host · source excerpt 23; read surrounding instructions
bun "$EXAMPLES/package-lab/inline/run.ts"

The script prints metadata containing providerPromptSent: false, the inline_notebook tool and the registered command names, then disposes the host. It never calls session.prompt().

That is not a blanket no-network guarantee for arbitrary host startup: model registries and other host configuration can perform discovery. The recorded isolated run had network denied.

Binding is not initialization

createAgentSession() builds the session and binds factories. A mode adapter subsequently initializes the extension runner’s live actions/UI.

The public metadata script does not pretend to be a full TUI/RPC host. Its notebook tool uses closure state, so the proof could exercise it through the real adapter without needing message actions. An embedding that uses appendEntry(), message delivery, dialogs or session actions must wire the real runtime—not replace those methods with successful no-ops.

The inline tool’s discover, inspect and query all return its notebook contract and current selection. It does not implement a separate per-note inspection record. act requires reed or fern.

Prepared factories versus bound instances

Prepared factories can be rebound to a fresh runtime. Already-bound extension instances close over their original API and must not be forwarded to an independently constructed SDK session.

The CLI’s early preload is a special same-owner optimization for flag parsing. It is not a general pattern for sharing a parent’s loaded instances with a child.

Exercise: share a module-scope mutable selection between two SDK hosts. What risk have you introduced?

Answer: the module may be cached, so both factory bindings can reach the same mutable variable. Put binding state inside the factory, and use explicit storage for any state intentionally shared across bindings.

Source, snapshot 2026-08-29: linked Package Lab files; packages/coding-agent/src/sdk.ts, CreateAgentSessionOptions, createAgentSessionScoped; packages/coding-agent/src/extensibility/extensions/loader.ts, loadExtensions, bindPreparedExtensions; recorded loading/rebinding scenarios.

Extensions inside those boundaries · Source chapter: extensions/package-lab-one-file-to-an-embedded-host. Original evidence remains scoped to its recorded snapshot.

Read this chapter as Markdown

Your lesson ticks

A self-reported reading checklist, not proof of real OMP behavior. Only these ticks are saved in this browser. Reading a milestone does not resume, fork, reset or export a session.

Chapters I have worked through
Start here 1
Sessions, resets, and reviewable history 19
Memory and reusable knowledge 14
Tangent work and live control 17
Tool permissions and approvals 15
Extensions inside those boundaries 23
Connections and next steps 8
0 of 97 checked

Checklist saving needs JavaScript and available browser storage.