@mongodb-js/agent-engine-sdk-langgraph 0.11.3
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/CHANGELOG.md +39 -0
- package/LICENSE.md +201 -0
- package/README.md +149 -0
- package/dist/agent.d.ts +53 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +508 -0
- package/dist/agent.js.map +1 -0
- package/dist/backends/tool_sandbox.d.ts +15 -0
- package/dist/backends/tool_sandbox.d.ts.map +1 -0
- package/dist/backends/tool_sandbox.js +15 -0
- package/dist/backends/tool_sandbox.js.map +1 -0
- package/dist/backends/toolpod.d.ts +68 -0
- package/dist/backends/toolpod.d.ts.map +1 -0
- package/dist/backends/toolpod.js +451 -0
- package/dist/backends/toolpod.js.map +1 -0
- package/dist/call_interrupt.d.ts +20 -0
- package/dist/call_interrupt.d.ts.map +1 -0
- package/dist/call_interrupt.js +23 -0
- package/dist/call_interrupt.js.map +1 -0
- package/dist/checkpoint_branch.d.ts +58 -0
- package/dist/checkpoint_branch.d.ts.map +1 -0
- package/dist/checkpoint_branch.js +239 -0
- package/dist/checkpoint_branch.js.map +1 -0
- package/dist/checkpointer.d.ts +21 -0
- package/dist/checkpointer.d.ts.map +1 -0
- package/dist/checkpointer.js +31 -0
- package/dist/checkpointer.js.map +1 -0
- package/dist/deep_agent.d.ts +49 -0
- package/dist/deep_agent.d.ts.map +1 -0
- package/dist/deep_agent.js +110 -0
- package/dist/deep_agent.js.map +1 -0
- package/dist/deep_agent_checkpointer.d.ts +11 -0
- package/dist/deep_agent_checkpointer.d.ts.map +1 -0
- package/dist/deep_agent_checkpointer.js +19 -0
- package/dist/deep_agent_checkpointer.js.map +1 -0
- package/dist/deep_agent_task.d.ts +26 -0
- package/dist/deep_agent_task.d.ts.map +1 -0
- package/dist/deep_agent_task.js +49 -0
- package/dist/deep_agent_task.js.map +1 -0
- package/dist/durable_deep_agent.d.ts +23 -0
- package/dist/durable_deep_agent.d.ts.map +1 -0
- package/dist/durable_deep_agent.js +176 -0
- package/dist/durable_deep_agent.js.map +1 -0
- package/dist/durable_message_identity.d.ts +5 -0
- package/dist/durable_message_identity.d.ts.map +1 -0
- package/dist/durable_message_identity.js +106 -0
- package/dist/durable_message_identity.js.map +1 -0
- package/dist/durable_session.d.ts +30 -0
- package/dist/durable_session.d.ts.map +1 -0
- package/dist/durable_session.js +400 -0
- package/dist/durable_session.js.map +1 -0
- package/dist/durable_subgraphs.d.ts +13 -0
- package/dist/durable_subgraphs.d.ts.map +1 -0
- package/dist/durable_subgraphs.js +82 -0
- package/dist/durable_subgraphs.js.map +1 -0
- package/dist/durable_tools.d.ts +12 -0
- package/dist/durable_tools.d.ts.map +1 -0
- package/dist/durable_tools.js +41 -0
- package/dist/durable_tools.js.map +1 -0
- package/dist/execution_session.d.ts +63 -0
- package/dist/execution_session.d.ts.map +1 -0
- package/dist/execution_session.js +300 -0
- package/dist/execution_session.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/llm_adapter.d.ts +41 -0
- package/dist/llm_adapter.d.ts.map +1 -0
- package/dist/llm_adapter.js +177 -0
- package/dist/llm_adapter.js.map +1 -0
- package/dist/messages.d.ts +56 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +592 -0
- package/dist/messages.js.map +1 -0
- package/dist/node_logger_adapter.d.ts +51 -0
- package/dist/node_logger_adapter.d.ts.map +1 -0
- package/dist/node_logger_adapter.js +163 -0
- package/dist/node_logger_adapter.js.map +1 -0
- package/dist/platform_checkpointer.d.ts +70 -0
- package/dist/platform_checkpointer.d.ts.map +1 -0
- package/dist/platform_checkpointer.js +252 -0
- package/dist/platform_checkpointer.js.map +1 -0
- package/dist/query.d.ts +103 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +623 -0
- package/dist/query.js.map +1 -0
- package/dist/runtime.d.ts +336 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +881 -0
- package/dist/runtime.js.map +1 -0
- package/dist/secure_llm.d.ts +93 -0
- package/dist/secure_llm.d.ts.map +1 -0
- package/dist/secure_llm.js +423 -0
- package/dist/secure_llm.js.map +1 -0
- package/dist/session_factory.d.ts +6 -0
- package/dist/session_factory.d.ts.map +1 -0
- package/dist/session_factory.js +29 -0
- package/dist/session_factory.js.map +1 -0
- package/dist/session_fork.d.ts +76 -0
- package/dist/session_fork.d.ts.map +1 -0
- package/dist/session_fork.js +428 -0
- package/dist/session_fork.js.map +1 -0
- package/dist/subagents.d.ts +37 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +67 -0
- package/dist/subagents.js.map +1 -0
- package/dist/suspend.d.ts +27 -0
- package/dist/suspend.d.ts.map +1 -0
- package/dist/suspend.js +87 -0
- package/dist/suspend.js.map +1 -0
- package/dist/thread_id.d.ts +30 -0
- package/dist/thread_id.d.ts.map +1 -0
- package/dist/thread_id.js +69 -0
- package/dist/thread_id.js.map +1 -0
- package/dist/workflow_json.d.ts +11 -0
- package/dist/workflow_json.d.ts.map +1 -0
- package/dist/workflow_json.js +49 -0
- package/dist/workflow_json.js.map +1 -0
- package/dist/workflow_message.d.ts +11 -0
- package/dist/workflow_message.d.ts.map +1 -0
- package/dist/workflow_message.js +179 -0
- package/dist/workflow_message.js.map +1 -0
- package/dist/workflow_state.d.ts +7 -0
- package/dist/workflow_state.d.ts.map +1 -0
- package/dist/workflow_state.js +62 -0
- package/dist/workflow_state.js.map +1 -0
- package/package.json +66 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain SDK Runtime.
|
|
3
|
+
*
|
|
4
|
+
* Port of `agent_engine_sdk_langgraph/runtime.py`.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { interrupt as langgraphInterrupt, isGraphBubbleUp, } from "@langchain/langgraph";
|
|
9
|
+
import * as CallbackManagerModule from "@langchain/core/callbacks/manager";
|
|
10
|
+
import { tool as lcTool } from "@langchain/core/tools";
|
|
11
|
+
import { MongoDBSaver } from "@langchain/langgraph-checkpoint-mongodb";
|
|
12
|
+
import { LangChainInstrumentation } from "@arizeai/openinference-instrumentation-langchain";
|
|
13
|
+
import { MongoClient } from "mongodb";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { BaseApp, LLMToolSchema, } from "@mongodb-js/agent-engine-sdk";
|
|
16
|
+
import { Memory } from "@mongodb-js/agent-engine-sdk-memory";
|
|
17
|
+
import { AppBoundCrudClient, AppBoundRuntime, clearWorkflowAdapter, createSecureToolFunction, currentAttemptContext, discoverMcpTools, getCurrentWrapper, getEnvBool, entrypointScope, runWithCustomerOrigin, getCheckpointWorkspaceId, getLogger, getStoreDbName, resolveStoreDbName, makeMcpToolCallable, mcpServerNetworkHosts, normalizeOptionalStr, registerInstrumentor, registerLlm, registerLLMAdapterFactory, registerQueryPlugin, registerSuspendHandler, registerWorkflowAdapter, requestSessionFinish, resetLlmRegistry, runInstrumentor, RuntimeMode, suspendPayloadToJson, TenantRuntime, getTracer, GRAPH_BUILD, OPENINFERENCE_SPAN_KIND, OpenInferenceSpanKind, ATTR_CACHE_HIT, } from "@mongodb-js/agent-engine-runner-shared";
|
|
18
|
+
import { withEmptyBatchGuard } from "./checkpointer.js";
|
|
19
|
+
import { withDurableToolResultIdentity } from "./durable_tools.js";
|
|
20
|
+
import { LangGraphBaseAgent } from "./agent.js";
|
|
21
|
+
import { createAgentEngineDeepAgent, } from "./deep_agent.js";
|
|
22
|
+
import { AgentEngineToolPodBackend } from "./backends/toolpod.js";
|
|
23
|
+
import { LangChainLLMAdapter } from "./llm_adapter.js";
|
|
24
|
+
import { LangGraphQueryPlugin } from "./query.js";
|
|
25
|
+
import { LangGraphCallbackAdapter } from "./node_logger_adapter.js";
|
|
26
|
+
import { PlatformCheckpointer, UnsupportedDurableGraphError, } from "./platform_checkpointer.js";
|
|
27
|
+
import { SecureWrappedLLM } from "./secure_llm.js";
|
|
28
|
+
const logger = getLogger("agent_engine_sdk_langgraph.runtime");
|
|
29
|
+
/**
|
|
30
|
+
* This package's version, used as the workflow adapter version OE records.
|
|
31
|
+
* Mirrors Python `_adapter_version()` (installed `agent-engine-sdk-langgraph`
|
|
32
|
+
* version) so the declaration tracks releases. Falls back to the same
|
|
33
|
+
* "0.0.0" unknown sentinel Python uses when package.json is unreachable
|
|
34
|
+
* (bundled layouts) — never a concrete release number, which would
|
|
35
|
+
* masquerade as the current version once the package moves past it.
|
|
36
|
+
*/
|
|
37
|
+
function adapterVersion() {
|
|
38
|
+
try {
|
|
39
|
+
const require = createRequire(import.meta.url);
|
|
40
|
+
const version = require("../package.json")
|
|
41
|
+
.version;
|
|
42
|
+
if (typeof version === "string" && version.trim() !== "")
|
|
43
|
+
return version;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Fall through to the fallback below.
|
|
47
|
+
}
|
|
48
|
+
return "0.0.0";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Open a `graph.build` span around graph materialization. Opens on a
|
|
52
|
+
* cache hit too (near-zero duration) so a trace can show the build was
|
|
53
|
+
* skipped rather than omitting the span. Covers only graph materialization,
|
|
54
|
+
* not the rest of `getAgent()` (already covered by AER's `aer.build_agent`
|
|
55
|
+
* span).
|
|
56
|
+
*/
|
|
57
|
+
function tracedGraphBuild(cacheHit, fn) {
|
|
58
|
+
return getTracer("runner-shared.agent-engine-sdk-langgraph").startActiveSpan(GRAPH_BUILD, {
|
|
59
|
+
attributes: {
|
|
60
|
+
[OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.CHAIN,
|
|
61
|
+
[ATTR_CACHE_HIT]: cacheHit,
|
|
62
|
+
},
|
|
63
|
+
}, (span) => {
|
|
64
|
+
try {
|
|
65
|
+
return fn();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
span.end();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert a tool's Zod schema to JSON Schema, degrading to `{}` on failure.
|
|
74
|
+
*
|
|
75
|
+
* Called at `tool()` decoration (module-eval) time. Zod 4's `toJSONSchema`
|
|
76
|
+
* throws for shapes it cannot represent, so a bad schema must warn and fall
|
|
77
|
+
* back rather than crash agent startup.
|
|
78
|
+
*/
|
|
79
|
+
function deriveArgsSchema(name, schema) {
|
|
80
|
+
try {
|
|
81
|
+
return z.toJSONSchema(schema);
|
|
82
|
+
}
|
|
83
|
+
catch (exc) {
|
|
84
|
+
logger.warn(`Failed to derive args_schema for tool ${name}: ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* LangChain SDK for the Atlas Agent Engine.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { App } from '@mongodb-js/agent-engine-sdk-langgraph';
|
|
94
|
+
*
|
|
95
|
+
* const app = new App({ appName: 'My Agent' });
|
|
96
|
+
*
|
|
97
|
+
* app.tool()((args: { query: string }) => 'result');
|
|
98
|
+
*
|
|
99
|
+
* app.entrypoint(() => {
|
|
100
|
+
* const llm = app.llm(chatModel);
|
|
101
|
+
* const checkpointer = app.checkpointer();
|
|
102
|
+
* const tools = app.getTools();
|
|
103
|
+
* // Build LangGraph...
|
|
104
|
+
* return graph;
|
|
105
|
+
* });
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export class App extends BaseApp {
|
|
109
|
+
// BaseApp owns `readonly name: string` — no override needed.
|
|
110
|
+
runtime;
|
|
111
|
+
builderFn = null;
|
|
112
|
+
// Cached graph.build result. Safe to reuse for the process lifetime: the
|
|
113
|
+
// builder function is fixed once app.entrypoint() is called, and Node's
|
|
114
|
+
// single-threaded event loop can't interleave two synchronous builds, so
|
|
115
|
+
// no lock is needed (unlike the Python port).
|
|
116
|
+
graphCache;
|
|
117
|
+
prepareInputFn = null;
|
|
118
|
+
resolveThreadIdFn = null;
|
|
119
|
+
toolDefs = [];
|
|
120
|
+
lcTools = new Map();
|
|
121
|
+
mongoClient = null;
|
|
122
|
+
_checkpointer = null;
|
|
123
|
+
// Resolved once configured MCP servers have been discovered and registered
|
|
124
|
+
// as tools. `TenantRuntime.runAsync()` awaits `ready()` (which returns this
|
|
125
|
+
// promise) before binding the server, so no request can land before MCP
|
|
126
|
+
// tools are registered. Discovery is async (a `tools/list` HTTP round trip
|
|
127
|
+
// per server) but the constructor itself must stay synchronous, unlike
|
|
128
|
+
// Python's `App.__init__` which can block on `asyncio.run()`.
|
|
129
|
+
mcpToolsReady;
|
|
130
|
+
// Per-project-resolved store DB name for the checkpointer + query plugin.
|
|
131
|
+
// Resolved asynchronously in run() (the Node driver's listDatabases is async,
|
|
132
|
+
// unlike pymongo) so the synchronous checkpointer() can read it. Null until
|
|
133
|
+
// resolved → callers fall back to the unscoped base name.
|
|
134
|
+
resolvedCheckpointDb = null;
|
|
135
|
+
_memory = null;
|
|
136
|
+
/**
|
|
137
|
+
* Unified memory facade over app-bound adapters.
|
|
138
|
+
*
|
|
139
|
+
* Lazily constructed on first access and cached (a long-lived singleton over
|
|
140
|
+
* per-request context). Operations resolve the end-user and session from the
|
|
141
|
+
* ambient execution context, so agent code calls `app.memory.saveSemantic(...)`
|
|
142
|
+
* without threading identity through. Requests route through the OE memory
|
|
143
|
+
* proxy; memory is reachable only while handling a platform request.
|
|
144
|
+
*/
|
|
145
|
+
get memory() {
|
|
146
|
+
if (this._memory === null) {
|
|
147
|
+
this._memory = new Memory({
|
|
148
|
+
runtime: new AppBoundRuntime(),
|
|
149
|
+
client: new AppBoundCrudClient(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return this._memory;
|
|
153
|
+
}
|
|
154
|
+
constructor(options) {
|
|
155
|
+
super(options.appName);
|
|
156
|
+
if (options.orgId != null) {
|
|
157
|
+
logger.warn("App({ orgId }) is deprecated and ignored. " +
|
|
158
|
+
"Set the ORG_ID environment variable instead; " +
|
|
159
|
+
"this option will be removed in a future release.");
|
|
160
|
+
}
|
|
161
|
+
// AppOptions uses camelCase (per repo TS coding standard); pass through to
|
|
162
|
+
// TenantRuntimeOptions which also uses camelCase. orgId is intentionally
|
|
163
|
+
// NOT forwarded — it is deprecated and ignored; the org is taken
|
|
164
|
+
// from the ORG_ID env var, which the platform injects.
|
|
165
|
+
const runtimeOpts = {
|
|
166
|
+
appName: options.appName,
|
|
167
|
+
...(options.appVersion !== undefined && {
|
|
168
|
+
appVersion: options.appVersion,
|
|
169
|
+
}),
|
|
170
|
+
...(options.mongodbUri !== undefined && {
|
|
171
|
+
mongodbUri: options.mongodbUri,
|
|
172
|
+
}),
|
|
173
|
+
...(options.databaseName !== undefined && {
|
|
174
|
+
databaseName: options.databaseName,
|
|
175
|
+
}),
|
|
176
|
+
...(options.tracesCollectionName !== undefined && {
|
|
177
|
+
tracesCollectionName: options.tracesCollectionName,
|
|
178
|
+
}),
|
|
179
|
+
};
|
|
180
|
+
this.runtime = new TenantRuntime(runtimeOpts);
|
|
181
|
+
this.mcpToolsReady = this.registerMcpTools();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolved once configured MCP servers have been discovered and registered
|
|
185
|
+
* as tools. `TenantRuntime.runAsync()` awaits this (via `GraphBuilderLike.ready()`)
|
|
186
|
+
* before starting the server.
|
|
187
|
+
*/
|
|
188
|
+
async ready() {
|
|
189
|
+
return this.mcpToolsReady;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Discover configured MCP servers' tools and register them the same way
|
|
193
|
+
* `App.tool()` registers author-declared tools. Port of Python's
|
|
194
|
+
* `_register_mcp_tools_from_config`.
|
|
195
|
+
*/
|
|
196
|
+
async registerMcpTools() {
|
|
197
|
+
const mcpConfig = this.runtime.getAgentConfig().mcp;
|
|
198
|
+
if (Object.keys(mcpConfig.servers).length === 0)
|
|
199
|
+
return;
|
|
200
|
+
const bindings = await discoverMcpTools(mcpConfig);
|
|
201
|
+
for (const binding of bindings) {
|
|
202
|
+
const callable = makeMcpToolCallable(binding);
|
|
203
|
+
const description = binding.description.trim();
|
|
204
|
+
const langchainTool = lcTool((args) => callable(args), {
|
|
205
|
+
name: binding.sdkToolName,
|
|
206
|
+
description,
|
|
207
|
+
schema: binding.inputSchema,
|
|
208
|
+
});
|
|
209
|
+
this.registerToolDefinition({
|
|
210
|
+
name: binding.sdkToolName,
|
|
211
|
+
func: ((args) => callable(args)),
|
|
212
|
+
description,
|
|
213
|
+
argsSchema: binding.inputSchema,
|
|
214
|
+
isLocal: false,
|
|
215
|
+
providerType: null,
|
|
216
|
+
scopes: [],
|
|
217
|
+
network: mcpServerNetworkHosts(binding.serverConfig),
|
|
218
|
+
timeoutSeconds: binding.serverConfig.timeout_seconds,
|
|
219
|
+
redactFields: [],
|
|
220
|
+
langchainTool,
|
|
221
|
+
mcpServer: binding.serverName,
|
|
222
|
+
mcpTool: binding.toolName,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
logger.info(`Registered ${bindings.length} MCP tool(s)`);
|
|
226
|
+
}
|
|
227
|
+
// ----- Read-only properties -----
|
|
228
|
+
get agentConfig() {
|
|
229
|
+
return this.runtime.getAgentConfig();
|
|
230
|
+
}
|
|
231
|
+
// ----- BaseApp contract -----
|
|
232
|
+
getToolDefinitions() {
|
|
233
|
+
return [...this.toolDefs];
|
|
234
|
+
}
|
|
235
|
+
tools() {
|
|
236
|
+
return [...this.getTools()];
|
|
237
|
+
}
|
|
238
|
+
// ----- Tool registration -----
|
|
239
|
+
registerToolDefinition(args) {
|
|
240
|
+
if (args.name in this.runtime.tools || this.lcTools.has(args.name)) {
|
|
241
|
+
throw new Error(`tool '${args.name}' is already registered`);
|
|
242
|
+
}
|
|
243
|
+
const description = args.description.trim();
|
|
244
|
+
const metadata = {
|
|
245
|
+
name: args.name,
|
|
246
|
+
description,
|
|
247
|
+
is_local: args.isLocal,
|
|
248
|
+
provider_type: args.providerType,
|
|
249
|
+
scopes: args.scopes,
|
|
250
|
+
network: args.network,
|
|
251
|
+
timeout_seconds: args.timeoutSeconds,
|
|
252
|
+
redact_fields: args.redactFields,
|
|
253
|
+
...(args.mcpServer !== undefined && { mcp_server: args.mcpServer }),
|
|
254
|
+
...(args.mcpTool !== undefined && { mcp_tool: args.mcpTool }),
|
|
255
|
+
};
|
|
256
|
+
// His TenantRuntime.registerTool signature: (name, func, metadata) positional.
|
|
257
|
+
this.runtime.registerTool(args.name, args.func, metadata);
|
|
258
|
+
const storedLcTool = args.langchainTool !== undefined
|
|
259
|
+
? args.langchainTool
|
|
260
|
+
: lcTool(args.func, { name: args.name, description });
|
|
261
|
+
// getTools() reads the per-call Stop opt-in off the stored LangChain tool,
|
|
262
|
+
// but withCallInterruptSupport brands the registered callable — carry the
|
|
263
|
+
// brand over or every App.tool() registration would answer not_cancellable.
|
|
264
|
+
if (args.func
|
|
265
|
+
.supportsCallInterrupt === true) {
|
|
266
|
+
storedLcTool.supportsCallInterrupt = true;
|
|
267
|
+
}
|
|
268
|
+
this.lcTools.set(args.name, storedLcTool);
|
|
269
|
+
this.toolDefs.push({
|
|
270
|
+
name: args.name,
|
|
271
|
+
description,
|
|
272
|
+
args_schema: args.argsSchema,
|
|
273
|
+
callable: args.func,
|
|
274
|
+
remote: !args.isLocal,
|
|
275
|
+
...(args.providerType !== null && { provider_type: args.providerType }),
|
|
276
|
+
scopes: [...args.scopes],
|
|
277
|
+
network: [...args.network],
|
|
278
|
+
timeout_seconds: args.timeoutSeconds,
|
|
279
|
+
redact_fields: [...args.redactFields],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Register a tool function.
|
|
284
|
+
*
|
|
285
|
+
* Returns a function that, when applied to a tool function, registers it
|
|
286
|
+
* and returns the function unchanged. Mirrors Python's `@app.tool()` shape.
|
|
287
|
+
*/
|
|
288
|
+
tool(options = {}) {
|
|
289
|
+
const isLocal = options.isLocal ?? true;
|
|
290
|
+
const providerType = options.providerType ?? null;
|
|
291
|
+
const scopes = options.scopes ?? [];
|
|
292
|
+
const network = options.network ?? [];
|
|
293
|
+
const timeout = options.timeout ?? 30;
|
|
294
|
+
const redactFields = options.redactFields ?? [];
|
|
295
|
+
const description = options.description ?? "";
|
|
296
|
+
const schema = options.schema;
|
|
297
|
+
const explicitName = options.name;
|
|
298
|
+
return (fn) => {
|
|
299
|
+
const name = explicitName ?? (fn.name === "" ? "anonymous_tool" : fn.name);
|
|
300
|
+
// Build a LangChain tool with description + Zod schema so the LLM sees
|
|
301
|
+
// the same metadata Python derives from docstrings & type hints.
|
|
302
|
+
const langchainTool = schema
|
|
303
|
+
? lcTool(fn, { name, description, schema })
|
|
304
|
+
: lcTool(fn, { name, description });
|
|
305
|
+
this.registerToolDefinition({
|
|
306
|
+
name,
|
|
307
|
+
func: fn,
|
|
308
|
+
description,
|
|
309
|
+
// Derive JSON Schema from the tool's Zod schema so downstream consumers
|
|
310
|
+
// such as API documentation see its parameters. No schema means a
|
|
311
|
+
// parameterless tool. Runs at decoration (module-eval)
|
|
312
|
+
// time; `toJSONSchema` throws for shapes it can't represent (recursive
|
|
313
|
+
// z.lazy() without cycles, z.function(), ...), so degrade to {} and warn
|
|
314
|
+
// rather than crash agent startup.
|
|
315
|
+
argsSchema: schema ? deriveArgsSchema(name, schema) : {},
|
|
316
|
+
isLocal,
|
|
317
|
+
providerType,
|
|
318
|
+
scopes,
|
|
319
|
+
network,
|
|
320
|
+
timeoutSeconds: timeout,
|
|
321
|
+
redactFields,
|
|
322
|
+
langchainTool,
|
|
323
|
+
});
|
|
324
|
+
return fn;
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** Mark the graph builder function. */
|
|
328
|
+
entrypoint(fn) {
|
|
329
|
+
this.builderFn = fn;
|
|
330
|
+
return fn;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Register a hook that builds the graph's starting input from the caller's
|
|
334
|
+
* `AgentInput` and `RequestContext` for a fresh execution. Resume stays
|
|
335
|
+
* platform-managed. Returns the function unchanged so it can be used as a
|
|
336
|
+
* decorator. Equivalent to the Python SDK's `@app.prepare_agent_input`.
|
|
337
|
+
*/
|
|
338
|
+
prepareAgentInput(fn) {
|
|
339
|
+
this.prepareInputFn = fn;
|
|
340
|
+
return fn;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Register a hook that builds the LangGraph checkpoint `thread_id`.
|
|
344
|
+
*
|
|
345
|
+
* Callers manage Atlas Agent Engine `session_id` (and authenticated `user_id`). The
|
|
346
|
+
* agent owns how those map to the LangGraph checkpoint key. When registered,
|
|
347
|
+
* the hook's return value is used verbatim on every invocation — fresh and
|
|
348
|
+
* resume — with no workspace suffix appended. When no hook is registered,
|
|
349
|
+
* the adapter derives `session_id:workspace_id` as today.
|
|
350
|
+
*
|
|
351
|
+
* Custom keys are invisible to Atlas Agent Engine session-history queries
|
|
352
|
+
* (`/query/sessions*`), which still look up only the default
|
|
353
|
+
* session/workspace-derived keys. Agents that bypass workspace scoping also
|
|
354
|
+
* own collision isolation within the checkpoint database.
|
|
355
|
+
*
|
|
356
|
+
* Equivalent to the Python SDK's `@app.resolve_thread_id`.
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```ts
|
|
360
|
+
* app.resolveThreadId((ctx) => `${ctx.sessionId}__${actorFrom(ctx.userId)}`);
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
resolveThreadId(fn) {
|
|
364
|
+
this.resolveThreadIdFn = fn;
|
|
365
|
+
return fn;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Build and return a `LangGraphBaseAgent` instance.
|
|
369
|
+
*
|
|
370
|
+
* Matches `GraphBuilderLike.getAgent({ callbacks? })` — agent-engine-runner-shared's
|
|
371
|
+
* `TenantRuntime.registerAndRun` introspects this method to compile the graph.
|
|
372
|
+
* Accepts an options object with an optional `callbacks` array, each wrapped
|
|
373
|
+
* in `LangGraphCallbackAdapter` before being passed to the graph.
|
|
374
|
+
*/
|
|
375
|
+
getAgent(opts) {
|
|
376
|
+
const adaptedCallbacks = [];
|
|
377
|
+
if (opts?.callbacks) {
|
|
378
|
+
for (const cb of opts.callbacks) {
|
|
379
|
+
adaptedCallbacks.push(new LangGraphCallbackAdapter(cb));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const graph = this.getOrBuildGraph();
|
|
383
|
+
// Durable eligibility depends on the materialized graph: only the
|
|
384
|
+
// PlatformCheckpointer provides fence-keyed scratch and release, so a
|
|
385
|
+
// graph compiled with a custom saver (or none) must stay native.
|
|
386
|
+
// Re-runs on every call, including cache hits: registration is global
|
|
387
|
+
// mutable state, so skipping it on a hit could leave eligibility stale.
|
|
388
|
+
// Mirrors Python `runtime.py` adapter registration.
|
|
389
|
+
if (graph.checkpointer instanceof
|
|
390
|
+
PlatformCheckpointer) {
|
|
391
|
+
registerWorkflowAdapter("langgraph", adapterVersion());
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
clearWorkflowAdapter();
|
|
395
|
+
}
|
|
396
|
+
return new LangGraphBaseAgent(graph, adaptedCallbacks, this.prepareInputFn, this.resolveThreadIdFn);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Return the materialized graph, building it at most once.
|
|
400
|
+
*
|
|
401
|
+
* `getAgent()` used to call the entrypoint on every `/execute` — real,
|
|
402
|
+
* measurable first-invoke latency. The graph carries no per-request
|
|
403
|
+
* state (callbacks/agent wrapper are still rebuilt fresh by every
|
|
404
|
+
* `getAgent()` call), so caching it is safe.
|
|
405
|
+
*/
|
|
406
|
+
getOrBuildGraph() {
|
|
407
|
+
if (this.builderFn === null) {
|
|
408
|
+
throw new Error("No entrypoint registered. Use app.entrypoint() to mark the graph builder function.");
|
|
409
|
+
}
|
|
410
|
+
const builderFn = this.builderFn;
|
|
411
|
+
const cacheHit = this.graphCache !== undefined;
|
|
412
|
+
return tracedGraphBuild(cacheHit, () => {
|
|
413
|
+
if (cacheHit)
|
|
414
|
+
return this.graphCache;
|
|
415
|
+
resetLlmRegistry();
|
|
416
|
+
const graph = entrypointScope(() => runWithCustomerOrigin(() => builderFn()));
|
|
417
|
+
this.graphCache = graph;
|
|
418
|
+
return graph;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Build and cache the graph when explicitly requested. The TypeScript AER
|
|
423
|
+
* intentionally leaves construction on the existing lazy `/execute` path:
|
|
424
|
+
* this synchronous builder cannot run during standby warming without
|
|
425
|
+
* blocking the Node event loop and server health.
|
|
426
|
+
*/
|
|
427
|
+
warmUp() {
|
|
428
|
+
if (this.builderFn === null)
|
|
429
|
+
return;
|
|
430
|
+
this.getOrBuildGraph();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Start the agent service.
|
|
434
|
+
*
|
|
435
|
+
* Async because the per-project store-DB name is resolved against the live
|
|
436
|
+
* cluster (an async listDatabases round trip in the Node driver) before the
|
|
437
|
+
* query plugin and checkpointer are wired, so AER writes land in the same
|
|
438
|
+
* database the OE reads. The synchronous framework hooks are still registered
|
|
439
|
+
* before the first `await`, preserving their ordering relative to startup.
|
|
440
|
+
*/
|
|
441
|
+
async run(options = {}) {
|
|
442
|
+
if (this.builderFn === null) {
|
|
443
|
+
throw new Error("No app.entrypoint() registered. Mark your graph-builder function with " +
|
|
444
|
+
"app.entrypoint() before calling run().");
|
|
445
|
+
}
|
|
446
|
+
App.registerHooks();
|
|
447
|
+
// Run the LangChain instrumentor now that its hook is registered.
|
|
448
|
+
// TenantRuntime's constructor already calls `setupTracing()` at boot, but
|
|
449
|
+
// our instrumentor isn't registered until `registerHooks()` above runs, and
|
|
450
|
+
// `setupTracing()` early-returns once a tracer provider exists — so calling
|
|
451
|
+
// it a second time here would NOT re-run the instrumentor (the original bug).
|
|
452
|
+
// Invoke the instrumentor directly instead. `manuallyInstrument` patches
|
|
453
|
+
// LangChain's CallbackManager independently of the tracer provider, so this
|
|
454
|
+
// is correct regardless of provider-init ordering.
|
|
455
|
+
runInstrumentor();
|
|
456
|
+
await this.resolveCheckpointDbName();
|
|
457
|
+
this.registerQueryPlugin();
|
|
458
|
+
this.runtime.registerAndRun(this, options);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Resolve the store DB name once, before the checkpointer and query plugin
|
|
462
|
+
* are constructed. Honors `CHECKPOINT_DB_NAME` as an exact override (no
|
|
463
|
+
* project scoping). No-op outside AER mode or without a MongoDB URI when
|
|
464
|
+
* the override is unset (the checkpointer returns null in those cases).
|
|
465
|
+
*/
|
|
466
|
+
async resolveCheckpointDbName() {
|
|
467
|
+
if (this.runtime.mode !== RuntimeMode.AER ||
|
|
468
|
+
this.resolvedCheckpointDb !== null) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Exact database override when configured. Must not apply project
|
|
472
|
+
// scoping or discovery — the value is the final DB name.
|
|
473
|
+
const checkpointDbOverride = this.checkpointDbOverride();
|
|
474
|
+
if (checkpointDbOverride !== null) {
|
|
475
|
+
this.resolvedCheckpointDb = checkpointDbOverride;
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const mongodbUri = this.runtime.getMongodbUri() ?? process.env["MONGODB_URI"] ?? "";
|
|
479
|
+
if (mongodbUri === "")
|
|
480
|
+
return;
|
|
481
|
+
const client = new MongoClient(mongodbUri);
|
|
482
|
+
try {
|
|
483
|
+
this.resolvedCheckpointDb = await resolveStoreDbName(client);
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
void client.close();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Exact `CHECKPOINT_DB_NAME` override when set, else null.
|
|
491
|
+
* Shared by resolve / checkpointer / query plugin so the three paths cannot
|
|
492
|
+
* drift on trim/precedence.
|
|
493
|
+
*/
|
|
494
|
+
checkpointDbOverride() {
|
|
495
|
+
const override = (process.env["CHECKPOINT_DB_NAME"] ?? "").trim();
|
|
496
|
+
return override !== "" ? override : null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Final MongoDBSaver / query-plugin database name for this app.
|
|
500
|
+
* Prefer the exact env override; otherwise the resolved (or base) store DB.
|
|
501
|
+
*/
|
|
502
|
+
checkpointDbName() {
|
|
503
|
+
return (this.checkpointDbOverride() ??
|
|
504
|
+
this.resolvedCheckpointDb ??
|
|
505
|
+
getStoreDbName());
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Register the LangGraph-backed `AERQueryPlugin` (AER mode only). A
|
|
509
|
+
* missing MongoDB URI or a failing checkpointer construction degrades to
|
|
510
|
+
* "no plugin" (routes return 501) instead of failing startup.
|
|
511
|
+
*/
|
|
512
|
+
registerQueryPlugin() {
|
|
513
|
+
if (this.runtime.mode !== RuntimeMode.AER)
|
|
514
|
+
return;
|
|
515
|
+
try {
|
|
516
|
+
const saver = this.checkpointer();
|
|
517
|
+
const native = saver?.native;
|
|
518
|
+
if (native === null ||
|
|
519
|
+
native === undefined ||
|
|
520
|
+
this.mongoClient === null) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
registerQueryPlugin(new LangGraphQueryPlugin({
|
|
524
|
+
saver: native,
|
|
525
|
+
client: this.mongoClient,
|
|
526
|
+
dbName: this.checkpointDbName(),
|
|
527
|
+
workspaceIdResolver: getCheckpointWorkspaceId,
|
|
528
|
+
}));
|
|
529
|
+
logger.info("Registered LangGraph session query plugin");
|
|
530
|
+
}
|
|
531
|
+
catch (exc) {
|
|
532
|
+
// Log only the error class: driver URI-parse errors can echo
|
|
533
|
+
// connection-string contents (credentials included) in their message.
|
|
534
|
+
logger.warn("Skipping session query plugin registration; checkpointer " +
|
|
535
|
+
`unavailable: ${exc instanceof Error ? exc.name : typeof exc}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Register framework-specific hooks so agent-engine-runner-shared can dispatch to
|
|
540
|
+
* LangChain/LangGraph code without importing this package. Mirrors Python's
|
|
541
|
+
* `App._register_hooks` (static method).
|
|
542
|
+
*/
|
|
543
|
+
static registerHooks() {
|
|
544
|
+
// Suspend handler — wires LangGraph's `interrupt()` so runner-shared can
|
|
545
|
+
// pause a graph from inside `SuspendPayload` flows. Lazy import keeps
|
|
546
|
+
// langgraph out of the cold path for non-AER modes.
|
|
547
|
+
registerSuspendHandler(((payload) => {
|
|
548
|
+
if (currentAttemptContext() !== null) {
|
|
549
|
+
throw new UnsupportedDurableGraphError("App.suspend framework suspension is not supported on durable_workflow sessions");
|
|
550
|
+
}
|
|
551
|
+
return langgraphInterrupt(payload);
|
|
552
|
+
}));
|
|
553
|
+
// LLM adapter factory — runner-shared's Tool Pod /invoke_llm route uses
|
|
554
|
+
// this to construct a LangChainLLMAdapter from the customer's BaseChatModel.
|
|
555
|
+
registerLLMAdapterFactory((rawLlm, opts) => new LangChainLLMAdapter(rawLlm, opts?.tools, opts?.tool_choice));
|
|
556
|
+
// Instrumentor — installs OpenInference LangChain instrumentation when
|
|
557
|
+
// `setupTracing()` fires. Equivalent to Python's
|
|
558
|
+
// `register_instrumentor(lambda: LangChainInstrumentor().instrument())`.
|
|
559
|
+
//
|
|
560
|
+
// JS LangChain has a non-traditional module layout, so the OpenInference
|
|
561
|
+
// package patches the already-imported CallbackManager module explicitly
|
|
562
|
+
// via `manuallyInstrument(...)` (see the package README).
|
|
563
|
+
registerInstrumentor((() => {
|
|
564
|
+
const lcInstrumentation = new LangChainInstrumentation();
|
|
565
|
+
lcInstrumentation.manuallyInstrument(CallbackManagerModule);
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
// ----- LLM API -----
|
|
569
|
+
/**
|
|
570
|
+
* Wrap a LangChain LLM for audited I/O through the Orchestration Engine.
|
|
571
|
+
*
|
|
572
|
+
* In AER mode the LLM is wrapped in `SecureWrappedLLM`. In TOOL mode the raw
|
|
573
|
+
* LLM is returned unwrapped (the tool pod is the execution end of the chain).
|
|
574
|
+
*
|
|
575
|
+
* For agents with a single LLM, call without `llmId`. For agents with
|
|
576
|
+
* multiple LLMs every call must supply a unique `llmId`:
|
|
577
|
+
* const fast = app.llm(ChatOpenAI("gpt-5.4-mini"), "fast")
|
|
578
|
+
* const primary = app.llm(ChatOpenAI("gpt-5.4"), "primary")
|
|
579
|
+
* Unnamed calls register under the sentinel id `"__default__"`; a second
|
|
580
|
+
* unnamed call therefore raises the same duplicate-id error as a second
|
|
581
|
+
* named call with the same id.
|
|
582
|
+
*/
|
|
583
|
+
llm(llm, llmId) {
|
|
584
|
+
const resolvedId = llmId ?? "__default__";
|
|
585
|
+
// Register in the named-LLM registry regardless of mode — mirrors Python's
|
|
586
|
+
// App.llm() which calls register_llm() before the mode branch. The Tool Pod
|
|
587
|
+
// needs it for /invoke_llm resolution; the AER side needs it for tests and
|
|
588
|
+
// future named-LLM lookup.
|
|
589
|
+
registerLlm(resolvedId, llm);
|
|
590
|
+
if (this.runtime.mode === RuntimeMode.TOOL) {
|
|
591
|
+
return llm;
|
|
592
|
+
}
|
|
593
|
+
return new SecureWrappedLLM(llm, () => getCurrentWrapper(), resolvedId);
|
|
594
|
+
}
|
|
595
|
+
// ----- Checkpointing -----
|
|
596
|
+
/**
|
|
597
|
+
* Get the platform checkpointer for LangGraph.
|
|
598
|
+
*
|
|
599
|
+
* Lazily constructs and wraps a `MongoDBSaver` on first call in AER mode.
|
|
600
|
+
* Native sessions use that saver through the request-scoped platform
|
|
601
|
+
* wrapper. The underlying `MongoClient` is closed by `App.close()`.
|
|
602
|
+
*
|
|
603
|
+
* The database name is read from `CHECKPOINT_DB_NAME` when set (exact
|
|
604
|
+
* override, no project scoping). Otherwise the existing
|
|
605
|
+
* `MDB_AGENTIC_STORE_DB` / per-project store resolution is used
|
|
606
|
+
* (default: `"mdb_store"`). The URI comes from `MONGODB_URI` — the same
|
|
607
|
+
* source `TenantRuntime` uses internally.
|
|
608
|
+
*
|
|
609
|
+
* Mirrors Python `runtime.py:checkpointer()`.
|
|
610
|
+
*/
|
|
611
|
+
checkpointer() {
|
|
612
|
+
if (this.runtime.mode === RuntimeMode.AER) {
|
|
613
|
+
if (this._checkpointer !== null)
|
|
614
|
+
return this._checkpointer;
|
|
615
|
+
const checkpointDb = this.checkpointDbName();
|
|
616
|
+
// Prefer the runtime-resolved URI (which honors the constructor's
|
|
617
|
+
// `mongodbUri` option) over the env var directly. Falls back to env for
|
|
618
|
+
// any code path that hasn't been migrated to the constructor option yet.
|
|
619
|
+
const mongodbUri = this.runtime.getMongodbUri() ?? process.env["MONGODB_URI"] ?? "";
|
|
620
|
+
if (mongodbUri !== "") {
|
|
621
|
+
logger.info(`Using MongoDB checkpointer: db=${checkpointDb}`);
|
|
622
|
+
const client = new MongoClient(mongodbUri);
|
|
623
|
+
try {
|
|
624
|
+
// `@langchain/langgraph-checkpoint-mongodb` pins `mongodb@^6` while our
|
|
625
|
+
// top-level dep is `mongodb@^7`. The two `MongoClient` types differ
|
|
626
|
+
// only in deeply-nested option shapes that `MongoDBSaver` never
|
|
627
|
+
// touches — it calls `client.db(name)` and `client.close()`, both
|
|
628
|
+
// identical across versions. Cast to bridge the type mismatch.
|
|
629
|
+
const native = withEmptyBatchGuard(new MongoDBSaver({
|
|
630
|
+
client: client,
|
|
631
|
+
dbName: checkpointDb,
|
|
632
|
+
}));
|
|
633
|
+
this._checkpointer = new PlatformCheckpointer({ native });
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
// MongoDBSaver construction may fail (e.g., bad URI, auth). Close
|
|
637
|
+
// the freshly-opened client before the exception propagates so
|
|
638
|
+
// every failed init does not leak a pool connection.
|
|
639
|
+
void client.close();
|
|
640
|
+
throw err;
|
|
641
|
+
}
|
|
642
|
+
this.mongoClient = client;
|
|
643
|
+
return this._checkpointer;
|
|
644
|
+
}
|
|
645
|
+
logger.warn("MongoDB URI not defined, required for checkpointer");
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
logger.warn(`app.checkpointer() called in ${this.runtime.mode} mode; checkpointing is managed ` +
|
|
649
|
+
"by the platform in non-AER modes — returning null");
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
// ----- Deep agents -----
|
|
653
|
+
/**
|
|
654
|
+
* Build a deepagents graph pre-wired for Atlas Agent Engine AER.
|
|
655
|
+
*
|
|
656
|
+
* Wraps `llm` in `SecureWrappedLLM`, resolves relative skill paths, validates
|
|
657
|
+
* the subagent tree (string models are rejected — they would bypass OE
|
|
658
|
+
* routing), then delegates to deepagents' `createDeepAgent`.
|
|
659
|
+
*
|
|
660
|
+
* Each `skills` entry is a parent source directory. At runtime, deepagents
|
|
661
|
+
* lists it through the configured backend and treats each immediate child
|
|
662
|
+
* directory containing `SKILL.md` as one skill; discovery is not recursive.
|
|
663
|
+
* deepagents skips unreadable or unparsable frontmatter and skills missing
|
|
664
|
+
* `name` or `description`; it warns but may still load Agent Skills naming or
|
|
665
|
+
* directory-name violations.
|
|
666
|
+
*
|
|
667
|
+
* Mirrors Python `runtime.py:App.deep_agent()`.
|
|
668
|
+
*
|
|
669
|
+
* @throws {Error} `features.deep_agent` is not enabled, or a subagent spec
|
|
670
|
+
* uses a string model, or nesting exceeds the recursion cap.
|
|
671
|
+
*/
|
|
672
|
+
deepAgent(llm, options = {}) {
|
|
673
|
+
// Fail fast if the tenant hasn't opted into the Tool Pod's built-in
|
|
674
|
+
// filesystem/shell handlers — otherwise a AgentEngineToolPodBackend call would
|
|
675
|
+
// fail at runtime with "unknown tool", a confusing error several layers
|
|
676
|
+
// from the cause. Requiring the flag at construction time gives an
|
|
677
|
+
// immediate, actionable message.
|
|
678
|
+
if (!this.agentConfig.featureEnabled("deep_agent", false)) {
|
|
679
|
+
throw new Error("App.deepAgent() requires 'features.deep_agent: true' in agent.yaml. " +
|
|
680
|
+
"Without it the Tool Pod does not register the built-in filesystem + " +
|
|
681
|
+
"shell handlers that deep agents rely on. Add:\n\n" +
|
|
682
|
+
" features:\n" +
|
|
683
|
+
" deep_agent: true\n\n" +
|
|
684
|
+
"to your agent.yaml and redeploy the Tool Pod.");
|
|
685
|
+
}
|
|
686
|
+
// No resetLlmRegistry() here. `getAgent()` already clears the registry
|
|
687
|
+
// immediately before running the entrypoint, so a second reset is
|
|
688
|
+
// redundant — and harmful: JS evaluates call arguments before the callee,
|
|
689
|
+
// so a subagent model built inline as `app.llm(model, "researcher")` in the
|
|
690
|
+
// `subagents` option registers "researcher" *before* this method body runs.
|
|
691
|
+
// Clearing here would wipe that registration, and the Tool Pod's
|
|
692
|
+
// /invoke_llm route would then throw `llm_id "researcher" not registered`
|
|
693
|
+
// when the subagent's model is resolved. Registering the orchestrator's
|
|
694
|
+
// __default__ below via `this.llm()` leaves prior subagent entries intact;
|
|
695
|
+
// a genuine duplicate id surfaces through `registerLlm`'s own guard.
|
|
696
|
+
const secureLlm = this.llm(llm);
|
|
697
|
+
// Checkpointer resolution:
|
|
698
|
+
// undefined (omitted) -> app.checkpointer() (returns null outside AER)
|
|
699
|
+
// false -> disable checkpointing
|
|
700
|
+
// instance -> use directly
|
|
701
|
+
const checkpointer = options.checkpointer === undefined
|
|
702
|
+
? this.checkpointer()
|
|
703
|
+
: options.checkpointer;
|
|
704
|
+
const skillsBaseDir = this.resolveSkillsBaseDir();
|
|
705
|
+
// Default to the secure Tool Pod backend so filesystem/shell ops are
|
|
706
|
+
// OE-audited and sandboxed. A caller-supplied backend still overrides —
|
|
707
|
+
// matching Python `deep_agent()`. Without this, deepagents would fall back
|
|
708
|
+
// to its in-memory StateBackend, which bypasses the OE entirely.
|
|
709
|
+
const { checkpointer: _c, backend, ...rest } = options;
|
|
710
|
+
const resolvedBackend = backend ?? new AgentEngineToolPodBackend();
|
|
711
|
+
return createAgentEngineDeepAgent(secureLlm, resolvedBackend, {
|
|
712
|
+
...rest,
|
|
713
|
+
...(checkpointer != null && { checkpointer }),
|
|
714
|
+
...(skillsBaseDir !== undefined && { skillsBaseDir }),
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Resolve the base directory for relative skill paths: the directory
|
|
719
|
+
* containing `agent.yaml`, optionally narrowed by the source-root-relative
|
|
720
|
+
* `AGENTIC_SKILLS_DIR` override.
|
|
721
|
+
*/
|
|
722
|
+
resolveSkillsBaseDir() {
|
|
723
|
+
const configPath = this.agentConfig.path;
|
|
724
|
+
const agentDir = configPath != null ? path.dirname(path.resolve(configPath)) : undefined;
|
|
725
|
+
const configured = process.env["AGENTIC_SKILLS_DIR"];
|
|
726
|
+
if (configured === undefined || configured === "")
|
|
727
|
+
return agentDir;
|
|
728
|
+
if (path.isAbsolute(configured)) {
|
|
729
|
+
throw new Error("AGENTIC_SKILLS_DIR must be relative to the agent source root; " +
|
|
730
|
+
`got '${configured}'`);
|
|
731
|
+
}
|
|
732
|
+
if (agentDir === undefined) {
|
|
733
|
+
throw new Error("AGENTIC_SKILLS_DIR requires agent.yaml to determine the agent source root");
|
|
734
|
+
}
|
|
735
|
+
const resolved = path.resolve(agentDir, configured);
|
|
736
|
+
const rel = path.relative(agentDir, resolved);
|
|
737
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
738
|
+
throw new Error("AGENTIC_SKILLS_DIR must stay within the agent source root; " +
|
|
739
|
+
`got '${configured}'`);
|
|
740
|
+
}
|
|
741
|
+
return resolved;
|
|
742
|
+
}
|
|
743
|
+
/** Release resources held by this App instance. */
|
|
744
|
+
async close() {
|
|
745
|
+
if (this.mongoClient !== null) {
|
|
746
|
+
const client = this.mongoClient;
|
|
747
|
+
// Clear fields before awaiting so repeated/concurrent calls are idempotent.
|
|
748
|
+
this.mongoClient = null;
|
|
749
|
+
this._checkpointer = null;
|
|
750
|
+
// mongodb v7 `MongoClient.close()` returns Promise<void>; await it so the
|
|
751
|
+
// connection pool drains before the process exits (no leaked sockets).
|
|
752
|
+
if (typeof client.close === "function")
|
|
753
|
+
await client.close();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// ----- Tools -----
|
|
757
|
+
/**
|
|
758
|
+
* Get wrapped tools for LangGraph's `ToolNode`.
|
|
759
|
+
*
|
|
760
|
+
* In AER mode, every tool is wrapped with `SecureToolWrapper` (via
|
|
761
|
+
* `createSecureToolFunction`) so executions route through OE for logging
|
|
762
|
+
* and policy enforcement. In other modes, the raw LangChain tools are
|
|
763
|
+
* returned unchanged.
|
|
764
|
+
*/
|
|
765
|
+
getTools() {
|
|
766
|
+
const toolsList = [...this.lcTools.values()];
|
|
767
|
+
if (this.runtime.mode !== RuntimeMode.AER)
|
|
768
|
+
return toolsList;
|
|
769
|
+
const allowDirect = getEnvBool("RUNNER_ALLOW_DIRECT_TOOL_EXECUTION", false);
|
|
770
|
+
const wrappedTools = [];
|
|
771
|
+
for (const toolObj of toolsList) {
|
|
772
|
+
const toolAny = toolObj;
|
|
773
|
+
const toolName = toolAny.name ?? "anonymous";
|
|
774
|
+
const toolMetadata = this.runtime.getToolMetadata(toolName);
|
|
775
|
+
let toolCallMetadata;
|
|
776
|
+
const mcpServer = toolMetadata["mcp_server"];
|
|
777
|
+
const mcpTool = toolMetadata["mcp_tool"];
|
|
778
|
+
if (typeof mcpServer === "string" && typeof mcpTool === "string") {
|
|
779
|
+
toolCallMetadata = { mcp_server: mcpServer, mcp_tool: mcpTool };
|
|
780
|
+
}
|
|
781
|
+
const providerType = normalizeOptionalStr(toolMetadata["provider_type"]);
|
|
782
|
+
let scopes = [];
|
|
783
|
+
const rawScopes = toolMetadata["scopes"];
|
|
784
|
+
if (Array.isArray(rawScopes)) {
|
|
785
|
+
scopes = rawScopes
|
|
786
|
+
.filter((scope) => typeof scope === "string" && scope.trim().length > 0)
|
|
787
|
+
.map((scope) => scope.trim());
|
|
788
|
+
}
|
|
789
|
+
const toolDeclaredFormat = toolAny.responseFormat ===
|
|
790
|
+
"content_and_artifact"
|
|
791
|
+
? "content_and_artifact"
|
|
792
|
+
: "content";
|
|
793
|
+
// Opt-in per-call Stop support, branded by withCallInterruptSupport.
|
|
794
|
+
const supportsCallInterrupt = toolAny
|
|
795
|
+
.supportsCallInterrupt === true;
|
|
796
|
+
// The tool's redact_fields policy must reach the wrapper's debug
|
|
797
|
+
// argument dump.
|
|
798
|
+
const rawRedactFields = toolMetadata["redact_fields"];
|
|
799
|
+
const redactFields = Array.isArray(rawRedactFields)
|
|
800
|
+
? rawRedactFields.filter((f) => typeof f === "string")
|
|
801
|
+
: [];
|
|
802
|
+
const wrapperFunc = createSecureToolFunction(toolObj, toolName, allowDirect, {
|
|
803
|
+
metadata: toolCallMetadata,
|
|
804
|
+
isLocal: toolMetadata["is_local"] !== false,
|
|
805
|
+
providerType,
|
|
806
|
+
scopes,
|
|
807
|
+
responseFormat: "content_and_artifact",
|
|
808
|
+
toolDeclaredFormat,
|
|
809
|
+
redactFields,
|
|
810
|
+
isFrameworkControlFlow: isGraphBubbleUp,
|
|
811
|
+
supportsCallInterrupt,
|
|
812
|
+
});
|
|
813
|
+
const durableWrapperFunc = withDurableToolResultIdentity(wrapperFunc, toolName);
|
|
814
|
+
// Forward the original tool's Zod schema. Without it, lcTool() builds a
|
|
815
|
+
// DynamicTool (single `input: string` shape) which strips structured
|
|
816
|
+
// tool_call args before reaching the wrapper — every kwarg arrives
|
|
817
|
+
// undefined at the Tool Pod.
|
|
818
|
+
const wrapped = lcTool(durableWrapperFunc, {
|
|
819
|
+
name: toolName,
|
|
820
|
+
description: toolAny.description ?? "",
|
|
821
|
+
responseFormat: "content_and_artifact",
|
|
822
|
+
...(toolAny.schema !== undefined && {
|
|
823
|
+
schema: toolAny.schema,
|
|
824
|
+
}),
|
|
825
|
+
});
|
|
826
|
+
wrappedTools.push(wrapped);
|
|
827
|
+
logger.debug(`Wrapped tool: ${toolName}`);
|
|
828
|
+
}
|
|
829
|
+
return wrappedTools;
|
|
830
|
+
}
|
|
831
|
+
/** Get tool schemas for `llm.bindTools()`. Always the unwrapped LangChain tools. */
|
|
832
|
+
getToolSchemas() {
|
|
833
|
+
return [...this.lcTools.values()];
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Generate a suspend command. If a tool should suspend, return the result of
|
|
837
|
+
* this method instead of completing normally.
|
|
838
|
+
*/
|
|
839
|
+
suspend(reason, context) {
|
|
840
|
+
const payload = {
|
|
841
|
+
suspend_reason: reason,
|
|
842
|
+
suspend_context: context,
|
|
843
|
+
};
|
|
844
|
+
return suspendPayloadToJson(payload);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Mark this session finished so the platform frees its compute now.
|
|
848
|
+
*
|
|
849
|
+
* Call it when the agent is done with the session. The current turn keeps
|
|
850
|
+
* running and returns its result normally; once it completes, the platform
|
|
851
|
+
* cancels any live sub-agent runs and releases the session's AER and tool
|
|
852
|
+
* pods instead of holding them until the idle timeout expires.
|
|
853
|
+
*
|
|
854
|
+
* Safe to call more than once: the first call returns "requested", later
|
|
855
|
+
* ones "already_requested". Outside an agent run (local scripts, tool pods)
|
|
856
|
+
* there is no session to finish and the call returns "unavailable" without
|
|
857
|
+
* throwing. Calling it after the turn has already ended - e.g. from a
|
|
858
|
+
* setTimeout or a floating promise scheduled during the turn but resolving
|
|
859
|
+
* after it - also returns "unavailable": by then nothing is listening for
|
|
860
|
+
* the request anymore, so reporting "requested" would promise a release
|
|
861
|
+
* that will never happen.
|
|
862
|
+
*
|
|
863
|
+
* A turn that suspends for human review, or that fails, keeps its
|
|
864
|
+
* resources so it stays resumable and diagnosable; the session then falls
|
|
865
|
+
* back to the idle timeout.
|
|
866
|
+
*/
|
|
867
|
+
finishSession() {
|
|
868
|
+
return requestSessionFinish();
|
|
869
|
+
}
|
|
870
|
+
// ----- Context -----
|
|
871
|
+
getCurrentUserId() {
|
|
872
|
+
return this.runtime.getCurrentUserId();
|
|
873
|
+
}
|
|
874
|
+
getCurrentSessionId() {
|
|
875
|
+
return this.runtime.getCurrentSessionId();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Re-exports — callers commonly need these alongside App.
|
|
879
|
+
export { LLMToolSchema };
|
|
880
|
+
export { RuntimeMode };
|
|
881
|
+
//# sourceMappingURL=runtime.js.map
|