Skip to main content

Attachments

Attachments carry typed context beside prompt text. They do not become part of the system prompt and do not require authors to interpolate large values into a string.

Three attachment kinds

AiAttachment is a discriminated union:

KindCarriesRequired fields
textModel-readable textlabel, modelRepresentation
binaryEncoded binary contentlabel, data, contentType
resourceA resource addresslabel, uri

Every kind may include displayKind, which describes how the attachment should be presented without changing its model payload.

Attach text to a one-shot answer

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
, type type AiAttachment = AiTextAttachment | AiBinaryAttachment | AiResourceAttachmentAiAttachment } from '@modular/sdk';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Decision Review";
        readonly description: "Review decisions with explicit context";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Decision Review";
        readonly description: "Review decisions with explicit context";
    };
    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: "Decision Review";
    readonly description: "Review decisions with explicit context";
}
metadata
: {
displayName: "Decision Review"displayName: 'Decision Review', description: "Review decisions with explicit context"description: 'Review decisions with explicit context', }, 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: 'decision-review.example', title: stringtitle: 'Review Example Decision', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const attachments: readonly AiAttachment[]attachments: readonly type AiAttachment = AiTextAttachment | AiBinaryAttachment | AiResourceAttachmentAiAttachment[] = [ { AiTextAttachment.kind: "text"kind: 'text', AiAttachmentBase.label: stringlabel: 'Decision record', AiAttachmentBase.displayKind?: string | undefineddisplayKind: 'document', AiTextAttachment.modelRepresentation: stringmodelRepresentation: 'Decision: sessions own history; turns own submission lifecycle.', }, ]; const const answer: stringanswer = 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(
'Identify the invariant and one consequence.', const attachments: readonly AiAttachment[]attachments ); 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 answer: stringanswer });
}, }), ], });

Attach a workspace resource to a session turn

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
, type type AiAttachment = AiTextAttachment | AiBinaryAttachment | AiResourceAttachmentAiAttachment } from '@modular/sdk';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Resource Review";
        readonly description: "Review workspace resources in AI sessions";
    };
    readonly commands: readonly [Command<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Resource Review";
        readonly description: "Review workspace resources in 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: "Resource Review";
    readonly description: "Review workspace resources in AI sessions";
}
metadata
: {
displayName: "Resource Review"displayName: 'Resource Review', description: "Review workspace resources in AI sessions"description: 'Review workspace resources in 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: 'resource-review.readme', title: stringtitle: 'Review Workspace README', 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]; if (const folder: WorkspaceFolderfolder === var undefinedundefined) { return; } const const readme: AiAttachmentreadme: type AiAttachment = AiTextAttachment | AiBinaryAttachment | AiResourceAttachmentAiAttachment = { AiResourceAttachment.kind: "resource"kind: 'resource', AiAttachmentBase.label: stringlabel: 'Workspace README', AiResourceAttachment.uri: stringuri: `${const folder: WorkspaceFolderfolder.WorkspaceFolder.uri: stringuri}/README.md`, AiResourceAttachment.contentType?: string | undefinedcontentType: 'text/markdown', }; 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: 'README review', AiSessionConfig.workingDirectory?: string | undefinedworkingDirectory: const folder: WorkspaceFolderfolder.WorkspaceFolder.uri: stringuri, }); 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 purpose and list the documented commands.', { AiSendOptions.attachments?: readonly AiAttachment[] | undefinedattachments: [const readme: AiResourceAttachmentreadme] } ); 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.ask receives attachments as its second argument. session.run and session.send receive them through AiSendOptions.

Ownership rules

  • label is for people and model context inspection.
  • modelRepresentation is the exact text supplied for a text attachment.
  • uri identifies a resource. Keep the prompt focused on what to do with it.
  • contentType describes binary or resource content. Do not infer it from the label.
  • Attachments belong to one submitted prompt. They are not mutable session state and should not be used as a second persistence system.