Skip to main content

Turns

A session owns conversation history. A turn owns one submitted unit of work.

MethodWaits for completionReturnsUse it when
run(prompt)Yes{ text }The caller needs the final answer
run<T>(prompt)YesValidated TThe caller needs domain data, not prose
send(message)NoAiTurnHandleThe caller needs observation, cancellation, or an exact-turn fork
notify(...)NoAiTurnHandleBackground work must report without impersonating the user

Completed text

import { 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';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Summaries";
        readonly description: "Generate concise summaries";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Summaries";
        readonly description: "Generate concise summaries";
    };
    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: "Summaries";
    readonly description: "Generate concise summaries";
}
metadata
: {
displayName: "Summaries"displayName: 'Summaries', description: "Generate concise summaries"description: 'Generate concise summaries', }, 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: 'summaries.workspace', title: stringtitle: 'Summarize Workspace', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const session: AiSessionHandlesession = await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApi
One off answers, agents, models, and sessions.
ai
.AiRuntimeApi.sessions: AiSessionsApisessions.AiSessionsApi.create(config?: AiSessionConfig): Promise<AiSessionHandle>
Start a new root session.
create
({ AiSessionConfig.title?: string | undefinedtitle: 'Summary' });
const const result: AiRunResultresult = 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
(
'Summarize the current workspace in five concrete bullets.' ); ctx: CommandContextctx.ContextBase.logger: Logger
Your mod's log channel.
logger
.Logger.info(message: string, ...args: readonly unknown[]): voidinfo(const result: AiRunResultresult.AiRunResult.text: stringtext);
}, }), ], });

The text overload returns AiRunResult. It rejects when the turn fails or is cancelled.

Validated domain data

The structured overload derives a runtime contract from the TypeScript type argument. The model response must satisfy that contract before the promise resolves.

import { 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';
interface ReleaseRisk { readonly ReleaseRisk.area: "data" | "runtime" | "interface"area: 'data' | 'runtime' | 'interface'; readonly ReleaseRisk.severity: "low" | "medium" | "high"severity: 'low' | 'medium' | 'high'; readonly ReleaseRisk.evidence: readonly string[]evidence: readonly string[]; } interface ReleaseReview { readonly ReleaseReview.summary: stringsummary: string; readonly ReleaseReview.risks: readonly ReleaseRisk[]risks: readonly ReleaseRisk[]; } export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Release Review";
        readonly description: "Produce validated release reviews";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Release Review";
        readonly description: "Produce validated release reviews";
    };
    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: "Release Review";
    readonly description: "Produce validated release reviews";
}
metadata
: {
displayName: "Release Review"displayName: 'Release Review', description: "Produce validated release reviews"description: 'Produce validated release reviews', }, 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: 'release-review.structured', title: stringtitle: 'Run Structured Release Review', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const session: AiSessionHandlesession = await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApi
One off answers, agents, models, and sessions.
ai
.AiRuntimeApi.sessions: AiSessionsApisessions.AiSessionsApi.create(config?: AiSessionConfig): Promise<AiSessionHandle>
Start a new root session.
create
({ AiSessionConfig.title?: string | undefinedtitle: 'Release review' });
const const review: ReleaseReviewreview = await const session: AiSessionHandlesession.AiSessionHandle.run<ReleaseReview>(prompt: string, options?: AiSendOptions): Promise<ReleaseReview> (+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
<ReleaseReview>(
'Return a release review that matches the requested structure.' ); ctx: CommandContextctx.ContextBase.logger: Logger
Your mod's log channel.
logger
.Logger.info(message: string, ...args: readonly unknown[]): voidinfo(`${const review: ReleaseReviewreview.ReleaseReview.risks: readonly ReleaseRisk[]risks.ReadonlyArray<T>.length: number
Gets the length of the array. This is a number one higher than the highest element defined in an array.
length
} risks: ${const review: ReleaseReviewreview.ReleaseReview.summary: stringsummary}`);
}, }), ], });

run<T> rejects on turn failure, cancellation, missing delivery, or invalid data. Do not catch that error and parse the prose as a fallback. That would discard the guarantee the structured overload exists to provide.

Observe and cancel a submission

send accepts a typed prompt or notification message and returns immediately after the host accepts the submission.

