Skip to main content

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): Agent
Declare 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.
@exampleA reviewer agent sharing its prompt with the composer ```ts const reviewPrompt = prompt({ name: 'review-checklist', description: 'Insert the standard review checklist.', content: 'Review correctness, cancellation, and cleanup.', }); const reviewer = agent({ name: 'reviewer', description: 'Reviews a change and reports concrete defects.', prompt: reviewPrompt, }); ```@categoryAuthoring
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.
@categoryAuthoring
command
, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): Mod
The 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.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
, function prompt(input: PromptInput): Prompt
Declare 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.
@example```ts const commitStyle = prompt({ name: 'commit-style', description: 'Insert the commit message rules for this repo.', content: ` # Commit message Start with a bracketed area, for example `[terminal]`. `, }); ```@categoryAuthoring
prompt
} from '@modular/sdk';
const const reviewInstructions: PromptreviewInstructions = function prompt(input: PromptInput): Prompt
Declare 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.
@example```ts const commitStyle = prompt({ name: 'commit-style', description: 'Insert the commit message rules for this repo.', content: ` # Commit message Start with a bracketed area, for example `[terminal]`. `, }); ```@categoryAuthoring
prompt
({
PromptInput.name: string
Short identifier, also what the user sees when picking the prompt.
name
: 'review-instructions',
PromptInput.description: string
One line telling the user what inserting this will do.
description
: 'Insert the standard code-review instructions.',
PromptInput.content: string
The 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): Agent
Declare 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.
@exampleA reviewer agent sharing its prompt with the composer ```ts const reviewPrompt = prompt({ name: 'review-checklist', description: 'Insert the standard review checklist.', content: 'Review correctness, cancellation, and cleanup.', }); const reviewer = agent({ name: 'reviewer', description: 'Reviews a change and reports concrete defects.', prompt: reviewPrompt, }); ```@categoryAuthoring
agent
({
AgentInput.name: string
Short identifier. The callable agent id is `<extensionId>/<name>`.
name
: 'reviewer',
AgentInput.description: string
What 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 | Prompt
The 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.
@categoryAuthoring
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: AiRuntimeApi
One 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: UiCapability
Notifications, 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 default
defineMod<{
    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">];
}): Mod
The 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.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
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:

MethodResultUse it when
run(prompt)A completed answerThe caller needs one result now
spawn(config?)A durable root sessionThe work needs follow-up turns or its own visible conversation
import { function agent(input: AgentInput): Agent
Declare 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.
@exampleA reviewer agent sharing its prompt with the composer ```ts const reviewPrompt = prompt({ name: 'review-checklist', description: 'Insert the standard review checklist.', content: 'Review correctness, cancellation, and cleanup.', }); const reviewer = agent({ name: 'reviewer', description: 'Reviews a change and reports concrete defects.', prompt: reviewPrompt, }); ```@categoryAuthoring
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.
@categoryAuthoring
command
, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): Mod
The 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.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
} from '@modular/sdk';
const const researcher: Agentresearcher = function agent(input: AgentInput): Agent
Declare 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.
@exampleA reviewer agent sharing its prompt with the composer ```ts const reviewPrompt = prompt({ name: 'review-checklist', description: 'Insert the standard review checklist.', content: 'Review correctness, cancellation, and cleanup.', }); const reviewer = agent({ name: 'reviewer', description: 'Reviews a change and reports concrete defects.', prompt: reviewPrompt, }); ```@categoryAuthoring
agent
({
AgentInput.name: string
Short identifier. The callable agent id is `<extensionId>/<name>`.
name
: 'researcher',
AgentInput.description: string
What 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 | Prompt
The system prompt, either inline text or a {@link Prompt } shared with the composer.
prompt
: 'Separate verified facts from unresolved questions.',
}); export default
defineMod<{
    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>];
}): Mod
The 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.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
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.
@categoryAuthoring
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: AiRuntimeApi
One 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.

  • description explains the role to people and model routing.
  • prompt accepts inline text or a declared prompt.
  • model pins a route. Leave it out to inherit the calling session's model.
  • tools is 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.