@databricks/appkit 0.31.0 → 0.33.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.
- package/CLAUDE.md +54 -1
- package/NOTICE.md +2 -0
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +8 -3
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +16 -1
- package/dist/beta.js +14 -1
- package/dist/connectors/index.js +3 -0
- package/dist/connectors/mcp/client.d.ts +85 -0
- package/dist/connectors/mcp/client.d.ts.map +1 -0
- package/dist/connectors/mcp/client.js +296 -0
- package/dist/connectors/mcp/client.js.map +1 -0
- package/dist/connectors/mcp/host-policy.d.ts +51 -0
- package/dist/connectors/mcp/host-policy.d.ts.map +1 -0
- package/dist/connectors/mcp/host-policy.js +168 -0
- package/dist/connectors/mcp/host-policy.js.map +1 -0
- package/dist/connectors/mcp/index.d.ts +3 -0
- package/dist/connectors/mcp/index.js +4 -0
- package/dist/connectors/mcp/types.d.ts +16 -0
- package/dist/connectors/mcp/types.d.ts.map +1 -0
- package/dist/context/index.js +1 -1
- package/dist/core/agent/build-toolkit.d.ts +2 -0
- package/dist/core/agent/build-toolkit.js +45 -0
- package/dist/core/agent/build-toolkit.js.map +1 -0
- package/dist/core/agent/consume-adapter-stream.js +33 -0
- package/dist/core/agent/consume-adapter-stream.js.map +1 -0
- package/dist/core/agent/create-agent.d.ts +27 -0
- package/dist/core/agent/create-agent.d.ts.map +1 -0
- package/dist/core/agent/create-agent.js +50 -0
- package/dist/core/agent/create-agent.js.map +1 -0
- package/dist/core/agent/load-agents.d.ts +72 -0
- package/dist/core/agent/load-agents.d.ts.map +1 -0
- package/dist/core/agent/load-agents.js +268 -0
- package/dist/core/agent/load-agents.js.map +1 -0
- package/dist/core/agent/normalize-result.js +39 -0
- package/dist/core/agent/normalize-result.js.map +1 -0
- package/dist/core/agent/plugins-map.js +44 -0
- package/dist/core/agent/plugins-map.js.map +1 -0
- package/dist/core/agent/run-agent.d.ts +58 -0
- package/dist/core/agent/run-agent.d.ts.map +1 -0
- package/dist/core/agent/run-agent.js +257 -0
- package/dist/core/agent/run-agent.js.map +1 -0
- package/dist/core/agent/system-prompt.js +38 -0
- package/dist/core/agent/system-prompt.js.map +1 -0
- package/dist/core/agent/toolkit-options.js +28 -0
- package/dist/core/agent/toolkit-options.js.map +1 -0
- package/dist/core/agent/toolkit-resolver.js +44 -0
- package/dist/core/agent/toolkit-resolver.js.map +1 -0
- package/dist/core/agent/tools/define-tool.d.ts +66 -0
- package/dist/core/agent/tools/define-tool.d.ts.map +1 -0
- package/dist/core/agent/tools/define-tool.js +50 -0
- package/dist/core/agent/tools/define-tool.js.map +1 -0
- package/dist/core/agent/tools/function-tool.d.ts +38 -0
- package/dist/core/agent/tools/function-tool.d.ts.map +1 -0
- package/dist/core/agent/tools/function-tool.js +22 -0
- package/dist/core/agent/tools/function-tool.js.map +1 -0
- package/dist/core/agent/tools/hosted-tools.d.ts +47 -0
- package/dist/core/agent/tools/hosted-tools.d.ts.map +1 -0
- package/dist/core/agent/tools/hosted-tools.js +67 -0
- package/dist/core/agent/tools/hosted-tools.js.map +1 -0
- package/dist/core/agent/tools/index.d.ts +5 -0
- package/dist/core/agent/tools/index.js +7 -0
- package/dist/core/agent/tools/json-schema.js +24 -0
- package/dist/core/agent/tools/json-schema.js.map +1 -0
- package/dist/core/agent/tools/sql-policy.js +256 -0
- package/dist/core/agent/tools/sql-policy.js.map +1 -0
- package/dist/core/agent/tools/tool.d.ts +63 -0
- package/dist/core/agent/tools/tool.d.ts.map +1 -0
- package/dist/core/agent/tools/tool.js +42 -0
- package/dist/core/agent/tools/tool.js.map +1 -0
- package/dist/core/agent/types.d.ts +299 -0
- package/dist/core/agent/types.d.ts.map +1 -0
- package/dist/core/agent/types.js +12 -0
- package/dist/core/agent/types.js.map +1 -0
- package/dist/core/appkit.d.ts +1 -0
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +31 -4
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/plugin-context.d.ts +133 -0
- package/dist/core/plugin-context.d.ts.map +1 -0
- package/dist/core/plugin-context.js +220 -0
- package/dist/core/plugin-context.js.map +1 -0
- package/dist/index.d.ts +11 -11
- package/dist/internal-telemetry/appkit-log.js +19 -0
- package/dist/internal-telemetry/appkit-log.js.map +1 -0
- package/dist/internal-telemetry/config.js +15 -0
- package/dist/internal-telemetry/config.js.map +1 -0
- package/dist/internal-telemetry/index.js +4 -0
- package/dist/internal-telemetry/reporter.js +132 -0
- package/dist/internal-telemetry/reporter.js.map +1 -0
- package/dist/plugin/plugin.d.ts +18 -3
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +26 -2
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugin/to-plugin.d.ts +3 -2
- package/dist/plugin/to-plugin.d.ts.map +1 -1
- package/dist/plugin/to-plugin.js +7 -4
- package/dist/plugin/to-plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +186 -0
- package/dist/plugins/agents/agents.d.ts.map +1 -0
- package/dist/plugins/agents/agents.js +979 -0
- package/dist/plugins/agents/agents.js.map +1 -0
- package/dist/plugins/agents/defaults.js +13 -0
- package/dist/plugins/agents/defaults.js.map +1 -0
- package/dist/plugins/agents/event-channel.js +64 -0
- package/dist/plugins/agents/event-channel.js.map +1 -0
- package/dist/plugins/agents/event-translator.js +224 -0
- package/dist/plugins/agents/event-translator.js.map +1 -0
- package/dist/plugins/agents/index.d.ts +4 -0
- package/dist/plugins/agents/index.js +6 -0
- package/dist/plugins/agents/manifest.js +26 -0
- package/dist/plugins/agents/manifest.js.map +1 -0
- package/dist/plugins/agents/schemas.js +51 -0
- package/dist/plugins/agents/schemas.js.map +1 -0
- package/dist/plugins/agents/thread-store.js +58 -0
- package/dist/plugins/agents/thread-store.js.map +1 -0
- package/dist/plugins/agents/tool-approval-gate.js +75 -0
- package/dist/plugins/agents/tool-approval-gate.js.map +1 -0
- package/dist/plugins/analytics/analytics.d.ts +15 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +37 -2
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/index.js +1 -0
- package/dist/plugins/analytics/types.js +15 -0
- package/dist/plugins/analytics/types.js.map +1 -0
- package/dist/plugins/beta-exports.generated.d.ts +2 -0
- package/dist/plugins/beta-exports.generated.js +4 -0
- package/dist/plugins/files/plugin.d.ts +20 -2
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +120 -2
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +17 -3
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +61 -2
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/genie/types.d.ts +10 -2
- package/dist/plugins/genie/types.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js +1 -1
- package/dist/plugins/lakebase/index.d.ts +2 -2
- package/dist/plugins/lakebase/index.js +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +31 -3
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +77 -5
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/lakebase/types.d.ts +39 -1
- package/dist/plugins/lakebase/types.d.ts.map +1 -1
- package/dist/plugins/server/index.d.ts +12 -0
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +47 -10
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/types.d.ts +11 -3
- package/dist/plugins/server/types.d.ts.map +1 -1
- package/dist/shared/src/agent.d.ts +75 -1
- package/dist/shared/src/agent.d.ts.map +1 -1
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +8 -0
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/docs/api/appkit/Class.AppKitMcpClient.md +157 -0
- package/docs/api/appkit/Class.DatabricksAdapter.md +151 -0
- package/docs/api/appkit/Class.Plugin.md +65 -23
- package/docs/api/appkit/Function.agentIdFromMarkdownPath.md +18 -0
- package/docs/api/appkit/Function.createAgent.md +33 -0
- package/docs/api/appkit/Function.createApp.md +10 -8
- package/docs/api/appkit/Function.defineTool.md +26 -0
- package/docs/api/appkit/Function.executeFromRegistry.md +25 -0
- package/docs/api/appkit/Function.functionToolToDefinition.md +16 -0
- package/docs/api/appkit/Function.isFunctionTool.md +16 -0
- package/docs/api/appkit/Function.isHostedTool.md +16 -0
- package/docs/api/appkit/Function.isToolkitEntry.md +18 -0
- package/docs/api/appkit/Function.loadAgentFromFile.md +21 -0
- package/docs/api/appkit/Function.loadAgentsFromDir.md +26 -0
- package/docs/api/appkit/Function.mcpServer.md +28 -0
- package/docs/api/appkit/Function.parseTextToolCalls.md +26 -0
- package/docs/api/appkit/Function.resolveHostedTools.md +16 -0
- package/docs/api/appkit/Function.runAgent.md +26 -0
- package/docs/api/appkit/Function.tool.md +28 -0
- package/docs/api/appkit/Function.toolsFromRegistry.md +20 -0
- package/docs/api/appkit/Interface.AgentAdapter.md +21 -0
- package/docs/api/appkit/Interface.AgentDefinition.md +112 -0
- package/docs/api/appkit/Interface.AgentInput.md +37 -0
- package/docs/api/appkit/Interface.AgentRunContext.md +32 -0
- package/docs/api/appkit/Interface.AgentToolDefinition.md +37 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +241 -0
- package/docs/api/appkit/Interface.AutoInheritToolsConfig.md +27 -0
- package/docs/api/appkit/Interface.BasePluginConfig.md +1 -0
- package/docs/api/appkit/Interface.FunctionTool.md +80 -0
- package/docs/api/appkit/Interface.McpConnectAllResult.md +38 -0
- package/docs/api/appkit/Interface.Message.md +55 -0
- package/docs/api/appkit/Interface.PluginToolkitProvider.md +22 -0
- package/docs/api/appkit/Interface.PromptContext.md +30 -0
- package/docs/api/appkit/Interface.RegisteredAgent.md +75 -0
- package/docs/api/appkit/Interface.RunAgentInput.md +34 -0
- package/docs/api/appkit/Interface.RunAgentResult.md +23 -0
- package/docs/api/appkit/Interface.Thread.md +46 -0
- package/docs/api/appkit/Interface.ThreadStore.md +103 -0
- package/docs/api/appkit/Interface.ToolAnnotations.md +56 -0
- package/docs/api/appkit/Interface.ToolConfig.md +72 -0
- package/docs/api/appkit/Interface.ToolEntry.md +73 -0
- package/docs/api/appkit/Interface.ToolProvider.md +38 -0
- package/docs/api/appkit/Interface.ToolkitEntry.md +59 -0
- package/docs/api/appkit/Interface.ToolkitOptions.md +45 -0
- package/docs/api/appkit/TypeAlias.AgentEvent.md +299 -0
- package/docs/api/appkit/TypeAlias.AgentTool.md +11 -0
- package/docs/api/appkit/TypeAlias.AgentTools.md +8 -0
- package/docs/api/appkit/TypeAlias.AgentToolsFn.md +20 -0
- package/docs/api/appkit/TypeAlias.BaseSystemPromptOption.md +9 -0
- package/docs/api/appkit/TypeAlias.HostedTool.md +10 -0
- package/docs/api/appkit/TypeAlias.Plugins.md +26 -0
- package/docs/api/appkit/TypeAlias.ResolvedToolEntry.md +29 -0
- package/docs/api/appkit/TypeAlias.ToolRegistry.md +6 -0
- package/docs/api/appkit/Variable.agents.md +19 -0
- package/docs/api/appkit.md +113 -62
- package/docs/plugins/agents.md +441 -0
- package/docs/privacy.md +41 -0
- package/llms.txt +54 -1
- package/package.json +4 -2
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { applyToolkitOptions } from "./toolkit-options.js";
|
|
2
|
+
import { toToolJSONSchema } from "./tools/json-schema.js";
|
|
3
|
+
|
|
4
|
+
//#region src/core/agent/build-toolkit.ts
|
|
5
|
+
/**
|
|
6
|
+
* Converts a plugin's internal `ToolRegistry` into a keyed record of
|
|
7
|
+
* `ToolkitEntry` markers suitable for spreading into an `AgentDefinition.tools`
|
|
8
|
+
* record.
|
|
9
|
+
*
|
|
10
|
+
* The `opts` record controls shape and filtering:
|
|
11
|
+
* - `prefix` — overrides the default `${pluginName}.` prefix; `""` drops it.
|
|
12
|
+
* - `only` — allowlist of local tool names to include (post-prefix).
|
|
13
|
+
* - `except` — denylist of local names.
|
|
14
|
+
* - `rename` — per-tool key remapping (applied after prefix/filter).
|
|
15
|
+
*
|
|
16
|
+
* Each entry carries `pluginName` + `localName` so the agents plugin can
|
|
17
|
+
* dispatch back through `PluginContext.executeTool` for OBO + telemetry.
|
|
18
|
+
*/
|
|
19
|
+
function buildToolkitEntries(pluginName, registry, opts = {}) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [localName, entry] of Object.entries(registry)) {
|
|
22
|
+
const key = applyToolkitOptions(localName, pluginName, opts);
|
|
23
|
+
if (key === null) continue;
|
|
24
|
+
const parameters = toToolJSONSchema(entry.schema);
|
|
25
|
+
const def = {
|
|
26
|
+
name: key,
|
|
27
|
+
description: entry.description,
|
|
28
|
+
parameters
|
|
29
|
+
};
|
|
30
|
+
if (entry.annotations) def.annotations = entry.annotations;
|
|
31
|
+
out[key] = {
|
|
32
|
+
__toolkitRef: true,
|
|
33
|
+
pluginName,
|
|
34
|
+
localName,
|
|
35
|
+
def,
|
|
36
|
+
annotations: entry.annotations,
|
|
37
|
+
autoInheritable: entry.autoInheritable
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { buildToolkitEntries };
|
|
45
|
+
//# sourceMappingURL=build-toolkit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-toolkit.js","names":[],"sources":["../../../src/core/agent/build-toolkit.ts"],"sourcesContent":["import type { AgentToolDefinition } from \"shared\";\nimport { applyToolkitOptions } from \"./toolkit-options\";\nimport type { ToolRegistry } from \"./tools/define-tool\";\nimport { toToolJSONSchema } from \"./tools/json-schema\";\nimport type { ToolkitEntry, ToolkitOptions } from \"./types\";\n\n/**\n * Converts a plugin's internal `ToolRegistry` into a keyed record of\n * `ToolkitEntry` markers suitable for spreading into an `AgentDefinition.tools`\n * record.\n *\n * The `opts` record controls shape and filtering:\n * - `prefix` — overrides the default `${pluginName}.` prefix; `\"\"` drops it.\n * - `only` — allowlist of local tool names to include (post-prefix).\n * - `except` — denylist of local names.\n * - `rename` — per-tool key remapping (applied after prefix/filter).\n *\n * Each entry carries `pluginName` + `localName` so the agents plugin can\n * dispatch back through `PluginContext.executeTool` for OBO + telemetry.\n */\nexport function buildToolkitEntries(\n pluginName: string,\n registry: ToolRegistry,\n opts: ToolkitOptions = {},\n): Record<string, ToolkitEntry> {\n const out: Record<string, ToolkitEntry> = {};\n\n for (const [localName, entry] of Object.entries(registry)) {\n const key = applyToolkitOptions(localName, pluginName, opts);\n if (key === null) continue;\n\n const parameters = toToolJSONSchema(\n entry.schema,\n ) as unknown as AgentToolDefinition[\"parameters\"];\n\n const def: AgentToolDefinition = {\n name: key,\n description: entry.description,\n parameters,\n };\n if (entry.annotations) {\n def.annotations = entry.annotations;\n }\n\n out[key] = {\n __toolkitRef: true,\n pluginName,\n localName,\n def,\n annotations: entry.annotations,\n autoInheritable: entry.autoInheritable,\n };\n }\n\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,SAAgB,oBACd,YACA,UACA,OAAuB,EAAE,EACK;CAC9B,MAAM,MAAoC,EAAE;AAE5C,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,EAAE;EACzD,MAAM,MAAM,oBAAoB,WAAW,YAAY,KAAK;AAC5D,MAAI,QAAQ,KAAM;EAElB,MAAM,aAAa,iBACjB,MAAM,OACP;EAED,MAAM,MAA2B;GAC/B,MAAM;GACN,aAAa,MAAM;GACnB;GACD;AACD,MAAI,MAAM,YACR,KAAI,cAAc,MAAM;AAG1B,MAAI,OAAO;GACT,cAAc;GACd;GACA;GACA;GACA,aAAa,MAAM;GACnB,iBAAiB,MAAM;GACxB;;AAGH,QAAO"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/core/agent/consume-adapter-stream.ts
|
|
2
|
+
/**
|
|
3
|
+
* Consume an adapter's event stream and aggregate the assistant's final text.
|
|
4
|
+
*
|
|
5
|
+
* Accumulation rule (shared across all agent-execution paths in AppKit):
|
|
6
|
+
*
|
|
7
|
+
* - `message_delta` events append their `content` to the running text.
|
|
8
|
+
* - A `message` event *replaces* the running text with its `content`.
|
|
9
|
+
*
|
|
10
|
+
* The two branches coexist because different adapters emit different shapes:
|
|
11
|
+
* streaming adapters (Databricks, Vercel AI) emit deltas chunk-by-chunk,
|
|
12
|
+
* while `LangChain`'s `on_chain_end` path emits a single final `message`.
|
|
13
|
+
* Without the replace branch, LangChain conversations silently dropped the
|
|
14
|
+
* assistant turn from thread history.
|
|
15
|
+
*
|
|
16
|
+
* Kept pure (no I/O, no mutable external state beyond the caller's `onEvent`
|
|
17
|
+
* side effect) so each execution path — HTTP streaming, sub-agents, and the
|
|
18
|
+
* standalone `runAgent` — can share one loop.
|
|
19
|
+
*/
|
|
20
|
+
async function consumeAdapterStream(stream, opts = {}) {
|
|
21
|
+
let text = "";
|
|
22
|
+
for await (const event of stream) {
|
|
23
|
+
if (opts.signal?.aborted) break;
|
|
24
|
+
if (event.type === "message_delta") text += event.content;
|
|
25
|
+
else if (event.type === "message") text = event.content;
|
|
26
|
+
opts.onEvent?.(event);
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { consumeAdapterStream };
|
|
33
|
+
//# sourceMappingURL=consume-adapter-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consume-adapter-stream.js","names":[],"sources":["../../../src/core/agent/consume-adapter-stream.ts"],"sourcesContent":["import type { AgentEvent } from \"shared\";\n\ninterface ConsumeAdapterStreamOptions {\n /**\n * Optional abort signal. When aborted, the loop stops consuming (the caller\n * is expected to have forwarded the same signal to `adapter.run` to stop\n * upstream work). `undefined` is valid — standalone `runAgent` runs without\n * a signal.\n */\n signal?: AbortSignal;\n /**\n * Side-effect callback invoked once per adapter event, after the content\n * accumulator has been updated. Use to fan events out to SSE translators,\n * collect a raw event list for tests, or emit telemetry.\n */\n onEvent?: (event: AgentEvent) => void;\n}\n\n/**\n * Consume an adapter's event stream and aggregate the assistant's final text.\n *\n * Accumulation rule (shared across all agent-execution paths in AppKit):\n *\n * - `message_delta` events append their `content` to the running text.\n * - A `message` event *replaces* the running text with its `content`.\n *\n * The two branches coexist because different adapters emit different shapes:\n * streaming adapters (Databricks, Vercel AI) emit deltas chunk-by-chunk,\n * while `LangChain`'s `on_chain_end` path emits a single final `message`.\n * Without the replace branch, LangChain conversations silently dropped the\n * assistant turn from thread history.\n *\n * Kept pure (no I/O, no mutable external state beyond the caller's `onEvent`\n * side effect) so each execution path — HTTP streaming, sub-agents, and the\n * standalone `runAgent` — can share one loop.\n */\nexport async function consumeAdapterStream(\n stream: AsyncIterable<AgentEvent>,\n opts: ConsumeAdapterStreamOptions = {},\n): Promise<string> {\n let text = \"\";\n for await (const event of stream) {\n if (opts.signal?.aborted) break;\n if (event.type === \"message_delta\") {\n text += event.content;\n } else if (event.type === \"message\") {\n text = event.content;\n }\n opts.onEvent?.(event);\n }\n return text;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoCA,eAAsB,qBACpB,QACA,OAAoC,EAAE,EACrB;CACjB,IAAI,OAAO;AACX,YAAW,MAAM,SAAS,QAAQ;AAChC,MAAI,KAAK,QAAQ,QAAS;AAC1B,MAAI,MAAM,SAAS,gBACjB,SAAQ,MAAM;WACL,MAAM,SAAS,UACxB,QAAO,MAAM;AAEf,OAAK,UAAU,MAAM;;AAEvB,QAAO"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AgentDefinition } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/agent/create-agent.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pure factory for agent definitions. Returns the passed-in definition after
|
|
6
|
+
* cycle-detecting the sub-agent graph. Accepts the full `AgentDefinition` shape
|
|
7
|
+
* and is safe to call at module top-level.
|
|
8
|
+
*
|
|
9
|
+
* The returned value is a plain `AgentDefinition` — no adapter construction,
|
|
10
|
+
* no side effects. Register it with `agents({ agents: { name: def } })` or run
|
|
11
|
+
* it standalone via `runAgent(def, input)`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const support = createAgent({
|
|
16
|
+
* instructions: "You help customers.",
|
|
17
|
+
* model: "databricks-claude-sonnet-4-5",
|
|
18
|
+
* tools: {
|
|
19
|
+
* get_weather: tool({ ... }),
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare function createAgent(def: AgentDefinition): AgentDefinition;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { createAgent };
|
|
27
|
+
//# sourceMappingURL=create-agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-agent.d.ts","names":[],"sources":["../../../src/core/agent/create-agent.ts"],"mappings":";;;;;AAuBA;;;;;;;;;;;;;;;;;;iBAAgB,WAAA,CAAY,GAAA,EAAK,eAAA,GAAkB,eAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ConfigurationError } from "../../errors/configuration.js";
|
|
2
|
+
import { init_errors } from "../../errors/index.js";
|
|
3
|
+
|
|
4
|
+
//#region src/core/agent/create-agent.ts
|
|
5
|
+
init_errors();
|
|
6
|
+
/**
|
|
7
|
+
* Pure factory for agent definitions. Returns the passed-in definition after
|
|
8
|
+
* cycle-detecting the sub-agent graph. Accepts the full `AgentDefinition` shape
|
|
9
|
+
* and is safe to call at module top-level.
|
|
10
|
+
*
|
|
11
|
+
* The returned value is a plain `AgentDefinition` — no adapter construction,
|
|
12
|
+
* no side effects. Register it with `agents({ agents: { name: def } })` or run
|
|
13
|
+
* it standalone via `runAgent(def, input)`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const support = createAgent({
|
|
18
|
+
* instructions: "You help customers.",
|
|
19
|
+
* model: "databricks-claude-sonnet-4-5",
|
|
20
|
+
* tools: {
|
|
21
|
+
* get_weather: tool({ ... }),
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function createAgent(def) {
|
|
27
|
+
detectCycles(def);
|
|
28
|
+
return def;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Walks the `agents: { ... }` sub-agent tree via DFS and throws if a cycle is
|
|
32
|
+
* found. Cycles would cause infinite recursion at tool-invocation time.
|
|
33
|
+
*/
|
|
34
|
+
function detectCycles(def) {
|
|
35
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
36
|
+
const visited = /* @__PURE__ */ new Set();
|
|
37
|
+
const walk = (current, path) => {
|
|
38
|
+
if (visited.has(current)) return;
|
|
39
|
+
if (visiting.has(current)) throw new ConfigurationError(`Agent sub-agent cycle detected: ${path.join(" -> ")}`);
|
|
40
|
+
visiting.add(current);
|
|
41
|
+
for (const [childKey, child] of Object.entries(current.agents ?? {})) walk(child, [...path, childKey]);
|
|
42
|
+
visiting.delete(current);
|
|
43
|
+
visited.add(current);
|
|
44
|
+
};
|
|
45
|
+
walk(def, [def.name ?? "(root)"]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { createAgent };
|
|
50
|
+
//# sourceMappingURL=create-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-agent.js","names":[],"sources":["../../../src/core/agent/create-agent.ts"],"sourcesContent":["import { ConfigurationError } from \"../../errors\";\nimport type { AgentDefinition } from \"./types\";\n\n/**\n * Pure factory for agent definitions. Returns the passed-in definition after\n * cycle-detecting the sub-agent graph. Accepts the full `AgentDefinition` shape\n * and is safe to call at module top-level.\n *\n * The returned value is a plain `AgentDefinition` — no adapter construction,\n * no side effects. Register it with `agents({ agents: { name: def } })` or run\n * it standalone via `runAgent(def, input)`.\n *\n * @example\n * ```ts\n * const support = createAgent({\n * instructions: \"You help customers.\",\n * model: \"databricks-claude-sonnet-4-5\",\n * tools: {\n * get_weather: tool({ ... }),\n * },\n * });\n * ```\n */\nexport function createAgent(def: AgentDefinition): AgentDefinition {\n detectCycles(def);\n return def;\n}\n\n/**\n * Walks the `agents: { ... }` sub-agent tree via DFS and throws if a cycle is\n * found. Cycles would cause infinite recursion at tool-invocation time.\n */\nfunction detectCycles(def: AgentDefinition): void {\n const visiting = new Set<AgentDefinition>();\n const visited = new Set<AgentDefinition>();\n\n const walk = (current: AgentDefinition, path: string[]): void => {\n if (visited.has(current)) return;\n if (visiting.has(current)) {\n throw new ConfigurationError(\n `Agent sub-agent cycle detected: ${path.join(\" -> \")}`,\n );\n }\n visiting.add(current);\n for (const [childKey, child] of Object.entries(current.agents ?? {})) {\n walk(child, [...path, childKey]);\n }\n visiting.delete(current);\n visited.add(current);\n };\n\n walk(def, [def.name ?? \"(root)\"]);\n}\n"],"mappings":";;;;aAAkD;;;;;;;;;;;;;;;;;;;;;AAuBlD,SAAgB,YAAY,KAAuC;AACjE,cAAa,IAAI;AACjB,QAAO;;;;;;AAOT,SAAS,aAAa,KAA4B;CAChD,MAAM,2BAAW,IAAI,KAAsB;CAC3C,MAAM,0BAAU,IAAI,KAAsB;CAE1C,MAAM,QAAQ,SAA0B,SAAyB;AAC/D,MAAI,QAAQ,IAAI,QAAQ,CAAE;AAC1B,MAAI,SAAS,IAAI,QAAQ,CACvB,OAAM,IAAI,mBACR,mCAAmC,KAAK,KAAK,OAAO,GACrD;AAEH,WAAS,IAAI,QAAQ;AACrB,OAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,QAAQ,UAAU,EAAE,CAAC,CAClE,MAAK,OAAO,CAAC,GAAG,MAAM,SAAS,CAAC;AAElC,WAAS,OAAO,QAAQ;AACxB,UAAQ,IAAI,QAAQ;;AAGtB,MAAK,KAAK,CAAC,IAAI,QAAQ,SAAS,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { AgentAdapter } from "../../shared/src/agent.js";
|
|
2
|
+
import "../../shared/src/index.js";
|
|
3
|
+
import { AgentDefinition, AgentTool, ToolkitOptions } from "./types.js";
|
|
4
|
+
|
|
5
|
+
//#region src/core/agent/load-agents.d.ts
|
|
6
|
+
interface ToolkitProvider {
|
|
7
|
+
toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface LoadContext {
|
|
10
|
+
/** Default model when frontmatter has no `endpoint` and the def has no `model`. */
|
|
11
|
+
defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;
|
|
12
|
+
/** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */
|
|
13
|
+
availableTools?: Record<string, AgentTool>;
|
|
14
|
+
/**
|
|
15
|
+
* Registered plugin toolkits referenced by `plugin:NAME` entries in the
|
|
16
|
+
* unified `tools:` frontmatter list. Keyed by plugin name; each value
|
|
17
|
+
* exposes the same `toolkit(opts?)` surface as the `plugins` argument to
|
|
18
|
+
* `tools(plugins) => Record<...>` in the code form.
|
|
19
|
+
*/
|
|
20
|
+
plugins?: Map<string, ToolkitProvider>;
|
|
21
|
+
/**
|
|
22
|
+
* Code-defined agents contributed by `agents({ agents: { ... } })`. The
|
|
23
|
+
* directory loader resolves `agents:` frontmatter references against
|
|
24
|
+
* these alongside sibling markdown files, so a markdown parent can
|
|
25
|
+
* delegate to a code-defined child. Code-defined names win on collision
|
|
26
|
+
* with markdown names, matching the plugin's top-level merge precedence.
|
|
27
|
+
*/
|
|
28
|
+
codeAgents?: Record<string, AgentDefinition>;
|
|
29
|
+
}
|
|
30
|
+
interface LoadResult {
|
|
31
|
+
/** Agent definitions keyed by agent id (directory name under `dir`). */
|
|
32
|
+
defs: Record<string, AgentDefinition>;
|
|
33
|
+
/** First agent with `default: true` frontmatter (sorted id order), or `null`. */
|
|
34
|
+
defaultAgent: string | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Derives the logical agent id from a markdown path. When the file is named
|
|
38
|
+
* `agent.md`, the id is the parent directory name (folder-based layout);
|
|
39
|
+
* otherwise the id is the file stem (e.g. legacy single-file paths).
|
|
40
|
+
*/
|
|
41
|
+
declare function agentIdFromMarkdownPath(filePath: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Loads a single markdown agent file and resolves its frontmatter against
|
|
44
|
+
* registered plugin toolkits + ambient tool library.
|
|
45
|
+
*
|
|
46
|
+
* Rejects non-empty `agents:` frontmatter because single-file loads have
|
|
47
|
+
* no siblings to resolve sub-agent references against — callers must use
|
|
48
|
+
* {@link loadAgentsFromDir} when markdown agents delegate to one another.
|
|
49
|
+
*/
|
|
50
|
+
declare function loadAgentFromFile(filePath: string, ctx: LoadContext): Promise<AgentDefinition>;
|
|
51
|
+
/**
|
|
52
|
+
* Scans a directory for one subdirectory per agent, each containing
|
|
53
|
+
* `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed
|
|
54
|
+
* by agent id (folder name). Throws on frontmatter errors or unresolved
|
|
55
|
+
* references. Returns an empty map if the directory does not exist.
|
|
56
|
+
*
|
|
57
|
+
* Legacy top-level `*.md` files are rejected with an error — migrate each to
|
|
58
|
+
* `<id>/agent.md` under a sibling folder named for the agent id.
|
|
59
|
+
*
|
|
60
|
+
* Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)
|
|
61
|
+
* can be resolved regardless of directory iteration order:
|
|
62
|
+
*
|
|
63
|
+
* 1. Build every agent's definition from its own `agent.md`.
|
|
64
|
+
* 2. Walk `agents:` references and wire `def.agents = { child: childDef }`
|
|
65
|
+
* by looking them up in the complete map. Dangling names and
|
|
66
|
+
* self-references fail loudly; mutual delegation is allowed and bounded
|
|
67
|
+
* at runtime by `limits.maxSubAgentDepth`.
|
|
68
|
+
*/
|
|
69
|
+
declare function loadAgentsFromDir(dir: string, ctx: LoadContext): Promise<LoadResult>;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { LoadContext, LoadResult, agentIdFromMarkdownPath, loadAgentFromFile, loadAgentsFromDir };
|
|
72
|
+
//# sourceMappingURL=load-agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;UAiBU,eAAA;EACR,OAAA,GAAU,IAAA,GAAO,cAAA,KAAmB,MAAA;AAAA;AAAA,UAGrB,WAAA;EAJQ;EAMvB,YAAA,GAAe,YAAA,GAAe,OAAA,CAAQ,YAAA;EALI;EAO1C,cAAA,GAAiB,MAAA,SAAe,SAAA;EAPf;;;;;AAGnB;EAWE,OAAA,GAAU,GAAA,SAAY,eAAA;;;;;;;;EAQtB,UAAA,GAAa,MAAA,SAAe,eAAA;AAAA;AAAA,UAGb,UAAA;EAHI;EAKnB,IAAA,EAAM,MAAA,SAAe,eAAA;EAtBrB;EAwBA,YAAA;AAAA;;;;;;iBA0Dc,uBAAA,CAAwB,QAAA;;;;;;;;;iBA8BlB,iBAAA,CACpB,QAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,eAAA;;;;;;;;;;;;;;;;;;;iBAgCW,iBAAA,CACpB,GAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,UAAA"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
import { isToolkitEntry } from "./types.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
|
|
7
|
+
//#region src/core/agent/load-agents.ts
|
|
8
|
+
const logger = createLogger("agents:loader");
|
|
9
|
+
const PLUGIN_PREFIX = "plugin:";
|
|
10
|
+
/**
|
|
11
|
+
* Derives the logical agent id from a markdown path. When the file is named
|
|
12
|
+
* `agent.md`, the id is the parent directory name (folder-based layout);
|
|
13
|
+
* otherwise the id is the file stem (e.g. legacy single-file paths).
|
|
14
|
+
*/
|
|
15
|
+
function agentIdFromMarkdownPath(filePath) {
|
|
16
|
+
const normalized = path.normalize(filePath);
|
|
17
|
+
const base = path.basename(normalized);
|
|
18
|
+
const parent = path.basename(path.dirname(normalized));
|
|
19
|
+
if (base === "agent.md" && parent && parent !== "." && parent !== "..") return parent;
|
|
20
|
+
return path.basename(normalized, ".md");
|
|
21
|
+
}
|
|
22
|
+
const ALLOWED_KEYS = new Set([
|
|
23
|
+
"endpoint",
|
|
24
|
+
"model",
|
|
25
|
+
"tools",
|
|
26
|
+
"agents",
|
|
27
|
+
"maxSteps",
|
|
28
|
+
"maxTokens",
|
|
29
|
+
"default",
|
|
30
|
+
"baseSystemPrompt",
|
|
31
|
+
"ephemeral"
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Loads a single markdown agent file and resolves its frontmatter against
|
|
35
|
+
* registered plugin toolkits + ambient tool library.
|
|
36
|
+
*
|
|
37
|
+
* Rejects non-empty `agents:` frontmatter because single-file loads have
|
|
38
|
+
* no siblings to resolve sub-agent references against — callers must use
|
|
39
|
+
* {@link loadAgentsFromDir} when markdown agents delegate to one another.
|
|
40
|
+
*/
|
|
41
|
+
async function loadAgentFromFile(filePath, ctx) {
|
|
42
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
43
|
+
const name = agentIdFromMarkdownPath(filePath);
|
|
44
|
+
const { data } = parseFrontmatter(raw, filePath);
|
|
45
|
+
if (Array.isArray(data?.agents) && data.agents.length > 0) throw new Error(`Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, which requires loadAgentsFromDir to resolve sibling references. Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`);
|
|
46
|
+
return buildDefinition(name, raw, filePath, ctx);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Scans a directory for one subdirectory per agent, each containing
|
|
50
|
+
* `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed
|
|
51
|
+
* by agent id (folder name). Throws on frontmatter errors or unresolved
|
|
52
|
+
* references. Returns an empty map if the directory does not exist.
|
|
53
|
+
*
|
|
54
|
+
* Legacy top-level `*.md` files are rejected with an error — migrate each to
|
|
55
|
+
* `<id>/agent.md` under a sibling folder named for the agent id.
|
|
56
|
+
*
|
|
57
|
+
* Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)
|
|
58
|
+
* can be resolved regardless of directory iteration order:
|
|
59
|
+
*
|
|
60
|
+
* 1. Build every agent's definition from its own `agent.md`.
|
|
61
|
+
* 2. Walk `agents:` references and wire `def.agents = { child: childDef }`
|
|
62
|
+
* by looking them up in the complete map. Dangling names and
|
|
63
|
+
* self-references fail loudly; mutual delegation is allowed and bounded
|
|
64
|
+
* at runtime by `limits.maxSubAgentDepth`.
|
|
65
|
+
*/
|
|
66
|
+
async function loadAgentsFromDir(dir, ctx) {
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err.code === "ENOENT") return {
|
|
72
|
+
defs: {},
|
|
73
|
+
defaultAgent: null
|
|
74
|
+
};
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
const orphanMd = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
|
|
78
|
+
if (orphanMd.length > 0) {
|
|
79
|
+
const hint = orphanMd.map((f) => `${path.basename(f, ".md")}/agent.md`).join(", ");
|
|
80
|
+
throw new Error(`Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(", ")}. Use one folder per agent with a fixed entry file, e.g. ${hint}.`);
|
|
81
|
+
}
|
|
82
|
+
/** Reserved folder name until per-agent skills land; not an agent package. */
|
|
83
|
+
const RESERVED_DIRS = new Set(["skills"]);
|
|
84
|
+
const agentIds = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !RESERVED_DIRS.has(name)).sort();
|
|
85
|
+
const defs = {};
|
|
86
|
+
const subAgentRefs = {};
|
|
87
|
+
let defaultAgent = null;
|
|
88
|
+
for (const id of agentIds) {
|
|
89
|
+
const agentPath = path.join(dir, id, "agent.md");
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = await fs.readFile(agentPath, "utf-8");
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err.code === "ENOENT") throw new Error(`Agents subdirectory '${path.join(dir, id)}' must contain agent.md.`);
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
defs[id] = buildDefinition(id, raw, agentPath, ctx);
|
|
98
|
+
const { data } = parseFrontmatter(raw, agentPath);
|
|
99
|
+
if (data?.agents !== void 0) subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);
|
|
100
|
+
if (data?.default === true && !defaultAgent) defaultAgent = id;
|
|
101
|
+
}
|
|
102
|
+
for (const [name, refs] of Object.entries(subAgentRefs)) {
|
|
103
|
+
if (refs.length === 0) continue;
|
|
104
|
+
const children = {};
|
|
105
|
+
const missing = [];
|
|
106
|
+
for (const ref of refs) {
|
|
107
|
+
if (ref === name) throw new Error(`Agent '${name}' (${path.join(dir, name, "agent.md")}) cannot reference itself in 'agents:'.`);
|
|
108
|
+
const sibling = ctx.codeAgents?.[ref] ?? defs[ref];
|
|
109
|
+
if (!sibling) {
|
|
110
|
+
missing.push(ref);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
children[ref] = sibling;
|
|
114
|
+
}
|
|
115
|
+
if (missing.length > 0) {
|
|
116
|
+
const available = [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)].sort().join(", ") || "<none>";
|
|
117
|
+
throw new Error(`Agent '${name}' references sub-agent(s) '${missing.join(", ")}' in 'agents:', but no markdown or code agent(s) with those names exist. Available: ${available}.`);
|
|
118
|
+
}
|
|
119
|
+
defs[name].agents = children;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
defs,
|
|
123
|
+
defaultAgent
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Validates that `agents:` frontmatter is an array of non-empty strings and
|
|
128
|
+
* returns it with duplicates removed. Throws with a clear per-file message
|
|
129
|
+
* on malformed input rather than silently ignoring.
|
|
130
|
+
*/
|
|
131
|
+
function normalizeAgentsFrontmatter(value, agentName, filePath) {
|
|
132
|
+
if (!Array.isArray(value)) throw new Error(`Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: expected an array of sibling agent ids, got ${typeof value}.`);
|
|
133
|
+
const out = [];
|
|
134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const item of value) {
|
|
136
|
+
if (typeof item !== "string" || item.trim() === "") throw new Error(`Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: expected non-empty string, got ${JSON.stringify(item)}.`);
|
|
137
|
+
if (seen.has(item)) continue;
|
|
138
|
+
seen.add(item);
|
|
139
|
+
out.push(item);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/** Exposed for tests. Parses `--- yaml ---\nbody` and validates frontmatter keys. */
|
|
144
|
+
function parseFrontmatter(raw, sourcePath) {
|
|
145
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
146
|
+
if (!match) return {
|
|
147
|
+
data: null,
|
|
148
|
+
content: raw.trim()
|
|
149
|
+
};
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = yaml.load(match[1]);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const src = sourcePath ? ` (${sourcePath})` : "";
|
|
155
|
+
throw new Error(`Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`);
|
|
156
|
+
}
|
|
157
|
+
if (parsed === null || parsed === void 0) return {
|
|
158
|
+
data: {},
|
|
159
|
+
content: match[2].trim()
|
|
160
|
+
};
|
|
161
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
162
|
+
const src = sourcePath ? ` (${sourcePath})` : "";
|
|
163
|
+
throw new Error(`Frontmatter must be a YAML object${src}`);
|
|
164
|
+
}
|
|
165
|
+
const data = parsed;
|
|
166
|
+
for (const key of Object.keys(data)) if (!ALLOWED_KEYS.has(key)) logger.warn("Ignoring unknown frontmatter key '%s' in %s", key, sourcePath ?? "<inline>");
|
|
167
|
+
return {
|
|
168
|
+
data,
|
|
169
|
+
content: match[2].trim()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function buildDefinition(name, raw, filePath, ctx) {
|
|
173
|
+
const { data, content } = parseFrontmatter(raw, filePath);
|
|
174
|
+
const fm = data ?? {};
|
|
175
|
+
const tools = resolveFrontmatterTools(name, fm, filePath, ctx);
|
|
176
|
+
const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;
|
|
177
|
+
let baseSystemPrompt;
|
|
178
|
+
if (fm.baseSystemPrompt === false) baseSystemPrompt = false;
|
|
179
|
+
else if (typeof fm.baseSystemPrompt === "string") baseSystemPrompt = fm.baseSystemPrompt;
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
instructions: content,
|
|
183
|
+
model,
|
|
184
|
+
tools: Object.keys(tools).length > 0 ? tools : void 0,
|
|
185
|
+
maxSteps: typeof fm.maxSteps === "number" ? fm.maxSteps : void 0,
|
|
186
|
+
maxTokens: typeof fm.maxTokens === "number" ? fm.maxTokens : void 0,
|
|
187
|
+
baseSystemPrompt,
|
|
188
|
+
ephemeral: typeof fm.ephemeral === "boolean" ? fm.ephemeral : void 0
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function resolveFrontmatterTools(agentName, fm, filePath, ctx) {
|
|
192
|
+
const out = {};
|
|
193
|
+
const pluginIdx = ctx.plugins ?? /* @__PURE__ */ new Map();
|
|
194
|
+
for (const entry of fm.tools ?? []) {
|
|
195
|
+
const parsed = parseToolEntry(entry, filePath, agentName);
|
|
196
|
+
if (parsed.kind === "plugin") {
|
|
197
|
+
const provider = pluginIdx.get(parsed.pluginName);
|
|
198
|
+
if (!provider) {
|
|
199
|
+
const available = pluginIdx.size > 0 ? Array.from(pluginIdx.keys()).join(", ") : "<none>";
|
|
200
|
+
throw new Error(`Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`);
|
|
201
|
+
}
|
|
202
|
+
const entries = provider.toolkit(parsed.opts);
|
|
203
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
204
|
+
if (!isToolkitEntry(value)) throw new Error(`Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`);
|
|
205
|
+
out[key] = value;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
const tool = ctx.availableTools?.[parsed.toolName];
|
|
209
|
+
if (!tool) {
|
|
210
|
+
const available = ctx.availableTools ? Object.keys(ctx.availableTools).join(", ") : "<none>";
|
|
211
|
+
throw new Error(`Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. If you meant to reference a plugin, use the 'plugin:NAME' prefix.`);
|
|
212
|
+
}
|
|
213
|
+
out[parsed.toolName] = tool;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Classify one item in the `tools:` frontmatter list into either a plugin
|
|
220
|
+
* reference (with optional ToolkitOptions) or an ambient tool lookup.
|
|
221
|
+
*
|
|
222
|
+
* Strings starting with `plugin:` are bare plugin references. Strings
|
|
223
|
+
* without the prefix are ambient tool names. Object entries are
|
|
224
|
+
* single-key mappings keyed by `plugin:NAME`; the value is either an
|
|
225
|
+
* array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.
|
|
226
|
+
*/
|
|
227
|
+
function parseToolEntry(entry, filePath, agentName) {
|
|
228
|
+
if (typeof entry === "string") {
|
|
229
|
+
if (entry.startsWith(PLUGIN_PREFIX)) {
|
|
230
|
+
const pluginName = entry.slice(7);
|
|
231
|
+
if (pluginName.length === 0) throw new Error(`Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`);
|
|
232
|
+
return {
|
|
233
|
+
kind: "plugin",
|
|
234
|
+
pluginName,
|
|
235
|
+
opts: void 0
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (entry.length === 0) throw new Error(`Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`);
|
|
239
|
+
return {
|
|
240
|
+
kind: "ambient",
|
|
241
|
+
toolName: entry
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (typeof entry !== "object" || entry === null) throw new Error(`Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`);
|
|
245
|
+
const keys = Object.keys(entry);
|
|
246
|
+
if (keys.length !== 1) throw new Error(`Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(", ")}`);
|
|
247
|
+
const key = keys[0];
|
|
248
|
+
if (key === "plugin") throw new Error(`Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`);
|
|
249
|
+
if (!key.startsWith(PLUGIN_PREFIX)) throw new Error(`Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. Use a bare string for ambient tools (e.g. \`- get_weather\`).`);
|
|
250
|
+
const pluginName = key.slice(7);
|
|
251
|
+
if (pluginName.length === 0) throw new Error(`Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`);
|
|
252
|
+
const value = entry[key];
|
|
253
|
+
if (Array.isArray(value)) return {
|
|
254
|
+
kind: "plugin",
|
|
255
|
+
pluginName,
|
|
256
|
+
opts: { only: value }
|
|
257
|
+
};
|
|
258
|
+
if (typeof value === "object" && value !== null) return {
|
|
259
|
+
kind: "plugin",
|
|
260
|
+
pluginName,
|
|
261
|
+
opts: value
|
|
262
|
+
};
|
|
263
|
+
throw new Error(`Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
export { agentIdFromMarkdownPath, loadAgentFromFile, loadAgentsFromDir, parseFrontmatter };
|
|
268
|
+
//# sourceMappingURL=load-agents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\nimport type {\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /**\n * Registered plugin toolkits referenced by `plugin:NAME` entries in the\n * unified `tools:` frontmatter list. Keyed by plugin name; each value\n * exposes the same `toolkit(opts?)` surface as the `plugins` argument to\n * `tools(plugins) => Record<...>` in the code form.\n */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n /**\n * Unified tool list. Each entry is one of:\n *\n * - **`plugin:<name>`** (string) — pull every tool from the named plugin.\n * - **`plugin:<name>: [tool1, tool2]`** — pull only the listed tools\n * (shorthand for `{ only: [...] }`).\n * - **`plugin:<name>: { ...ToolkitOptions }`** — pass full\n * `prefix` / `only` / `except` / `rename` options.\n * - **`<key>`** (string, no `plugin:` prefix) — ambient tool name\n * resolved against the `agents({ tools: { ... } })` config.\n *\n * Mirrors the TS function form `tools(plugins) { ... }` where plugin\n * tools and inline tools live in the same record.\n */\n tools?: FrontmatterToolEntry[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n maxSteps?: number;\n maxTokens?: number;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n}\n\n/**\n * Each item in {@link Frontmatter.tools}. Strings are either ambient tool\n * names (no prefix) or bare plugin references (`plugin:NAME`). Objects are\n * single-key mappings whose key is `plugin:NAME` and whose value is either\n * an array of local tool names (sugar for `{ only: [...] }`) or a full\n * `ToolkitOptions` record.\n *\n * Named `FrontmatterToolEntry` to avoid colliding with the exported\n * `ToolEntry` from `tools/define-tool.ts` — that is the plugin-author API\n * surface (`defineTool({ ... }) : ToolEntry`); this is the frontmatter\n * parse type. They are unrelated and live in different layers.\n */\ntype FrontmatterToolEntry =\n | string\n | { [key: string]: ToolkitOptions | string[] };\n\nconst PLUGIN_PREFIX = \"plugin:\";\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"tools\",\n \"agents\",\n \"maxSteps\",\n \"maxTokens\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n /** Reserved folder name until per-agent skills land; not an agent package. */\n const RESERVED_DIRS = new Set([\"skills\"]);\n\n const agentIds = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .filter((name) => !RESERVED_DIRS.has(name))\n .sort();\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new Error(\n `Agents subdirectory '${path.join(dir, id)}' must contain agent.md.`,\n );\n }\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!match) {\n return { data: null, content: raw.trim() };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(match[1]);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: match[2].trim() };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: match[2].trim() };\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const entry of fm.tools ?? []) {\n const parsed = parseToolEntry(entry, filePath, agentName);\n if (parsed.kind === \"plugin\") {\n const provider = pluginIdx.get(parsed.pluginName);\n if (!provider) {\n const available =\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`,\n );\n }\n const entries = provider.toolkit(parsed.opts) as Record<string, unknown>;\n for (const [key, value] of Object.entries(entries)) {\n if (!isToolkitEntry(value)) {\n throw new Error(\n `Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = value as ToolkitEntry;\n }\n } else {\n const tool = ctx.availableTools?.[parsed.toolName];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. ` +\n \"If you meant to reference a plugin, use the 'plugin:NAME' prefix.\",\n );\n }\n out[parsed.toolName] = tool;\n }\n }\n\n return out;\n}\n\ntype ParsedToolEntry =\n | { kind: \"plugin\"; pluginName: string; opts: ToolkitOptions | undefined }\n | { kind: \"ambient\"; toolName: string };\n\n/**\n * Classify one item in the `tools:` frontmatter list into either a plugin\n * reference (with optional ToolkitOptions) or an ambient tool lookup.\n *\n * Strings starting with `plugin:` are bare plugin references. Strings\n * without the prefix are ambient tool names. Object entries are\n * single-key mappings keyed by `plugin:NAME`; the value is either an\n * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.\n */\nfunction parseToolEntry(\n entry: FrontmatterToolEntry,\n filePath: string,\n agentName: string,\n): ParsedToolEntry {\n if (typeof entry === \"string\") {\n if (entry.startsWith(PLUGIN_PREFIX)) {\n const pluginName = entry.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n return { kind: \"plugin\", pluginName, opts: undefined };\n }\n if (entry.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`,\n );\n }\n return { kind: \"ambient\", toolName: entry };\n }\n if (typeof entry !== \"object\" || entry === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`,\n );\n }\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const key = keys[0];\n // Bare `- plugin:` (no name after the colon) parses as a mapping with the\n // key `\"plugin\"`. Catch that as a friendly error rather than dumping it\n // through the generic \"expected key 'plugin:NAME'\" branch.\n if (key === \"plugin\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n if (!key.startsWith(PLUGIN_PREFIX)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. ` +\n \"Use a bare string for ambient tools (e.g. `- get_weather`).\",\n );\n }\n const pluginName = key.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n const value = entry[key];\n if (Array.isArray(value)) {\n return { kind: \"plugin\", pluginName, opts: { only: value } };\n }\n if (typeof value === \"object\" && value !== null) {\n return {\n kind: \"plugin\",\n pluginName,\n opts: value as ToolkitOptions,\n };\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/core/agent/normalize-result.ts
|
|
2
|
+
/**
|
|
3
|
+
* Maximum serialized length of a tool result before we truncate with a
|
|
4
|
+
* human-readable marker. 50k chars is roughly ~12k tokens — enough for
|
|
5
|
+
* reasonable SQL result sets and JSON blobs, well short of the per-call
|
|
6
|
+
* context limits on current frontier models.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_TOOL_RESULT_CHARS = 5e4;
|
|
9
|
+
/**
|
|
10
|
+
* Normalise a raw tool-execution result for the LLM as a single string:
|
|
11
|
+
*
|
|
12
|
+
* - `undefined` → empty string. A `void` return is a legitimate outcome for
|
|
13
|
+
* side-effecting tools ("send notification"); surfacing `undefined` to the
|
|
14
|
+
* adapter would otherwise read as "execution failed".
|
|
15
|
+
* - strings are returned as-is.
|
|
16
|
+
* - `null` and every other shape are JSON-stringified (so `null` becomes
|
|
17
|
+
* the literal string `"null"`).
|
|
18
|
+
* - results longer than {@link MAX_TOOL_RESULT_CHARS} are truncated and
|
|
19
|
+
* annotated so the model sees the cut rather than silent data loss.
|
|
20
|
+
*
|
|
21
|
+
* Always returns `string`. Earlier shapes returned the raw object for short
|
|
22
|
+
* non-string results, which forced every adapter to repeat the same
|
|
23
|
+
* `typeof === "string" ? : JSON.stringify(...)` dance and gave the LLM
|
|
24
|
+
* different shapes for short-vs-long results without any observable benefit
|
|
25
|
+
* — every downstream consumer stringified the value at the wire boundary
|
|
26
|
+
* anyway.
|
|
27
|
+
*
|
|
28
|
+
* Pure function; safe to unit-test in isolation.
|
|
29
|
+
*/
|
|
30
|
+
function normalizeToolResult(result, maxChars = MAX_TOOL_RESULT_CHARS) {
|
|
31
|
+
if (result === void 0) return "";
|
|
32
|
+
const serialized = typeof result === "string" ? result : JSON.stringify(result);
|
|
33
|
+
if (serialized.length > maxChars) return `${serialized.slice(0, maxChars)}\n\n[Result truncated: ${serialized.length} chars exceeds ${maxChars} limit]`;
|
|
34
|
+
return serialized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { normalizeToolResult };
|
|
39
|
+
//# sourceMappingURL=normalize-result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-result.js","names":[],"sources":["../../../src/core/agent/normalize-result.ts"],"sourcesContent":["/**\n * Maximum serialized length of a tool result before we truncate with a\n * human-readable marker. 50k chars is roughly ~12k tokens — enough for\n * reasonable SQL result sets and JSON blobs, well short of the per-call\n * context limits on current frontier models.\n */\nexport const MAX_TOOL_RESULT_CHARS = 50_000;\n\n/**\n * Normalise a raw tool-execution result for the LLM as a single string:\n *\n * - `undefined` → empty string. A `void` return is a legitimate outcome for\n * side-effecting tools (\"send notification\"); surfacing `undefined` to the\n * adapter would otherwise read as \"execution failed\".\n * - strings are returned as-is.\n * - `null` and every other shape are JSON-stringified (so `null` becomes\n * the literal string `\"null\"`).\n * - results longer than {@link MAX_TOOL_RESULT_CHARS} are truncated and\n * annotated so the model sees the cut rather than silent data loss.\n *\n * Always returns `string`. Earlier shapes returned the raw object for short\n * non-string results, which forced every adapter to repeat the same\n * `typeof === \"string\" ? : JSON.stringify(...)` dance and gave the LLM\n * different shapes for short-vs-long results without any observable benefit\n * — every downstream consumer stringified the value at the wire boundary\n * anyway.\n *\n * Pure function; safe to unit-test in isolation.\n */\nexport function normalizeToolResult(\n result: unknown,\n maxChars: number = MAX_TOOL_RESULT_CHARS,\n): string {\n if (result === undefined) return \"\";\n const serialized =\n typeof result === \"string\" ? result : JSON.stringify(result);\n if (serialized.length > maxChars) {\n return `${serialized.slice(0, maxChars)}\\n\\n[Result truncated: ${serialized.length} chars exceeds ${maxChars} limit]`;\n }\n return serialized;\n}\n"],"mappings":";;;;;;;AAMA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;AAuBrC,SAAgB,oBACd,QACA,WAAmB,uBACX;AACR,KAAI,WAAW,OAAW,QAAO;CACjC,MAAM,aACJ,OAAO,WAAW,WAAW,SAAS,KAAK,UAAU,OAAO;AAC9D,KAAI,WAAW,SAAS,SACtB,QAAO,GAAG,WAAW,MAAM,GAAG,SAAS,CAAC,yBAAyB,WAAW,OAAO,iBAAiB,SAAS;AAE/G,QAAO"}
|