Skip to content

manifest

Manifest barrel — agent config conversion and tool schema handling.

Used by aai-cli (bundler) and aai-server (rpc-schemas). Generated bundle entries call toAgentConfig, which is why this subpath is published.

agentToolsets(def): Toolset[]

Every toolset an agent definition carries, in precedence order: its tools/ files, then what agent() and a host step attached (toolsets — the roster, MCP). A dialog.tool needs no layer here: its own execute refuses out of state, and that check travels with the def wherever it is declared — a spec, a subagent, a direct call. Builtins are the runtime’s to append, since they resolve against host options.

ToolBearingDef

Toolset[]


agentToolsToSchemas(toolsets): ToolSchema[]

readonly Toolset[]

ToolSchema[]


composeToolsets(sets, onShadowed?): ToolTable

Compose toolsets, FIRST WINS: a later set’s tool of an already-taken name is dropped, and onShadowed hears about it — the precedence is the order.

readonly Toolset[]

(name, kept, dropped) => void

ToolTable


gateToolset(set, gates): Toolset

Layer extra gates over a set — every gate must pass, the set’s own first.

Toolset

readonly ToolGate[]

Toolset


normalizeToolMessages(input): ToolMessages | undefined

Author input → the wire shape, dropping every kind the tool did not declare.

Answers undefined for a tool with nothing to say, so a schema for an ordinary tool is byte-identical to what it was before this field existed — which is what keeps messages off every deployed agent’s tool declarations and out of every snapshot that did not opt in.

ToolMessagesInput | undefined

ToolMessages | undefined


toAgentConfig(source): object

Convert an agent definition into its serializable AgentConfig, injecting the default providers, deriving the session mode, and running the cross-field validation rules. Called from generated bundle entries and the runtime.

AgentConfigSource

object

optional builtinTools?: readonly string[]

{ sampleRate?: number; }

optional errorPhrase?: string

greeting: string

optional idleTimeoutMs?: number

optional interruption?: "off" | { backoffMs?: number; continuationWindowMs?: number; minDurationMs?: number; minWords?: number; resumeFalseInterruption?: boolean; }

optional listing?: string

optional llm?: ProviderDescriptor<string, Record<string, unknown>>

optional maxOutputTokens?: number

optional maxRetries?: number

optional maxSteps?: number

optional mcpServers?: Record<string, { allowedTools?: readonly string[]; pinnedTools?: Record<string, string>; tokenEnv?: string; url?: string; }>

optional mode?: "s2s" | "pipeline" | "text" | "workflow-app"

name: string

optional promptContext?: string

optional requiredEnv?: readonly string[]

optional resetToolChoice?: boolean

optional s2s?: ProviderDescriptor<string, Record<string, unknown>>

{ deadAirCoverMs?: number; nudge?: { afterMs: number; prompt?: string; }; }

optional startFailurePhrase?: string

optional stt?: ProviderDescriptor<string, Record<string, unknown>>

optional sttPrompt?: string

systemPrompt: string

optional telephony?: boolean | readonly string[]

optional temperature?: number

optional toolChoice?: "auto" | "required" | "none" | { toolName: string; type: "tool"; }

optional tts?: ProviderDescriptor<string, Record<string, unknown>>

{ detection?: string; maxSilenceMs?: number; minSilenceMs?: number; preemptiveGeneration?: boolean; startSpeakingFloorMs?: number; userTurnLimit?: { maxDurationMs?: number; maxWords?: number; }; }

{ totalTokens?: number; }

optional voicePresets?: readonly string[]


toolEntry(def): ToolsetEntry

Classify one def into an entry: the ONE place a def’s identity is inspected. A clientTool (its brand) is executed by the page; everything else here.

ToolDef

ToolsetEntry


toolRegistry(modules): ToolRegistry

Build a checked registry from already-loaded modules.

Synchronous, because the caller that matters most — the generated worker entry — has the modules statically imported already, and a Promise there would put top-level await in a bundle the guest loads.

