Skip to main content

Sessions

A session owns a durable conversation: history, model, working directory, metadata, status, and lineage.

one answer                         durable conversation
ctx.ai.ask(prompt)                ctx.ai.sessions.create(config)
       │                                      │
       ▼                                      ▼
    string                              AiSessionHandle

                              run · send · notify · fork · spawn

One-shot answers

Use ctx.ai.ask when the caller needs text and no session handle afterward.

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: "Release Notes";
        readonly description: "Generate release notes from the current workspace";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Release Notes";
        readonly description: "Generate release notes from the current workspace";
    };
    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 Notes";
    readonly description: "Generate release notes from the current workspace";
}
metadata
: {
displayName: "Release Notes"displayName: 'Release Notes', description: "Generate release notes from the current workspace"description: 'Generate release notes from the current workspace', }, 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-notes.draft', title: stringtitle: 'Draft Release Notes', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const text: stringtext = await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApi
One off answers, agents, models, and sessions.
ai
.AiRuntimeApi.ask(prompt: string, attachments?: readonly AiAttachment[], config?: AiSessionConfig): Promise<string>ask(
'Draft concise release notes for the current workspace changes.' ); 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 text: stringtext });
}, }), ], });

ask accepts optional attachments and an optional AiSessionConfig. It still returns only the answer text. Use a session when the caller needs identity, history, follow-up work, or lifecycle control.

Create a durable session

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: "Release Review";
        readonly description: "Run durable release reviews";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Release Review";
        readonly description: "Run durable 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: "Run durable release reviews";
}
metadata
: {
displayName: "Release Review"displayName: 'Release Review', description: "Run durable release reviews"description: 'Run durable 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.start', title: stringtitle: 'Start Release Review', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const folders: readonly WorkspaceFolder[]folders = await ctx: CommandContextctx.ContextBase.workspace: WorkspaceApi
Folders, files, and configuration of the open workspace.
workspace
.WorkspaceApi.getFolders(): Promise<readonly WorkspaceFolder[]>getFolders();
const const folder: WorkspaceFolderfolder = const folders: readonly WorkspaceFolder[]folders[0]; 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 folder: WorkspaceFolderfolder === var undefinedundefined ? {} : { AiSessionConfig.workingDirectory?: string | undefinedworkingDirectory: const folder: WorkspaceFolderfolder.WorkspaceFolder.uri: stringuri }), AiSessionConfig.foreground?: boolean | undefinedforeground: true, AiSessionConfig.metadata?: AiSessionDisplayMetadata | undefined
Immutable metadata stored with the session.
metadata
: {
label: stringlabel: 'Release review', icon: stringicon: 'CodiconChecklist', }, }); 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
(
'Review correctness, migration risk, rollback, and observability.' ); 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);
}, }), ], });

ctx.ai.sessions.create() creates a root session. The returned AiSessionHandle is the live capability. The catalog stores metadata; the handle performs conversation work.

Session configuration

AiSessionConfig contains only creation-time choices:

MemberMeaning
modelExplicit model route; otherwise inherit the caller or workspace choice
titleHuman-readable session title
workingDirectoryWorkspace used by tools and the model
autoApprovedefault, autoApprove, or autopilot
appendSystemPromptAdditional system instructions for this session
foregroundOpen the created session immediately
metadataImmutable label and icon used to classify the session

Display metadata must contain a label, an icon, or both. It is immutable after creation. Use sessions.rename(...) for the mutable title.

Current session or catalog

Inside a tool or slash command, ctx.session is already the current session. Use it instead of looking the same session up again.

NeedSurface
Work in the current conversationctx.session
Create a new root conversationctx.ai.sessions.create()
Require a known sessionctx.ai.sessions.get(id)
Probe for an optional sessionctx.ai.sessions.find(id)
Render or inspect the catalogctx.ai.sessions.list() or observe()

Catalog lifecycle

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: "Session Catalog";
        readonly description: "Manage AI sessions";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Session Catalog";
        readonly description: "Manage AI sessions";
    };
    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: "Session Catalog";
    readonly description: "Manage AI sessions";
}
metadata
: {
displayName: "Session Catalog"displayName: 'Session Catalog', description: "Manage AI sessions"description: 'Manage AI sessions', }, 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: 'sessions.archive-finished', title: stringtitle: 'Archive Finished AI Sessions', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const sessions: readonly AiSessionMetadata[]sessions = await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApi
One off answers, agents, models, and sessions.
ai
.AiRuntimeApi.sessions: AiSessionsApisessions.AiSessionsApi.list(): Promise<readonly AiSessionMetadata[]>
Every session in the workspace, archived ones included.
list
();
const const finished: AiSessionMetadata[]finished = const sessions: readonly AiSessionMetadata[]sessions.ReadonlyArray<AiSessionMetadata>.filter(predicate: (value: AiSessionMetadata, index: number, array: readonly AiSessionMetadata[]) => unknown, thisArg?: any): AiSessionMetadata[] (+1 overload)
Returns the elements of an array that meet the condition specified in a callback function.
@parampredicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
filter
(
session: AiSessionMetadatasession => session: AiSessionMetadatasession.AiSessionMetadata.status: AiSessionStatusstatus === 'completed' && !session: AiSessionMetadatasession.AiSessionMetadata.isArchived: booleanisArchived ); for (const const session: AiSessionMetadatasession of const finished: AiSessionMetadata[]finished) { await ctx: CommandContextctx.ContextBase.ai: AiRuntimeApi
One off answers, agents, models, and sessions.
ai
.AiRuntimeApi.sessions: AiSessionsApisessions.AiSessionsApi.archive(sessionId: string): Promise<void>
Hide a session from the active list without deleting its history.
archive
(const session: AiSessionMetadatasession.AiSessionMetadata.id: string & $brand<"SessionId">id);
} }, }), ], });

The catalog operations have distinct semantics:

MethodResult
rename(id, title)Changes the mutable display title
archive(id)Hides the session from the active list but keeps history
unarchive(id)Returns an archived session to the active list
delete(id)Returns { kind: 'deleted' } or { kind: 'cancelled' }

Do not treat archive as deletion, and do not assume deletion succeeded without checking its discriminated result.