@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,979 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
import { Plugin } from "../../plugin/plugin.js";
|
|
3
|
+
import { toPlugin } from "../../plugin/to-plugin.js";
|
|
4
|
+
import "../../plugin/index.js";
|
|
5
|
+
import { buildMcpHostPolicy } from "../../connectors/mcp/host-policy.js";
|
|
6
|
+
import { AppKitMcpClient } from "../../connectors/mcp/client.js";
|
|
7
|
+
import "../../connectors/mcp/index.js";
|
|
8
|
+
import { consumeAdapterStream } from "../../core/agent/consume-adapter-stream.js";
|
|
9
|
+
import { createPluginsProxy } from "../../core/agent/plugins-map.js";
|
|
10
|
+
import { resolveToolkitFromProvider } from "../../core/agent/toolkit-resolver.js";
|
|
11
|
+
import { functionToolToDefinition, isFunctionTool } from "../../core/agent/tools/function-tool.js";
|
|
12
|
+
import { isHostedTool, resolveHostedTools } from "../../core/agent/tools/hosted-tools.js";
|
|
13
|
+
import { isToolkitEntry } from "../../core/agent/types.js";
|
|
14
|
+
import "../../core/agent/tools/index.js";
|
|
15
|
+
import { loadAgentsFromDir } from "../../core/agent/load-agents.js";
|
|
16
|
+
import { normalizeToolResult } from "../../core/agent/normalize-result.js";
|
|
17
|
+
import { buildBaseSystemPrompt, composeSystemPrompt } from "../../core/agent/system-prompt.js";
|
|
18
|
+
import { agentStreamDefaults } from "./defaults.js";
|
|
19
|
+
import { EventChannel } from "./event-channel.js";
|
|
20
|
+
import { AgentEventTranslator } from "./event-translator.js";
|
|
21
|
+
import manifest_default from "./manifest.js";
|
|
22
|
+
import { approvalRequestSchema, cancelRequestSchema, chatRequestSchema, invocationsRequestSchema } from "./schemas.js";
|
|
23
|
+
import { InMemoryThreadStore } from "./thread-store.js";
|
|
24
|
+
import { ToolApprovalGate } from "./tool-approval-gate.js";
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import pc from "picocolors";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
|
|
29
|
+
//#region src/plugins/agents/agents.ts
|
|
30
|
+
const logger = createLogger("agents");
|
|
31
|
+
const DEFAULT_AGENTS_DIR = "./config/agents";
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether a tool call must traverse the approval gate. Honours both
|
|
34
|
+
* the modern `effect` field (mutating values: write / update / destructive)
|
|
35
|
+
* and the legacy `destructive: true` boolean. The contract is documented on
|
|
36
|
+
* `ToolAnnotations.effect` in shared/agent.ts.
|
|
37
|
+
*
|
|
38
|
+
* Without this, a tool authored only with `effect: "destructive"` (the
|
|
39
|
+
* preferred API) bypassed the gate entirely.
|
|
40
|
+
*/
|
|
41
|
+
function requiresApproval(annotations) {
|
|
42
|
+
if (!annotations) return false;
|
|
43
|
+
if (annotations.destructive === true) return true;
|
|
44
|
+
switch (annotations.effect) {
|
|
45
|
+
case "write":
|
|
46
|
+
case "update":
|
|
47
|
+
case "destructive": return true;
|
|
48
|
+
case "read":
|
|
49
|
+
case void 0: return false;
|
|
50
|
+
default:
|
|
51
|
+
annotations.effect;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var AgentsPlugin = class extends Plugin {
|
|
56
|
+
static manifest = manifest_default;
|
|
57
|
+
static phase = "deferred";
|
|
58
|
+
agents = /* @__PURE__ */ new Map();
|
|
59
|
+
defaultAgentName = null;
|
|
60
|
+
activeStreams = /* @__PURE__ */ new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Per-user stream count, kept in sync with `activeStreams` so the
|
|
63
|
+
* concurrent-stream rate limit check is O(1) instead of O(n) over every
|
|
64
|
+
* active stream on every request. Mutated only via {@link trackStream}
|
|
65
|
+
* and {@link untrackStream}.
|
|
66
|
+
*/
|
|
67
|
+
userStreamCounts = /* @__PURE__ */ new Map();
|
|
68
|
+
mcpClient = null;
|
|
69
|
+
threadStore;
|
|
70
|
+
approvalGate = new ToolApprovalGate();
|
|
71
|
+
constructor(config) {
|
|
72
|
+
super(config);
|
|
73
|
+
this.config = config;
|
|
74
|
+
if (config.threadStore) this.threadStore = config.threadStore;
|
|
75
|
+
else {
|
|
76
|
+
this.threadStore = new InMemoryThreadStore();
|
|
77
|
+
if (process.env.NODE_ENV === "production") logger.warn("InMemoryThreadStore is in use in a production build (NODE_ENV=production). Thread history is unbounded and lost on restart. Pass agents({ threadStore: <persistent impl> }) for real deployments.");
|
|
78
|
+
else logger.info("Using default InMemoryThreadStore (dev-only — threads are lost on restart and grow without bound).");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Effective approval policy with defaults applied. Memoised so the
|
|
83
|
+
* `timeoutMs` validation warning fires at most once per plugin instance —
|
|
84
|
+
* `resolvedApprovalPolicy` gets hit on every chat stream and a noisy
|
|
85
|
+
* misconfig would otherwise spam the logs.
|
|
86
|
+
*
|
|
87
|
+
* `timeoutMs` is clamped to a 1s floor so a misconfigured value (`0`,
|
|
88
|
+
* negative, or `NaN`) can't degrade into immediate auto-denial of every
|
|
89
|
+
* mutating tool call.
|
|
90
|
+
*/
|
|
91
|
+
cachedApprovalPolicy = null;
|
|
92
|
+
get resolvedApprovalPolicy() {
|
|
93
|
+
if (this.cachedApprovalPolicy) return this.cachedApprovalPolicy;
|
|
94
|
+
const cfg = this.config.approval ?? {};
|
|
95
|
+
const APPROVAL_TIMEOUT_FLOOR_MS = 1e3;
|
|
96
|
+
const APPROVAL_TIMEOUT_DEFAULT_MS = 6e4;
|
|
97
|
+
let timeoutMs = cfg.timeoutMs ?? APPROVAL_TIMEOUT_DEFAULT_MS;
|
|
98
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < APPROVAL_TIMEOUT_FLOOR_MS) {
|
|
99
|
+
logger.warn("approval.timeoutMs=%s is below the %sms floor; using default %sms instead. Mutating tool calls would otherwise auto-deny before any UI could respond.", cfg.timeoutMs, APPROVAL_TIMEOUT_FLOOR_MS, APPROVAL_TIMEOUT_DEFAULT_MS);
|
|
100
|
+
timeoutMs = APPROVAL_TIMEOUT_DEFAULT_MS;
|
|
101
|
+
}
|
|
102
|
+
this.cachedApprovalPolicy = {
|
|
103
|
+
requireForDestructive: cfg.requireForDestructive ?? true,
|
|
104
|
+
timeoutMs
|
|
105
|
+
};
|
|
106
|
+
return this.cachedApprovalPolicy;
|
|
107
|
+
}
|
|
108
|
+
/** Effective DoS limits with defaults applied. */
|
|
109
|
+
get resolvedLimits() {
|
|
110
|
+
const cfg = this.config.limits ?? {};
|
|
111
|
+
return {
|
|
112
|
+
maxConcurrentStreamsPerUser: cfg.maxConcurrentStreamsPerUser ?? 5,
|
|
113
|
+
maxToolCalls: cfg.maxToolCalls ?? 50,
|
|
114
|
+
maxSubAgentDepth: cfg.maxSubAgentDepth ?? 3,
|
|
115
|
+
toolCallTimeoutMs: cfg.toolCallTimeoutMs ?? 3e5
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Count active streams owned by a given user. O(1). */
|
|
119
|
+
countUserStreams(userId) {
|
|
120
|
+
return this.userStreamCounts.get(userId) ?? 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Register a stream for `userId` and bump the per-user counter. Paired
|
|
124
|
+
* with {@link untrackStream}; the two helpers are the only writers to
|
|
125
|
+
* `activeStreams` + `userStreamCounts`, so the counter cannot drift from
|
|
126
|
+
* the map.
|
|
127
|
+
*/
|
|
128
|
+
trackStream(requestId, userId, controller) {
|
|
129
|
+
this.activeStreams.set(requestId, {
|
|
130
|
+
controller,
|
|
131
|
+
userId
|
|
132
|
+
});
|
|
133
|
+
this.userStreamCounts.set(userId, (this.userStreamCounts.get(userId) ?? 0) + 1);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Remove a stream from the active map and decrement the per-user
|
|
137
|
+
* counter. Idempotent — calling twice for the same `requestId` is a
|
|
138
|
+
* no-op (the second call sees no entry and returns early).
|
|
139
|
+
*/
|
|
140
|
+
untrackStream(requestId) {
|
|
141
|
+
const entry = this.activeStreams.get(requestId);
|
|
142
|
+
if (!entry) return;
|
|
143
|
+
this.activeStreams.delete(requestId);
|
|
144
|
+
const next = (this.userStreamCounts.get(entry.userId) ?? 0) - 1;
|
|
145
|
+
if (next <= 0) this.userStreamCounts.delete(entry.userId);
|
|
146
|
+
else this.userStreamCounts.set(entry.userId, next);
|
|
147
|
+
}
|
|
148
|
+
async setup() {
|
|
149
|
+
const { agents, defaultAgentName } = await this.buildAgentRegistry();
|
|
150
|
+
this.agents = agents;
|
|
151
|
+
this.defaultAgentName = defaultAgentName;
|
|
152
|
+
this.mountInvocationsRoute();
|
|
153
|
+
this.printRegistry();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reload agents from the configured directory, preserving code-defined
|
|
157
|
+
* agents. Builds a fresh registry first and only swaps on success — if
|
|
158
|
+
* `loadAgents` throws (malformed markdown, missing tool reference) the
|
|
159
|
+
* existing live registry stays in place and serving requests keep working.
|
|
160
|
+
*/
|
|
161
|
+
async reload() {
|
|
162
|
+
const next = await this.buildAgentRegistry();
|
|
163
|
+
this.agents = next.agents;
|
|
164
|
+
this.defaultAgentName = next.defaultAgentName;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Builds the agent registry into a fresh `Map` without touching live state.
|
|
168
|
+
* Called by both `setup` and `reload`; the latter only swaps the live
|
|
169
|
+
* registry once this resolves successfully (atomic reload).
|
|
170
|
+
*/
|
|
171
|
+
async buildAgentRegistry() {
|
|
172
|
+
const { defs: fileDefs, defaultAgent: fileDefault } = await this.loadFileDefinitions();
|
|
173
|
+
const codeDefs = this.config.agents ?? {};
|
|
174
|
+
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);
|
|
175
|
+
const merged = {};
|
|
176
|
+
for (const [name, def] of Object.entries(fileDefs)) merged[name] = {
|
|
177
|
+
def,
|
|
178
|
+
src: { origin: "file" }
|
|
179
|
+
};
|
|
180
|
+
for (const [name, def] of Object.entries(codeDefs)) merged[name] = {
|
|
181
|
+
def,
|
|
182
|
+
src: { origin: "code" }
|
|
183
|
+
};
|
|
184
|
+
const agents = /* @__PURE__ */ new Map();
|
|
185
|
+
let defaultAgentName = null;
|
|
186
|
+
if (Object.keys(merged).length === 0) {
|
|
187
|
+
logger.info("No agents registered (no files in %s, no code-defined agents)", this.resolvedAgentsDir() ?? "<disabled>");
|
|
188
|
+
return {
|
|
189
|
+
agents,
|
|
190
|
+
defaultAgentName
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
for (const [name, { def, src }] of Object.entries(merged)) try {
|
|
194
|
+
const registered = await this.buildRegisteredAgent(name, def, src);
|
|
195
|
+
agents.set(name, registered);
|
|
196
|
+
if (!defaultAgentName) defaultAgentName = name;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
throw new Error(`Failed to register agent '${name}' (${src.origin}): ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
|
|
199
|
+
}
|
|
200
|
+
if (this.config.defaultAgent) {
|
|
201
|
+
if (!agents.has(this.config.defaultAgent)) throw new Error(`defaultAgent '${this.config.defaultAgent}' is not registered. Available: ${Array.from(agents.keys()).join(", ")}`);
|
|
202
|
+
defaultAgentName = this.config.defaultAgent;
|
|
203
|
+
} else if (fileDefault && agents.has(fileDefault)) defaultAgentName = fileDefault;
|
|
204
|
+
return {
|
|
205
|
+
agents,
|
|
206
|
+
defaultAgentName
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
resolvedAgentsDir() {
|
|
210
|
+
if (this.config.dir === false) return null;
|
|
211
|
+
const dir = this.config.dir ?? DEFAULT_AGENTS_DIR;
|
|
212
|
+
return path.isAbsolute(dir) ? dir : path.resolve(process.cwd(), dir);
|
|
213
|
+
}
|
|
214
|
+
async loadFileDefinitions() {
|
|
215
|
+
const dir = this.resolvedAgentsDir();
|
|
216
|
+
if (!dir) return {
|
|
217
|
+
defs: {},
|
|
218
|
+
defaultAgent: null
|
|
219
|
+
};
|
|
220
|
+
const pluginToolProviders = this.pluginProviderIndex();
|
|
221
|
+
const ambient = this.config.tools ?? {};
|
|
222
|
+
return await loadAgentsFromDir(dir, {
|
|
223
|
+
defaultModel: this.config.defaultModel,
|
|
224
|
+
availableTools: ambient,
|
|
225
|
+
plugins: pluginToolProviders,
|
|
226
|
+
codeAgents: this.config.agents
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Builds the map of plugin-name → toolkit that the markdown loader consults
|
|
231
|
+
* when resolving `plugin:NAME` entries in the unified `tools:` frontmatter
|
|
232
|
+
* list (and, equivalently, that the code form passes as the `plugins`
|
|
233
|
+
* argument to `tools(plugins) => Record<...>`).
|
|
234
|
+
*/
|
|
235
|
+
pluginProviderIndex() {
|
|
236
|
+
const out = /* @__PURE__ */ new Map();
|
|
237
|
+
if (!this.context) return out;
|
|
238
|
+
for (const { name, provider } of this.context.getToolProviders()) {
|
|
239
|
+
const withToolkit = provider;
|
|
240
|
+
if (typeof withToolkit.toolkit === "function") out.set(name, { toolkit: withToolkit.toolkit.bind(withToolkit) });
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
async buildRegisteredAgent(name, def, src) {
|
|
245
|
+
const adapter = await this.resolveAdapter(def, name);
|
|
246
|
+
const toolIndex = await this.buildToolIndex(name, def, src);
|
|
247
|
+
return {
|
|
248
|
+
name,
|
|
249
|
+
instructions: def.instructions,
|
|
250
|
+
adapter,
|
|
251
|
+
toolIndex,
|
|
252
|
+
baseSystemPrompt: def.baseSystemPrompt,
|
|
253
|
+
maxSteps: def.maxSteps,
|
|
254
|
+
maxTokens: def.maxTokens,
|
|
255
|
+
ephemeral: def.ephemeral
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async resolveAdapter(def, name) {
|
|
259
|
+
const source = def.model ?? this.config.defaultModel;
|
|
260
|
+
const adapterOptions = {};
|
|
261
|
+
if (def.maxSteps !== void 0) adapterOptions.maxSteps = def.maxSteps;
|
|
262
|
+
if (def.maxTokens !== void 0) adapterOptions.maxTokens = def.maxTokens;
|
|
263
|
+
if (!source) {
|
|
264
|
+
const { DatabricksAdapter } = await import("../../agents/databricks.js");
|
|
265
|
+
try {
|
|
266
|
+
return await DatabricksAdapter.fromModelServing(void 0, adapterOptions);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
throw new Error(`Agent '${name}' has no model configured and no DATABRICKS_SERVING_ENDPOINT_NAME default available`, { cause: err instanceof Error ? err : void 0 });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (typeof source === "string") {
|
|
272
|
+
const { DatabricksAdapter } = await import("../../agents/databricks.js");
|
|
273
|
+
return DatabricksAdapter.fromModelServing(source, adapterOptions);
|
|
274
|
+
}
|
|
275
|
+
return await source;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Resolves an agent's tool record into a per-agent dispatch index. Connects
|
|
279
|
+
* hosted tools via MCP client. Applies `autoInheritTools` defaults when the
|
|
280
|
+
* definition has no declared tools/agents.
|
|
281
|
+
*/
|
|
282
|
+
async buildToolIndex(agentName, def, src) {
|
|
283
|
+
const index = /* @__PURE__ */ new Map();
|
|
284
|
+
const hasDeclaredTools = def.tools !== void 0;
|
|
285
|
+
const toolsRecord = this.resolveDefTools(agentName, def);
|
|
286
|
+
const hasExplicitSubAgents = def.agents && Object.keys(def.agents).length > 0;
|
|
287
|
+
const inheritDefaults = normalizeAutoInherit(this.config.autoInheritTools);
|
|
288
|
+
if (!hasDeclaredTools && !hasExplicitSubAgents && (src.origin === "file" ? inheritDefaults.file : inheritDefaults.code)) await this.applyAutoInherit(agentName, index);
|
|
289
|
+
for (const [childKey, childDef] of Object.entries(def.agents ?? {})) {
|
|
290
|
+
const toolName = `agent-${childKey}`;
|
|
291
|
+
index.set(toolName, {
|
|
292
|
+
source: "subagent",
|
|
293
|
+
agentName: childDef.name ?? childKey,
|
|
294
|
+
def: {
|
|
295
|
+
name: toolName,
|
|
296
|
+
description: childDef.instructions.slice(0, 120) || `Delegate to the ${childKey} sub-agent`,
|
|
297
|
+
parameters: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: { input: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "Message to send to the sub-agent."
|
|
302
|
+
} },
|
|
303
|
+
required: ["input"]
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const hostedToCollect = [];
|
|
309
|
+
for (const [key, tool] of Object.entries(toolsRecord)) {
|
|
310
|
+
if (isToolkitEntry(tool)) {
|
|
311
|
+
index.set(key, {
|
|
312
|
+
source: "toolkit",
|
|
313
|
+
pluginName: tool.pluginName,
|
|
314
|
+
localName: tool.localName,
|
|
315
|
+
def: {
|
|
316
|
+
...tool.def,
|
|
317
|
+
name: key
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (isFunctionTool(tool)) {
|
|
323
|
+
index.set(key, {
|
|
324
|
+
source: "function",
|
|
325
|
+
functionTool: tool,
|
|
326
|
+
def: {
|
|
327
|
+
...functionToolToDefinition(tool),
|
|
328
|
+
name: key
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (isHostedTool(tool)) {
|
|
334
|
+
hostedToCollect.push(tool);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
throw new Error(`Agent '${agentName}' tool '${key}' has an unrecognized shape`);
|
|
338
|
+
}
|
|
339
|
+
if (hostedToCollect.length > 0) await this.connectHostedTools(hostedToCollect, index);
|
|
340
|
+
return index;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Resolves an `AgentDefinition.tools` field to a plain tool record. The
|
|
344
|
+
* function form is invoked exactly once at agent setup with the typed
|
|
345
|
+
* {@link Plugins} map; the result replaces the function reference for the
|
|
346
|
+
* remainder of the registered agent's lifetime.
|
|
347
|
+
*
|
|
348
|
+
* Plain object form is returned as-is; an undefined `tools` returns an
|
|
349
|
+
* empty record. The function form is wrapped in a try/catch so a thrown
|
|
350
|
+
* callback fails registration with a useful message instead of leaking
|
|
351
|
+
* the raw stack.
|
|
352
|
+
*/
|
|
353
|
+
resolveDefTools(agentName, def) {
|
|
354
|
+
if (typeof def.tools !== "function") return def.tools ?? {};
|
|
355
|
+
try {
|
|
356
|
+
return def.tools(this.buildPluginsMap());
|
|
357
|
+
} catch (err) {
|
|
358
|
+
throw new Error(`Agent '${agentName}': tools(plugins) callback threw: ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Builds the typed {@link Plugins} map passed to the function form of
|
|
363
|
+
* `AgentDefinition.tools`. Each entry exposes the plugin instance directly
|
|
364
|
+
* (so user code can call typed instance methods including `.toolkit()`);
|
|
365
|
+
* plugins missing `.toolkit()` get a synthesized fallback that walks
|
|
366
|
+
* `getAgentTools()` via `resolveToolkitFromProvider`.
|
|
367
|
+
*
|
|
368
|
+
* Wrapped in {@link createPluginsProxy} so that accessing an unknown
|
|
369
|
+
* plugin name throws a named "not registered, Available: ..." error
|
|
370
|
+
* instead of bubbling up a generic `Cannot read properties of undefined`
|
|
371
|
+
* from the agent's `tools(plugins)` callback.
|
|
372
|
+
*/
|
|
373
|
+
buildPluginsMap() {
|
|
374
|
+
const out = {};
|
|
375
|
+
if (!this.context) return createPluginsProxy(out, `Agent '${this.name}': tools(plugins)`);
|
|
376
|
+
for (const { name, provider } of this.context.getToolProviders()) if (typeof provider.toolkit === "function") out[name] = provider;
|
|
377
|
+
else out[name] = { toolkit: (opts) => resolveToolkitFromProvider(name, provider, opts) };
|
|
378
|
+
return createPluginsProxy(out, `Agent '${this.name}': tools(plugins)`);
|
|
379
|
+
}
|
|
380
|
+
async applyAutoInherit(agentName, index) {
|
|
381
|
+
if (!this.context) return;
|
|
382
|
+
const inherited = [];
|
|
383
|
+
const skippedByPlugin = /* @__PURE__ */ new Map();
|
|
384
|
+
const recordSkip = (pluginName, localName) => {
|
|
385
|
+
const list = skippedByPlugin.get(pluginName) ?? [];
|
|
386
|
+
list.push(localName);
|
|
387
|
+
skippedByPlugin.set(pluginName, list);
|
|
388
|
+
};
|
|
389
|
+
for (const { name: pluginName, provider } of this.context.getToolProviders()) {
|
|
390
|
+
if (pluginName === this.name) continue;
|
|
391
|
+
const entries = resolveToolkitFromProvider(pluginName, provider);
|
|
392
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
393
|
+
if (entry.autoInheritable !== true) {
|
|
394
|
+
recordSkip(entry.pluginName, entry.localName);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
index.set(key, {
|
|
398
|
+
source: "toolkit",
|
|
399
|
+
pluginName: entry.pluginName,
|
|
400
|
+
localName: entry.localName,
|
|
401
|
+
def: {
|
|
402
|
+
...entry.def,
|
|
403
|
+
name: key
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
inherited.push(key);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (inherited.length > 0) logger.info("[agent %s] auto-inherited %d tool(s): %s", agentName, inherited.length, inherited.join(", "));
|
|
410
|
+
if (skippedByPlugin.size > 0) {
|
|
411
|
+
const summary = Array.from(skippedByPlugin.entries()).map(([p, tools]) => `${p}(${tools.length})`).join(", ");
|
|
412
|
+
logger.info("[agent %s] auto-inherit skipped %d tool(s) not marked autoInheritable: %s. Wire them explicitly via `tools:` if needed.", agentName, Array.from(skippedByPlugin.values()).reduce((n, list) => n + list.length, 0), summary);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async connectHostedTools(hostedTools, index) {
|
|
416
|
+
let host;
|
|
417
|
+
let authenticate;
|
|
418
|
+
try {
|
|
419
|
+
const { getWorkspaceClient } = await import("../../context/index.js");
|
|
420
|
+
const wsClient = getWorkspaceClient();
|
|
421
|
+
await wsClient.config.ensureResolved();
|
|
422
|
+
host = wsClient.config.host;
|
|
423
|
+
authenticate = async () => {
|
|
424
|
+
const headers = new Headers();
|
|
425
|
+
await wsClient.config.authenticate(headers);
|
|
426
|
+
return Object.fromEntries(headers.entries());
|
|
427
|
+
};
|
|
428
|
+
} catch {
|
|
429
|
+
host = process.env.DATABRICKS_HOST;
|
|
430
|
+
authenticate = async () => {
|
|
431
|
+
const token = process.env.DATABRICKS_TOKEN;
|
|
432
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (!host) {
|
|
436
|
+
logger.warn("No Databricks host available — skipping %d hosted tool(s)", hostedTools.length);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (!this.mcpClient) {
|
|
440
|
+
const policy = buildMcpHostPolicy(this.config.mcp, host);
|
|
441
|
+
this.mcpClient = new AppKitMcpClient(host, authenticate, policy);
|
|
442
|
+
}
|
|
443
|
+
const endpoints = resolveHostedTools(hostedTools);
|
|
444
|
+
const result = await this.mcpClient.connectAll(endpoints);
|
|
445
|
+
if (result.failed.length > 0) logger.warn("MCP: %s of %s endpoints failed to connect (%s). Agents that reference these endpoints will boot without their hosted tools.", result.failed.length, endpoints.length, result.failed.map((f) => f.name).join(", "));
|
|
446
|
+
for (const def of this.mcpClient.getAllToolDefinitions()) index.set(def.name, {
|
|
447
|
+
source: "mcp",
|
|
448
|
+
mcpToolName: def.name,
|
|
449
|
+
def
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
getAgentTools() {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
async executeAgentTool() {
|
|
456
|
+
throw new Error("AgentsPlugin does not expose executeAgentTool directly");
|
|
457
|
+
}
|
|
458
|
+
mountInvocationsRoute() {
|
|
459
|
+
if (!this.context) return;
|
|
460
|
+
this.context.addRoute("post", "/invocations", (req, res) => {
|
|
461
|
+
this._handleInvocations(req, res);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
injectRoutes(router) {
|
|
465
|
+
this.route(router, {
|
|
466
|
+
name: "chat",
|
|
467
|
+
method: "post",
|
|
468
|
+
path: "/chat",
|
|
469
|
+
handler: async (req, res) => this._handleChat(req, res)
|
|
470
|
+
});
|
|
471
|
+
this.route(router, {
|
|
472
|
+
name: "cancel",
|
|
473
|
+
method: "post",
|
|
474
|
+
path: "/cancel",
|
|
475
|
+
handler: async (req, res) => this._handleCancel(req, res)
|
|
476
|
+
});
|
|
477
|
+
this.route(router, {
|
|
478
|
+
name: "approve",
|
|
479
|
+
method: "post",
|
|
480
|
+
path: "/approve",
|
|
481
|
+
handler: async (req, res) => this._handleApprove(req, res)
|
|
482
|
+
});
|
|
483
|
+
this.route(router, {
|
|
484
|
+
name: "threads",
|
|
485
|
+
method: "get",
|
|
486
|
+
path: "/threads",
|
|
487
|
+
handler: async (req, res) => this._handleListThreads(req, res)
|
|
488
|
+
});
|
|
489
|
+
this.route(router, {
|
|
490
|
+
name: "thread",
|
|
491
|
+
method: "get",
|
|
492
|
+
path: "/threads/:threadId",
|
|
493
|
+
handler: async (req, res) => this._handleGetThread(req, res)
|
|
494
|
+
});
|
|
495
|
+
this.route(router, {
|
|
496
|
+
name: "deleteThread",
|
|
497
|
+
method: "delete",
|
|
498
|
+
path: "/threads/:threadId",
|
|
499
|
+
handler: async (req, res) => this._handleDeleteThread(req, res)
|
|
500
|
+
});
|
|
501
|
+
this.route(router, {
|
|
502
|
+
name: "info",
|
|
503
|
+
method: "get",
|
|
504
|
+
path: "/info",
|
|
505
|
+
handler: async (_req, res) => {
|
|
506
|
+
res.json({
|
|
507
|
+
agents: Array.from(this.agents.keys()),
|
|
508
|
+
defaultAgent: this.defaultAgentName
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
clientConfig() {
|
|
514
|
+
return {
|
|
515
|
+
agents: Array.from(this.agents.keys()),
|
|
516
|
+
defaultAgent: this.defaultAgentName
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
async _handleChat(req, res) {
|
|
520
|
+
const parsed = chatRequestSchema.safeParse(req.body);
|
|
521
|
+
if (!parsed.success) {
|
|
522
|
+
res.status(400).json({
|
|
523
|
+
error: "Invalid request",
|
|
524
|
+
details: parsed.error.flatten().fieldErrors
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const { message, threadId, agent: agentName } = parsed.data;
|
|
529
|
+
const registered = this.resolveAgent(agentName);
|
|
530
|
+
if (!registered) {
|
|
531
|
+
res.status(400).json({ error: agentName ? `Agent "${agentName}" not found` : "No agent registered" });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const userId = this.resolveUserId(req);
|
|
535
|
+
const limits = this.resolvedLimits;
|
|
536
|
+
if (this.countUserStreams(userId) >= limits.maxConcurrentStreamsPerUser) {
|
|
537
|
+
res.setHeader("Retry-After", "5");
|
|
538
|
+
res.status(429).json({ error: `Too many concurrent streams for this user (limit ${limits.maxConcurrentStreamsPerUser}). Wait for an existing stream to complete before starting another.` });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
let thread;
|
|
542
|
+
try {
|
|
543
|
+
const existing = threadId ? await this.threadStore.get(threadId, userId) : null;
|
|
544
|
+
if (threadId && !existing) {
|
|
545
|
+
res.status(404).json({ error: `Thread ${threadId} not found` });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
thread = existing ?? await this.threadStore.create(userId);
|
|
549
|
+
const userMessage = {
|
|
550
|
+
id: randomUUID(),
|
|
551
|
+
role: "user",
|
|
552
|
+
content: message,
|
|
553
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
554
|
+
};
|
|
555
|
+
await this.threadStore.addMessage(thread.id, userId, userMessage);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
logger.error("threadStore failed in /chat: %O", err);
|
|
558
|
+
res.status(500).json({ error: "Thread operation failed" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
return this._streamAgent(req, res, registered, thread, userId);
|
|
562
|
+
}
|
|
563
|
+
async _handleInvocations(req, res) {
|
|
564
|
+
const parsed = invocationsRequestSchema.safeParse(req.body);
|
|
565
|
+
if (!parsed.success) {
|
|
566
|
+
res.status(400).json({
|
|
567
|
+
error: "Invalid request",
|
|
568
|
+
details: parsed.error.flatten().fieldErrors
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const { input } = parsed.data;
|
|
573
|
+
const registered = this.resolveAgent();
|
|
574
|
+
if (!registered) {
|
|
575
|
+
res.status(400).json({ error: "No agent registered" });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const userId = this.resolveUserId(req);
|
|
579
|
+
const limits = this.resolvedLimits;
|
|
580
|
+
if (this.countUserStreams(userId) >= limits.maxConcurrentStreamsPerUser) {
|
|
581
|
+
res.setHeader("Retry-After", "5");
|
|
582
|
+
res.status(429).json({ error: `Too many concurrent streams for this user (limit ${limits.maxConcurrentStreamsPerUser}). Wait for an existing stream to complete before starting another.` });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
let thread;
|
|
586
|
+
try {
|
|
587
|
+
thread = await this.threadStore.create(userId);
|
|
588
|
+
if (typeof input === "string") await this.threadStore.addMessage(thread.id, userId, {
|
|
589
|
+
id: randomUUID(),
|
|
590
|
+
role: "user",
|
|
591
|
+
content: input,
|
|
592
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
593
|
+
});
|
|
594
|
+
else for (const item of input) {
|
|
595
|
+
const role = item.role ?? "user";
|
|
596
|
+
const content = typeof item.content === "string" ? item.content : JSON.stringify(item.content ?? "");
|
|
597
|
+
if (!content) continue;
|
|
598
|
+
await this.threadStore.addMessage(thread.id, userId, {
|
|
599
|
+
id: randomUUID(),
|
|
600
|
+
role,
|
|
601
|
+
content,
|
|
602
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
} catch (err) {
|
|
606
|
+
logger.error("threadStore failed in /invocations: %O", err);
|
|
607
|
+
res.status(500).json({ error: "Thread operation failed" });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
return this._streamAgent(req, res, registered, thread, userId);
|
|
611
|
+
}
|
|
612
|
+
async _streamAgent(req, res, registered, thread, userId) {
|
|
613
|
+
const abortController = new AbortController();
|
|
614
|
+
const signal = abortController.signal;
|
|
615
|
+
const requestId = randomUUID();
|
|
616
|
+
this.trackStream(requestId, userId, abortController);
|
|
617
|
+
const tools = Array.from(registered.toolIndex.values()).map((e) => e.def);
|
|
618
|
+
const approvalPolicy = this.resolvedApprovalPolicy;
|
|
619
|
+
const limits = this.resolvedLimits;
|
|
620
|
+
const outboundEvents = new EventChannel();
|
|
621
|
+
const translator = new AgentEventTranslator();
|
|
622
|
+
const runState = {
|
|
623
|
+
req,
|
|
624
|
+
userId,
|
|
625
|
+
requestId,
|
|
626
|
+
abortController,
|
|
627
|
+
signal,
|
|
628
|
+
approvalPolicy,
|
|
629
|
+
limits,
|
|
630
|
+
translator,
|
|
631
|
+
outboundEvents,
|
|
632
|
+
toolCallsUsed: { count: 0 }
|
|
633
|
+
};
|
|
634
|
+
const executeTool = (name, args) => this.dispatchToolCall(runState, registered.toolIndex, name, args, 0);
|
|
635
|
+
const driver = (async () => {
|
|
636
|
+
try {
|
|
637
|
+
for (const evt of translator.translate({
|
|
638
|
+
type: "metadata",
|
|
639
|
+
data: { threadId: thread.id }
|
|
640
|
+
})) outboundEvents.push(evt);
|
|
641
|
+
const pluginNames = this.context ? this.context.getPluginNames().filter((n) => n !== this.name && n !== "server") : [];
|
|
642
|
+
const messagesWithSystem = [{
|
|
643
|
+
id: "system",
|
|
644
|
+
role: "system",
|
|
645
|
+
content: composePromptForAgent(registered, this.config.baseSystemPrompt, {
|
|
646
|
+
agentName: registered.name,
|
|
647
|
+
pluginNames,
|
|
648
|
+
toolNames: tools.map((t) => t.name)
|
|
649
|
+
}),
|
|
650
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
651
|
+
}, ...thread.messages];
|
|
652
|
+
const fullContent = await consumeAdapterStream(registered.adapter.run({
|
|
653
|
+
messages: messagesWithSystem,
|
|
654
|
+
tools,
|
|
655
|
+
threadId: thread.id,
|
|
656
|
+
signal
|
|
657
|
+
}, {
|
|
658
|
+
executeTool,
|
|
659
|
+
signal
|
|
660
|
+
}), {
|
|
661
|
+
signal,
|
|
662
|
+
onEvent: (event) => {
|
|
663
|
+
for (const translated of translator.translate(event)) outboundEvents.push(translated);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
if (fullContent) await this.threadStore.addMessage(thread.id, userId, {
|
|
667
|
+
id: randomUUID(),
|
|
668
|
+
role: "assistant",
|
|
669
|
+
content: fullContent,
|
|
670
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
671
|
+
});
|
|
672
|
+
for (const evt of translator.finalize()) outboundEvents.push(evt);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (signal.aborted) {
|
|
675
|
+
outboundEvents.close();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
logger.error("Agent chat error: %O", error);
|
|
679
|
+
outboundEvents.close(error);
|
|
680
|
+
return;
|
|
681
|
+
} finally {
|
|
682
|
+
this.approvalGate.abortStream(requestId);
|
|
683
|
+
this.untrackStream(requestId);
|
|
684
|
+
if (registered.ephemeral) try {
|
|
685
|
+
await this.threadStore.delete(thread.id, userId);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
logger.warn("Failed to delete ephemeral thread %s: %O", thread.id, err);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
outboundEvents.close();
|
|
691
|
+
})();
|
|
692
|
+
await this.executeStream(res, async function* () {
|
|
693
|
+
try {
|
|
694
|
+
for await (const ev of outboundEvents) yield ev;
|
|
695
|
+
} finally {
|
|
696
|
+
await driver.catch(() => void 0);
|
|
697
|
+
}
|
|
698
|
+
}, {
|
|
699
|
+
...agentStreamDefaults,
|
|
700
|
+
stream: {
|
|
701
|
+
...agentStreamDefaults.stream,
|
|
702
|
+
streamId: requestId
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Dispatch a single tool call from either the top-level adapter or a
|
|
708
|
+
* sub-agent. Centralising this in one method is what makes the budget
|
|
709
|
+
* counter, approval gate, and abort signal observe sub-agent activity:
|
|
710
|
+
* `runSubAgent` reuses the same `runState` and so increments the same
|
|
711
|
+
* counter and emits approval events through the same channel.
|
|
712
|
+
*
|
|
713
|
+
* `depth` is the current sub-agent recursion depth (0 at the top level).
|
|
714
|
+
* It is forwarded to `runSubAgent` when the dispatched entry is itself a
|
|
715
|
+
* sub-agent, so depth limits remain enforced.
|
|
716
|
+
*/
|
|
717
|
+
async dispatchToolCall(runState, toolIndex, name, args, depth) {
|
|
718
|
+
if (runState.toolCallsUsed.count >= runState.limits.maxToolCalls) {
|
|
719
|
+
runState.abortController.abort(/* @__PURE__ */ new Error(`Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}).`));
|
|
720
|
+
throw new Error(`Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}). Raise agents({ limits: { maxToolCalls } }) or review the agent's tool-selection logic.`);
|
|
721
|
+
}
|
|
722
|
+
runState.toolCallsUsed.count++;
|
|
723
|
+
const entry = toolIndex.get(name);
|
|
724
|
+
if (!entry) throw new Error(`Unknown tool: ${name}`);
|
|
725
|
+
if (runState.approvalPolicy.requireForDestructive && requiresApproval(entry.def.annotations)) {
|
|
726
|
+
const approvalId = randomUUID();
|
|
727
|
+
for (const ev of runState.translator.translate({
|
|
728
|
+
type: "approval_pending",
|
|
729
|
+
approvalId,
|
|
730
|
+
streamId: runState.requestId,
|
|
731
|
+
toolName: name,
|
|
732
|
+
args,
|
|
733
|
+
annotations: entry.def.annotations
|
|
734
|
+
})) runState.outboundEvents.push(ev);
|
|
735
|
+
if (await this.approvalGate.wait({
|
|
736
|
+
approvalId,
|
|
737
|
+
streamId: runState.requestId,
|
|
738
|
+
userId: runState.userId,
|
|
739
|
+
timeoutMs: runState.approvalPolicy.timeoutMs
|
|
740
|
+
}) === "deny") return `Tool execution denied by user approval gate (tool: ${name}).`;
|
|
741
|
+
}
|
|
742
|
+
let result;
|
|
743
|
+
if (entry.source === "toolkit") {
|
|
744
|
+
if (!this.context) throw new Error("Plugin tool execution requires PluginContext; this should never happen through createApp");
|
|
745
|
+
result = await this.context.executeTool(runState.req, entry.pluginName, entry.localName, args, runState.signal, runState.limits.toolCallTimeoutMs);
|
|
746
|
+
} else if (entry.source === "function") {
|
|
747
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) throw new Error(`Function tool '${name}' received non-object arguments (got ${args === null ? "null" : Array.isArray(args) ? "array" : typeof args}); expected a JSON object.`);
|
|
748
|
+
result = await entry.functionTool.execute(args);
|
|
749
|
+
} else if (entry.source === "mcp") {
|
|
750
|
+
if (!this.mcpClient) throw new Error("MCP client not connected");
|
|
751
|
+
const oboToken = runState.req.headers["x-forwarded-access-token"];
|
|
752
|
+
const mcpAuth = typeof oboToken === "string" ? { Authorization: `Bearer ${oboToken}` } : void 0;
|
|
753
|
+
result = await this.mcpClient.callTool(entry.mcpToolName, args, mcpAuth);
|
|
754
|
+
} else if (entry.source === "subagent") {
|
|
755
|
+
const childAgent = this.agents.get(entry.agentName);
|
|
756
|
+
if (!childAgent) throw new Error(`Sub-agent not found: ${entry.agentName}`);
|
|
757
|
+
result = await this.runSubAgent(runState, childAgent, args, depth + 1);
|
|
758
|
+
}
|
|
759
|
+
return normalizeToolResult(result);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Runs a sub-agent in response to an `agent-<key>` tool call. Returns the
|
|
763
|
+
* concatenated text output to hand back to the parent adapter as the tool
|
|
764
|
+
* result.
|
|
765
|
+
*
|
|
766
|
+
* `depth` starts at 1 for a top-level sub-agent invocation (i.e. the
|
|
767
|
+
* outer `_streamAgent` calls `runSubAgent(..., 1)`) and increments on
|
|
768
|
+
* each nested `runSubAgent` call. Depths exceeding
|
|
769
|
+
* `limits.maxSubAgentDepth` are rejected before any adapter work.
|
|
770
|
+
*
|
|
771
|
+
* Sub-agent tool calls run through `dispatchToolCall` with the same
|
|
772
|
+
* `runState` as the parent — the budget counter and approval gate are
|
|
773
|
+
* therefore enforced for every nested call, not only at the top level.
|
|
774
|
+
*/
|
|
775
|
+
async runSubAgent(runState, child, args, depth) {
|
|
776
|
+
if (depth > runState.limits.maxSubAgentDepth) throw new Error(`Sub-agent depth exceeded (limit ${runState.limits.maxSubAgentDepth}). Raise agents({ limits: { maxSubAgentDepth } }) or break the delegation cycle.`);
|
|
777
|
+
const input = typeof args === "object" && args !== null && typeof args.input === "string" ? args.input : JSON.stringify(args);
|
|
778
|
+
const childTools = Array.from(child.toolIndex.values()).map((e) => e.def);
|
|
779
|
+
const childExecute = (name, childArgs) => this.dispatchToolCall(runState, child.toolIndex, name, childArgs, depth);
|
|
780
|
+
const runContext = {
|
|
781
|
+
executeTool: childExecute,
|
|
782
|
+
signal: runState.signal
|
|
783
|
+
};
|
|
784
|
+
const pluginNames = this.context ? this.context.getPluginNames().filter((n) => n !== this.name && n !== "server") : [];
|
|
785
|
+
const messages = [{
|
|
786
|
+
id: "system",
|
|
787
|
+
role: "system",
|
|
788
|
+
content: composePromptForAgent(child, this.config.baseSystemPrompt, {
|
|
789
|
+
agentName: child.name,
|
|
790
|
+
pluginNames,
|
|
791
|
+
toolNames: childTools.map((t) => t.name)
|
|
792
|
+
}),
|
|
793
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
794
|
+
}, {
|
|
795
|
+
id: randomUUID(),
|
|
796
|
+
role: "user",
|
|
797
|
+
content: input,
|
|
798
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
799
|
+
}];
|
|
800
|
+
return consumeAdapterStream(child.adapter.run({
|
|
801
|
+
messages,
|
|
802
|
+
tools: childTools,
|
|
803
|
+
threadId: randomUUID(),
|
|
804
|
+
signal: runState.signal
|
|
805
|
+
}, runContext), {
|
|
806
|
+
signal: runState.signal,
|
|
807
|
+
onEvent: (event) => {
|
|
808
|
+
if (event.type === "metadata") return;
|
|
809
|
+
for (const translated of runState.translator.translate(event)) runState.outboundEvents.push(translated);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
async _handleCancel(req, res) {
|
|
814
|
+
const parsed = cancelRequestSchema.safeParse(req.body);
|
|
815
|
+
if (!parsed.success) {
|
|
816
|
+
res.status(400).json({
|
|
817
|
+
error: "Invalid request",
|
|
818
|
+
details: parsed.error.flatten().fieldErrors
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const { streamId } = parsed.data;
|
|
823
|
+
const entry = this.activeStreams.get(streamId);
|
|
824
|
+
if (!entry) {
|
|
825
|
+
res.json({ cancelled: true });
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const userId = this.resolveUserId(req);
|
|
829
|
+
if (entry.userId !== userId) {
|
|
830
|
+
res.status(403).json({ error: "Forbidden" });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
entry.controller.abort("Cancelled by user");
|
|
834
|
+
this.untrackStream(streamId);
|
|
835
|
+
this.approvalGate.abortStream(streamId);
|
|
836
|
+
res.json({ cancelled: true });
|
|
837
|
+
}
|
|
838
|
+
async _handleApprove(req, res) {
|
|
839
|
+
const parsed = approvalRequestSchema.safeParse(req.body);
|
|
840
|
+
if (!parsed.success) {
|
|
841
|
+
res.status(400).json({
|
|
842
|
+
error: "Invalid request",
|
|
843
|
+
details: parsed.error.flatten().fieldErrors
|
|
844
|
+
});
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const { streamId, approvalId, decision } = parsed.data;
|
|
848
|
+
const streamEntry = this.activeStreams.get(streamId);
|
|
849
|
+
if (!streamEntry) {
|
|
850
|
+
res.status(404).json({ error: "Stream not found or already completed" });
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const userId = this.resolveUserId(req);
|
|
854
|
+
if (streamEntry.userId !== userId) {
|
|
855
|
+
res.status(403).json({ error: "Forbidden" });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const result = this.approvalGate.submit({
|
|
859
|
+
approvalId,
|
|
860
|
+
userId,
|
|
861
|
+
decision
|
|
862
|
+
});
|
|
863
|
+
if (!result.ok) {
|
|
864
|
+
if (result.reason === "forbidden") {
|
|
865
|
+
res.status(403).json({ error: "Forbidden" });
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
res.status(404).json({ error: "Approval not found or already settled" });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
res.json({ decision });
|
|
872
|
+
}
|
|
873
|
+
async _handleListThreads(req, res) {
|
|
874
|
+
const userId = this.resolveUserId(req);
|
|
875
|
+
const threads = await this.threadStore.list(userId);
|
|
876
|
+
res.json({ threads });
|
|
877
|
+
}
|
|
878
|
+
async _handleGetThread(req, res) {
|
|
879
|
+
const userId = this.resolveUserId(req);
|
|
880
|
+
const thread = await this.threadStore.get(req.params.threadId, userId);
|
|
881
|
+
if (!thread) {
|
|
882
|
+
res.status(404).json({ error: "Thread not found" });
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
res.json(thread);
|
|
886
|
+
}
|
|
887
|
+
async _handleDeleteThread(req, res) {
|
|
888
|
+
const userId = this.resolveUserId(req);
|
|
889
|
+
if (!await this.threadStore.delete(req.params.threadId, userId)) {
|
|
890
|
+
res.status(404).json({ error: "Thread not found" });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
res.json({ deleted: true });
|
|
894
|
+
}
|
|
895
|
+
resolveAgent(name) {
|
|
896
|
+
if (name) return this.agents.get(name) ?? null;
|
|
897
|
+
if (this.defaultAgentName) return this.agents.get(this.defaultAgentName) ?? null;
|
|
898
|
+
const first = this.agents.values().next();
|
|
899
|
+
return first.done ? null : first.value;
|
|
900
|
+
}
|
|
901
|
+
printRegistry() {
|
|
902
|
+
if (this.agents.size === 0) return;
|
|
903
|
+
console.log("");
|
|
904
|
+
console.log(` ${pc.bold("Agents")} ${pc.dim(`(${this.agents.size})`)}`);
|
|
905
|
+
console.log(` ${pc.dim("─".repeat(60))}`);
|
|
906
|
+
for (const [name, reg] of this.agents) {
|
|
907
|
+
const tools = reg.toolIndex.size;
|
|
908
|
+
const marker = name === this.defaultAgentName ? pc.green("●") : " ";
|
|
909
|
+
console.log(` ${marker} ${pc.bold(name.padEnd(24))} ${pc.dim(`${tools} tools`)}`);
|
|
910
|
+
}
|
|
911
|
+
console.log(` ${pc.dim("─".repeat(60))}`);
|
|
912
|
+
console.log("");
|
|
913
|
+
}
|
|
914
|
+
async shutdown() {
|
|
915
|
+
this.approvalGate.abortAll();
|
|
916
|
+
if (this.mcpClient) {
|
|
917
|
+
await this.mcpClient.close();
|
|
918
|
+
this.mcpClient = null;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
exports() {
|
|
922
|
+
return {
|
|
923
|
+
register: (name, def) => this.registerCodeAgent(name, def),
|
|
924
|
+
list: () => Array.from(this.agents.keys()),
|
|
925
|
+
get: (name) => this.agents.get(name) ?? null,
|
|
926
|
+
reload: () => this.reload(),
|
|
927
|
+
getDefault: () => this.defaultAgentName,
|
|
928
|
+
getThreads: (userId) => this.threadStore.list(userId)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async registerCodeAgent(name, def) {
|
|
932
|
+
const registered = await this.buildRegisteredAgent(name, def, { origin: "code" });
|
|
933
|
+
this.agents.set(name, registered);
|
|
934
|
+
if (!this.defaultAgentName) this.defaultAgentName = name;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
function normalizeAutoInherit(value) {
|
|
938
|
+
if (value === void 0) return {
|
|
939
|
+
file: false,
|
|
940
|
+
code: false
|
|
941
|
+
};
|
|
942
|
+
if (typeof value === "boolean") return {
|
|
943
|
+
file: value,
|
|
944
|
+
code: value
|
|
945
|
+
};
|
|
946
|
+
return {
|
|
947
|
+
file: value.file ?? false,
|
|
948
|
+
code: value.code ?? false
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function composePromptForAgent(registered, pluginLevel, ctx) {
|
|
952
|
+
const perAgent = registered.baseSystemPrompt;
|
|
953
|
+
const resolved = perAgent !== void 0 ? perAgent : pluginLevel;
|
|
954
|
+
let base = "";
|
|
955
|
+
if (resolved === false) base = "";
|
|
956
|
+
else if (typeof resolved === "string") base = resolved;
|
|
957
|
+
else if (typeof resolved === "function") base = resolved(ctx);
|
|
958
|
+
else base = buildBaseSystemPrompt(ctx);
|
|
959
|
+
return composeSystemPrompt(base, registered.instructions);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Plugin factory for the agents plugin. Reads `config/agents/*.md` by default,
|
|
963
|
+
* resolves toolkits/tools from registered plugins, exposes `appkit.agents.*`
|
|
964
|
+
* runtime API and mounts `/invocations`.
|
|
965
|
+
*
|
|
966
|
+
* @example
|
|
967
|
+
* ```ts
|
|
968
|
+
* import { agents, analytics, createApp, server } from "@databricks/appkit";
|
|
969
|
+
*
|
|
970
|
+
* await createApp({
|
|
971
|
+
* plugins: [server(), analytics(), agents()],
|
|
972
|
+
* });
|
|
973
|
+
* ```
|
|
974
|
+
*/
|
|
975
|
+
const agents = toPlugin(AgentsPlugin);
|
|
976
|
+
|
|
977
|
+
//#endregion
|
|
978
|
+
export { AgentsPlugin, agents };
|
|
979
|
+
//# sourceMappingURL=agents.js.map
|