@mongodb-js/agent-engine-runner-shared 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 (220) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +29 -0
  4. package/dist/agent_config.d.ts +167 -0
  5. package/dist/agent_config.d.ts.map +1 -0
  6. package/dist/agent_config.js +544 -0
  7. package/dist/call_interrupted.d.ts +12 -0
  8. package/dist/call_interrupted.d.ts.map +1 -0
  9. package/dist/call_interrupted.js +11 -0
  10. package/dist/checkpoint_workspace.d.ts +25 -0
  11. package/dist/checkpoint_workspace.d.ts.map +1 -0
  12. package/dist/checkpoint_workspace.js +44 -0
  13. package/dist/context.d.ts +235 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +322 -0
  16. package/dist/db_config.d.ts +28 -0
  17. package/dist/db_config.d.ts.map +1 -0
  18. package/dist/db_config.js +66 -0
  19. package/dist/db_naming.d.ts +54 -0
  20. package/dist/db_naming.d.ts.map +1 -0
  21. package/dist/db_naming.js +94 -0
  22. package/dist/error_reporting.d.ts +67 -0
  23. package/dist/error_reporting.d.ts.map +1 -0
  24. package/dist/error_reporting.js +311 -0
  25. package/dist/generated/workflow/v1/activity_pb.d.ts +342 -0
  26. package/dist/generated/workflow/v1/activity_pb.d.ts.map +1 -0
  27. package/dist/generated/workflow/v1/activity_pb.js +115 -0
  28. package/dist/generated/workflow/v1/common_pb.d.ts +184 -0
  29. package/dist/generated/workflow/v1/common_pb.d.ts.map +1 -0
  30. package/dist/generated/workflow/v1/common_pb.js +86 -0
  31. package/dist/generated/workflow/v1/runtime_pb.d.ts +200 -0
  32. package/dist/generated/workflow/v1/runtime_pb.d.ts.map +1 -0
  33. package/dist/generated/workflow/v1/runtime_pb.js +40 -0
  34. package/dist/generated/workflow/v1/state_pb.d.ts +254 -0
  35. package/dist/generated/workflow/v1/state_pb.d.ts.map +1 -0
  36. package/dist/generated/workflow/v1/state_pb.js +68 -0
  37. package/dist/guardrails_evaluator/core.d.ts +23 -0
  38. package/dist/guardrails_evaluator/core.d.ts.map +1 -0
  39. package/dist/guardrails_evaluator/core.js +122 -0
  40. package/dist/guardrails_evaluator/index.d.ts +10 -0
  41. package/dist/guardrails_evaluator/index.d.ts.map +1 -0
  42. package/dist/guardrails_evaluator/index.js +11 -0
  43. package/dist/guardrails_evaluator/regex.d.ts +20 -0
  44. package/dist/guardrails_evaluator/regex.d.ts.map +1 -0
  45. package/dist/guardrails_evaluator/regex.js +233 -0
  46. package/dist/hooks.d.ts +109 -0
  47. package/dist/hooks.d.ts.map +1 -0
  48. package/dist/hooks.js +216 -0
  49. package/dist/http_path.d.ts +18 -0
  50. package/dist/http_path.d.ts.map +1 -0
  51. package/dist/http_path.js +53 -0
  52. package/dist/index.d.ts +35 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +41 -0
  55. package/dist/launcher.d.ts +130 -0
  56. package/dist/launcher.d.ts.map +1 -0
  57. package/dist/launcher.js +325 -0
  58. package/dist/logger.d.ts +96 -0
  59. package/dist/logger.d.ts.map +1 -0
  60. package/dist/logger.js +204 -0
  61. package/dist/mcp_oauth.d.ts +51 -0
  62. package/dist/mcp_oauth.d.ts.map +1 -0
  63. package/dist/mcp_oauth.js +389 -0
  64. package/dist/mcp_oauth_secret.d.ts +21 -0
  65. package/dist/mcp_oauth_secret.d.ts.map +1 -0
  66. package/dist/mcp_oauth_secret.js +122 -0
  67. package/dist/mcp_tools.d.ts +71 -0
  68. package/dist/mcp_tools.d.ts.map +1 -0
  69. package/dist/mcp_tools.js +301 -0
  70. package/dist/memory_appbound.d.ts +42 -0
  71. package/dist/memory_appbound.d.ts.map +1 -0
  72. package/dist/memory_appbound.js +159 -0
  73. package/dist/memory_writer.d.ts +49 -0
  74. package/dist/memory_writer.d.ts.map +1 -0
  75. package/dist/memory_writer.js +171 -0
  76. package/dist/metrics.d.ts +84 -0
  77. package/dist/metrics.d.ts.map +1 -0
  78. package/dist/metrics.js +205 -0
  79. package/dist/models.d.ts +1458 -0
  80. package/dist/models.d.ts.map +1 -0
  81. package/dist/models.js +1726 -0
  82. package/dist/node_logger.d.ts +43 -0
  83. package/dist/node_logger.d.ts.map +1 -0
  84. package/dist/node_logger.js +158 -0
  85. package/dist/owner_callback.d.ts +16 -0
  86. package/dist/owner_callback.d.ts.map +1 -0
  87. package/dist/owner_callback.js +40 -0
  88. package/dist/progress.d.ts +57 -0
  89. package/dist/progress.d.ts.map +1 -0
  90. package/dist/progress.js +140 -0
  91. package/dist/runtime.d.ts +131 -0
  92. package/dist/runtime.d.ts.map +1 -0
  93. package/dist/runtime.js +351 -0
  94. package/dist/secure_llm_proxy.d.ts +115 -0
  95. package/dist/secure_llm_proxy.d.ts.map +1 -0
  96. package/dist/secure_llm_proxy.js +922 -0
  97. package/dist/secure_wrapper.d.ts +332 -0
  98. package/dist/secure_wrapper.d.ts.map +1 -0
  99. package/dist/secure_wrapper.js +1249 -0
  100. package/dist/server/aer.d.ts +61 -0
  101. package/dist/server/aer.d.ts.map +1 -0
  102. package/dist/server/aer.js +1124 -0
  103. package/dist/server/auth.d.ts +56 -0
  104. package/dist/server/auth.d.ts.map +1 -0
  105. package/dist/server/auth.js +132 -0
  106. package/dist/server/base.d.ts +104 -0
  107. package/dist/server/base.d.ts.map +1 -0
  108. package/dist/server/base.js +150 -0
  109. package/dist/server/callInterrupt.d.ts +49 -0
  110. package/dist/server/callInterrupt.d.ts.map +1 -0
  111. package/dist/server/callInterrupt.js +68 -0
  112. package/dist/server/callback_delivery.d.ts +14 -0
  113. package/dist/server/callback_delivery.d.ts.map +1 -0
  114. package/dist/server/callback_delivery.js +141 -0
  115. package/dist/server/chunk_types.d.ts +50 -0
  116. package/dist/server/chunk_types.d.ts.map +1 -0
  117. package/dist/server/chunk_types.js +62 -0
  118. package/dist/server/cors.d.ts +52 -0
  119. package/dist/server/cors.d.ts.map +1 -0
  120. package/dist/server/cors.js +107 -0
  121. package/dist/server/drain.d.ts +169 -0
  122. package/dist/server/drain.d.ts.map +1 -0
  123. package/dist/server/drain.js +455 -0
  124. package/dist/server/function.d.ts +77 -0
  125. package/dist/server/function.d.ts.map +1 -0
  126. package/dist/server/function.js +337 -0
  127. package/dist/server/http_retry.d.ts +37 -0
  128. package/dist/server/http_retry.d.ts.map +1 -0
  129. package/dist/server/http_retry.js +157 -0
  130. package/dist/server/index.d.ts +7 -0
  131. package/dist/server/index.d.ts.map +1 -0
  132. package/dist/server/index.js +5 -0
  133. package/dist/server/metadata.d.ts +50 -0
  134. package/dist/server/metadata.d.ts.map +1 -0
  135. package/dist/server/metadata.js +193 -0
  136. package/dist/server/oe_url.d.ts +36 -0
  137. package/dist/server/oe_url.d.ts.map +1 -0
  138. package/dist/server/oe_url.js +50 -0
  139. package/dist/server/owner_url.d.ts +35 -0
  140. package/dist/server/owner_url.d.ts.map +1 -0
  141. package/dist/server/owner_url.js +146 -0
  142. package/dist/server/query.d.ts +42 -0
  143. package/dist/server/query.d.ts.map +1 -0
  144. package/dist/server/query.js +28 -0
  145. package/dist/server/tool.d.ts +138 -0
  146. package/dist/server/tool.d.ts.map +1 -0
  147. package/dist/server/tool.js +1017 -0
  148. package/dist/span_names.d.ts +21 -0
  149. package/dist/span_names.d.ts.map +1 -0
  150. package/dist/span_names.js +31 -0
  151. package/dist/structured_logging/constants.d.ts +17 -0
  152. package/dist/structured_logging/constants.d.ts.map +1 -0
  153. package/dist/structured_logging/constants.js +71 -0
  154. package/dist/structured_logging/env.d.ts +18 -0
  155. package/dist/structured_logging/env.d.ts.map +1 -0
  156. package/dist/structured_logging/env.js +39 -0
  157. package/dist/structured_logging/install.d.ts +56 -0
  158. package/dist/structured_logging/install.d.ts.map +1 -0
  159. package/dist/structured_logging/install.js +107 -0
  160. package/dist/structured_logging/layout.d.ts +9 -0
  161. package/dist/structured_logging/layout.d.ts.map +1 -0
  162. package/dist/structured_logging/layout.js +144 -0
  163. package/dist/structured_logging/serialize.d.ts +27 -0
  164. package/dist/structured_logging/serialize.d.ts.map +1 -0
  165. package/dist/structured_logging/serialize.js +61 -0
  166. package/dist/structured_logging/stdio_capture.d.ts +59 -0
  167. package/dist/structured_logging/stdio_capture.d.ts.map +1 -0
  168. package/dist/structured_logging/stdio_capture.js +164 -0
  169. package/dist/structured_logging/uncaught.d.ts +14 -0
  170. package/dist/structured_logging/uncaught.d.ts.map +1 -0
  171. package/dist/structured_logging/uncaught.js +58 -0
  172. package/dist/structured_logging.d.ts +48 -0
  173. package/dist/structured_logging.d.ts.map +1 -0
  174. package/dist/structured_logging.js +47 -0
  175. package/dist/tls_client.d.ts +61 -0
  176. package/dist/tls_client.d.ts.map +1 -0
  177. package/dist/tls_client.js +298 -0
  178. package/dist/tool_api_error.d.ts +62 -0
  179. package/dist/tool_api_error.d.ts.map +1 -0
  180. package/dist/tool_api_error.js +399 -0
  181. package/dist/tool_memory_ownership.d.ts +10 -0
  182. package/dist/tool_memory_ownership.d.ts.map +1 -0
  183. package/dist/tool_memory_ownership.js +36 -0
  184. package/dist/toolpod_handlers.d.ts +126 -0
  185. package/dist/toolpod_handlers.d.ts.map +1 -0
  186. package/dist/toolpod_handlers.js +1016 -0
  187. package/dist/tracing/exporters.d.ts +51 -0
  188. package/dist/tracing/exporters.d.ts.map +1 -0
  189. package/dist/tracing/exporters.js +327 -0
  190. package/dist/tracing/index.d.ts +3 -0
  191. package/dist/tracing/index.d.ts.map +1 -0
  192. package/dist/tracing/index.js +2 -0
  193. package/dist/tracing/setup.d.ts +76 -0
  194. package/dist/tracing/setup.d.ts.map +1 -0
  195. package/dist/tracing/setup.js +436 -0
  196. package/dist/utils.d.ts +204 -0
  197. package/dist/utils.d.ts.map +1 -0
  198. package/dist/utils.js +867 -0
  199. package/dist/workflow/activity.d.ts +71 -0
  200. package/dist/workflow/activity.d.ts.map +1 -0
  201. package/dist/workflow/activity.js +357 -0
  202. package/dist/workflow/attempt.d.ts +12 -0
  203. package/dist/workflow/attempt.d.ts.map +1 -0
  204. package/dist/workflow/attempt.js +96 -0
  205. package/dist/workflow/client.d.ts +46 -0
  206. package/dist/workflow/client.d.ts.map +1 -0
  207. package/dist/workflow/client.js +299 -0
  208. package/dist/workflow/context.d.ts +37 -0
  209. package/dist/workflow/context.d.ts.map +1 -0
  210. package/dist/workflow/context.js +350 -0
  211. package/dist/workflow/heartbeat.d.ts +15 -0
  212. package/dist/workflow/heartbeat.d.ts.map +1 -0
  213. package/dist/workflow/heartbeat.js +78 -0
  214. package/dist/workflow/index.d.ts +14 -0
  215. package/dist/workflow/index.d.ts.map +1 -0
  216. package/dist/workflow/index.js +10 -0
  217. package/dist/workflow/memory.d.ts +17 -0
  218. package/dist/workflow/memory.d.ts.map +1 -0
  219. package/dist/workflow/memory.js +184 -0
  220. package/package.json +73 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Execution-scoped drain receiver for Runner SDK servers.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.drain`. Both runtimes are parallel
5
+ * implementations of the same server architecture, so this file and its
6
+ * Python twin must be changed together.
7
+ *
8
+ * When the OE durably cancels an execution it POSTs `/drain` to the Tool/AER
9
+ * runtime owning that execution. The runtime blocks new work for the
10
+ * execution, aborts tracked in-flight work where a signal channel exists,
11
+ * and records an idempotent final outcome the caller retrieves by re-sending
12
+ * the same request.
13
+ *
14
+ * Contract (pinned by the shared fixture in
15
+ * `client-libraries/test-fixtures/drain/contract.json`):
16
+ *
17
+ * - `POST /drain {request_id, execution_id, reason, deadline_at_ms, workspace_id}`
18
+ * — `workspace_id` is mandatory on workspace-scoped runtimes (`APP_ID`
19
+ * set) and must match it exactly.
20
+ * - `202 {"outcome": "accepted"}` while draining; `200` with the final
21
+ * outcome (`completed` / `timed_out` / `delivery_failed`) afterwards.
22
+ * - `deadline_at_ms` is absolute. The runtime never extends it.
23
+ * - `accepted` is HTTP-level acceptance only; `completed` is the only
24
+ * quiescence signal.
25
+ * - An execution this runtime never served is `delivery_failed` with
26
+ * `reason_code=execution_not_found` — exact OE targeting does not prove a
27
+ * false `completed` safe, since one execution can span AER and Tool
28
+ * runtimes independently.
29
+ *
30
+ * TS-vs-Python divergence, matching the existing boundary tests: Python can
31
+ * cancel coroutine tasks outright; a promise has no cancellation, so "notify
32
+ * active work" here means aborting an AbortController. That reaches work
33
+ * only where a signal channel already exists (AER execution context, LLM
34
+ * streams). Plain tool functions receive no signal — they are tracked and
35
+ * admission-blocked, and outliving the deadline is an honest `timed_out`.
36
+ *
37
+ * State is process-local by design: a restarted runtime has lost its active
38
+ * work and answers `delivery_failed`; reconciliation across restarts belongs
39
+ * to the caller's durable record, not to this registry.
40
+ */
41
+ import { z } from "zod";
42
+ import { getLogger } from "../logger.js";
43
+ const logger = getLogger("agent_engine_runner_shared.server.drain");
44
+ /** How long a finalized drain record (and never-drained tombstone) survives
45
+ * for idempotent retries. Must be >= the caller-side retry window. */
46
+ const DEFAULT_RECORD_TTL_S = 900;
47
+ /** Upper bound on the caller-chosen drain window; the runtime never extends
48
+ * a deadline, and rejects ones set unreasonably far out. */
49
+ const DEFAULT_MAX_DEADLINE_MS = 60_000;
50
+ export const DrainRequestSchema = z.object({
51
+ request_id: z
52
+ .string()
53
+ .min(1)
54
+ .max(128)
55
+ .regex(/^[A-Za-z0-9._:-]+$/),
56
+ execution_id: z.string().min(1).max(256),
57
+ reason: z.enum(["execution_cancelled", "rolling_restart"]),
58
+ deadline_at_ms: z.number().int().positive(),
59
+ // Null is an accepted spelling of omission, matching the Python twin and
60
+ // the published OpenAPI contract (string|null). The route's truthiness
61
+ // checks treat it as a missing scope claim either way.
62
+ workspace_id: z.string().max(256).nullish(),
63
+ });
64
+ /**
65
+ * `AbortController.abort()` reason used by the per-call abort, distinct from
66
+ * DRAIN_ABORT_REASON so error paths can tell the two apart.
67
+ */
68
+ export const CALL_INTERRUPT_REASON = "call_interrupted";
69
+ /**
70
+ * `AbortController.abort()` reason used by the drain, so error paths can tell
71
+ * "stopped by cancellation" apart from the execution timeout, which shares
72
+ * the same controller in the AER.
73
+ */
74
+ export const DRAIN_ABORT_REASON = "execution_drained";
75
+ /**
76
+ * How long an abort that preceded its call's registration stays claimable by
77
+ * `beginWork`. The window covers the OE-claim → in-graph-dispatch handoff; the
78
+ * platform's dispatch claim makes a same-step collision inside it impossible.
79
+ * Mirrors Python's `PRE_ABORT_RETENTION_S`.
80
+ */
81
+ const PRE_ABORT_RETENTION_MS = 60_000;
82
+ function newIdle() {
83
+ let resolve;
84
+ const promise = new Promise((r) => {
85
+ resolve = r;
86
+ });
87
+ return { promise, resolve };
88
+ }
89
+ function drainingError() {
90
+ return Object.assign(new Error("execution_draining"), {
91
+ statusCode: 409,
92
+ });
93
+ }
94
+ /**
95
+ * Reject malformed drain tunables rather than silently collapsing retention
96
+ * or the deadline ceiling — `Number("bogus")` is NaN, which would evict
97
+ * records immediately or disable the deadline check entirely. Mirrors
98
+ * Python's fail-fast `_positive`.
99
+ */
100
+ function parsePositiveNumber(raw, fallback, name) {
101
+ if (raw === undefined || raw.trim() === "")
102
+ return fallback;
103
+ const value = Number(raw);
104
+ if (!Number.isFinite(value) || value <= 0) {
105
+ throw new Error(`${name} must be a positive finite number, got ${raw}`);
106
+ }
107
+ return value;
108
+ }
109
+ /**
110
+ * Execution-scoped admission + active-work registry for one runtime process.
111
+ *
112
+ * Mutating methods are synchronous, so check-and-set is atomic on Node's
113
+ * single thread. The registry is created per server; per-process state only.
114
+ */
115
+ export class DrainRegistry {
116
+ recordTtlMs;
117
+ maxDeadlineMsValue;
118
+ entries = new Map();
119
+ /**
120
+ * Aborts that arrived before their call registered, keyed by execution and
121
+ * step, valued by expiry. The window is the OE-claim → in-graph-dispatch
122
+ * handoff; the retention covers graph scheduling delays, not call lifetimes.
123
+ */
124
+ preAborted = new Map();
125
+ constructor(opts = {}) {
126
+ const env = opts.env ?? process.env;
127
+ this.recordTtlMs =
128
+ opts.recordTtlMs ??
129
+ parsePositiveNumber(env["RUNNER_DRAIN_RECORD_TTL_S"], DEFAULT_RECORD_TTL_S, "RUNNER_DRAIN_RECORD_TTL_S") * 1000;
130
+ this.maxDeadlineMsValue =
131
+ opts.maxDeadlineMs ??
132
+ parsePositiveNumber(env["RUNNER_DRAIN_MAX_DEADLINE_MS"], DEFAULT_MAX_DEADLINE_MS, "RUNNER_DRAIN_MAX_DEADLINE_MS");
133
+ }
134
+ get maxDeadlineMs() {
135
+ return this.maxDeadlineMsValue;
136
+ }
137
+ /** Reject work for a draining execution without registering it. */
138
+ checkAdmission(executionId) {
139
+ const entry = this.entries.get(executionId);
140
+ if (entry?.draining)
141
+ throw drainingError();
142
+ }
143
+ /**
144
+ * Register in-flight work for an execution, rejecting drained ones.
145
+ *
146
+ * `controller` is the abort handle the drain fires; omit it for work the
147
+ * runtime can only wait out (e.g. a tool function with no signal channel).
148
+ * `stepNumber` makes the work addressable by the per-call abort
149
+ * (`abortCall`); work registered without it is only ever drained
150
+ * execution-wide.
151
+ *
152
+ * Returns true when a per-call abort preceded this registration (the Stop
153
+ * landed in the OE-claim → in-graph-dispatch handoff): the caller must not
154
+ * start the body and must settle the call interrupted.
155
+ */
156
+ beginWork(executionId, controller, stepNumber) {
157
+ let entry = this.entries.get(executionId);
158
+ if (entry === undefined) {
159
+ entry = {
160
+ activeCount: 0,
161
+ controllers: new Set(),
162
+ draining: false,
163
+ drain: null,
164
+ idle: newIdle(),
165
+ byStep: new Map(),
166
+ };
167
+ this.entries.set(executionId, entry);
168
+ }
169
+ else {
170
+ if (entry.draining)
171
+ throw drainingError();
172
+ this.cancelEviction(entry);
173
+ if (entry.activeCount === 0) {
174
+ // Reviving a tombstone (e.g. a HITL resume reusing the id).
175
+ entry.idle = newIdle();
176
+ }
177
+ }
178
+ entry.activeCount += 1;
179
+ if (controller !== undefined)
180
+ entry.controllers.add(controller);
181
+ let preAborted = false;
182
+ if (stepNumber !== undefined) {
183
+ preAborted = this.popPreAborted(executionId, stepNumber);
184
+ // A step maps to one in-flight call by the platform's dispatch claim,
185
+ // so this can only replace a settled entry.
186
+ entry.byStep.set(stepNumber, {
187
+ controller,
188
+ settled: false,
189
+ aborted: preAborted,
190
+ });
191
+ if (preAborted && controller !== undefined) {
192
+ controller.abort(CALL_INTERRUPT_REASON);
193
+ }
194
+ }
195
+ return preAborted;
196
+ }
197
+ preAbortKey(executionId, stepNumber) {
198
+ return `${executionId}\n${stepNumber}`;
199
+ }
200
+ popPreAborted(executionId, stepNumber) {
201
+ const key = this.preAbortKey(executionId, stepNumber);
202
+ const expiry = this.preAborted.get(key);
203
+ this.preAborted.delete(key);
204
+ this.sweepPreAborted();
205
+ return expiry !== undefined && expiry > Date.now();
206
+ }
207
+ sweepPreAborted() {
208
+ if (this.preAborted.size === 0)
209
+ return;
210
+ const now = Date.now();
211
+ for (const [key, expiry] of this.preAborted) {
212
+ if (expiry <= now)
213
+ this.preAborted.delete(key);
214
+ }
215
+ }
216
+ /**
217
+ * Signal exactly one in-flight call, leaving the execution open — the
218
+ * surgical sibling of a drain: no admission latch, no record, no deadline,
219
+ * and later steps of the same execution proceed. Idempotent: a repeat
220
+ * abort of the same step re-reads the same outcome.
221
+ */
222
+ abortCall(executionId, stepNumber) {
223
+ const entry = this.entries.get(executionId);
224
+ const work = entry?.byStep.get(stepNumber);
225
+ if (work === undefined) {
226
+ // The abort can race the call's registration: OE claims the call, then
227
+ // the runtime registers only when the in-graph body starts. Retain it
228
+ // briefly so beginWork settles the late registration as interrupted
229
+ // instead of running a call the caller stopped.
230
+ this.sweepPreAborted();
231
+ this.preAborted.set(this.preAbortKey(executionId, stepNumber), Date.now() + PRE_ABORT_RETENTION_MS);
232
+ return "not_found";
233
+ }
234
+ if (work.settled)
235
+ return "already_settled";
236
+ // The missing-controller check precedes the repeat check: a repeat abort
237
+ // of track-only work must keep answering not_cancellable, matching the
238
+ // Python receiver.
239
+ if (work.controller === undefined)
240
+ return "not_cancellable";
241
+ if (work.aborted)
242
+ return "interrupted";
243
+ work.aborted = true;
244
+ work.controller.abort(CALL_INTERRUPT_REASON);
245
+ return "interrupted";
246
+ }
247
+ /**
248
+ * Close a call's interruptibility as its result report begins: the body
249
+ * already produced its outcome, so a late abort must read already_settled
250
+ * rather than claim a stop the durable record will contradict.
251
+ */
252
+ claimSettlement(executionId, stepNumber) {
253
+ const work = this.entries.get(executionId)?.byStep.get(stepNumber);
254
+ if (work !== undefined)
255
+ work.settled = true;
256
+ }
257
+ /**
258
+ * Attach an abort handle to already-registered work. The AER builds its
259
+ * execution-wide controller inside the handler, after admission — a drain
260
+ * that landed in between aborts it here, at attach time.
261
+ */
262
+ attachController(executionId, controller) {
263
+ const entry = this.entries.get(executionId);
264
+ if (entry === undefined)
265
+ return;
266
+ entry.controllers.add(controller);
267
+ // Abort with the drain reason: the AER shares this controller with its
268
+ // execution timeout and tells them apart by signal.reason.
269
+ if (entry.draining)
270
+ controller.abort(DRAIN_ABORT_REASON);
271
+ }
272
+ endWork(executionId, controller, stepNumber) {
273
+ const entry = this.entries.get(executionId);
274
+ if (entry === undefined)
275
+ return;
276
+ if (controller !== undefined)
277
+ entry.controllers.delete(controller);
278
+ if (stepNumber !== undefined) {
279
+ // Keep the settled step mapped: a late abort must read already_settled,
280
+ // not not_found. The entry's TTL eviction reclaims it.
281
+ const work = entry.byStep.get(stepNumber);
282
+ if (work !== undefined)
283
+ work.settled = true;
284
+ }
285
+ entry.activeCount -= 1;
286
+ if (entry.activeCount > 0)
287
+ return;
288
+ // No work remains: drop every tracked handle (drained or not) so a
289
+ // tombstone never retains controllers and their listeners for its TTL.
290
+ entry.controllers.clear();
291
+ entry.idle.resolve();
292
+ // A pending drain's finalizer owns eviction from here; otherwise the
293
+ // tombstone (and any finalized drain record) expires on the TTL.
294
+ if (entry.drain === null || entry.drain.outcome !== null) {
295
+ this.scheduleEviction(executionId, entry);
296
+ }
297
+ }
298
+ /** Apply one drain request; idempotent by request_id, coalescing per execution. */
299
+ apply(request) {
300
+ const entry = this.entries.get(request.execution_id);
301
+ if (entry?.drain != null) {
302
+ // Same request_id or a different one: one drain per execution.
303
+ entry.drain.requestIds.add(request.request_id);
304
+ return this.current(entry.drain);
305
+ }
306
+ if (entry === undefined) {
307
+ // Unknown execution: not applied here, but still record the outcome
308
+ // and latch admission. A dispatch that slips past the OE gate and lands
309
+ // after this drain must not start, and a retry of the same request must
310
+ // see the same answer for the retention window.
311
+ const record = {
312
+ requestIds: new Set([request.request_id]),
313
+ reason: request.reason,
314
+ deadlineAtMs: request.deadline_at_ms,
315
+ outcome: "delivery_failed",
316
+ reasonCode: "execution_not_found",
317
+ finalizedAtMs: Date.now(),
318
+ };
319
+ const latched = {
320
+ activeCount: 0,
321
+ controllers: new Set(),
322
+ draining: true,
323
+ drain: record,
324
+ idle: newIdle(),
325
+ byStep: new Map(),
326
+ };
327
+ this.entries.set(request.execution_id, latched);
328
+ this.scheduleEviction(request.execution_id, latched);
329
+ return this.final("delivery_failed", "execution_not_found");
330
+ }
331
+ if (entry.activeCount === 0) {
332
+ // Known, already-ended execution: nothing to drain here. Latch
333
+ // admission anyway — a drained execution never accepts work again — and
334
+ // restart retention so the record lives a full TTL from now.
335
+ entry.draining = true;
336
+ const record = {
337
+ requestIds: new Set([request.request_id]),
338
+ reason: request.reason,
339
+ deadlineAtMs: request.deadline_at_ms,
340
+ outcome: "completed",
341
+ finalizedAtMs: Date.now(),
342
+ };
343
+ entry.drain = record;
344
+ this.scheduleEviction(request.execution_id, entry);
345
+ return this.final("completed");
346
+ }
347
+ entry.draining = true;
348
+ const record = {
349
+ requestIds: new Set([request.request_id]),
350
+ reason: request.reason,
351
+ deadlineAtMs: request.deadline_at_ms,
352
+ outcome: null,
353
+ };
354
+ entry.drain = record;
355
+ for (const controller of entry.controllers) {
356
+ controller.abort(DRAIN_ABORT_REASON);
357
+ }
358
+ void this.finalize(request.execution_id, entry, record);
359
+ logger.info(`Drain accepted for execution ${request.execution_id} (reason=${request.reason}, active=${entry.activeCount})`);
360
+ return { status: 202, body: { outcome: "accepted" } };
361
+ }
362
+ async finalize(executionId, entry, record) {
363
+ const remainingMs = record.deadlineAtMs - Date.now();
364
+ let timer;
365
+ const timedOut = await Promise.race([
366
+ entry.idle.promise.then(() => false),
367
+ new Promise((r) => {
368
+ timer = setTimeout(() => r(true), Math.max(0, remainingMs));
369
+ }),
370
+ ]);
371
+ clearTimeout(timer);
372
+ if (timedOut) {
373
+ // Late completion must not overwrite the recorded timeout.
374
+ record.outcome = "timed_out";
375
+ record.reasonCode = "deadline_exceeded";
376
+ }
377
+ else {
378
+ record.outcome = "completed";
379
+ }
380
+ record.finalizedAtMs = Date.now();
381
+ logger.info(`Drain ${record.outcome} for execution ${executionId}`);
382
+ if (entry.activeCount === 0) {
383
+ this.scheduleEviction(executionId, entry);
384
+ }
385
+ }
386
+ scheduleEviction(executionId, entry) {
387
+ this.cancelEviction(entry);
388
+ const timer = setTimeout(() => this.evict(executionId, entry), this.recordTtlMs);
389
+ // The timer is memory hygiene, not work the process must stay alive for.
390
+ timer.unref();
391
+ entry.eviction = timer;
392
+ }
393
+ cancelEviction(entry) {
394
+ if (entry.eviction !== undefined) {
395
+ clearTimeout(entry.eviction);
396
+ entry.eviction = undefined;
397
+ }
398
+ }
399
+ evict(executionId, entry) {
400
+ // A retried drain after eviction finds no entry and gets
401
+ // execution_not_found; it never re-runs side effects.
402
+ if (this.entries.get(executionId) === entry && entry.activeCount === 0) {
403
+ this.entries.delete(executionId);
404
+ }
405
+ }
406
+ current(record) {
407
+ if (record.outcome === null) {
408
+ return { status: 202, body: { outcome: "accepted" } };
409
+ }
410
+ return this.final(record.outcome, record.reasonCode);
411
+ }
412
+ final(outcome, reasonCode) {
413
+ return {
414
+ status: 200,
415
+ body: reasonCode === undefined
416
+ ? { outcome }
417
+ : { outcome, reason_code: reasonCode },
418
+ };
419
+ }
420
+ }
421
+ /**
422
+ * Register `POST /drain` on a runner server.
423
+ *
424
+ * The bearer-auth hook already gates the path; this adds the
425
+ * execution/workspace-scoped validation on top. Callers of this endpoint
426
+ * must not follow redirects — the bearer token must never be forwarded to
427
+ * another host.
428
+ *
429
+ * Scope check: the bearer token authenticates the caller but does not
430
+ * establish that the named drain belongs to this runtime, so when the
431
+ * platform scopes this process to a workspace (`APP_ID` set — every managed
432
+ * runtime), the request must name that workspace exactly; a missing
433
+ * `workspace_id` is a 400, a mismatch a 403. When `APP_ID` is unset (local
434
+ * development against a single unscoped runtime) no workspace check is
435
+ * possible and none is enforced.
436
+ */
437
+ export function registerDrainRoute(app, registry, env = process.env) {
438
+ const workspaceId = (env["APP_ID"] ?? "").trim();
439
+ app.post("/drain", async (request, reply) => {
440
+ const body = DrainRequestSchema.parse(request.body);
441
+ if (workspaceId) {
442
+ if (!body.workspace_id) {
443
+ return reply.code(400).send({ detail: "workspace_id required" });
444
+ }
445
+ if (body.workspace_id !== workspaceId) {
446
+ return reply.code(403).send({ detail: "workspace mismatch" });
447
+ }
448
+ }
449
+ if (body.deadline_at_ms > Date.now() + registry.maxDeadlineMs) {
450
+ return reply.code(400).send({ detail: "deadline exceeds maximum" });
451
+ }
452
+ const verdict = registry.apply(body);
453
+ return reply.code(verdict.status).send(verdict.body);
454
+ });
455
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Function mode for the Runner SDK.
3
+ *
4
+ * Function mode runs a tool CLI-style: the process boots, runs a single named
5
+ * tool from the registry, reports the result to the OE, and exits. It is the
6
+ * per-call counterpart to the long-running tool server (mode `tool`).
7
+ *
8
+ * Mirrors Python's `agent_engine_runner_shared/server/function.py`.
9
+ *
10
+ * How the invocation arrives: fctr seeds `RunRequest.metadata` into the guest
11
+ * metadata directory before the workload starts. The OE sets one key,
12
+ * `request`, to a JSON `ToolFunctionRequest` envelope carrying both the
13
+ * `ToolPodExecuteRequest` and the tool-call `step` number (required by
14
+ * `ToolResultRequest`, owned by OE dispatch). The result is POSTed to
15
+ * `{oe_url}/tool/result`, the same endpoint the AER already uses.
16
+ */
17
+ import { z } from "zod";
18
+ import { type ToolResultRequest } from "../models.js";
19
+ import type { ITenantRuntime } from "./base.js";
20
+ /** The function-mode invocation envelope delivered at /run/meta/request. */
21
+ export declare const ToolFunctionRequestSchema: z.ZodObject<{
22
+ request: z.ZodObject<{
23
+ platform_trace_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ execution_id: z.ZodString;
25
+ tool_name: z.ZodString;
26
+ arguments: z.ZodRecord<z.ZodString, z.ZodType<import("../models.js").JsonValue, unknown, z.core.$ZodTypeInternals<import("../models.js").JsonValue, unknown>>>;
27
+ tool_call_id: z.ZodOptional<z.ZodString>;
28
+ step_number: z.ZodOptional<z.ZodNumber>;
29
+ session_id: z.ZodString;
30
+ user_id: z.ZodOptional<z.ZodString>;
31
+ oe_url: z.ZodOptional<z.ZodString>;
32
+ oe_owner_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ authorization: z.ZodOptional<z.ZodObject<{
34
+ token: z.ZodString;
35
+ expires_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
36
+ }, z.core.$strip>>;
37
+ custom_headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
38
+ payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
39
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
40
+ }, z.core.$strip>;
41
+ step: z.ZodNumber;
42
+ }, z.core.$strip>;
43
+ export type ToolFunctionRequest = z.infer<typeof ToolFunctionRequestSchema>;
44
+ /**
45
+ * Encode the result to a JSON-safe object for the /tool/result POST.
46
+ *
47
+ * A tool may return a value the JSON encoder cannot handle (e.g. a circular
48
+ * reference). Downgrade any encode failure to an error report so the OE still
49
+ * receives a terminal signal. Mirrors Python's _result_payload.
50
+ */
51
+ export declare function buildResultPayload(result: ToolResultRequest, executionId: string): Record<string, unknown>;
52
+ /**
53
+ * Runs one metadata-delivered tool call and exits. Not a server.
54
+ *
55
+ * Reads the ToolFunctionRequest envelope from the guest metadata directory,
56
+ * invokes the named tool from the registry, reports the ToolResultRequest
57
+ * to {oe_url}/tool/result, and returns. Mirrors Python's ToolFunctionRunner.
58
+ *
59
+ * `metadataDir` is injectable for tests.
60
+ */
61
+ export declare class ToolFunctionRunner {
62
+ private readonly runtime;
63
+ private readonly metadataDir;
64
+ constructor(runtime: ITenantRuntime, { metadataDir }?: {
65
+ metadataDir?: string;
66
+ });
67
+ /**
68
+ * Run one tool from the metadata-delivered request and report the result.
69
+ *
70
+ * Re-raises if the result POST cannot be delivered after retries so fctr
71
+ * records the execution as failed.
72
+ */
73
+ run(): Promise<void>;
74
+ private _prepare;
75
+ private _invokeToolFn;
76
+ }
77
+ //# sourceMappingURL=function.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"function.d.ts","sourceRoot":"","sources":["../../src/server/function.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAgBtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAchD,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;iBAGpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAoE5E;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBzB;AAgGD;;;;;;;;GAQG;AACH,qBAAa,kBAAkB;IAI3B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGlB,OAAO,EAAE,cAAc,EACxC,EAAE,WAA0B,EAAE,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO;IAK/D;;;;;OAKG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA+E1B,OAAO,CAAC,QAAQ;YASF,aAAa;CA+E5B"}