@databricks/appkit 0.63.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/core/agent/agent-dirs.js +14 -0
  4. package/dist/core/agent/agent-dirs.js.map +1 -0
  5. package/dist/core/agent/create-agent.d.ts +5 -7
  6. package/dist/core/agent/create-agent.d.ts.map +1 -1
  7. package/dist/core/agent/create-agent.js +28 -7
  8. package/dist/core/agent/create-agent.js.map +1 -1
  9. package/dist/core/agent/load-agents.d.ts.map +1 -1
  10. package/dist/core/agent/load-agents.js +4 -4
  11. package/dist/core/agent/load-agents.js.map +1 -1
  12. package/dist/core/agent/load-code-agents.js +115 -0
  13. package/dist/core/agent/load-code-agents.js.map +1 -0
  14. package/dist/core/agent/types.d.ts +20 -6
  15. package/dist/core/agent/types.d.ts.map +1 -1
  16. package/dist/core/agent/types.js.map +1 -1
  17. package/dist/plugins/agents/agents.d.ts +35 -4
  18. package/dist/plugins/agents/agents.d.ts.map +1 -1
  19. package/dist/plugins/agents/agents.js +144 -38
  20. package/dist/plugins/agents/agents.js.map +1 -1
  21. package/dist/plugins/agents/tool-approval-gate.js.map +1 -1
  22. package/dist/shared/src/agent.d.ts +1 -1
  23. package/dist/tsdown/index.d.ts +58 -0
  24. package/dist/tsdown/index.d.ts.map +1 -0
  25. package/dist/tsdown/index.js +71 -0
  26. package/dist/tsdown/index.js.map +1 -0
  27. package/docs/api/appkit/Function.createAgent.md +1 -3
  28. package/docs/api/appkit/Interface.AgentDefinition.md +12 -1
  29. package/docs/api/appkit/Interface.AgentsPluginConfig.md +6 -15
  30. package/docs/api/appkit/TypeAlias.AgentEvent.md +1 -1
  31. package/docs/api/appkit/Variable.agents.md +1 -1
  32. package/docs/api/appkit.md +46 -46
  33. package/docs/plugins/agents.md +85 -21
  34. package/docs/plugins/testing.md +2 -2
  35. package/llms.txt +2 -2
  36. package/package.json +5 -1
  37. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/core/agent/types.ts"],"sourcesContent":["import type {\n AgentAdapter,\n AgentToolDefinition,\n BasePluginConfig,\n ThreadStore,\n ToolAnnotations,\n} from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type { McpHostPolicyConfig } from \"../../connectors/mcp\";\nimport type { FunctionTool } from \"./tools/function-tool\";\nimport type { HostedTool } from \"./tools/hosted-tools\";\n\n/**\n * A tool reference produced by a plugin's `.toolkit()` call. The agents plugin\n * recognizes the `__toolkitRef` brand and dispatches tool invocations through\n * `PluginContext.executeTool(req, pluginName, localName, ...)`, preserving\n * OBO (asUser) and telemetry spans.\n */\nexport interface ToolkitEntry {\n readonly __toolkitRef: true;\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n annotations?: ToolAnnotations;\n /**\n * Whether this tool is eligible for `autoInheritTools` spreading. Mirrors\n * {@link ToolEntry.autoInheritable} from the source registry so the agents\n * plugin can filter auto-inherited tools without re-walking the provider's\n * internal registry.\n */\n autoInheritable?: boolean;\n}\n\n/**\n * Any tool an agent can invoke: inline function tools (`tool()`), hosted MCP\n * tools (`mcpServer()` / raw hosted), toolkit references from plugins\n * (`analytics().toolkit()`), or adapter-hosted Supervisor-API tools\n * (`supervisorTools.*`).\n */\nexport type AgentTool =\n | FunctionTool\n | HostedTool\n | ToolkitEntry\n | import(\"../../agents/supervisor-api\").HostedSupervisorTool;\n\nexport interface ToolkitOptions {\n /** Key prefix to prepend to each tool's local name. Defaults to `${pluginName}.`. */\n prefix?: string;\n /** Only include tools whose local name matches one of these. */\n only?: string[];\n /** Exclude tools whose local name matches one of these. */\n except?: string[];\n /** Remap specific local names to different keys (applied after prefix). */\n rename?: Record<string, string>;\n}\n\n/**\n * Minimum shape every entry in the {@link Plugins} map must expose. Core\n * plugins (analytics, files, genie, lakebase) implement this directly via\n * their `.toolkit()` method. The agents plugin and standalone `runAgent`\n * synthesize this shape for any registered plugin that doesn't implement\n * `.toolkit()` directly (falling back to `getAgentTools()` walking).\n */\nexport interface PluginToolkitProvider {\n toolkit(opts?: ToolkitOptions): Record<string, ToolkitEntry>;\n}\n\n/**\n * Plugin map passed to the function form of {@link AgentDefinition.tools}.\n * Each entry exposes a `.toolkit(opts?)` method that returns a record of\n * {@link ToolkitEntry} markers ready to be spread into a tool record.\n *\n * AppKit does not statically know which plugins the surrounding\n * `createApp` will register, so this is a plain string-keyed record.\n * Refer to plugins by the name used in `createApp({ plugins: [...] })`;\n * unknown names resolve to `undefined` at runtime.\n *\n * @example\n * ```ts\n * const support = createAgent({\n * instructions: \"...\",\n * tools(plugins) {\n * return {\n * get_weather: tool({ ... }),\n * ...plugins.analytics.toolkit(),\n * ...plugins.files.toolkit({ only: [\"uploads.read\"] }),\n * };\n * },\n * });\n * ```\n */\nexport type Plugins = Record<string, PluginToolkitProvider>;\n\n/**\n * Context passed to `baseSystemPrompt` callbacks.\n */\nexport interface PromptContext {\n agentName: string;\n pluginNames: string[];\n toolNames: string[];\n}\n\nexport type BaseSystemPromptOption =\n | false\n | string\n | ((ctx: PromptContext) => string);\n\n/**\n * Per-agent tool record. String keys map to inline tools, toolkit entries,\n * hosted tools, etc.\n */\nexport type AgentTools = Record<string, AgentTool>;\n\n/**\n * Function form of `AgentDefinition.tools`. Receives the typed\n * {@link Plugins} map and returns a tool record. Invoked exactly once at\n * setup (or once per `runAgent` call in standalone mode); the result is\n * cached as the agent's resolved tool record.\n *\n * Use the function form when an agent needs tools from registered plugins.\n * The bare object form is fine when an agent only uses inline tools.\n */\nexport type AgentToolsFn = (plugins: Plugins) => AgentTools;\n\nexport interface AgentDefinition {\n /**\n * Stable identifier for the agent. **Optional and informational** —\n * when the definition is registered via `agents: { foo: def }` (code) or\n * lives at `config/agents/<id>/agent.md` (markdown), the **registry key\n * always wins** and `name` is ignored. The agent will be reachable as\n * `foo` (or `<id>`) regardless of what this field contains.\n *\n * Set `name` when:\n * - Running standalone via `runAgent({ agent: def })`, where there is\n * no enclosing key. The runtime uses it for the agent's slot in\n * error messages and OTel spans.\n * - Building a definition that may be passed to either form and you\n * want a consistent fallback label.\n *\n * Setting `name` to a value that differs from the registry key is\n * harmless but confusing — prefer keeping them aligned or omitting `name`\n * entirely.\n */\n name?: string;\n /** System prompt body. For markdown-loaded agents this is the file body. */\n instructions: string;\n /**\n * Model adapter (or endpoint-name string sugar for\n * `DatabricksAdapter.fromServingEndpoint({ endpointName })`). Optional —\n * falls back to the plugin's `defaultModel`.\n */\n model?: AgentAdapter | Promise<AgentAdapter> | string;\n /**\n * Per-agent tool record. Key is the LLM-visible tool-call name.\n *\n * Accepts either a plain record (for agents that only use inline tools)\n * or a function `(plugins) => Record<string, AgentTool>` that receives\n * the typed {@link Plugins} map and returns a tool record (for agents\n * that pull tools from registered plugins).\n *\n * The function is invoked once at agent setup; the result is cached.\n * Don't put per-request logic in there.\n */\n tools?: AgentTools | AgentToolsFn;\n /** Sub-agents, exposed as `agent-<key>` tools on this agent. */\n agents?: Record<string, AgentDefinition>;\n /** Override the plugin's baseSystemPrompt for this agent only. */\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional generation parameters (`temperature`, `top_p`, `stop`,\n * `frequency_penalty`, `presence_penalty`) forwarded to the OpenAI-compatible\n * serving request body. Only set keys are sent. Applied only when AppKit\n * builds the adapter itself (string or omitted `model`); when you pass a\n * pre-built `AgentAdapter`, configure generation params on it directly.\n */\n generationParams?: GenerationParams;\n /**\n * When true, the thread used for a chat request against this agent is\n * deleted from `ThreadStore` after the stream completes (success or\n * failure). Use for stateless one-shot agents — e.g. autocomplete, where\n * each request is independent and retaining history would both poison\n * future calls and accumulate unbounded state in the default\n * `InMemoryThreadStore`. Defaults to `false`.\n */\n ephemeral?: boolean;\n}\n\n/**\n * Auto-inherit configuration. When enabled for a given agent origin, agents\n * with no explicit `tools:` declaration receive every registered ToolProvider\n * plugin tool whose author marked `autoInheritable: true`. Tools without that\n * flag — destructive, state-mutating, or privilege-sensitive — never spread\n * automatically and must be wired via `tools:` (object or function form in\n * code, `plugin:NAME` entries in markdown frontmatter).\n *\n * Defaults are `false` for both origins (safe-by-default): developers must\n * consciously opt an origin in to any auto-inherit behaviour.\n */\nexport interface AutoInheritToolsConfig {\n /** Default for agents loaded from markdown files. Default: `false`. */\n file?: boolean;\n /** Default for code-defined agents (via `agents: { foo: createAgent(...) }`). Default: `false`. */\n code?: boolean;\n}\n\nexport interface AgentsPluginConfig extends BasePluginConfig {\n /** Directory of agent packages (`<id>/agent.md` each). Default `./config/agents`. Set to `false` to disable. */\n dir?: string | false;\n /** Code-defined agents, merged with file-loaded ones (code wins on key collision). */\n agents?: Record<string, AgentDefinition>;\n /** Agent used when clients don't specify one. Defaults to the first-registered agent or the file with `default: true` frontmatter. */\n defaultAgent?: string;\n /** Default model for agents that don't specify their own (in code or frontmatter). */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library. Keys may be referenced by markdown frontmatter via `tools: [key1, key2]`. */\n tools?: Record<string, AgentTool>;\n /** Whether to auto-inherit every ToolProvider plugin's toolkit. Accepts a boolean shorthand. */\n autoInheritTools?: boolean | AutoInheritToolsConfig;\n /** Persistent thread store. Default: in-memory. */\n threadStore?: ThreadStore;\n /** Customize or disable the AppKit base system prompt. */\n baseSystemPrompt?: BaseSystemPromptOption;\n /**\n * MCP server host policy. By default only same-origin Databricks workspace\n * URLs may be used as MCP endpoints; custom hosts must be explicitly\n * allowlisted here. Workspace credentials (SP / OBO) are never forwarded\n * to non-workspace hosts.\n */\n mcp?: McpHostPolicyConfig;\n /**\n * Human-in-the-loop approval gate for mutating tool calls. When enabled\n * (the default), the agents plugin emits an `appkit.approval_pending` SSE\n * event before executing any tool whose annotation flags it as mutating —\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean — and waits for a `POST /chat/approve`\n * decision from the same user who initiated the stream. A missing decision\n * after `timeoutMs` auto-denies the call.\n */\n approval?: {\n /**\n * Require human approval for tools that mutate state. Triggered by\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean. Default: `true`.\n */\n requireForDestructive?: boolean;\n /** Milliseconds to wait before auto-denying. Default: 60_000. */\n timeoutMs?: number;\n };\n /**\n * Runtime resource limits applied during agent execution. Defaults are\n * tuned to protect a single-instance deployment from a misbehaving user or\n * a runaway prompt injection; tighten or relax as appropriate for the\n * deployment's scale and trust model. Request-body caps (chat message\n * size, invocations input size / length) are enforced statically by the\n * Zod schemas and are not configurable here.\n */\n limits?: {\n /**\n * Max concurrent chat streams a single user may have open. Subsequent\n * `POST /chat` requests from that user while at-limit are rejected with\n * HTTP 429. Default: `5`.\n */\n maxConcurrentStreamsPerUser?: number;\n /**\n * Max tool invocations per agent run (across the full tool-call graph,\n * including sub-agent invocations). A run that exceeds the budget is\n * aborted with a terminal error event. Default: `50`.\n */\n maxToolCalls?: number;\n /**\n * Max sub-agent recursion depth. Protects against a prompt-injected\n * agent that delegates to a sub-agent which in turn delegates back to\n * itself (directly or transitively). Default: `3`.\n */\n maxSubAgentDepth?: number;\n /**\n * Per-call timeout for tools dispatched through `PluginContext`\n * (toolkit-routed tools — analytics SQL warehouse queries, Genie\n * messages, Lakebase queries). Independent of `maxToolCalls`: the\n * budget caps how many tools fire per run, this caps how long any\n * single tool call may run. The signal handed to plugin tool\n * implementations combines this timeout with the parent stream's\n * abort signal via `AbortSignal.any`. Function and MCP tools have\n * their own timeouts in their respective adapters and ignore this\n * setting. Default: `300_000` (5 minutes) — generous enough for cold\n * SQL Warehouse round-trips and long Genie conversations.\n */\n toolCallTimeoutMs?: number;\n };\n}\n\n/** Internal tool-index entry after a tool record has been resolved to a dispatchable form. */\nexport type ResolvedToolEntry =\n | {\n source: \"toolkit\";\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"function\";\n functionTool: FunctionTool;\n def: AgentToolDefinition;\n }\n | {\n source: \"mcp\";\n mcpToolName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"subagent\";\n agentName: string;\n def: AgentToolDefinition;\n }\n | {\n /**\n * Adapter-side hosted tool (executed by the model-host, not by the\n * Node process). Today: Supervisor API hosted tools (Genie spaces,\n * UC functions, etc.). The `spec` is opaque to the agents plugin —\n * it routes the entry into `AgentInput.extensions` for the adapter\n * that declared the matching `acceptsExtensions` key. `def` is a\n * synthetic placeholder kept so the index has a uniform shape; it\n * is intentionally NOT included in the `tools` array passed to\n * `adapter.run()` (those entries are not callable functions).\n */\n source: \"hosted-supervisor\";\n spec: import(\"../../agents/supervisor-api\").SupervisorTool;\n def: AgentToolDefinition;\n };\n\nexport interface RegisteredAgent {\n name: string;\n instructions: string;\n adapter: AgentAdapter;\n toolIndex: Map<string, ResolvedToolEntry>;\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /** Mirrors `AgentDefinition.generationParams`. */\n generationParams?: GenerationParams;\n /** Mirrors `AgentDefinition.ephemeral` — skip thread persistence. */\n ephemeral?: boolean;\n}\n\n/**\n * Type guard for `ToolkitEntry` — used by the agents plugin to differentiate\n * toolkit references from inline tools in a mixed `tools` record.\n */\nexport function isToolkitEntry(value: unknown): value is ToolkitEntry {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { __toolkitRef?: unknown }).__toolkitRef === true\n );\n}\n"],"mappings":";;;;;AA+VA,SAAgB,eAAe,OAAuC;AACpE,QACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAqC,iBAAiB"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/core/agent/types.ts"],"sourcesContent":["import type {\n AgentAdapter,\n AgentToolDefinition,\n BasePluginConfig,\n ThreadStore,\n ToolAnnotations,\n} from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type { McpHostPolicyConfig } from \"../../connectors/mcp\";\nimport type { FunctionTool } from \"./tools/function-tool\";\nimport type { HostedTool } from \"./tools/hosted-tools\";\n\n/**\n * A tool reference produced by a plugin's `.toolkit()` call. The agents plugin\n * recognizes the `__toolkitRef` brand and dispatches tool invocations through\n * `PluginContext.executeTool(req, pluginName, localName, ...)`, preserving\n * OBO (asUser) and telemetry spans.\n */\nexport interface ToolkitEntry {\n readonly __toolkitRef: true;\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n annotations?: ToolAnnotations;\n /**\n * Whether this tool is eligible for `autoInheritTools` spreading. Mirrors\n * {@link ToolEntry.autoInheritable} from the source registry so the agents\n * plugin can filter auto-inherited tools without re-walking the provider's\n * internal registry.\n */\n autoInheritable?: boolean;\n}\n\n/**\n * Any tool an agent can invoke: inline function tools (`tool()`), hosted MCP\n * tools (`mcpServer()` / raw hosted), toolkit references from plugins\n * (`analytics().toolkit()`), or adapter-hosted Supervisor-API tools\n * (`supervisorTools.*`).\n */\nexport type AgentTool =\n | FunctionTool\n | HostedTool\n | ToolkitEntry\n | import(\"../../agents/supervisor-api\").HostedSupervisorTool;\n\nexport interface ToolkitOptions {\n /** Key prefix to prepend to each tool's local name. Defaults to `${pluginName}.`. */\n prefix?: string;\n /** Only include tools whose local name matches one of these. */\n only?: string[];\n /** Exclude tools whose local name matches one of these. */\n except?: string[];\n /** Remap specific local names to different keys (applied after prefix). */\n rename?: Record<string, string>;\n}\n\n/**\n * Minimum shape every entry in the {@link Plugins} map must expose. Core\n * plugins (analytics, files, genie, lakebase) implement this directly via\n * their `.toolkit()` method. The agents plugin and standalone `runAgent`\n * synthesize this shape for any registered plugin that doesn't implement\n * `.toolkit()` directly (falling back to `getAgentTools()` walking).\n */\nexport interface PluginToolkitProvider {\n toolkit(opts?: ToolkitOptions): Record<string, ToolkitEntry>;\n}\n\n/**\n * Plugin map passed to the function form of {@link AgentDefinition.tools}.\n * Each entry exposes a `.toolkit(opts?)` method that returns a record of\n * {@link ToolkitEntry} markers ready to be spread into a tool record.\n *\n * AppKit does not statically know which plugins the surrounding\n * `createApp` will register, so this is a plain string-keyed record.\n * Refer to plugins by the name used in `createApp({ plugins: [...] })`;\n * unknown names resolve to `undefined` at runtime.\n *\n * @example\n * ```ts\n * const support = createAgent({\n * instructions: \"...\",\n * tools(plugins) {\n * return {\n * get_weather: tool({ ... }),\n * ...plugins.analytics.toolkit(),\n * ...plugins.files.toolkit({ only: [\"uploads.read\"] }),\n * };\n * },\n * });\n * ```\n */\nexport type Plugins = Record<string, PluginToolkitProvider>;\n\n/**\n * Context passed to `baseSystemPrompt` callbacks.\n */\nexport interface PromptContext {\n agentName: string;\n pluginNames: string[];\n toolNames: string[];\n}\n\nexport type BaseSystemPromptOption =\n | false\n | string\n | ((ctx: PromptContext) => string);\n\n/**\n * Per-agent tool record. String keys map to inline tools, toolkit entries,\n * hosted tools, etc.\n */\nexport type AgentTools = Record<string, AgentTool>;\n\n/**\n * Function form of `AgentDefinition.tools`. Receives the typed\n * {@link Plugins} map and returns a tool record. Invoked exactly once at\n * setup (or once per `runAgent` call in standalone mode); the result is\n * cached as the agent's resolved tool record.\n *\n * Use the function form when an agent needs tools from registered plugins.\n * The bare object form is fine when an agent only uses inline tools.\n */\nexport type AgentToolsFn = (plugins: Plugins) => AgentTools;\n\nexport interface AgentDefinition {\n /**\n * Stable identifier for the agent. **Optional and informational** —\n * when the definition is registered via `agents: { foo: def }` (code) or\n * lives at `server/agents/<id>/agent.md` (markdown), the **registry key\n * always wins** and `name` is ignored. The agent will be reachable as\n * `foo` (or `<id>`) regardless of what this field contains.\n *\n * Set `name` when:\n * - Running standalone via `runAgent({ agent: def })`, where there is\n * no enclosing key. The runtime uses it for the agent's slot in\n * error messages and OTel spans.\n * - Building a definition that may be passed to either form and you\n * want a consistent fallback label.\n *\n * Setting `name` to a value that differs from the registry key is\n * harmless but confusing — prefer keeping them aligned or omitting `name`\n * entirely.\n */\n name?: string;\n /**\n * Marks this agent as the default one chosen when a client doesn't name an\n * agent. Mirrors markdown frontmatter `default: true`. When several agents\n * set it, a code (discovered) agent wins over a markdown one, then the\n * lowest id; an explicit `agents({ defaultAgent })` always overrides it.\n * Defaults to `false`.\n */\n default?: boolean;\n /** System prompt body. For markdown-loaded agents this is the file body. */\n instructions: string;\n /**\n * Model adapter (or endpoint-name string sugar for\n * `DatabricksAdapter.fromServingEndpoint({ endpointName })`). Optional —\n * falls back to the plugin's `defaultModel`.\n */\n model?: AgentAdapter | Promise<AgentAdapter> | string;\n /**\n * Per-agent tool record. Key is the LLM-visible tool-call name.\n *\n * Accepts either a plain record (for agents that only use inline tools)\n * or a function `(plugins) => Record<string, AgentTool>` that receives\n * the typed {@link Plugins} map and returns a tool record (for agents\n * that pull tools from registered plugins).\n *\n * The function is invoked once at agent setup; the result is cached.\n * Don't put per-request logic in there.\n */\n tools?: AgentTools | AgentToolsFn;\n /** Sub-agents, exposed as `agent-<key>` tools on this agent. */\n agents?: Record<string, AgentDefinition>;\n /** Override the plugin's baseSystemPrompt for this agent only. */\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional generation parameters (`temperature`, `top_p`, `stop`,\n * `frequency_penalty`, `presence_penalty`) forwarded to the OpenAI-compatible\n * serving request body. Only set keys are sent. Applied only when AppKit\n * builds the adapter itself (string or omitted `model`); when you pass a\n * pre-built `AgentAdapter`, configure generation params on it directly.\n */\n generationParams?: GenerationParams;\n /**\n * When true, the thread used for a chat request against this agent is\n * deleted from `ThreadStore` after the stream completes (success or\n * failure). Use for stateless one-shot agents — e.g. autocomplete, where\n * each request is independent and retaining history would both poison\n * future calls and accumulate unbounded state in the default\n * `InMemoryThreadStore`. Defaults to `false`.\n */\n ephemeral?: boolean;\n}\n\n/**\n * Auto-inherit configuration. When enabled for a given agent origin, agents\n * with no explicit `tools:` declaration receive every registered ToolProvider\n * plugin tool whose author marked `autoInheritable: true`. Tools without that\n * flag — destructive, state-mutating, or privilege-sensitive — never spread\n * automatically and must be wired via `tools:` (object or function form in\n * code, `plugin:NAME` entries in markdown frontmatter).\n *\n * Defaults are `false` for both origins (safe-by-default): developers must\n * consciously opt an origin in to any auto-inherit behaviour.\n */\nexport interface AutoInheritToolsConfig {\n /** Default for agents loaded from markdown files. Default: `false`. */\n file?: boolean;\n /** Default for code-defined agents (via `agents: { foo: createAgent(...) }`). Default: `false`. */\n code?: boolean;\n}\n\nexport interface AgentsPluginConfig extends BasePluginConfig {\n /**\n * @deprecated Put each code agent in its own folder under\n * `server/agents/<id>/agent.ts` (`export default createAgent({ ... })`); it is\n * discovered automatically at startup and the call collapses to\n * `agents({ ... })` with no map. Still honored for backward compatibility\n * (emits a one-time deprecation warning) but will be removed in a future\n * minor. If both discovery and this map define the same id, discovery wins\n * and the map entry is ignored.\n */\n agents?: Record<string, AgentDefinition>;\n /** Agent used when clients don't specify one. Precedence: this value, else a code agent with `default: true`, else a markdown agent with `default: true`, else the first-registered agent. */\n defaultAgent?: string;\n /** Default model for agents that don't specify their own (in code or frontmatter). */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library. Keys may be referenced by markdown frontmatter via `tools: [key1, key2]`. */\n tools?: Record<string, AgentTool>;\n /** Whether to auto-inherit every ToolProvider plugin's toolkit. Accepts a boolean shorthand. */\n autoInheritTools?: boolean | AutoInheritToolsConfig;\n /** Persistent thread store. Default: in-memory. */\n threadStore?: ThreadStore;\n /** Customize or disable the AppKit base system prompt. */\n baseSystemPrompt?: BaseSystemPromptOption;\n /**\n * MCP server host policy. By default only same-origin Databricks workspace\n * URLs may be used as MCP endpoints; custom hosts must be explicitly\n * allowlisted here. Workspace credentials (SP / OBO) are never forwarded\n * to non-workspace hosts.\n */\n mcp?: McpHostPolicyConfig;\n /**\n * Human-in-the-loop approval gate for mutating tool calls. When enabled\n * (the default), the agents plugin emits an `appkit.approval_pending` SSE\n * event before executing any tool whose annotation flags it as mutating —\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean — and waits for a `POST /api/agents/approve`\n * decision from the same user who initiated the stream. A missing decision\n * after `timeoutMs` auto-denies the call.\n */\n approval?: {\n /**\n * Require human approval for tools that mutate state. Triggered by\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean. Default: `true`.\n */\n requireForDestructive?: boolean;\n /** Milliseconds to wait before auto-denying. Default: 60_000. */\n timeoutMs?: number;\n };\n /**\n * Runtime resource limits applied during agent execution. Defaults are\n * tuned to protect a single-instance deployment from a misbehaving user or\n * a runaway prompt injection; tighten or relax as appropriate for the\n * deployment's scale and trust model. Request-body caps (chat message\n * size, invocations input size / length) are enforced statically by the\n * Zod schemas and are not configurable here.\n */\n limits?: {\n /**\n * Max concurrent chat streams a single user may have open. Subsequent\n * `POST /chat` requests from that user while at-limit are rejected with\n * HTTP 429. Default: `5`.\n */\n maxConcurrentStreamsPerUser?: number;\n /**\n * Max tool invocations per agent run (across the full tool-call graph,\n * including sub-agent invocations). A run that exceeds the budget is\n * aborted with a terminal error event. Default: `50`.\n */\n maxToolCalls?: number;\n /**\n * Max sub-agent recursion depth. Protects against a prompt-injected\n * agent that delegates to a sub-agent which in turn delegates back to\n * itself (directly or transitively). Default: `3`.\n */\n maxSubAgentDepth?: number;\n /**\n * Per-call timeout for tools dispatched through `PluginContext`\n * (toolkit-routed tools — analytics SQL warehouse queries, Genie\n * messages, Lakebase queries). Independent of `maxToolCalls`: the\n * budget caps how many tools fire per run, this caps how long any\n * single tool call may run. The signal handed to plugin tool\n * implementations combines this timeout with the parent stream's\n * abort signal via `AbortSignal.any`. Function and MCP tools have\n * their own timeouts in their respective adapters and ignore this\n * setting. Default: `300_000` (5 minutes) — generous enough for cold\n * SQL Warehouse round-trips and long Genie conversations.\n */\n toolCallTimeoutMs?: number;\n };\n}\n\n/** Internal tool-index entry after a tool record has been resolved to a dispatchable form. */\nexport type ResolvedToolEntry =\n | {\n source: \"toolkit\";\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"function\";\n functionTool: FunctionTool;\n def: AgentToolDefinition;\n }\n | {\n source: \"mcp\";\n mcpToolName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"subagent\";\n agentName: string;\n def: AgentToolDefinition;\n }\n | {\n /**\n * Adapter-side hosted tool (executed by the model-host, not by the\n * Node process). Today: Supervisor API hosted tools (Genie spaces,\n * UC functions, etc.). The `spec` is opaque to the agents plugin —\n * it routes the entry into `AgentInput.extensions` for the adapter\n * that declared the matching `acceptsExtensions` key. `def` is a\n * synthetic placeholder kept so the index has a uniform shape; it\n * is intentionally NOT included in the `tools` array passed to\n * `adapter.run()` (those entries are not callable functions).\n */\n source: \"hosted-supervisor\";\n spec: import(\"../../agents/supervisor-api\").SupervisorTool;\n def: AgentToolDefinition;\n };\n\nexport interface RegisteredAgent {\n name: string;\n instructions: string;\n adapter: AgentAdapter;\n toolIndex: Map<string, ResolvedToolEntry>;\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /** Mirrors `AgentDefinition.generationParams`. */\n generationParams?: GenerationParams;\n /** Mirrors `AgentDefinition.ephemeral` — skip thread persistence. */\n ephemeral?: boolean;\n}\n\n/**\n * Type guard for `ToolkitEntry` — used by the agents plugin to differentiate\n * toolkit references from inline tools in a mixed `tools` record.\n */\nexport function isToolkitEntry(value: unknown): value is ToolkitEntry {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { __toolkitRef?: unknown }).__toolkitRef === true\n );\n}\n"],"mappings":";;;;;AA6WA,SAAgB,eAAe,OAAuC;AACpE,QACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAqC,iBAAiB"}
