@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE.md +201 -0
- package/README.md +29 -0
- package/dist/agent_config.d.ts +167 -0
- package/dist/agent_config.d.ts.map +1 -0
- package/dist/agent_config.js +544 -0
- package/dist/call_interrupted.d.ts +12 -0
- package/dist/call_interrupted.d.ts.map +1 -0
- package/dist/call_interrupted.js +11 -0
- package/dist/checkpoint_workspace.d.ts +25 -0
- package/dist/checkpoint_workspace.d.ts.map +1 -0
- package/dist/checkpoint_workspace.js +44 -0
- package/dist/context.d.ts +235 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +322 -0
- package/dist/db_config.d.ts +28 -0
- package/dist/db_config.d.ts.map +1 -0
- package/dist/db_config.js +66 -0
- package/dist/db_naming.d.ts +54 -0
- package/dist/db_naming.d.ts.map +1 -0
- package/dist/db_naming.js +94 -0
- package/dist/error_reporting.d.ts +67 -0
- package/dist/error_reporting.d.ts.map +1 -0
- package/dist/error_reporting.js +311 -0
- package/dist/generated/workflow/v1/activity_pb.d.ts +342 -0
- package/dist/generated/workflow/v1/activity_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/activity_pb.js +115 -0
- package/dist/generated/workflow/v1/common_pb.d.ts +184 -0
- package/dist/generated/workflow/v1/common_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/common_pb.js +86 -0
- package/dist/generated/workflow/v1/runtime_pb.d.ts +200 -0
- package/dist/generated/workflow/v1/runtime_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/runtime_pb.js +40 -0
- package/dist/generated/workflow/v1/state_pb.d.ts +254 -0
- package/dist/generated/workflow/v1/state_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/state_pb.js +68 -0
- package/dist/guardrails_evaluator/core.d.ts +23 -0
- package/dist/guardrails_evaluator/core.d.ts.map +1 -0
- package/dist/guardrails_evaluator/core.js +122 -0
- package/dist/guardrails_evaluator/index.d.ts +10 -0
- package/dist/guardrails_evaluator/index.d.ts.map +1 -0
- package/dist/guardrails_evaluator/index.js +11 -0
- package/dist/guardrails_evaluator/regex.d.ts +20 -0
- package/dist/guardrails_evaluator/regex.d.ts.map +1 -0
- package/dist/guardrails_evaluator/regex.js +233 -0
- package/dist/hooks.d.ts +109 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +216 -0
- package/dist/http_path.d.ts +18 -0
- package/dist/http_path.d.ts.map +1 -0
- package/dist/http_path.js +53 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/launcher.d.ts +130 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +325 -0
- package/dist/logger.d.ts +96 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +204 -0
- package/dist/mcp_oauth.d.ts +51 -0
- package/dist/mcp_oauth.d.ts.map +1 -0
- package/dist/mcp_oauth.js +389 -0
- package/dist/mcp_oauth_secret.d.ts +21 -0
- package/dist/mcp_oauth_secret.d.ts.map +1 -0
- package/dist/mcp_oauth_secret.js +122 -0
- package/dist/mcp_tools.d.ts +71 -0
- package/dist/mcp_tools.d.ts.map +1 -0
- package/dist/mcp_tools.js +301 -0
- package/dist/memory_appbound.d.ts +42 -0
- package/dist/memory_appbound.d.ts.map +1 -0
- package/dist/memory_appbound.js +159 -0
- package/dist/memory_writer.d.ts +49 -0
- package/dist/memory_writer.d.ts.map +1 -0
- package/dist/memory_writer.js +171 -0
- package/dist/metrics.d.ts +84 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +205 -0
- package/dist/models.d.ts +1458 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +1726 -0
- package/dist/node_logger.d.ts +43 -0
- package/dist/node_logger.d.ts.map +1 -0
- package/dist/node_logger.js +158 -0
- package/dist/owner_callback.d.ts +16 -0
- package/dist/owner_callback.d.ts.map +1 -0
- package/dist/owner_callback.js +40 -0
- package/dist/progress.d.ts +57 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +140 -0
- package/dist/runtime.d.ts +131 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +351 -0
- package/dist/secure_llm_proxy.d.ts +115 -0
- package/dist/secure_llm_proxy.d.ts.map +1 -0
- package/dist/secure_llm_proxy.js +922 -0
- package/dist/secure_wrapper.d.ts +332 -0
- package/dist/secure_wrapper.d.ts.map +1 -0
- package/dist/secure_wrapper.js +1249 -0
- package/dist/server/aer.d.ts +61 -0
- package/dist/server/aer.d.ts.map +1 -0
- package/dist/server/aer.js +1124 -0
- package/dist/server/auth.d.ts +56 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +132 -0
- package/dist/server/base.d.ts +104 -0
- package/dist/server/base.d.ts.map +1 -0
- package/dist/server/base.js +150 -0
- package/dist/server/callInterrupt.d.ts +49 -0
- package/dist/server/callInterrupt.d.ts.map +1 -0
- package/dist/server/callInterrupt.js +68 -0
- package/dist/server/callback_delivery.d.ts +14 -0
- package/dist/server/callback_delivery.d.ts.map +1 -0
- package/dist/server/callback_delivery.js +141 -0
- package/dist/server/chunk_types.d.ts +50 -0
- package/dist/server/chunk_types.d.ts.map +1 -0
- package/dist/server/chunk_types.js +62 -0
- package/dist/server/cors.d.ts +52 -0
- package/dist/server/cors.d.ts.map +1 -0
- package/dist/server/cors.js +107 -0
- package/dist/server/drain.d.ts +169 -0
- package/dist/server/drain.d.ts.map +1 -0
- package/dist/server/drain.js +455 -0
- package/dist/server/function.d.ts +77 -0
- package/dist/server/function.d.ts.map +1 -0
- package/dist/server/function.js +337 -0
- package/dist/server/http_retry.d.ts +37 -0
- package/dist/server/http_retry.d.ts.map +1 -0
- package/dist/server/http_retry.js +157 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/metadata.d.ts +50 -0
- package/dist/server/metadata.d.ts.map +1 -0
- package/dist/server/metadata.js +193 -0
- package/dist/server/oe_url.d.ts +36 -0
- package/dist/server/oe_url.d.ts.map +1 -0
- package/dist/server/oe_url.js +50 -0
- package/dist/server/owner_url.d.ts +35 -0
- package/dist/server/owner_url.d.ts.map +1 -0
- package/dist/server/owner_url.js +146 -0
- package/dist/server/query.d.ts +42 -0
- package/dist/server/query.d.ts.map +1 -0
- package/dist/server/query.js +28 -0
- package/dist/server/tool.d.ts +138 -0
- package/dist/server/tool.d.ts.map +1 -0
- package/dist/server/tool.js +1017 -0
- package/dist/span_names.d.ts +21 -0
- package/dist/span_names.d.ts.map +1 -0
- package/dist/span_names.js +31 -0
- package/dist/structured_logging/constants.d.ts +17 -0
- package/dist/structured_logging/constants.d.ts.map +1 -0
- package/dist/structured_logging/constants.js +71 -0
- package/dist/structured_logging/env.d.ts +18 -0
- package/dist/structured_logging/env.d.ts.map +1 -0
- package/dist/structured_logging/env.js +39 -0
- package/dist/structured_logging/install.d.ts +56 -0
- package/dist/structured_logging/install.d.ts.map +1 -0
- package/dist/structured_logging/install.js +107 -0
- package/dist/structured_logging/layout.d.ts +9 -0
- package/dist/structured_logging/layout.d.ts.map +1 -0
- package/dist/structured_logging/layout.js +144 -0
- package/dist/structured_logging/serialize.d.ts +27 -0
- package/dist/structured_logging/serialize.d.ts.map +1 -0
- package/dist/structured_logging/serialize.js +61 -0
- package/dist/structured_logging/stdio_capture.d.ts +59 -0
- package/dist/structured_logging/stdio_capture.d.ts.map +1 -0
- package/dist/structured_logging/stdio_capture.js +164 -0
- package/dist/structured_logging/uncaught.d.ts +14 -0
- package/dist/structured_logging/uncaught.d.ts.map +1 -0
- package/dist/structured_logging/uncaught.js +58 -0
- package/dist/structured_logging.d.ts +48 -0
- package/dist/structured_logging.d.ts.map +1 -0
- package/dist/structured_logging.js +47 -0
- package/dist/tls_client.d.ts +61 -0
- package/dist/tls_client.d.ts.map +1 -0
- package/dist/tls_client.js +298 -0
- package/dist/tool_api_error.d.ts +62 -0
- package/dist/tool_api_error.d.ts.map +1 -0
- package/dist/tool_api_error.js +399 -0
- package/dist/tool_memory_ownership.d.ts +10 -0
- package/dist/tool_memory_ownership.d.ts.map +1 -0
- package/dist/tool_memory_ownership.js +36 -0
- package/dist/toolpod_handlers.d.ts +126 -0
- package/dist/toolpod_handlers.d.ts.map +1 -0
- package/dist/toolpod_handlers.js +1016 -0
- package/dist/tracing/exporters.d.ts +51 -0
- package/dist/tracing/exporters.d.ts.map +1 -0
- package/dist/tracing/exporters.js +327 -0
- package/dist/tracing/index.d.ts +3 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +2 -0
- package/dist/tracing/setup.d.ts +76 -0
- package/dist/tracing/setup.d.ts.map +1 -0
- package/dist/tracing/setup.js +436 -0
- package/dist/utils.d.ts +204 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +867 -0
- package/dist/workflow/activity.d.ts +71 -0
- package/dist/workflow/activity.d.ts.map +1 -0
- package/dist/workflow/activity.js +357 -0
- package/dist/workflow/attempt.d.ts +12 -0
- package/dist/workflow/attempt.d.ts.map +1 -0
- package/dist/workflow/attempt.js +96 -0
- package/dist/workflow/client.d.ts +46 -0
- package/dist/workflow/client.d.ts.map +1 -0
- package/dist/workflow/client.js +299 -0
- package/dist/workflow/context.d.ts +37 -0
- package/dist/workflow/context.d.ts.map +1 -0
- package/dist/workflow/context.js +350 -0
- package/dist/workflow/heartbeat.d.ts +15 -0
- package/dist/workflow/heartbeat.d.ts.map +1 -0
- package/dist/workflow/heartbeat.js +78 -0
- package/dist/workflow/index.d.ts +14 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/memory.d.ts +17 -0
- package/dist/workflow/memory.d.ts.map +1 -0
- package/dist/workflow/memory.js +184 -0
- 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"}
|