ToolModules

ToolRegistry


toolset(source, tools, gate?): Toolset

Build a Toolset over a map of defs, optionally gated.

ToolSource

ToolMap

ToolGate

Toolset


withTools<D>(def, registry): D

Attach a registry to an agent definition, returning the def the runtime runs.

A NEW object rather than a mutation: the def a module default-exports is shared (a spec imports the same one the entry does), and a loader quietly rewriting it makes the order of two imports decide what an agent can do.

It is also the seam every registry NOT assembled by a bundler goes on through. Two do. withToolsDir (@alexkroman1/aai-runtime) scans a real directory for a self-hosted process and comes back here. And the studio’s own coding agent resolves its tool families from a session (aai-guest/studio-agent.ts), which is what makes that honest rather than an exception: a registry resolved from a session instead of from a directory, attached the same way.

Closing over a directory is NOT what puts a registry here — this said so, and templates/coding-agent/ disproves it: nine tools that all close over one directory, shipped as FILES, each re-exporting an entry from a registry shared.ts builds once. What the studio has that a template does not is a directory chosen per SESSION and re-materialized under a running process, where a file’s default export is evaluated once at import. The distinction is LIFETIME, not closure, and it matters because the closure reading would tell an author their tools cannot be files when they can.

A name the def ALREADY holds is an error. Through agent() that is now unreachable — it returns an empty table and refuses a tools argument — so what this catches is a hand-written export default { … tools: {…} } that skipped agent(), and a second withTools over a def that already has one.

A name the def declared as a BUILTIN is an error too, and that one an author can reach. builtinTools: ["calculate"] beside tools/calculate.ts is one file name away at all times, filenames being the only thing here a user picks freely — and it built clean: the runtime’s merge drops the colliding builtin (mergeBuiltinSurface), so the entry the author wrote did nothing and the only trace was one info line in a session log, minted at the first call rather than at the build. That is the same silence discovery was introduced to kill (“forgetting one line was silent”), reached by the other route, and it is the one collision where BOTH halves were declared on purpose — so it is a contradiction to report rather than a precedence to apply.

Structural rather than AgentDef, and it hands back what it was given: a caller keeps whatever else its def carries, and nothing this returns is described by a type the caller did not already name. builtinTools joins the constraint as optional and widened to readonly string[], so a def that carries none still passes and this module still names no builtin catalog.

It also RECORDS each def’s name (_testing-tool-names.ts). This is the one step every lowering goes through that pairs a def with the name the model calls, and a spec handed the def later — toolResultIn(calls, def), toolNameOf(def) — has nowhere else to learn it. A WeakMap write per tool, so a lowering that nobody tests costs nothing.

D extends object

D

ToolRegistry

D


withWorkflows<D>(def, registry, options?): D

Attach a discovered registry to an agent definition, returning the def the runtime runs — a NEW object, for withTools’ reason (the def a module exports is shared with every spec that imports it).

An explicit declaration wins a clash, and says so. withTools refuses a name declared twice, because a tool’s file IS its declaration and a second one is a contradiction. A workflow’s declaration of record is still agent({ workflows }), and the common clash is benign — the author registered workflows/digest.ts’s def as digest and the file ALSO default-exports it, which is the same object under the same name. That case is silent. A DIFFERENT def under a declared name keeps the declared one and warns, naming the file, so the file’s author learns it is not the one running rather than discovering it from a run that behaves like the other. The same def declared under ANOTHER name (workflows: { nightly: digest } beside workflows/digest.ts) registers ONCE, under the declared name, with a warning naming the file: a def under two names was listed twice by GET /workflows and could be started as either.

Structural rather than AgentDef, and it hands back what it was given, as withTools does.

D extends object

D

WorkflowRegistry

WithWorkflowsOptions

D


workflowRegistry(modules): WorkflowRegistry

The workflows a workflows/ directory declares, from its already-loaded modules. Synchronous, like toolRegistry, so the generated entry carries no top-level await.