import { function slashCommand<TId extends string>(slashCommandValue: SlashCommandInput<TId>): SlashCommand<TId>
Declare a command the user types inside a chat session. The `id` is the name typed after the slash, `description` is the line shown in the typeahead, and `argumentHint` is the placeholder for whatever the user types after the name. The handler receives that text, or `undefined` when the user typed nothing, plus a context carrying the AI session the command was typed into. Use this instead of `command` when the behavior belongs to a conversation and needs the session. Use `command` when it belongs to the workbench and runs from the palette. Use `tool` when the model, not the user, should decide to run it. Slash commands go in the same top-level `commands` array as palette commands.
@categoryAuthoring
slashCommand
, 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 investigate: SlashCommand<"investigate">investigate = slashCommand<"investigate">(slashCommandValue: SlashCommandInput<"investigate">): SlashCommand<"investigate">
Declare a command the user types inside a chat session. The `id` is the name typed after the slash, `description` is the line shown in the typeahead, and `argumentHint` is the placeholder for whatever the user types after the name. The handler receives that text, or `undefined` when the user typed nothing, plus a context carrying the AI session the command was typed into. Use this instead of `command` when the behavior belongs to a conversation and needs the session. Use `command` when it belongs to the workbench and runs from the palette. Use `tool` when the model, not the user, should decide to run it. Slash commands go in the same top-level `commands` array as palette commands.
@categoryAuthoring
slashCommand
({
id: "investigate"id: 'investigate', description: stringdescription: 'Start an investigation in the current conversation.', argumentHint?: string | undefinedargumentHint: '<question>', run: (argument: string | undefined, ctx: SlashCommandContext) => Awaitable<unknown>run: async (argument: string | undefinedargument, ctx: SlashCommandContextctx) => { const const question: string | undefinedquestion = argument: string | undefinedargument?.String.trim(): string
Removes the leading and trailing white space and line terminator characters from a string.
trim
();
if (const question: string | undefinedquestion === var undefinedundefined || const question: stringquestion.String.length: number
Returns the length of a String object.
length
=== 0) {
return 'Usage: /investigate <question>'; } const const turn: AiTurnHandleturn = await ctx: SlashCommandContextctx.session: AiSessionHandle
The chat session the command was typed into.
session
.AiSessionHandle.send(message: AiMessage, options?: AiSendOptions): Promise<AiTurnHandle>
Put a message into this session. The message's `type` says what kind of thing it is. Who produced it is stamped by the host from the calling context, so a mod cannot claim to be the user or another mod. Both kinds drive a turn in the session.
send
({
AiPromptMessage.type: "prompt"type: 'prompt', AiPromptMessage.text: stringtext: const question: stringquestion, }); const const terminal: AiTurnTerminalSnapshotterminal = await const turn: AiTurnHandleturn.AiTurnHandle.completion(): Promise<AiTurnTerminalSnapshot>completion(); switch (const terminal: AiTurnTerminalSnapshotterminal.kind: "completed" | "failed" | "cancelled"kind) { case 'completed': return
const terminal: {
    readonly kind: "completed";
    readonly submissionId: SubmissionId;
    readonly sessionId: SessionId;
    readonly turnId: TurnId;
    readonly result: AiRunResult;
    readonly delivery: AiTurnDelivery;
}
terminal
.result: AiRunResultresult.AiRunResult.text: stringtext;
case 'failed': return `Investigation failed: ${
const terminal: {
    readonly kind: "failed";
    readonly submissionId: SubmissionId;
    readonly sessionId: SessionId;
    readonly target: AiTurnTarget;
    readonly error: {
        readonly message: string;
    };
}
terminal
.
error: {
    readonly message: string;
}
error
.message: stringmessage}`;
case 'cancelled': return 'Investigation cancelled.'; } }, }); export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Investigation";
        readonly description: "Conversation investigation commands";
    };
    readonly commands: readonly [SlashCommand<"investigate">];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Investigation";
        readonly description: "Conversation investigation commands";
    };
    readonly commands: readonly [SlashCommand<"investigate">];
}): 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: "Investigation";
    readonly description: "Conversation investigation commands";
}
metadata
: {
displayName: "Investigation"displayName: 'Investigation', description: "Conversation investigation commands"description: 'Conversation investigation commands', }, commands: readonly [SlashCommand<"investigate">]commands: [const investigate: SlashCommand<"investigate">investigate], });

AiTurnHandle exposes:

MemberContract
submissionIdStable identity for this submission
get()Current queued, running, needs-input, or terminal snapshot
observe(listener)Snapshot changes until the listener is disposed
completion()One terminal completed, failed, or cancelled value
cancel()Requests cancellation and returns the terminal snapshot
fork(config?)Forks immediately after this completed turn

Notifications

session.notify({ title, body }) is shorthand for a notification message. The host marks it as a system notification. A mod cannot claim that the user sent it.

Notifications still drive a turn and return an AiTurnHandle. Use them for background reports, not for ordinary model prompts.