@@ -25,6 +25,10 @@ declare class AgentsPlugin extends Plugin implements ToolProvider {
25
25
  private mcpClient;
26
26
  private threadStore;
27
27
  private approvalGate;
28
+ /** Guards the `agents({ agents })` deprecation warning to once per instance. */
29
+ private agentsMapDeprecationWarned;
30
+ /** Guards the `config/agents` deprecation warning to once per instance. */
31
+ private configAgentsDeprecationWarned;
28
32
  constructor(config: AgentsPluginConfig);
29
33
  /**
30
34
  * Effective approval policy with defaults applied. Memoised so the
@@ -69,7 +73,32 @@ declare class AgentsPlugin extends Plugin implements ToolProvider {
69
73
  * registry once this resolves successfully (atomic reload).
70
74
  */
71
75
  private buildAgentRegistry;
76
+ /**
77
+ * Resolves the default agent. Precedence: explicit `config.defaultAgent` >
78
+ * a code/discovered agent flagged `default: true` (stable id order) >
79
+ * markdown `default: true` > first registered (insertion order).
80
+ */
81
+ private resolveDefaultAgent;
82
+ /**
83
+ * Emits the one-time deprecation warning for the `agents({ agents })` map.
84
+ * Guarded so `reload()` (which re-runs `buildAgentRegistry`) doesn't spam it.
85
+ */
86
+ private warnAgentsMapDeprecated;
87
+ /**
88
+ * One-time deprecation warning for markdown agents still living in
89
+ * `config/agents/`. Guarded so `reload()` doesn't spam it.
90
+ */
91
+ private warnConfigAgentsDeprecated;
72
92
  private resolvedAgentsDir;
93
+ /**
94
+ * Discovers code agents (see {@link resolveCodeAgentsDir} and
95
+ * {@link loadCodeAgentsFromDir}). Warns if sources exist but nothing was
96
+ * discovered — usually the build didn't emit the compiled agents — unless
97
+ * the deprecated `agents({ agents })` map is carrying them instead.
98
+ */
99
+ private loadCodeAgents;
100
+ /** True when `dir` holds at least one `<id>/agent.ts` folder. */
101
+ private hasCodeAgentSources;
73
102
  private loadFileDefinitions;
74
103
  /**
75
104
  * Builds the map of plugin-name → toolkit that the markdown loader consults
@@ -219,10 +248,12 @@ declare class AgentsPlugin extends Plugin implements ToolProvider {
219
248
  private registerCodeAgent;
220
249
  }
221
250
  /**
222
- * Plugin factory for the agents plugin. Reads `config/agents/*.md` by default,
223
- * resolves toolkits/tools from registered plugins, exposes `appkit.agents.*`
224
- * runtime API and mounts `POST /invocations` and `POST /responses` (aliased
225
- * non-streaming invoke endpoints) plus `POST /chat` (streaming, HITL-capable).
251
+ * Plugin factory for the agents plugin. Discovers agents from
252
+ * `server/agents/<id>/agent.{ts,md}` by default (markdown still in
253
+ * `config/agents/` is read as a deprecated fallback), resolves toolkits/tools
254
+ * from registered plugins, exposes the `appkit.agents.*` runtime API and mounts
255
+ * `POST /invocations` and `POST /responses` (aliased non-streaming invoke
256
+ * endpoints) plus `POST /chat` (streaming, HITL-capable).
226
257
  *
227
258
  * @example
228
259
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","names":[],"sources":["../../../src/plugins/agents/agents.ts"],"mappings":";;;;;;;;;;cA+Ia,YAAA,SAAqB,MAAA,YAAkB,YAAA;EAAA,OAK3C,QAAA,EAAkC,cAAA;EAAA,OAClC,KAAA,EAAO,WAAA;EAAA,UAEI,MAAA,EAAQ,kBAAA;EAAA,QAElB,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,aAAA;EAZgB;;;;;;EAAA,QAsBhB,gBAAA;EAAA,QACA,SAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;cAEI,MAAA,EAAQ,kBAAA;EA+rBJ;;;;;;;;;;EAAA,QAhqBR,oBAAA;EAAA,YAKI,sBAAA,CAAA;EA/DoB;EAAA,YAyFpB,cAAA,CAAA;EApFL;EAAA,QAuGC,gBAAA;EAtGD;;;;;;EAAA,QAgHC,WAAA;EAhGA;;;;;EAAA,QAiHA,aAAA;EAYF,KAAA,CAAA,GAAK,OAAA;EAzFH;;;;;;EAwGF,MAAA,CAAA,GAAU,OAAA;EAfL;;;;;EAAA,QAyCG,kBAAA;EAAA,QAmEN,iBAAA;EAAA,QAMM,mBAAA;EAoEA;;;;;;EAAA,QA1CN,mBAAA;EAAA,QAmBM,oBAAA;EAAA,QAuBA,cAAA;EAyUY;;;;;EAAA,QA7RZ,cAAA;EAqWE;;;;;;;;;;;EAAA,QArPR,eAAA;EAynCM;;;;;;;;;;;;EAAA,QA7lCN,eAAA;EAAA,QAkBM,gBAAA;EAAA,QA0DA,kBAAA;EAiEd,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CAAA,GAAoB,OAAA;;;;;;;UAYlB,iBAAA;EAUR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAkDrB,YAAA,CAAA,GAAgB,MAAA;EAAA,QAOF,WAAA;;;;;;;;;;;;UA6EN,gCAAA;;;;;;;;;;;UAuBM,aAAA;EAAA,QA2FA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;UA2NA,qBAAA;;;;;;;;;;;;UA0KA,gBAAA;;;;;;;;;;;;;;;UAmIA,WAAA;EAAA,QAiGA,aAAA;EAAA,QA2BA,cAAA;EAAA,QAuCA,kBAAA;EAAA,QASA,gBAAA;EAAA,QAUA,mBAAA;EAAA,QAaN,YAAA;EAAA,QASA,aAAA;EAgBF,QAAA,CAAA,GAAY,OAAA;EAQlB,OAAA,CAAA;6BAE2B,GAAA,EAAO,eAAA,KAAe,OAAA;;2BAG3B,eAAA;;;oCAGS,OAAA,CAAA,MAAA;EAAA;EAAA,QAIjB,iBAAA;AAAA;;;;;;;;;;;;;;;;cAsJH,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,kBAAA"}
1
+ {"version":3,"file":"agents.d.ts","names":[],"sources":["../../../src/plugins/agents/agents.ts"],"mappings":";;;;;;;;;;cAsJa,YAAA,SAAqB,MAAA,YAAkB,YAAA;EAAA,OAK3C,QAAA,EAAkC,cAAA;EAAA,OAClC,KAAA,EAAO,WAAA;EAAA,UAEI,MAAA,EAAQ,kBAAA;EAAA,QAElB,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,aAAA;EAZgB;;;;;;EAAA,QAsBhB,gBAAA;EAAA,QACA,SAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAqzBa;EAAA,QAnzBb,0BAAA;EA+wDU;EAAA,QA7wDV,6BAAA;cAEI,MAAA,EAAQ,kBAAA;EAwxDE;;;;;;;;;;EAAA,QAzvDd,oBAAA;EAAA,YAKI,sBAAA,CAAA;EA7DL;EAAA,YAuFK,cAAA,CAAA;EArFM;EAAA,QAwGV,gBAAA;EAtGA;;;;;;EAAA,QAgHA,WAAA;EA/FA;;;;;EAAA,QAgHA,aAAA;EAYF,KAAA,CAAA,GAAK,OAAA;EA1DC;;;;;;EAyEN,MAAA,CAAA,GAAU,OAAA;EAAA;;;;;EAAA,QA0BF,kBAAA;EAsKA;;;;;EAAA,QAjEN,mBAAA;EAsPM;;;;EAAA,QAxNN,uBAAA;EAifR;;;;EAAA,QAleQ,0BAAA;EAAA,QAUA,iBAAA;EAkfa;;;;;;EAAA,QAxeP,cAAA;EAguBA;EAAA,QAnsBN,mBAAA;EAAA,QAeM,mBAAA;EA4rCA;;;;;;EAAA,QAzoCN,mBAAA;EAAA,QAmBM,oBAAA;EAAA,QAuBA,cAAA;EA2zCI;;;;;EAAA,QA/wCJ,cAAA;EAyxCmC;;;;;;;;;;;EAAA,QAzqCzC,eAAA;EAmrCM;;;AAwJhB;;;;;;;;;EAxJgB,QAvpCN,eAAA;EAAA,QAkBM,gBAAA;EAAA,QA0DA,kBAAA;EAiEd,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CAAA,GAAoB,OAAA;;;;;;;UAYlB,iBAAA;EAUR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAkDrB,YAAA,CAAA,GAAgB,MAAA;EAAA,QAOF,WAAA;;;;;;;;;;;;UA6EN,gCAAA;;;;;;;;;;;UAuBM,aAAA;EAAA,QA2FA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;UA2NA,qBAAA;;;;;;;;;;;;UA0KA,gBAAA;;;;;;;;;;;;;;;UAmIA,WAAA;EAAA,QAiGA,aAAA;EAAA,QA2BA,cAAA;EAAA,QAuCA,kBAAA;EAAA,QASA,gBAAA;EAAA,QAUA,mBAAA;EAAA,QAaN,YAAA;EAAA,QASA,aAAA;EAgBF,QAAA,CAAA,GAAY,OAAA;EAQlB,OAAA,CAAA;6BAE2B,GAAA,EAAO,eAAA,KAAe,OAAA;;2BAG3B,eAAA;;;oCAGS,OAAA,CAAA,MAAA;EAAA;EAAA,QAIjB,iBAAA;AAAA;;;;;;;;;;;;;;;;;;cAwJH,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,kBAAA"}
@@ -14,6 +14,7 @@ import { isHostedTool, resolveHostedTools } from "../../core/agent/tools/hosted-
14
14
  import { isToolkitEntry } from "../../core/agent/types.js";
15
15
  import "../../core/agent/tools/index.js";
16
16
  import { loadAgentsFromDir } from "../../core/agent/load-agents.js";
17
+ import { CODE_AGENTS_SOURCE_DIR, loadCodeAgentsFromDir, resolveCodeAgentsDir } from "../../core/agent/load-code-agents.js";
17
18
  import { normalizeToolResult } from "../../core/agent/normalize-result.js";
18
19
  import { buildBaseSystemPrompt, composeSystemPrompt } from "../../core/agent/system-prompt.js";
19
20
  import { agentStreamDefaults } from "./defaults.js";
@@ -27,10 +28,12 @@ import { ToolApprovalGate } from "./tool-approval-gate.js";
27
28
  import { randomUUID } from "node:crypto";
28
29
  import pc from "picocolors";
29
30
  import path from "node:path";
31
+ import { existsSync, readdirSync } from "node:fs";
30
32
 
31
33
  //#region src/plugins/agents/agents.ts
32
34
  const logger = createLogger("agents");
33
- const DEFAULT_AGENTS_DIR = "./config/agents";
35
+ /** Deprecated markdown location, read as a fallback with a one-time warning. */
36
+ const LEGACY_MARKDOWN_DIR = "config/agents";
34
37
  /**
35
38
  * Decide whether a tool call must traverse the approval gate. Honours both
36
39
  * the modern `effect` field (mutating values: write / update / destructive)
@@ -70,6 +73,10 @@ var AgentsPlugin = class extends Plugin {
70
73
  mcpClient = null;
71
74
  threadStore;
72
75
  approvalGate = new ToolApprovalGate();
76
+ /** Guards the `agents({ agents })` deprecation warning to once per instance. */
77
+ agentsMapDeprecationWarned = false;
78
+ /** Guards the `config/agents` deprecation warning to once per instance. */
79
+ configAgentsDeprecationWarned = false;
73
80
  constructor(config) {
74
81
  super(config);
75
82
  this.config = config;
@@ -172,62 +179,159 @@ var AgentsPlugin = class extends Plugin {
172
179
  * registry once this resolves successfully (atomic reload).
173
180
  */
174
181
  async buildAgentRegistry() {
175
- const { defs: fileDefs, defaultAgent: fileDefault } = await this.loadFileDefinitions();
176
- const codeDefs = this.config.agents ?? {};
177
- for (const name of Object.keys(fileDefs)) if (codeDefs[name]) logger.warn("Agent '%s' defined in both code and a markdown file. Code definition takes precedence.", name);
182
+ const discovered = await this.loadCodeAgents();
183
+ const deprecatedMapRaw = this.config.agents ?? {};
184
+ if (Object.keys(deprecatedMapRaw).length > 0) this.warnAgentsMapDeprecated();
185
+ const deprecatedMap = {};
186
+ for (const [id, def] of Object.entries(deprecatedMapRaw)) {
187
+ if (discovered[id]) {
188
+ logger.warn("Agent '%s' is both discovered in %s and passed to agents({ agents }). Using the discovered file; ignoring the map entry.", id, CODE_AGENTS_SOURCE_DIR);
189
+ continue;
190
+ }
191
+ deprecatedMap[id] = def;
192
+ }
193
+ const codeAgents = {
194
+ ...discovered,
195
+ ...deprecatedMap
196
+ };
197
+ const { defs: fileDefs, defaultAgent: fileDefault } = await this.loadFileDefinitions(codeAgents);
178
198
  const merged = {};
179
199
  for (const [name, def] of Object.entries(fileDefs)) merged[name] = {
180
200
  def,
181
201
  src: { origin: "file" }
182
202
  };
183
- for (const [name, def] of Object.entries(codeDefs)) merged[name] = {
184
- def,
185
- src: { origin: "code" }
186
- };
203
+ for (const [name, def] of Object.entries(discovered)) {
204
+ if (merged[name]?.src.origin === "file") throw new Error(`Agent '${name}' is defined as both a code agent (agent.ts) and a markdown agent (agent.md) — in one folder, or across server/agents and the deprecated config/agents fallback. Keep a single kind per id. Available: ${Object.keys(merged).sort().join(", ")}`);
205
+ merged[name] = {
206
+ def,
207
+ src: { origin: "code" }
208
+ };
209
+ }
210
+ for (const [name, def] of Object.entries(deprecatedMap)) {
211
+ if (merged[name]?.src.origin === "file") logger.warn("Agent '%s' defined in both code and a markdown file. Code definition takes precedence.", name);
212
+ merged[name] = {
213
+ def,
214
+ src: { origin: "code" }
215
+ };
216
+ }
187
217
  const agents = /* @__PURE__ */ new Map();
188
- let defaultAgentName = null;
189
218
  if (Object.keys(merged).length === 0) {
190
- logger.info("No agents registered (no files in %s, no code-defined agents)", this.resolvedAgentsDir() ?? "<disabled>");
219
+ logger.info("No agents registered (no files in %s, no discovered or code-defined agents)", this.resolvedAgentsDir());
191
220
  return {
192
221
  agents,
193
- defaultAgentName
222
+ defaultAgentName: null
194
223
  };
195
224
  }
196
225
  for (const [name, { def, src }] of Object.entries(merged)) try {
197
- const registered = await this.buildRegisteredAgent(name, def, src);
198
- agents.set(name, registered);
199
- if (!defaultAgentName) defaultAgentName = name;
226
+ agents.set(name, await this.buildRegisteredAgent(name, def, src));
200
227
  } catch (err) {
201
228
  throw new Error(`Failed to register agent '${name}' (${src.origin}): ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
202
229
  }
203
- if (this.config.defaultAgent) {
204
- if (!agents.has(this.config.defaultAgent)) throw new Error(`defaultAgent '${this.config.defaultAgent}' is not registered. Available: ${Array.from(agents.keys()).join(", ")}`);
205
- defaultAgentName = this.config.defaultAgent;
206
- } else if (fileDefault && agents.has(fileDefault)) defaultAgentName = fileDefault;
207
230
  return {
208
231
  agents,
209
- defaultAgentName
232
+ defaultAgentName: this.resolveDefaultAgent(agents, merged, fileDefault)
210
233
  };
211
234
  }
235
+ /**
236
+ * Resolves the default agent. Precedence: explicit `config.defaultAgent` >
237
+ * a code/discovered agent flagged `default: true` (stable id order) >
238
+ * markdown `default: true` > first registered (insertion order).
239
+ */
240
+ resolveDefaultAgent(agents, merged, fileDefault) {
241
+ if (this.config.defaultAgent) {
242
+ if (!agents.has(this.config.defaultAgent)) throw new Error(`defaultAgent '${this.config.defaultAgent}' is not registered. Available: ${Array.from(agents.keys()).join(", ")}`);
243
+ return this.config.defaultAgent;
244
+ }
245
+ const codeDefault = Object.keys(merged).filter((id) => merged[id].src.origin === "code" && merged[id].def.default).sort()[0];
246
+ if (codeDefault) return codeDefault;
247
+ if (fileDefault && agents.has(fileDefault)) return fileDefault;
248
+ return agents.keys().next().value ?? null;
249
+ }
250
+ /**
251
+ * Emits the one-time deprecation warning for the `agents({ agents })` map.
252
+ * Guarded so `reload()` (which re-runs `buildAgentRegistry`) doesn't spam it.
253
+ */
254
+ warnAgentsMapDeprecated() {
255
+ if (this.agentsMapDeprecationWarned) return;
256
+ this.agentsMapDeprecationWarned = true;
257
+ logger.warn("agents({ agents: { ... } }) is deprecated. Put each code agent in its own folder under server/agents/<id>/agent.ts (export default createAgent({ ... })) and it is discovered automatically — the call collapses to agents({ ... }) with no agent map. The `agents` field still works but will be removed in a future minor. See docs/plugins/agents.md.");
258
+ }
259
+ /**
260
+ * One-time deprecation warning for markdown agents still living in
261
+ * `config/agents/`. Guarded so `reload()` doesn't spam it.
262
+ */
263
+ warnConfigAgentsDeprecated() {
264
+ if (this.configAgentsDeprecationWarned) return;
265
+ this.configAgentsDeprecationWarned = true;
266
+ logger.warn("Markdown agents under config/agents/ are deprecated. Move each config/agents/<id>/agent.md to server/agents/<id>/agent.md — every agent now lives in one place (server/agents). config/agents is still read for now but will be removed in a future minor. See docs/plugins/agents.md.");
267
+ }
212
268
  resolvedAgentsDir() {
213
- if (this.config.dir === false) return null;
214
- const dir = this.config.dir ?? DEFAULT_AGENTS_DIR;
215
- return path.isAbsolute(dir) ? dir : path.resolve(process.cwd(), dir);
269
+ return path.resolve(process.cwd(), CODE_AGENTS_SOURCE_DIR);
216
270
  }
217
- async loadFileDefinitions() {
218
- const dir = this.resolvedAgentsDir();
219
- if (!dir) return {
220
- defs: {},
221
- defaultAgent: null
222
- };
223
- const pluginToolProviders = this.pluginProviderIndex();
224
- const ambient = this.config.tools ?? {};
225
- return await loadAgentsFromDir(dir, {
271
+ /**
272
+ * Discovers code agents (see {@link resolveCodeAgentsDir} and
273
+ * {@link loadCodeAgentsFromDir}). Warns if sources exist but nothing was
274
+ * discovered — usually the build didn't emit the compiled agents — unless
275
+ * the deprecated `agents({ agents })` map is carrying them instead.
276
+ */
277
+ async loadCodeAgents() {
278
+ const resolved = resolveCodeAgentsDir({
279
+ cwd: process.cwd(),
280
+ exists: existsSync
281
+ });
282
+ const discovered = await loadCodeAgentsFromDir(resolved.dir, { extensions: resolved.extensions });
283
+ const usingDeprecatedMap = Object.keys(this.config.agents ?? {}).length > 0;
284
+ const sourceDir = this.resolvedAgentsDir();
285
+ if (Object.keys(discovered).length === 0 && !usingDeprecatedMap && this.hasCodeAgentSources(sourceDir)) logger.warn("Found code-agent sources in %s but discovered no code agents (scanned %s). In a production build, ensure `<dir>/*/agent.ts` is included as tsdown entries so the compiled agents are emitted.", sourceDir, resolved.dir);
286
+ return discovered;
287
+ }
288
+ /** True when `dir` holds at least one `<id>/agent.ts` folder. */
289
+ hasCodeAgentSources(dir) {
290
+ try {
291
+ return readdirSync(dir, { withFileTypes: true }).some((e) => {
292
+ if (!e.isDirectory() && !e.isSymbolicLink()) return false;
293
+ try {
294
+ return readdirSync(path.join(dir, e.name)).includes("agent.ts");
295
+ } catch {
296
+ return false;
297
+ }
298
+ });
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+ async loadFileDefinitions(codeAgents) {
304
+ const primaryDir = this.resolvedAgentsDir();
305
+ const baseCtx = {
226
306
  defaultModel: this.config.defaultModel,
227
- availableTools: ambient,
228
- plugins: pluginToolProviders,
229
- codeAgents: this.config.agents
307
+ availableTools: this.config.tools ?? {},
308
+ plugins: this.pluginProviderIndex()
309
+ };
310
+ const legacyDir = path.resolve(process.cwd(), LEGACY_MARKDOWN_DIR);
311
+ if (!existsSync(legacyDir)) return loadAgentsFromDir(primaryDir, {
312
+ ...baseCtx,
313
+ codeAgents
314
+ });
315
+ const legacy = await loadAgentsFromDir(legacyDir, {
316
+ ...baseCtx,
317
+ codeAgents
230
318
  });
319
+ const primary = await loadAgentsFromDir(primaryDir, {
320
+ ...baseCtx,
321
+ codeAgents: {
322
+ ...legacy.defs,
323
+ ...codeAgents
324
+ }
325
+ });
326
+ if (Object.keys(legacy.defs).length === 0) return primary;
327
+ this.warnConfigAgentsDeprecated();
328
+ return {
329
+ defs: {
330
+ ...legacy.defs,
331
+ ...primary.defs
332
+ },
333
+ defaultAgent: primary.defaultAgent ?? legacy.defaultAgent
334
+ };
231
335
  }
232
336
  /**
233
337
  * Builds the map of plugin-name → toolkit that the markdown loader consults
@@ -1207,10 +1311,12 @@ function warnOnCapabilityMismatch(agentName, adapter, toolIndex) {
1207
1311
  if (adapter.consumesInputTools === false && inputToolKeys.length > 0) logger.warn(`Agent '${agentName}' declares function tools / sub-agents / MCP tools (${inputToolKeys.join(", ")}) but its model adapter does not consume input.tools (Supervisor API owns its own tool loop). These tools will not be exposed to the model. See docs/plugins/agents.md.`);
1208
1312
  }
1209
1313
  /**
1210
- * Plugin factory for the agents plugin. Reads `config/agents/*.md` by default,
1211
- * resolves toolkits/tools from registered plugins, exposes `appkit.agents.*`
1212
- * runtime API and mounts `POST /invocations` and `POST /responses` (aliased
1213
- * non-streaming invoke endpoints) plus `POST /chat` (streaming, HITL-capable).
1314
+ * Plugin factory for the agents plugin. Discovers agents from
1315
+ * `server/agents/<id>/agent.{ts,md}` by default (markdown still in
1316
+ * `config/agents/` is read as a deprecated fallback), resolves toolkits/tools
1317
+ * from registered plugins, exposes the `appkit.agents.*` runtime API and mounts
1318
+ * `POST /invocations` and `POST /responses` (aliased non-streaming invoke
1319
+ * endpoints) plus `POST /chat` (streaming, HITL-capable).
1214
1320
  *
1215
1321
  * @example
1216
1322
  * ```ts