A module that does not default-export a def is skipped (see the module doc) — except a bodyless two-phase declaration, which is refused naming .body(). One that is a def and is nested, misnamed or a duplicate is an error naming the file.

WorkflowModules

WorkflowRegistry

One resolved name in a ToolTable.

readonly entry: ToolsetEntry

readonly name: string

readonly toolset: Toolset


readonly tools: ToolMap

readonly optional toolsets?: readonly Toolset[]


A source of tools, as the runtime consumes it.

The advertised list is fixed per session on every transport (an S2S service holds its tool list for the whole session), so a rule about WHEN a tool may run is a Toolset.gate at the call — which is also where the model is told why, and how to recover.

execute(name, args, ctx): unknown

Run name’s body with already-validated arguments. Gating is the caller’s.

string

unknown

ToolContext

unknown

gate(name, ctx): ToolRefusal | undefined

undefined when name may run now; otherwise the refusal the model reads.

string

ToolContext

ToolRefusal | undefined

list(): Readonly<Record<string, ToolsetEntry>>

Every tool this set advertises, by the name the model calls.

Readonly<Record<string, ToolsetEntry>>

readonly source: ToolSource


One advertised tool: the def the model reads, and who executes it.

readonly def: ToolDef

Description, schema, messages and onError — what the model and the executor read.

readonly executor: ToolExecutor

readonly optional timeoutMs?: number

A per-call deadline the entry carries (a clientTool’s timeoutMs).


Several toolsets composed into one name → tool lookup.

resolve(name): ResolvedTool | undefined

string

ResolvedTool | undefined

readonly tools: readonly ResolvedTool[]

Every advertised tool, in precedence order.

AgentConfig = z.infer<typeof AgentConfigSchema>

JSON-safe subset of the agent definition — the canonical serializable config that flows CLI → server → runtime unchanged.


AgentConfigSource = Omit<AgentConfig, "mode" | "systemPrompt" | "promptContext" | "mcpServers"> & object & { [K in HostOnlyAgentField]?: unknown }

What toAgentConfig accepts: every serializable AgentConfig field plus the host-only fields the deny-list strips. AgentDef is assignable to this by construction; the explicit | undefined on the host-only members keeps spread call sites ({...agent, stt: maybeUndefined}) legal under exactOptionalPropertyTypes.

optional mcpServers?: McpServers

Wider than the wire’s record for the same reason: an McpServerConfig may carry a url RESOLVER and headers, both host-only. toAgentConfig strips them (see wireMcpServers).

optional mode?: AgentMode

See AgentDef.mode; undefined from a spread means the default.

optional promptContext?: AgentPromptContext

The same widening for promptContext, whose function half is dropped the same way.

optional systemPrompt?: AgentSystemPrompt

Wider than the config’s own string, because AgentDef.systemPrompt may be a RESOLVER — a function this layer cannot serialize and must not hand onward. Widened here rather than on AgentConfig so AgentDef stays assignable to this by construction, which is what every toAgentConfig(agent) call site relies on. toAgentConfig drops it (see staticSystemPrompt); the runtime holds the agent’s own module and asks the function per request.


HostOnlyAgentField = typeof HOST_ONLY_AGENT_FIELDS[number]

A host-only AgentDef field name stripped by toAgentConfig (tools, events, …).


ResolvedAgentDef = AgentDef & object

An AgentDef as the RUNTIME holds it: plus every toolset beyond the tools/ files — RESOLVED, never authored. agent() attaches the roster’s and withMcpTools an MCP server’s; read the whole table through agentToolsets. Not on the authoring surface: an author never names a toolset, so the public AgentDef does not carry the field’s type.

readonly optional toolsets?: readonly Toolset[]


SessionMode = "s2s" | "pipeline" | "text"

Session mode derived from which provider fields are set.

toAgentConfig, createRuntime, and the server’s IsolateConfigSchema all use assertProviderTriple so there’s one source of truth for the validation.

