Seed Desk: welcome and inventory
Mara’s first goal is modest: stop repeating the fictional seed desk’s opening hours. Her obstacle is that a friendly human command alone would leave the agent without a supported way to retrieve the same information.
She chooses two entrances to one small domain: a human command and a read-only tool.
Milestone: a welcoming desk for both readers
Starting state: no reservation state, no inventory mutation and no external service.
Files:
Terminal shell—launch stage 1:
omp --no-extensions --no-skills -e "$EXAMPLES/seed-desk/01-welcome/index.ts"
Human OMP slash commands—enter in the composer:
/seeds welcome
/seeds hours
The hours response is:
Expected local output:
Our fictional desk opens Saturday, 10:00-12:00. No booking has been made.
The machine interface is seed_welcome.
Model tool arguments—call seed_welcome:
{"op":"discover"}
Model tool arguments—call seed_welcome:
{"op":"inspect","topic":"hours"}
The first operation advertises discover and inspect, the topics, scope and quiet flag in structured details. The second returns the same hours text used by the slash command.
Important distinction: details is useful to SDK callers, hosts and renderers. It is not automatically model-visible. This stage’s ordinary text content tells the model which topics it can inspect; the quiet flag is in details, not duplicated into that text.
What the code changes
The async factory reads its description from a real adjacent file. It registers:
seed_welcome, withapproval: "read"andloadMode: "essential";- the boolean flag
seed-quiet; - a
session_startnotification; /seeds, including argument completion.
It does not register a reservation operation.
Exact excerpt—completion behavior in stage 1’s entry; not a standalone replacement file:
getArgumentCompletions(prefix) {
const matches = choices.filter(value => value.startsWith(prefix.trimStart()));
// A completed sole match MUST release Enter to submit the command.
if (matches.length === 1 && matches[0] === prefix.trim()) return null;
return matches.length ? matches.map(value => ({ value: `${value} `, label: value })) : null;
},
Mara types /seeds wel. The completion offers the full argument text welcome .
When welcome is already the sole exact match, returning null lets Enter submit rather than continually reaccepting the completion. A completion-array equality test is helpful, but it is not a terminal-key-dispatch test.
Inspect progress
Observed:
welproducedwelcome.welcomeandwelcomeproduced no completion.- The tool’s greeting matched the headless command’s greeting.
- Quiet startup produced zero startup notices.
- A headless command produced one custom message.
The supplied Seed Desk proof simulated confirmation/notification UI. It did not exercise actual terminal autocomplete and Enter behavior.
Quiet is a startup choice, not a permission
Exit the stage and restart it with the flag.
Terminal shell—suppress the startup notice:
omp --no-extensions --no-skills -e "$EXAMPLES/seed-desk/01-welcome/index.ts" --seed-quiet
This suppresses only the startup notification. Commands and tools still work.
Register flag names without the leading --; use the leading -- in the shell. In this parser, a boolean flag’s presence means true. Do not assume --seed-quiet=false means false; omit the flag to use its false default.
Failure and mode boundaries
- An inspect request without a topic throws an error.
- An unknown human verb produces guidance and changes nothing.
- The tool checks its
AbortSignalbefore doing work. - With UI, the command uses
notify(). - Without UI, it uses
sendMessage(..., { triggerTurn: false }), so the answer is not lost in an inert notification. - Reloading code is not accomplished by directly running the TypeScript file. Restart the explicit launch when testing an edit.
Exercise: ask the agent to reserve basil through seed_welcome.
Checkpoint: it should report that no stock-changing operation exists, not invent reserve or attempt to invoke /seeds.
Milestone: the desk gains a real query surface
The next Saturday, Mara’s fictional desk has three seed varieties. Repeating a prose list has become awkward: the agent needs stable IDs, family filters and a clear error for an invented ID.
She replaces stage 1 with stage 2. These stages are independent, not cumulative imports.
Files:
Terminal shell—after exiting stage 1:
omp --no-extensions --no-skills -e "$EXAMPLES/seed-desk/02-catalog/index.ts"
Human OMP slash commands:
/seeds query
/seeds query herb
/seeds inspect basil-genovese
Model tool arguments—call seed_catalog:
{"op":"query","family":"herb"}
Model tool arguments—call seed_catalog:
{"op":"inspect","id":"basil-genovese"}
Stage 2 supports query and inspect only. It does not have a discover operation merely because stage 1 had one.
One shared domain, two entrances
This is the complete small helper used by both interfaces.
TypeScript source—stage 2’s catalog.ts:
import fixture from "./inventory.json";
export interface Seed {
id: string;
name: string;
family: string;
packets: number;
}
export const inventory: readonly Seed[] = fixture;
export function inspect(id: string): Seed {
const seed = inventory.find(item => item.id === id);
if (!seed) throw new Error("Unknown seed ID. Query the catalog for valid IDs. Nothing changed.");
return seed;
}
export function query(family?: string): readonly Seed[] {
return family ? inventory.filter(seed => seed.family === family) : inventory;
}
export function summary(seeds: readonly Seed[]): string {
return seeds.length
? seeds.map(seed => `${seed.id}: ${seed.name}, ${seed.packets} fictional packets (${seed.family})`).join("\n")
: "No fictional seeds match that family.";
}
Expected output for the herb query:
basil-genovese: Genovese basil, 8 fictional packets (herb)
The tool returns that summary in content, and records in:
details.scope: "fictional-fixture"details.seeds
This is better than asking the agent to scrape a notification. The tool gives it stable targets, while the host gets structured records.
Inspect progress and failure
Observed: the real loader, runner and intercepted tool adapter returned only basil-genovese for the herb query. Slash/tool summaries matched. Unknown inspection IDs threw. A pre-aborted call was refused through the intercepted path.
The family match is exact and case-sensitive. An unknown family gives an empty query result; an unknown inspect ID is an error.
No read appends reservation state.
Exercise: compare these two requests:
Model tool arguments—call seed_catalog, one request at a time:
{"op":"query","family":"unknown-family"}
{"op":"inspect","id":"unknown-seed"}
Answer: the query reports no matches. The inspection fails because it promises to identify one existing target.
What changes next?
Mara can now ask, “Which herbs are available?” But the eight packets are still a static fixture. To represent a reservation, she needs a state model, an ownership scope and a concurrency check—not merely another button.
That is the next chapter.
Source, snapshot 2026-08-29: the linked Seed Desk files; packages/coding-agent/src/extensibility/extensions/types.ts, ToolDefinition; packages/coding-agent/src/extensibility/extensions/wrapper.ts, RegisteredToolAdapter, ExtensionToolWrapper.
Extensions inside those boundaries · Source chapter: extensions/seed-desk-welcome-and-inventory. Original evidence remains scoped to its recorded snapshot.