Agents
An agent gives a model one reusable role: a name, a system prompt, and an optional model route. Declaring an agent registers the role. It does not run it.
Declare and run an agent
import { function agent(input: AgentInput): AgentDeclare a reusable agent: a name, a system prompt, and optionally a model. This describes an agent, it does not run one. There is no handler, and nothing executes when the mod activates. Put the result under `ai.agents` and the host registers it so the user or another agent can call it by id, `<extensionId>/<name>`. That is the line between this and the rest of the AI surface. To actually run something, use `ctx.ai.ask` for a single answer or `ctx.ai.sessions` for a conversation. To give the model a capability it can invoke, write a `tool`, which does have a handler.agent, function command<TId extends string>(commandValue: CommandInput<TId>): Command<TId>Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.command, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): ModThe entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.defineMod, function prompt(input: PromptInput): PromptDeclare reusable prompt text the user can drop into the composer. Put the result in the mod's top-level `prompts` array. It is text and nothing more: no model, no handler, no execution. An agent is the next step up, it binds prompt text to a model under a callable name. The same `Prompt` value can serve both, handed to `agent({ prompt })` and listed in `prompts`, so the agent and the user work from one wording.prompt } from '@modular/sdk'; const const reviewInstructions: PromptreviewInstructions = function prompt(input: PromptInput): PromptDeclare reusable prompt text the user can drop into the composer. Put the result in the mod's top-level `prompts` array. It is text and nothing more: no model, no handler, no execution. An agent is the next step up, it binds prompt text to a model under a callable name. The same `Prompt` value can serve both, handed to `agent({ prompt })` and listed in `prompts`, so the agent and the user work from one wording.prompt({ PromptInput.name: stringShort identifier, also what the user sees when picking the prompt.name: 'review-instructions', PromptInput.description: stringOne line telling the user what inserting this will do.description: 'Insert the standard code-review instructions.', PromptInput.content: stringThe text inserted into the composer. {@link prompt } removes the common indentation and surrounding whitespace, so multiline template literals can follow the surrounding TypeScript indentation.content: 'Review correctness, cancellation, and cleanup. Cite exact files.', }); const const reviewer: Agentreviewer = function agent(input: AgentInput): AgentDeclare a reusable agent: a name, a system prompt, and optionally a model. This describes an agent, it does not run one. There is no handler, and nothing executes when the mod activates. Put the result under `ai.agents` and the host registers it so the user or another agent can call it by id, `<extensionId>/<name>`. That is the line between this and the rest of the AI surface. To actually run something, use `ctx.ai.ask` for a single answer or `ctx.ai.sessions` for a conversation. To give the model a capability it can invoke, write a `tool`, which does have a handler.agent({ AgentInput.name: stringShort identifier. The callable agent id is `<extensionId>/<name>`.name: 'reviewer', AgentInput.description: stringWhat this agent is good for. Shown to the user and read by the model when routing.description: 'Reviews a change and reports concrete defects.', AgentInput.prompt: string | PromptThe system prompt, either inline text or a {@link Prompt } shared with the composer.prompt: const reviewInstructions: PromptreviewInstructions, }); const const reviewCurrentChange: Command<"research.review-current-change">reviewCurrentChange = command<"research.review-current-change">(commandValue: CommandInput<"research.review-current-change">): Command<"research.review-current-change">Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.command({ id: "research.review-current-change"id: 'research.review-current-change', title: stringtitle: 'Review Current Change', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const handle: AiAgentHandlehandle = await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApiOne off answers, agents, models, and sessions.ai.AiRuntimeApi.agents: AiAgentsApiagents.AiAgentsApi.get(agent: string | Agent): Promise<AiAgentHandle>get(const reviewer: Agentreviewer); const const result: AiRunResultresult = await const handle: AiAgentHandlehandle.AiAgentHandle.run(prompt: string, options?: AiSendOptions): Promise<AiRunResult> (+1 overload)run('Review the current workspace change.'); await ctx: CommandContextctx.ui: UiCapabilityNotifications, dialogs, quick input, and progress.ui.UiCapability.notification: UiNotificationCapabilitynotification.UiNotificationCapability.info(request: UiNotificationRequest): Promise<UiActionResult>info({ UiNotificationRequest.message: stringmessage: const result: AiRunResultresult.AiRunResult.text: stringtext }); }, }); export defaultdefineMod<{ readonly metadata: { readonly displayName: "Research"; readonly description: "Reusable research agents"; }; readonly prompts: readonly [Prompt]; readonly ai: { readonly agents: readonly [Agent]; }; readonly commands: readonly [Command<"research.review-current-change">]; }>(definition: { readonly metadata: { readonly displayName: "Research"; readonly description: "Reusable research agents"; }; readonly prompts: readonly [Prompt]; readonly ai: { readonly agents: readonly [Agent]; }; readonly commands: readonly [Command<"research.review-current-change">]; }): ModThe entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.defineMod({metadata: { readonly displayName: "Research"; readonly description: "Reusable research agents"; }metadata: { displayName: "Research"displayName: 'Research', description: "Reusable research agents"description: 'Reusable research agents', }, prompts: readonly [Prompt]prompts: [const reviewInstructions: PromptreviewInstructions],ai: { readonly agents: readonly [Agent]; }ai: { agents: readonly [Agent]agents: [const reviewer: Agentreviewer] }, commands: readonly [Command<"research.review-current-change">]commands: [const reviewCurrentChange: Command<"research.review-current-change">reviewCurrentChange], });
The same Prompt value is available in the composer and used as the agent's
system prompt. One value owns the wording.
Agents go under ai.agents, not a top-level agents slot. Their callable
identity is <extensionId>/<name>. Passing the declared Agent value to
ctx.ai.agents.get(...) avoids reconstructing that identity by hand.
Run or spawn
AiAgentHandle exposes two execution
shapes:
| Method | Result | Use it when |
|---|---|---|
run(prompt) | A completed answer | The caller needs one result now |
spawn(config?) | A durable root session | The work needs follow-up turns or its own visible conversation |
import { function agent(input: AgentInput): AgentDeclare a reusable agent: a name, a system prompt, and optionally a model. This describes an agent, it does not run one. There is no handler, and nothing executes when the mod activates. Put the result under `ai.agents` and the host registers it so the user or another agent can call it by id, `<extensionId>/<name>`. That is the line between this and the rest of the AI surface. To actually run something, use `ctx.ai.ask` for a single answer or `ctx.ai.sessions` for a conversation. To give the model a capability it can invoke, write a `tool`, which does have a handler.agent, function command<TId extends string>(commandValue: CommandInput<TId>): Command<TId>Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.command, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): ModThe entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.defineMod } from '@modular/sdk'; const const researcher: Agentresearcher = function agent(input: AgentInput): AgentDeclare a reusable agent: a name, a system prompt, and optionally a model. This describes an agent, it does not run one. There is no handler, and nothing executes when the mod activates. Put the result under `ai.agents` and the host registers it so the user or another agent can call it by id, `<extensionId>/<name>`. That is the line between this and the rest of the AI surface. To actually run something, use `ctx.ai.ask` for a single answer or `ctx.ai.sessions` for a conversation. To give the model a capability it can invoke, write a `tool`, which does have a handler.agent({ AgentInput.name: stringShort identifier. The callable agent id is `<extensionId>/<name>`.name: 'researcher', AgentInput.description: stringWhat this agent is good for. Shown to the user and read by the model when routing.description: 'Investigates a topic and records source-backed findings.', AgentInput.prompt: string | PromptThe system prompt, either inline text or a {@link Prompt } shared with the composer.prompt: 'Separate verified facts from unresolved questions.', }); export defaultdefineMod<{ readonly metadata: { readonly displayName: "Research"; readonly description: "Reusable research agents"; }; readonly ai: { readonly agents: readonly [Agent]; }; readonly commands: readonly [Command<string>]; }>(definition: { readonly metadata: { readonly displayName: "Research"; readonly description: "Reusable research agents"; }; readonly ai: { readonly agents: readonly [Agent]; }; readonly commands: readonly [Command<string>]; }): ModThe entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.defineMod({metadata: { readonly displayName: "Research"; readonly description: "Reusable research agents"; }metadata: { displayName: "Research"displayName: 'Research', description: "Reusable research agents"description: 'Reusable research agents', },ai: { readonly agents: readonly [Agent]; }ai: { agents: readonly [Agent]agents: [const researcher: Agentresearcher] }, commands: readonly [Command<string>]commands: [ command<string>(commandValue: CommandInput<string>): Command<string>Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.command({ id: stringid: 'research.start', title: stringtitle: 'Start Research Session', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const handle: AiAgentHandlehandle = await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApiOne off answers, agents, models, and sessions.ai.AiRuntimeApi.agents: AiAgentsApiagents.AiAgentsApi.get(agent: string | Agent): Promise<AiAgentHandle>get(const researcher: Agentresearcher); const const session: AiSessionHandlesession = await const handle: AiAgentHandlehandle.AiAgentHandle.spawn(config?: AiSessionConfig): Promise<AiSessionHandle>spawn({ AiSessionConfig.title?: string | undefinedtitle: 'Dependency research', AiSessionConfig.foreground?: boolean | undefinedforeground: true, }); await const session: AiSessionHandlesession.AiSessionHandle.run(prompt: string, options?: AiSendOptions): Promise<AiRunResult> (+1 overload)Send a prompt and wait for the finished answer. The generic overload validates the reply against `T` and rejects if the model returns something that does not match, so a caller never has to parse prose.run('Map the dependency boundary for this package.'); }, }), ], });
Members
Agent contains name, description,
prompt, optional model, optional tools, and kind.
descriptionexplains the role to people and model routing.promptaccepts inline text or a declared prompt.modelpins a route. Leave it out to inherit the calling session's model.toolsis reserved for tool scoping but is not executable yet. Leave it out.
Use ctx.ai.agents.list(), find(), get(), and observe() when the agent is
selected dynamically rather than imported as a declared value.
Declaration, metadata, configuration, execution
The public model keeps four states separate:
Agent authored declaration │ ▼ AiAgentMetadata catalog projection │ ▼ AiAgentHandle executable identity │ ├── config() resolved session configuration ├── run() one completed answer in a temporary root session └── spawn() durable root session configured by the agent
handle.config(overrides) resolves the declared prompt and model into an
AiSessionConfig without creating a session. handle.metadata() reads the
current registered definition. run() and spawn() re-read the definition, so
a retained handle follows a promoted declaration update rather than freezing an
old snapshot.
An agent's spawn() is not ctx.session.spawn(). The first creates a root
session configured by the agent. The second creates a child of the current
conversation. See Lineage before choosing between them.