"text" is the one mode with no audio path at all: the agent is an LLM, a system prompt and its tools, driven by createTextAgent (@alexkroman1/aai-runtime) over a message list rather than by a transport over a socket.


ToolExecutor = "host" | "client"

Who runs a tool’s body: this process ("host"), or the connected browser page ("client", a clientTool — the call waits for the page’s tool_result).


ToolGate = (name, def, ctx) => ToolRefusal | undefined

A gate over one def — what toolset composes into Toolset.gate.

string

ToolDef

ToolContext

ToolRefusal | undefined


ToolModules = Readonly<Record<string, unknown>>

path → module namespace, which is what both sources produce: Vite’s import.meta.glob (eager) and the static import list the CLI generates.


ToolRegistry = Readonly<Record<string, ToolDef>>

A checked set of tools, keyed by the name the model calls.


ToolSchema = object

A tool declaration in wire form: name, description, and JSON Schema parameters — the serializable counterpart of ToolDef.

description: string

optional effect?: ToolEffect

What the tool does to the world — see ToolDef.effect. A host-mode client declaring its own tools sets it the same way; an MCP tool’s comes from its server’s readOnlyHint. Never sent to the model.

optional messages?: ToolMessages

The tool’s spoken messages, NORMALIZED — see ToolMessages.

It rides on the wire declaration rather than beside it because that is what makes the feature mean the same thing in every mode: the deployed guest builds this from the agent’s own ToolDefs, and a host-mode client that supplies its own tool declarations gets the behaviour by declaring the field. Nothing here reaches the model — toVercelTools passes name, description and parameters to the provider and reads this itself.

Absent for every tool that declares none, which is what keeps an ordinary tool’s wire declaration byte-identical to what it was before the field existed.

name: string

parameters: JSONSchema7

type: "function"

optional ui?: ToolUi

How a page displays the tool — see ToolUi (tool-def.ts). Rides here for the reason messages does: the deployed guest builds the declaration from the agent’s own ToolDefs, and the pre-connection client config carries the same hints (toolUiMap, tool-ui.ts), so the default client renders the same thing everywhere. Absent for a tool that declares none.


ToolSource = "files" | "builtin" | "mcp" | "roster" | "subagent"

Where a Toolset’s tools come from — for a log line, a collision message, and a spec. It decides nothing about execution; the entry does.


WithWorkflowsOptions = object

Input

What withWorkflows may be told.

optional warn?: (message) => void

Where a name-clash warning goes. Default console.warn, which under aai build and aai dev reaches the terminal; the studio passes its own.

string

void


WorkflowModules = Readonly<Record<string, unknown>>

path → module namespace, as both sources produce it: the static import list the CLI generates, and a spec’s import.meta.glob (eager).


WorkflowRegistry = Readonly<Record<string, WorkflowDef>>

A checked set of workflows, keyed by the name they are started under.

const HOST_ONLY_AGENT_FIELDS: readonly ["tools", "syncState", "workflows", "roster", "toolsets", "dialogs", "events", "inputGuardrails", "outputGuardrails", "sessionContext", "onSessionEnd", "routes"]

AgentDef fields that must never cross the serialization boundary — the single deny-list toAgentConfig strips. Everything else on the agent definition flows into AgentConfig by default, so a new serializable field works CLI → server → runtime without touching a mapper. A field added to AgentDef must appear either in AgentConfigSchema or here — the type-level guard in the internal-types test enforces that subtraction.

It cannot catch a SUPERFLUOUS entry, which is the other direction and the one that went stale: state sat here after AgentDef.state was deleted with the ctx.state bag, denying a key nothing produces and telling every reader the bag still exists. An entry here is a claim that AgentDef has that field.

Re-exports ToolCompletionMessage


Re-exports ToolDelayedMessage


Re-exports ToolMessageCondition


Re-exports ToolMessages


Re-exports ToolMessagesInput


Re-exports ToolStartMessage