@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +149 -0
  4. package/dist/agent.d.ts +53 -0
  5. package/dist/agent.d.ts.map +1 -0
  6. package/dist/agent.js +508 -0
  7. package/dist/agent.js.map +1 -0
  8. package/dist/backends/tool_sandbox.d.ts +15 -0
  9. package/dist/backends/tool_sandbox.d.ts.map +1 -0
  10. package/dist/backends/tool_sandbox.js +15 -0
  11. package/dist/backends/tool_sandbox.js.map +1 -0
  12. package/dist/backends/toolpod.d.ts +68 -0
  13. package/dist/backends/toolpod.d.ts.map +1 -0
  14. package/dist/backends/toolpod.js +451 -0
  15. package/dist/backends/toolpod.js.map +1 -0
  16. package/dist/call_interrupt.d.ts +20 -0
  17. package/dist/call_interrupt.d.ts.map +1 -0
  18. package/dist/call_interrupt.js +23 -0
  19. package/dist/call_interrupt.js.map +1 -0
  20. package/dist/checkpoint_branch.d.ts +58 -0
  21. package/dist/checkpoint_branch.d.ts.map +1 -0
  22. package/dist/checkpoint_branch.js +239 -0
  23. package/dist/checkpoint_branch.js.map +1 -0
  24. package/dist/checkpointer.d.ts +21 -0
  25. package/dist/checkpointer.d.ts.map +1 -0
  26. package/dist/checkpointer.js +31 -0
  27. package/dist/checkpointer.js.map +1 -0
  28. package/dist/deep_agent.d.ts +49 -0
  29. package/dist/deep_agent.d.ts.map +1 -0
  30. package/dist/deep_agent.js +110 -0
  31. package/dist/deep_agent.js.map +1 -0
  32. package/dist/deep_agent_checkpointer.d.ts +11 -0
  33. package/dist/deep_agent_checkpointer.d.ts.map +1 -0
  34. package/dist/deep_agent_checkpointer.js +19 -0
  35. package/dist/deep_agent_checkpointer.js.map +1 -0
  36. package/dist/deep_agent_task.d.ts +26 -0
  37. package/dist/deep_agent_task.d.ts.map +1 -0
  38. package/dist/deep_agent_task.js +49 -0
  39. package/dist/deep_agent_task.js.map +1 -0
  40. package/dist/durable_deep_agent.d.ts +23 -0
  41. package/dist/durable_deep_agent.d.ts.map +1 -0
  42. package/dist/durable_deep_agent.js +176 -0
  43. package/dist/durable_deep_agent.js.map +1 -0
  44. package/dist/durable_message_identity.d.ts +5 -0
  45. package/dist/durable_message_identity.d.ts.map +1 -0
  46. package/dist/durable_message_identity.js +106 -0
  47. package/dist/durable_message_identity.js.map +1 -0
  48. package/dist/durable_session.d.ts +30 -0
  49. package/dist/durable_session.d.ts.map +1 -0
  50. package/dist/durable_session.js +400 -0
  51. package/dist/durable_session.js.map +1 -0
  52. package/dist/durable_subgraphs.d.ts +13 -0
  53. package/dist/durable_subgraphs.d.ts.map +1 -0
  54. package/dist/durable_subgraphs.js +82 -0
  55. package/dist/durable_subgraphs.js.map +1 -0
  56. package/dist/durable_tools.d.ts +12 -0
  57. package/dist/durable_tools.d.ts.map +1 -0
  58. package/dist/durable_tools.js +41 -0
  59. package/dist/durable_tools.js.map +1 -0
  60. package/dist/execution_session.d.ts +63 -0
  61. package/dist/execution_session.d.ts.map +1 -0
  62. package/dist/execution_session.js +300 -0
  63. package/dist/execution_session.js.map +1 -0
  64. package/dist/index.d.ts +19 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +17 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/llm_adapter.d.ts +41 -0
  69. package/dist/llm_adapter.d.ts.map +1 -0
  70. package/dist/llm_adapter.js +177 -0
  71. package/dist/llm_adapter.js.map +1 -0
  72. package/dist/messages.d.ts +56 -0
  73. package/dist/messages.d.ts.map +1 -0
  74. package/dist/messages.js +592 -0
  75. package/dist/messages.js.map +1 -0
  76. package/dist/node_logger_adapter.d.ts +51 -0
  77. package/dist/node_logger_adapter.d.ts.map +1 -0
  78. package/dist/node_logger_adapter.js +163 -0
  79. package/dist/node_logger_adapter.js.map +1 -0
  80. package/dist/platform_checkpointer.d.ts +70 -0
  81. package/dist/platform_checkpointer.d.ts.map +1 -0
  82. package/dist/platform_checkpointer.js +252 -0
  83. package/dist/platform_checkpointer.js.map +1 -0
  84. package/dist/query.d.ts +103 -0
  85. package/dist/query.d.ts.map +1 -0
  86. package/dist/query.js +623 -0
  87. package/dist/query.js.map +1 -0
  88. package/dist/runtime.d.ts +336 -0
  89. package/dist/runtime.d.ts.map +1 -0
  90. package/dist/runtime.js +881 -0
  91. package/dist/runtime.js.map +1 -0
  92. package/dist/secure_llm.d.ts +93 -0
  93. package/dist/secure_llm.d.ts.map +1 -0
  94. package/dist/secure_llm.js +423 -0
  95. package/dist/secure_llm.js.map +1 -0
  96. package/dist/session_factory.d.ts +6 -0
  97. package/dist/session_factory.d.ts.map +1 -0
  98. package/dist/session_factory.js +29 -0
  99. package/dist/session_factory.js.map +1 -0
  100. package/dist/session_fork.d.ts +76 -0
  101. package/dist/session_fork.d.ts.map +1 -0
  102. package/dist/session_fork.js +428 -0
  103. package/dist/session_fork.js.map +1 -0
  104. package/dist/subagents.d.ts +37 -0
  105. package/dist/subagents.d.ts.map +1 -0
  106. package/dist/subagents.js +67 -0
  107. package/dist/subagents.js.map +1 -0
  108. package/dist/suspend.d.ts +27 -0
  109. package/dist/suspend.d.ts.map +1 -0
  110. package/dist/suspend.js +87 -0
  111. package/dist/suspend.js.map +1 -0
  112. package/dist/thread_id.d.ts +30 -0
  113. package/dist/thread_id.d.ts.map +1 -0
  114. package/dist/thread_id.js +69 -0
  115. package/dist/thread_id.js.map +1 -0
  116. package/dist/workflow_json.d.ts +11 -0
  117. package/dist/workflow_json.d.ts.map +1 -0
  118. package/dist/workflow_json.js +49 -0
  119. package/dist/workflow_json.js.map +1 -0
  120. package/dist/workflow_message.d.ts +11 -0
  121. package/dist/workflow_message.d.ts.map +1 -0
  122. package/dist/workflow_message.js +179 -0
  123. package/dist/workflow_message.js.map +1 -0
  124. package/dist/workflow_state.d.ts +7 -0
  125. package/dist/workflow_state.d.ts.map +1 -0
  126. package/dist/workflow_state.js +62 -0
  127. package/dist/workflow_state.js.map +1 -0
  128. package/package.json +66 -0
@@ -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