@agentfield/sdk 0.1.101 → 0.1.102-rc.1

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/dist/index.js CHANGED
@@ -6,6 +6,9 @@ import { spawn } from 'child_process';
6
6
  import express from 'express';
7
7
  import rateLimit from 'express-rate-limit';
8
8
  import crypto2, { randomUUID, createHash } from 'crypto';
9
+ import axios5, { isAxiosError } from 'axios';
10
+ import http from 'http';
11
+ import https from 'https';
9
12
  import { AsyncLocalStorage } from 'async_hooks';
10
13
  import { tool, jsonSchema, generateText, stepCountIs, streamText, embed, embedMany, generateObject } from 'ai';
11
14
  import { createOpenAI } from '@ai-sdk/openai';
@@ -17,9 +20,6 @@ import { createXai } from '@ai-sdk/xai';
17
20
  import { createDeepSeek } from '@ai-sdk/deepseek';
18
21
  import { createCohere } from '@ai-sdk/cohere';
19
22
  import os from 'os';
20
- import axios4, { isAxiosError } from 'axios';
21
- import http from 'http';
22
- import https from 'https';
23
23
  import WebSocket from 'ws';
24
24
  import { Buffer as Buffer$1 } from 'buffer';
25
25
  import { zodToJsonSchema } from 'zod-to-json-schema';
@@ -1065,8 +1065,8 @@ var CancelRegistry = class {
1065
1065
  * so callers always get a usable signal even outside the control-plane
1066
1066
  * dispatch path (e.g. local manual invocations).
1067
1067
  */
1068
- register(executionId) {
1069
- const controller = new AbortController();
1068
+ register(executionId, existing) {
1069
+ const controller = existing ?? new AbortController();
1070
1070
  if (!executionId) {
1071
1071
  return { controller, release: () => {
1072
1072
  } };
@@ -1133,6 +1133,298 @@ function installCancelRoute(app, registry, logger) {
1133
1133
  }
1134
1134
  );
1135
1135
  }
1136
+
1137
+ // src/agent/pause.ts
1138
+ var ApprovalResult = class {
1139
+ decision;
1140
+ feedback;
1141
+ executionId;
1142
+ approvalRequestId;
1143
+ rawResponse;
1144
+ constructor(params) {
1145
+ this.decision = params.decision;
1146
+ this.feedback = params.feedback ?? "";
1147
+ this.executionId = params.executionId ?? "";
1148
+ this.approvalRequestId = params.approvalRequestId ?? "";
1149
+ this.rawResponse = params.rawResponse;
1150
+ }
1151
+ /** True when the human approved the request. */
1152
+ get approved() {
1153
+ return this.decision === "approved";
1154
+ }
1155
+ /** True when the human asked for changes rather than approving/rejecting. */
1156
+ get changesRequested() {
1157
+ return this.decision === "request_changes";
1158
+ }
1159
+ };
1160
+ var PauseClock = class {
1161
+ totalPausedMs = 0;
1162
+ pauseStartedAt = null;
1163
+ /**
1164
+ * Set by the watchdog when it aborts the reasoner for exceeding the active
1165
+ * budget. Distinguishes a budget-timeout abort from an external cooperative
1166
+ * cancel arriving via the cancel dispatcher.
1167
+ */
1168
+ timedOut = false;
1169
+ startPause() {
1170
+ if (this.pauseStartedAt === null) {
1171
+ this.pauseStartedAt = Date.now();
1172
+ }
1173
+ }
1174
+ endPause() {
1175
+ if (this.pauseStartedAt !== null) {
1176
+ this.totalPausedMs += Date.now() - this.pauseStartedAt;
1177
+ this.pauseStartedAt = null;
1178
+ }
1179
+ }
1180
+ /** Cumulative paused milliseconds, including any in-progress pause. */
1181
+ totalPaused() {
1182
+ if (this.pauseStartedAt === null) {
1183
+ return this.totalPausedMs;
1184
+ }
1185
+ return this.totalPausedMs + (Date.now() - this.pauseStartedAt);
1186
+ }
1187
+ };
1188
+ var PauseManager = class {
1189
+ pending = /* @__PURE__ */ new Map();
1190
+ /** execution_id -> approval_request_id, for fallback resolution. */
1191
+ execToRequest = /* @__PURE__ */ new Map();
1192
+ /**
1193
+ * Register a new pending pause and return the promise to await. Idempotent:
1194
+ * a second register() for the same `approvalRequestId` returns the existing
1195
+ * promise rather than replacing it.
1196
+ */
1197
+ register(approvalRequestId, executionId = "") {
1198
+ const existing = this.pending.get(approvalRequestId);
1199
+ if (existing) {
1200
+ return existing.promise;
1201
+ }
1202
+ let resolveFn;
1203
+ const promise = new Promise((resolve2) => {
1204
+ resolveFn = resolve2;
1205
+ });
1206
+ this.pending.set(approvalRequestId, { resolve: resolveFn, promise });
1207
+ if (executionId) {
1208
+ this.execToRequest.set(executionId, approvalRequestId);
1209
+ }
1210
+ return promise;
1211
+ }
1212
+ /**
1213
+ * Resolve a pending pause by `approvalRequestId`. Returns true if a waiter
1214
+ * was found and resolved, false otherwise.
1215
+ */
1216
+ resolve(approvalRequestId, result) {
1217
+ const entry = this.pending.get(approvalRequestId);
1218
+ if (!entry) {
1219
+ return false;
1220
+ }
1221
+ this.pending.delete(approvalRequestId);
1222
+ for (const [eid, rid] of this.execToRequest) {
1223
+ if (rid === approvalRequestId) {
1224
+ this.execToRequest.delete(eid);
1225
+ break;
1226
+ }
1227
+ }
1228
+ entry.resolve(result);
1229
+ return true;
1230
+ }
1231
+ /**
1232
+ * Fallback: resolve by `executionId` when the callback omits the
1233
+ * `approvalRequestId`. Returns true if a waiter was found.
1234
+ */
1235
+ resolveByExecutionId(executionId, result) {
1236
+ const requestId = this.execToRequest.get(executionId);
1237
+ if (!requestId) {
1238
+ return false;
1239
+ }
1240
+ return this.resolve(requestId, result);
1241
+ }
1242
+ /**
1243
+ * Resolve every pending pause with a `cancelled` result. Used on shutdown so
1244
+ * a reasoner blocked in `ctx.pause()` doesn't hang the process forever.
1245
+ */
1246
+ cancelAll() {
1247
+ for (const [approvalRequestId, entry] of this.pending) {
1248
+ entry.resolve(
1249
+ new ApprovalResult({
1250
+ decision: "cancelled",
1251
+ feedback: "agent shutting down",
1252
+ approvalRequestId
1253
+ })
1254
+ );
1255
+ }
1256
+ this.pending.clear();
1257
+ this.execToRequest.clear();
1258
+ }
1259
+ /** Number of currently-pending pauses. Useful for tests. */
1260
+ pendingCount() {
1261
+ return this.pending.size;
1262
+ }
1263
+ };
1264
+ function parseRawResponse(raw) {
1265
+ if (raw && typeof raw === "object") {
1266
+ return raw;
1267
+ }
1268
+ if (typeof raw === "string" && raw.trim()) {
1269
+ try {
1270
+ const parsed = JSON.parse(raw);
1271
+ if (parsed && typeof parsed === "object") {
1272
+ return parsed;
1273
+ }
1274
+ } catch {
1275
+ return { text: raw };
1276
+ }
1277
+ }
1278
+ return void 0;
1279
+ }
1280
+ function installApprovalWebhookRoute(app, manager, logger) {
1281
+ app.post(
1282
+ "/webhooks/approval",
1283
+ (req, res) => {
1284
+ const body = req.body ?? {};
1285
+ const executionId = String(body.execution_id ?? "");
1286
+ const approvalRequestId = String(body.approval_request_id ?? "");
1287
+ const decision = String(body.decision ?? "");
1288
+ const feedback = typeof body.feedback === "string" ? body.feedback : "";
1289
+ const rawResponse = parseRawResponse(body.response);
1290
+ const result = new ApprovalResult({
1291
+ decision,
1292
+ feedback,
1293
+ executionId,
1294
+ approvalRequestId,
1295
+ rawResponse
1296
+ });
1297
+ let resolved = false;
1298
+ if (approvalRequestId) {
1299
+ resolved = manager.resolve(approvalRequestId, result);
1300
+ }
1301
+ if (!resolved && executionId) {
1302
+ resolved = manager.resolveByExecutionId(executionId, result);
1303
+ }
1304
+ logger?.info("approval-webhook received", {
1305
+ executionId,
1306
+ approvalRequestId,
1307
+ decision,
1308
+ resolved
1309
+ });
1310
+ res.status(200).json({ status: "received", resolved });
1311
+ }
1312
+ );
1313
+ }
1314
+ var httpAgent = new http.Agent({
1315
+ keepAlive: true,
1316
+ maxSockets: 10,
1317
+ maxTotalSockets: 50,
1318
+ maxFreeSockets: 5
1319
+ });
1320
+ var httpsAgent = new https.Agent({
1321
+ keepAlive: true,
1322
+ maxSockets: 10,
1323
+ maxTotalSockets: 50,
1324
+ maxFreeSockets: 5
1325
+ });
1326
+
1327
+ // src/approval/ApprovalClient.ts
1328
+ var ApprovalClient = class {
1329
+ http;
1330
+ nodeId;
1331
+ headers;
1332
+ constructor(opts) {
1333
+ this.http = axios5.create({
1334
+ baseURL: opts.baseURL.replace(/\/$/, ""),
1335
+ timeout: 3e4,
1336
+ httpAgent,
1337
+ httpsAgent
1338
+ });
1339
+ this.nodeId = opts.nodeId;
1340
+ const merged = { ...opts.headers ?? {} };
1341
+ if (opts.apiKey) {
1342
+ merged["X-API-Key"] = opts.apiKey;
1343
+ }
1344
+ this.headers = merged;
1345
+ }
1346
+ /**
1347
+ * Request human approval, transitioning the execution to `waiting`.
1348
+ *
1349
+ * Calls `POST /api/v1/agents/{node}/executions/{id}/request-approval`.
1350
+ */
1351
+ async requestApproval(executionId, payload) {
1352
+ const body = {
1353
+ approval_request_id: payload.approvalRequestId,
1354
+ expires_in_hours: payload.expiresInHours ?? 72
1355
+ };
1356
+ if (payload.approvalRequestUrl) {
1357
+ body.approval_request_url = payload.approvalRequestUrl;
1358
+ }
1359
+ if (payload.callbackUrl) {
1360
+ body.callback_url = payload.callbackUrl;
1361
+ }
1362
+ const res = await this.http.post(
1363
+ `/api/v1/agents/${encodeURIComponent(this.nodeId)}/executions/${encodeURIComponent(executionId)}/request-approval`,
1364
+ body,
1365
+ { headers: { ...this.headers, "Content-Type": "application/json" } }
1366
+ );
1367
+ return {
1368
+ approvalRequestId: res.data.approval_request_id ?? "",
1369
+ approvalRequestUrl: res.data.approval_request_url ?? ""
1370
+ };
1371
+ }
1372
+ /**
1373
+ * Get the current approval status for an execution.
1374
+ *
1375
+ * Calls `GET /api/v1/agents/{node}/executions/{id}/approval-status`.
1376
+ */
1377
+ async getApprovalStatus(executionId) {
1378
+ const res = await this.http.get(
1379
+ `/api/v1/agents/${encodeURIComponent(this.nodeId)}/executions/${encodeURIComponent(executionId)}/approval-status`,
1380
+ { headers: this.headers }
1381
+ );
1382
+ const data = res.data;
1383
+ return {
1384
+ status: data.status ?? "pending",
1385
+ response: data.response,
1386
+ requestUrl: data.request_url,
1387
+ requestedAt: data.requested_at,
1388
+ respondedAt: data.responded_at
1389
+ };
1390
+ }
1391
+ /**
1392
+ * Poll approval status with exponential backoff until resolved.
1393
+ *
1394
+ * Returns once the status is no longer `pending` (i.e. approved, rejected,
1395
+ * or expired).
1396
+ */
1397
+ async waitForApproval(executionId, opts) {
1398
+ const pollInterval = opts?.pollIntervalMs ?? 5e3;
1399
+ const maxInterval = opts?.maxIntervalMs ?? 6e4;
1400
+ const timeout = opts?.timeoutMs;
1401
+ const backoffFactor = 2;
1402
+ const startTime = Date.now();
1403
+ let interval = pollInterval;
1404
+ while (true) {
1405
+ if (timeout != null && Date.now() - startTime >= timeout) {
1406
+ throw new Error(
1407
+ `Approval for execution ${executionId} timed out after ${timeout}ms`
1408
+ );
1409
+ }
1410
+ await sleep(interval);
1411
+ let data;
1412
+ try {
1413
+ data = await this.getApprovalStatus(executionId);
1414
+ } catch {
1415
+ interval = Math.min(interval * backoffFactor, maxInterval);
1416
+ continue;
1417
+ }
1418
+ if (data.status !== "pending") {
1419
+ return data;
1420
+ }
1421
+ interval = Math.min(interval * backoffFactor, maxInterval);
1422
+ }
1423
+ }
1424
+ };
1425
+ function sleep(ms) {
1426
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1427
+ }
1136
1428
  var store = new AsyncLocalStorage();
1137
1429
  var ExecutionContext = class {
1138
1430
  input;
@@ -1543,6 +1835,19 @@ var ReasonerContext = class {
1543
1835
  call(target, input) {
1544
1836
  return this.agent.call(target, input);
1545
1837
  }
1838
+ /**
1839
+ * Pause this execution for external approval / resumption.
1840
+ *
1841
+ * Transitions the execution to `waiting` on the control plane and blocks
1842
+ * until a decision arrives via the agent's approval webhook, or the timeout
1843
+ * elapses (returning `{ decision: 'expired' }`). The caller creates the
1844
+ * approval request on an external service first and passes its
1845
+ * `approvalRequestId`. Delegates to {@link Agent.pause}. See its docs for the
1846
+ * async-execution requirement that lets a pause outlive the dispatch ceiling.
1847
+ */
1848
+ pause(opts) {
1849
+ return this.agent.pause({ ...opts, executionId: this.executionId });
1850
+ }
1546
1851
  discover(options) {
1547
1852
  return this.agent.discover(options);
1548
1853
  }
@@ -2224,18 +2529,6 @@ var ExecutionLogger = class {
2224
2529
  function createExecutionLogger(options = {}) {
2225
2530
  return new ExecutionLogger(options);
2226
2531
  }
2227
- var httpAgent = new http.Agent({
2228
- keepAlive: true,
2229
- maxSockets: 10,
2230
- maxTotalSockets: 50,
2231
- maxFreeSockets: 5
2232
- });
2233
- var httpsAgent = new https.Agent({
2234
- keepAlive: true,
2235
- maxSockets: 10,
2236
- maxTotalSockets: 50,
2237
- maxFreeSockets: 5
2238
- });
2239
2532
  var HEADER_CALLER_DID = "X-Caller-DID";
2240
2533
  var HEADER_DID_SIGNATURE = "X-DID-Signature";
2241
2534
  var HEADER_DID_TIMESTAMP = "X-DID-Timestamp";
@@ -2318,6 +2611,67 @@ function parsePrivateKeyJWK(jwkJSON) {
2318
2611
  });
2319
2612
  }
2320
2613
 
2614
+ // src/status/ExecutionStatus.ts
2615
+ var ExecutionStatus = {
2616
+ PENDING: "pending",
2617
+ QUEUED: "queued",
2618
+ WAITING: "waiting",
2619
+ RUNNING: "running",
2620
+ SUCCEEDED: "succeeded",
2621
+ FAILED: "failed",
2622
+ CANCELLED: "cancelled",
2623
+ TIMEOUT: "timeout",
2624
+ UNKNOWN: "unknown"
2625
+ };
2626
+ var CANONICAL_STATUSES = new Set(
2627
+ Object.values(ExecutionStatus)
2628
+ );
2629
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
2630
+ ExecutionStatus.SUCCEEDED,
2631
+ ExecutionStatus.FAILED,
2632
+ ExecutionStatus.CANCELLED,
2633
+ ExecutionStatus.TIMEOUT
2634
+ ]);
2635
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
2636
+ ExecutionStatus.PENDING,
2637
+ ExecutionStatus.QUEUED,
2638
+ ExecutionStatus.WAITING,
2639
+ ExecutionStatus.RUNNING
2640
+ ]);
2641
+ var STATUS_ALIASES = {
2642
+ success: "succeeded",
2643
+ successful: "succeeded",
2644
+ completed: "succeeded",
2645
+ complete: "succeeded",
2646
+ done: "succeeded",
2647
+ ok: "succeeded",
2648
+ error: "failed",
2649
+ failure: "failed",
2650
+ errored: "failed",
2651
+ canceled: "cancelled",
2652
+ cancel: "cancelled",
2653
+ timed_out: "timeout",
2654
+ wait: "queued",
2655
+ awaiting_approval: "waiting",
2656
+ awaiting_human: "waiting",
2657
+ approval_pending: "waiting",
2658
+ in_progress: "running",
2659
+ processing: "running"
2660
+ };
2661
+ function normalizeStatus(status) {
2662
+ if (status == null) return "unknown";
2663
+ const normalized = status.trim().toLowerCase();
2664
+ if (!normalized) return "unknown";
2665
+ if (CANONICAL_STATUSES.has(normalized)) return normalized;
2666
+ return STATUS_ALIASES[normalized] ?? "unknown";
2667
+ }
2668
+ function isTerminal(status) {
2669
+ return TERMINAL_STATUSES.has(normalizeStatus(status));
2670
+ }
2671
+ function isActive(status) {
2672
+ return ACTIVE_STATUSES.has(normalizeStatus(status));
2673
+ }
2674
+
2321
2675
  // src/client/AgentFieldClient.ts
2322
2676
  var AgentFieldClient = class {
2323
2677
  http;
@@ -2326,7 +2680,7 @@ var AgentFieldClient = class {
2326
2680
  didAuthenticator;
2327
2681
  constructor(config) {
2328
2682
  const baseURL = (config.agentFieldUrl ?? "http://localhost:8080").replace(/\/$/, "");
2329
- this.http = axios4.create({
2683
+ this.http = axios5.create({
2330
2684
  baseURL,
2331
2685
  timeout: 3e4,
2332
2686
  httpAgent,
@@ -2366,21 +2720,7 @@ var AgentFieldClient = class {
2366
2720
  return res.data;
2367
2721
  }
2368
2722
  async execute(target, input, metadata) {
2369
- const headers = {};
2370
- if (metadata?.runId) headers["X-Run-ID"] = metadata.runId;
2371
- if (metadata?.workflowId) headers["X-Workflow-ID"] = metadata.workflowId;
2372
- if (metadata?.rootWorkflowId) headers["X-Root-Workflow-ID"] = metadata.rootWorkflowId;
2373
- if (metadata?.parentExecutionId) headers["X-Parent-Execution-ID"] = metadata.parentExecutionId;
2374
- if (metadata?.reasonerId) headers["X-Reasoner-ID"] = metadata.reasonerId;
2375
- if (metadata?.sessionId) headers["X-Session-ID"] = metadata.sessionId;
2376
- if (metadata?.actorId) headers["X-Actor-ID"] = metadata.actorId;
2377
- if (metadata?.callerDid) headers["X-Caller-DID"] = metadata.callerDid;
2378
- if (metadata?.targetDid) headers["X-Target-DID"] = metadata.targetDid;
2379
- if (metadata?.agentNodeDid) headers["X-Agent-Node-DID"] = metadata.agentNodeDid;
2380
- if (metadata?.agentNodeId) headers["X-Agent-Node-ID"] = metadata.agentNodeId;
2381
- if (metadata?.replaySourceRunId) headers["X-AgentField-Replay-Source-Run-ID"] = metadata.replaySourceRunId;
2382
- if (metadata?.replayBeforeExecutionId) headers["X-AgentField-Replay-Before-Execution-ID"] = metadata.replayBeforeExecutionId;
2383
- if (metadata?.replayMode) headers["X-AgentField-Replay-Mode"] = metadata.replayMode;
2723
+ const headers = this.buildExecuteHeaders(metadata);
2384
2724
  const bodyStr = JSON.stringify({ input });
2385
2725
  const authHeaders = this.didAuthenticator.signRequest(Buffer.from(bodyStr));
2386
2726
  try {
@@ -2502,6 +2842,219 @@ var AgentFieldClient = class {
2502
2842
  headers: this.mergeHeaders({ "Content-Type": "application/json", ...authHeaders })
2503
2843
  });
2504
2844
  }
2845
+ /** Build the `X-*` dispatch headers shared by execute / executeAsync. */
2846
+ buildExecuteHeaders(metadata) {
2847
+ const headers = {};
2848
+ if (metadata?.runId) headers["X-Run-ID"] = metadata.runId;
2849
+ if (metadata?.workflowId) headers["X-Workflow-ID"] = metadata.workflowId;
2850
+ if (metadata?.rootWorkflowId) headers["X-Root-Workflow-ID"] = metadata.rootWorkflowId;
2851
+ if (metadata?.parentExecutionId) headers["X-Parent-Execution-ID"] = metadata.parentExecutionId;
2852
+ if (metadata?.reasonerId) headers["X-Reasoner-ID"] = metadata.reasonerId;
2853
+ if (metadata?.sessionId) headers["X-Session-ID"] = metadata.sessionId;
2854
+ if (metadata?.actorId) headers["X-Actor-ID"] = metadata.actorId;
2855
+ if (metadata?.callerDid) headers["X-Caller-DID"] = metadata.callerDid;
2856
+ if (metadata?.targetDid) headers["X-Target-DID"] = metadata.targetDid;
2857
+ if (metadata?.agentNodeDid) headers["X-Agent-Node-DID"] = metadata.agentNodeDid;
2858
+ if (metadata?.agentNodeId) headers["X-Agent-Node-ID"] = metadata.agentNodeId;
2859
+ if (metadata?.replaySourceRunId) headers["X-AgentField-Replay-Source-Run-ID"] = metadata.replaySourceRunId;
2860
+ if (metadata?.replayBeforeExecutionId) headers["X-AgentField-Replay-Before-Execution-ID"] = metadata.replayBeforeExecutionId;
2861
+ if (metadata?.replayMode) headers["X-AgentField-Replay-Mode"] = metadata.replayMode;
2862
+ return headers;
2863
+ }
2864
+ /**
2865
+ * Submit an async execution and return its `execution_id`.
2866
+ *
2867
+ * POSTs to `/api/v1/execute/async/{target}`, which enqueues the execution
2868
+ * and responds `202 Accepted` immediately (the control plane runs it and
2869
+ * tracks status out-of-band). Use with {@link waitForExecutionResult} to
2870
+ * poll for the terminal result without holding a synchronous connection —
2871
+ * this is what lets a parent await a descendant that legitimately pauses
2872
+ * (WAITING) for a long time without hitting the dispatch ceiling.
2873
+ */
2874
+ async executeAsync(target, input, metadata) {
2875
+ const headers = this.buildExecuteHeaders(metadata);
2876
+ const bodyStr = JSON.stringify({ input });
2877
+ const authHeaders = this.didAuthenticator.signRequest(Buffer.from(bodyStr));
2878
+ try {
2879
+ const res = await this.http.post(
2880
+ `/api/v1/execute/async/${target}`,
2881
+ bodyStr,
2882
+ { headers: this.mergeHeaders({ "Content-Type": "application/json", ...headers, ...authHeaders }) }
2883
+ );
2884
+ const executionId = res.data?.execution_id ?? res.data?.executionId ?? (typeof res.headers?.["x-execution-id"] === "string" ? res.headers["x-execution-id"] : void 0);
2885
+ if (!executionId) {
2886
+ throw new Error(`execute async ${target} returned no execution_id`);
2887
+ }
2888
+ return executionId;
2889
+ } catch (err) {
2890
+ const respData = err?.response?.data;
2891
+ if (respData) {
2892
+ const status = err.response.status;
2893
+ const msg = respData.message || respData.error || JSON.stringify(respData);
2894
+ const enriched = Object.assign(
2895
+ new Error(`execute async ${target} failed (${status}): ${msg}`),
2896
+ { status, responseData: respData }
2897
+ );
2898
+ throw enriched;
2899
+ }
2900
+ throw err;
2901
+ }
2902
+ }
2903
+ /** Fetch the current status snapshot for an execution. */
2904
+ async getExecutionStatus(executionId) {
2905
+ const res = await this.http.get(
2906
+ `/api/v1/executions/${encodeURIComponent(executionId)}`,
2907
+ { headers: this.mergeHeaders({}) }
2908
+ );
2909
+ const data = res.data ?? {};
2910
+ return {
2911
+ executionId: data.execution_id ?? executionId,
2912
+ status: data.status ?? "unknown",
2913
+ statusReason: data.status_reason ?? void 0,
2914
+ result: data.result,
2915
+ error: data.error ?? void 0,
2916
+ errorDetails: data.error_details,
2917
+ durationMs: data.duration_ms ?? void 0
2918
+ };
2919
+ }
2920
+ /**
2921
+ * Poll an async execution until it reaches a terminal status, returning its
2922
+ * result (or throwing on failure/cancellation/timeout).
2923
+ *
2924
+ * While polling, this observes the child's status transitions: when the
2925
+ * child enters `waiting` it starts the supplied pause-clock and fires
2926
+ * `onChildWaiting`; when the child leaves `waiting` it stops the clock and
2927
+ * fires `onChildRunning`. Paused time is excluded from `timeoutMs`, so a
2928
+ * parent awaiting a paused descendant does not time out while the descendant
2929
+ * legitimately waits. Mirrors the Python SDK's `wait_for_execution_result`.
2930
+ */
2931
+ async waitForExecutionResult(executionId, opts = {}) {
2932
+ const pollInterval = opts.pollIntervalMs ?? 1e3;
2933
+ const maxInterval = opts.maxIntervalMs ?? 5e3;
2934
+ const pauseClock = opts.pauseClock;
2935
+ const start = Date.now();
2936
+ let interval = pollInterval;
2937
+ let childWaiting = false;
2938
+ const activeElapsed = () => Date.now() - start - (pauseClock?.totalPaused() ?? 0);
2939
+ try {
2940
+ while (true) {
2941
+ let snapshot;
2942
+ try {
2943
+ snapshot = await this.getExecutionStatus(executionId);
2944
+ } catch {
2945
+ if (opts.timeoutMs != null && activeElapsed() >= opts.timeoutMs) {
2946
+ throw new Error(`waitForExecutionResult(${executionId}) timed out`);
2947
+ }
2948
+ await sleep2(interval);
2949
+ interval = Math.min(interval * 2, maxInterval);
2950
+ continue;
2951
+ }
2952
+ const status = normalizeStatus(snapshot.status);
2953
+ if (status === "waiting" && !childWaiting) {
2954
+ childWaiting = true;
2955
+ pauseClock?.startPause();
2956
+ await safePauseCallback(opts.onChildWaiting);
2957
+ } else if (status !== "waiting" && childWaiting) {
2958
+ childWaiting = false;
2959
+ pauseClock?.endPause();
2960
+ await safePauseCallback(opts.onChildRunning);
2961
+ }
2962
+ if (isTerminal(status)) {
2963
+ if (status === "succeeded") {
2964
+ return snapshot.result?.result ?? snapshot.result;
2965
+ }
2966
+ const detail = snapshot.error || snapshot.statusReason || status;
2967
+ const failure = Object.assign(
2968
+ new Error(`execution ${executionId} ${status}: ${detail}`),
2969
+ { status: 0, responseData: snapshot.errorDetails ?? snapshot.error }
2970
+ );
2971
+ throw failure;
2972
+ }
2973
+ if (opts.timeoutMs != null && activeElapsed() >= opts.timeoutMs) {
2974
+ throw new Error(`waitForExecutionResult(${executionId}) timed out`);
2975
+ }
2976
+ await sleep2(interval);
2977
+ interval = Math.min(interval * 2, maxInterval);
2978
+ }
2979
+ } finally {
2980
+ if (childWaiting) {
2981
+ pauseClock?.endPause();
2982
+ }
2983
+ }
2984
+ }
2985
+ /**
2986
+ * Notify the control plane that THIS execution is now `waiting` or `running`
2987
+ * because of its awaited child's state — the multi-hop pause propagation
2988
+ * hook. Distinct from request-approval: no approval id, no webhook, no human.
2989
+ * It exists purely so ancestors watching this execution see WAITING
2990
+ * transitively while a descendant is paused, and stop counting wall-clock.
2991
+ *
2992
+ * POSTs to `/api/v1/agents/{node}/executions/{id}/awaiter-status`.
2993
+ */
2994
+ async notifyAwaiterStatus(executionId, status, reason = "") {
2995
+ if (status !== "waiting" && status !== "running") {
2996
+ throw new Error(`notifyAwaiterStatus: status must be 'waiting' or 'running', got '${status}'`);
2997
+ }
2998
+ const nodeId = this.config.nodeId ?? "";
2999
+ const payload = { status };
3000
+ if (reason) payload.reason = reason;
3001
+ const bodyStr = JSON.stringify(payload);
3002
+ const authHeaders = this.didAuthenticator.signRequest(Buffer.from(bodyStr));
3003
+ const res = await this.http.post(
3004
+ `/api/v1/agents/${encodeURIComponent(nodeId)}/executions/${encodeURIComponent(executionId)}/awaiter-status`,
3005
+ bodyStr,
3006
+ {
3007
+ headers: this.mergeHeaders({ "Content-Type": "application/json", ...authHeaders }),
3008
+ timeout: 1e4
3009
+ }
3010
+ );
3011
+ if (res.status >= 400) {
3012
+ throw new Error(`awaiter-status update failed (${res.status})`);
3013
+ }
3014
+ }
3015
+ /**
3016
+ * Deliver a terminal execution result to the control plane, with retries.
3017
+ *
3018
+ * This is the out-of-band completion callback used by async-execution
3019
+ * dispatch: after a reasoner has been 202-acked and run detached, its final
3020
+ * status (`succeeded` / `failed` / `cancelled`) is POSTed to
3021
+ * `/api/v1/executions/{id}/status`. Retries with exponential backoff because
3022
+ * a dropped terminal callback would leave the execution stuck forever.
3023
+ */
3024
+ async reportExecutionResult(executionId, payload, maxRetries = 5) {
3025
+ const body = {
3026
+ status: payload.status,
3027
+ execution_id: executionId,
3028
+ duration_ms: payload.durationMs,
3029
+ completed_at: payload.completedAt,
3030
+ reasoner: payload.reasoner
3031
+ };
3032
+ if (payload.result !== void 0) body.result = wrapResult(payload.result);
3033
+ if (payload.error !== void 0) body.error = payload.error;
3034
+ if (payload.errorDetails !== void 0) body.error_details = payload.errorDetails;
3035
+ const bodyStr = JSON.stringify(body);
3036
+ const authHeaders = this.didAuthenticator.signRequest(Buffer.from(bodyStr));
3037
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
3038
+ try {
3039
+ const res = await this.http.post(
3040
+ `/api/v1/executions/${encodeURIComponent(executionId)}/status`,
3041
+ bodyStr,
3042
+ {
3043
+ headers: this.mergeHeaders({ "Content-Type": "application/json", ...authHeaders }),
3044
+ timeout: 3e4
3045
+ }
3046
+ );
3047
+ if (res.status >= 200 && res.status < 300) {
3048
+ return true;
3049
+ }
3050
+ } catch {
3051
+ }
3052
+ if (attempt < maxRetries - 1) {
3053
+ await sleep2(2 ** attempt * 1e3);
3054
+ }
3055
+ }
3056
+ return false;
3057
+ }
2505
3058
  async discoverCapabilities(options = {}) {
2506
3059
  const format = (options.format ?? "json").toLowerCase();
2507
3060
  const params = { format };
@@ -2670,7 +3223,7 @@ var AgentFieldClient = class {
2670
3223
  ...executionHeaders,
2671
3224
  ...authHeaders
2672
3225
  });
2673
- axios4.post(`${uiApiBaseUrl}/executions/note`, bodyStr, {
3226
+ axios5.post(`${uiApiBaseUrl}/executions/note`, bodyStr, {
2674
3227
  headers,
2675
3228
  timeout: devMode ? 5e3 : 1e4,
2676
3229
  httpAgent,
@@ -2679,11 +3232,30 @@ var AgentFieldClient = class {
2679
3232
  });
2680
3233
  }
2681
3234
  };
3235
+ function sleep2(ms) {
3236
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3237
+ }
3238
+ async function safePauseCallback(cb) {
3239
+ if (!cb) return;
3240
+ try {
3241
+ await Promise.race([
3242
+ Promise.resolve().then(cb),
3243
+ sleep2(2e3)
3244
+ ]);
3245
+ } catch {
3246
+ }
3247
+ }
3248
+ function wrapResult(result) {
3249
+ if (result !== null && typeof result === "object" && !Array.isArray(result)) {
3250
+ return result;
3251
+ }
3252
+ return { result };
3253
+ }
2682
3254
  var MemoryClientBase = class {
2683
3255
  http;
2684
3256
  defaultHeaders;
2685
3257
  constructor(baseUrl, defaultHeaders) {
2686
- this.http = axios4.create({
3258
+ this.http = axios5.create({
2687
3259
  baseURL: baseUrl.replace(/\/$/, ""),
2688
3260
  timeout: 3e4,
2689
3261
  httpAgent,
@@ -3111,7 +3683,7 @@ var DidClient = class {
3111
3683
  http;
3112
3684
  defaultHeaders;
3113
3685
  constructor(baseUrl, defaultHeaders) {
3114
- this.http = axios4.create({
3686
+ this.http = axios5.create({
3115
3687
  baseURL: baseUrl.replace(/\/$/, ""),
3116
3688
  timeout: 3e4,
3117
3689
  httpAgent,
@@ -3554,7 +4126,7 @@ var LocalVerifier = class {
3554
4126
  }
3555
4127
  let success = true;
3556
4128
  try {
3557
- const resp = await axios4.get(`${this.agentFieldUrl}/api/v1/policies`, {
4129
+ const resp = await axios5.get(`${this.agentFieldUrl}/api/v1/policies`, {
3558
4130
  headers,
3559
4131
  timeout: 1e4
3560
4132
  });
@@ -3567,7 +4139,7 @@ var LocalVerifier = class {
3567
4139
  success = false;
3568
4140
  }
3569
4141
  try {
3570
- const resp = await axios4.get(`${this.agentFieldUrl}/api/v1/revocations`, {
4142
+ const resp = await axios5.get(`${this.agentFieldUrl}/api/v1/revocations`, {
3571
4143
  headers,
3572
4144
  timeout: 1e4
3573
4145
  });
@@ -3580,7 +4152,7 @@ var LocalVerifier = class {
3580
4152
  success = false;
3581
4153
  }
3582
4154
  try {
3583
- const resp = await axios4.get(`${this.agentFieldUrl}/api/v1/registered-dids`, {
4155
+ const resp = await axios5.get(`${this.agentFieldUrl}/api/v1/registered-dids`, {
3584
4156
  headers,
3585
4157
  timeout: 1e4
3586
4158
  });
@@ -3593,7 +4165,7 @@ var LocalVerifier = class {
3593
4165
  success = false;
3594
4166
  }
3595
4167
  try {
3596
- const resp = await axios4.get(`${this.agentFieldUrl}/api/v1/admin/public-key`, {
4168
+ const resp = await axios5.get(`${this.agentFieldUrl}/api/v1/admin/public-key`, {
3597
4169
  headers,
3598
4170
  timeout: 1e4
3599
4171
  });
@@ -4025,6 +4597,17 @@ var Agent = class {
4025
4597
  * code that respects `signal.aborted` (fetch, anthropic SDK, openai
4026
4598
  * SDK, etc.). See ./cancel.ts. */
4027
4599
  cancelRegistry = new CancelRegistry();
4600
+ /** Registry of pending `ctx.pause()` promises, resolved by the
4601
+ * `/webhooks/approval` route when the control plane delivers a decision.
4602
+ * See ./pause.ts. */
4603
+ pauseManager = new PauseManager();
4604
+ /** Per-execution pause clocks, keyed by execution_id. Present only for
4605
+ * detached (async-execution) reasoners; used to exclude pause/await time
4606
+ * from the reasoner's active wall-clock budget and from an awaiter's wait
4607
+ * timeout. See ./pause.ts. */
4608
+ pauseClocks = /* @__PURE__ */ new Map();
4609
+ /** Execution-scoped approval client used by `Agent.pause()`. */
4610
+ approvalClient;
4028
4611
  constructor(config) {
4029
4612
  this.config = {
4030
4613
  port: 8001,
@@ -4032,12 +4615,19 @@ var Agent = class {
4032
4615
  host: "0.0.0.0",
4033
4616
  ...config,
4034
4617
  didEnabled: config.didEnabled ?? true,
4035
- deploymentType: config.deploymentType ?? "long_running"
4618
+ deploymentType: config.deploymentType ?? "long_running",
4619
+ asyncExecution: config.asyncExecution ?? true
4036
4620
  };
4037
4621
  this.app = express();
4038
4622
  this.app.use(express.json());
4039
4623
  this.aiClient = new AIClient(this.config.aiConfig);
4040
4624
  this.agentFieldClient = new AgentFieldClient(this.config);
4625
+ this.approvalClient = new ApprovalClient({
4626
+ baseURL: this.config.agentFieldUrl ?? "http://localhost:8080",
4627
+ nodeId: this.config.nodeId,
4628
+ apiKey: this.config.apiKey,
4629
+ headers: this.sanitizeDefaultHeaders(this.config.defaultHeaders)
4630
+ });
4041
4631
  this.memoryClient = new MemoryClient(this.config.agentFieldUrl, this.config.defaultHeaders);
4042
4632
  this.memoryEventClient = new MemoryEventClient(this.config.agentFieldUrl, this.config.defaultHeaders);
4043
4633
  this.didClient = new DidClient(this.config.agentFieldUrl, this.config.defaultHeaders);
@@ -4063,6 +4653,29 @@ var Agent = class {
4063
4653
  installCancelRoute(this.app, this.cancelRegistry, {
4064
4654
  info: (message, meta) => this.executionLogger.system("execution.cancel.received", message, meta ?? {})
4065
4655
  });
4656
+ installApprovalWebhookRoute(this.app, this.pauseManager, {
4657
+ info: (message, meta) => this.executionLogger.system("execution.approval.received", message, meta ?? {})
4658
+ });
4659
+ }
4660
+ /** Coerce the loosely-typed `defaultHeaders` config into string headers. */
4661
+ sanitizeDefaultHeaders(headers) {
4662
+ const out = {};
4663
+ for (const [key, value] of Object.entries(headers ?? {})) {
4664
+ if (value !== void 0 && value !== null) {
4665
+ out[key] = String(value);
4666
+ }
4667
+ }
4668
+ return out;
4669
+ }
4670
+ /**
4671
+ * Resolve the externally-reachable base URL for this agent — the URL the
4672
+ * control plane uses to call back (approval webhook, cancel). Mirrors the
4673
+ * value published at registration time.
4674
+ */
4675
+ resolvePublicUrl() {
4676
+ const port = this.config.port ?? 8001;
4677
+ const hostForUrl = this.config.publicUrl ? void 0 : this.config.host && this.config.host !== "0.0.0.0" ? this.config.host : "127.0.0.1";
4678
+ return this.config.publicUrl ?? `http://${hostForUrl ?? "127.0.0.1"}:${port}`;
4066
4679
  }
4067
4680
  reasoner(name, handler, options) {
4068
4681
  this.reasoners.register(name, handler, options);
@@ -4185,6 +4798,84 @@ var Agent = class {
4185
4798
  }
4186
4799
  this.agentFieldClient.sendNote(message, tags, this.config.nodeId, execMetadata, uiApiUrl, this.config.devMode);
4187
4800
  }
4801
+ /**
4802
+ * Pause the current execution for external approval / resumption.
4803
+ *
4804
+ * Transitions the execution to `waiting` on the control plane, then blocks
4805
+ * until the approval webhook callback resolves it or the timeout elapses.
4806
+ * The caller is responsible for creating the approval request on an external
4807
+ * service (e.g. hax-sdk) *before* calling this and passing the resulting
4808
+ * `approvalRequestId`.
4809
+ *
4810
+ * Requires the agent to be serving (a reachable callback URL) and, to survive
4811
+ * past the control plane's synchronous dispatch ceiling, requires
4812
+ * async-execution dispatch to be enabled (the default). When async dispatch
4813
+ * is disabled the pause still works but is bounded by that ceiling.
4814
+ *
4815
+ * Returns an {@link ApprovalResult}. On timeout it returns
4816
+ * `{ decision: 'expired' }` rather than throwing. Mirrors the Python SDK's
4817
+ * `Agent.pause()`.
4818
+ */
4819
+ async pause(opts) {
4820
+ const executionId = opts.executionId ?? ExecutionContext.getCurrent()?.metadata.executionId;
4821
+ if (!executionId) {
4822
+ throw new Error("No execution_id available \u2014 cannot pause");
4823
+ }
4824
+ const callbackUrl = `${this.resolvePublicUrl()}/webhooks/approval`;
4825
+ const expiresInHours = opts.expiresInHours ?? 72;
4826
+ const future = this.pauseManager.register(opts.approvalRequestId, executionId);
4827
+ try {
4828
+ await this.approvalClient.requestApproval(executionId, {
4829
+ approvalRequestId: opts.approvalRequestId,
4830
+ approvalRequestUrl: opts.approvalRequestUrl,
4831
+ callbackUrl,
4832
+ expiresInHours
4833
+ });
4834
+ } catch (err) {
4835
+ this.pauseManager.resolve(
4836
+ opts.approvalRequestId,
4837
+ new ApprovalResult({
4838
+ decision: "error",
4839
+ feedback: "failed to notify control plane",
4840
+ executionId,
4841
+ approvalRequestId: opts.approvalRequestId
4842
+ })
4843
+ );
4844
+ throw err;
4845
+ }
4846
+ this.note(`Execution paused \u2014 waiting for approval ${opts.approvalRequestId}`, [
4847
+ "approval",
4848
+ "waiting"
4849
+ ]);
4850
+ const timeoutMs = opts.timeoutMs ?? expiresInHours * 36e5;
4851
+ const pauseClock = this.pauseClocks.get(executionId);
4852
+ pauseClock?.startPause();
4853
+ let timer;
4854
+ try {
4855
+ const result = await Promise.race([
4856
+ future,
4857
+ new Promise((resolve2) => {
4858
+ timer = setTimeout(() => {
4859
+ resolve2(
4860
+ new ApprovalResult({
4861
+ decision: "expired",
4862
+ feedback: "timed out waiting for approval",
4863
+ executionId,
4864
+ approvalRequestId: opts.approvalRequestId
4865
+ })
4866
+ );
4867
+ }, timeoutMs);
4868
+ })
4869
+ ]);
4870
+ if (result.decision === "expired") {
4871
+ this.pauseManager.resolve(opts.approvalRequestId, result);
4872
+ }
4873
+ return result;
4874
+ } finally {
4875
+ if (timer) clearTimeout(timer);
4876
+ pauseClock?.endPause();
4877
+ }
4878
+ }
4188
4879
  buildExecutionLogContext(metadata) {
4189
4880
  const current = metadata ?? ExecutionContext.getCurrent()?.metadata;
4190
4881
  if (!current) return void 0;
@@ -4228,6 +4919,7 @@ var Agent = class {
4228
4919
  if (this.heartbeatTimer) {
4229
4920
  clearInterval(this.heartbeatTimer);
4230
4921
  }
4922
+ this.pauseManager.cancelAll();
4231
4923
  await new Promise((resolve2, reject) => {
4232
4924
  this.server?.close((err) => {
4233
4925
  if (err) reject(err);
@@ -4424,23 +5116,24 @@ var Agent = class {
4424
5116
  rootWorkflowId,
4425
5117
  reasonerId: name
4426
5118
  });
5119
+ const executeMetadata = {
5120
+ runId,
5121
+ workflowId,
5122
+ rootWorkflowId,
5123
+ parentExecutionId: parentMetadata?.executionId,
5124
+ reasonerId: name,
5125
+ sessionId: parentMetadata?.sessionId,
5126
+ actorId: parentMetadata?.actorId,
5127
+ callerDid: parentMetadata?.callerDid,
5128
+ targetDid: parentMetadata?.targetDid,
5129
+ agentNodeDid: parentMetadata?.agentNodeDid,
5130
+ agentNodeId: this.config.nodeId,
5131
+ replaySourceRunId: parentMetadata?.replaySourceRunId,
5132
+ replayBeforeExecutionId: parentMetadata?.replayBeforeExecutionId,
5133
+ replayMode: parentMetadata?.replayMode
5134
+ };
4427
5135
  try {
4428
- const result = await this.agentFieldClient.execute(target, input, {
4429
- runId,
4430
- workflowId,
4431
- rootWorkflowId,
4432
- parentExecutionId: parentMetadata?.executionId,
4433
- reasonerId: name,
4434
- sessionId: parentMetadata?.sessionId,
4435
- actorId: parentMetadata?.actorId,
4436
- callerDid: parentMetadata?.callerDid,
4437
- targetDid: parentMetadata?.targetDid,
4438
- agentNodeDid: parentMetadata?.agentNodeDid,
4439
- agentNodeId: this.config.nodeId,
4440
- replaySourceRunId: parentMetadata?.replaySourceRunId,
4441
- replayBeforeExecutionId: parentMetadata?.replayBeforeExecutionId,
4442
- replayMode: parentMetadata?.replayMode
4443
- });
5136
+ const result = this.config.asyncExecution === false ? await this.agentFieldClient.execute(target, input, executeMetadata) : await this.callRemoteAsync(target, input, executeMetadata, parentMetadata?.executionId);
4444
5137
  this.executionLogger.system("agent.call.completed", "Remote agent call completed", {
4445
5138
  target,
4446
5139
  agentNodeId: agentId,
@@ -4471,6 +5164,44 @@ var Agent = class {
4471
5164
  throw err;
4472
5165
  }
4473
5166
  }
5167
+ /**
5168
+ * Remote call variant that submits the execution asynchronously and polls for
5169
+ * its result, instead of holding a synchronous connection open. This lets a
5170
+ * caller await a descendant that legitimately pauses (WAITING) for a long
5171
+ * time without tripping the control plane's synchronous dispatch ceiling.
5172
+ *
5173
+ * Multi-hop pause propagation: if the caller itself is a detached reasoner
5174
+ * (has a registered pause-clock), then when the awaited child enters WAITING
5175
+ * we push the caller's own execution to WAITING via awaiter-status — so any
5176
+ * ancestor awaiting the caller transitively sees WAITING too and doesn't time
5177
+ * out. Mirrors the propagation wiring in the Python SDK's `Agent.call`.
5178
+ */
5179
+ async callRemoteAsync(target, input, executeMetadata, parentExecutionId) {
5180
+ const childExecutionId = await this.agentFieldClient.executeAsync(target, input, executeMetadata);
5181
+ const parentPauseClock = parentExecutionId ? this.pauseClocks.get(parentExecutionId) : void 0;
5182
+ let onChildWaiting;
5183
+ let onChildRunning;
5184
+ if (parentExecutionId && parentPauseClock) {
5185
+ const reason = `awaiting child ${childExecutionId}`;
5186
+ onChildWaiting = async () => {
5187
+ try {
5188
+ await this.agentFieldClient.notifyAwaiterStatus(parentExecutionId, "waiting", reason);
5189
+ } catch {
5190
+ }
5191
+ };
5192
+ onChildRunning = async () => {
5193
+ try {
5194
+ await this.agentFieldClient.notifyAwaiterStatus(parentExecutionId, "running", reason);
5195
+ } catch {
5196
+ }
5197
+ };
5198
+ }
5199
+ return this.agentFieldClient.waitForExecutionResult(childExecutionId, {
5200
+ pauseClock: parentPauseClock,
5201
+ onChildWaiting,
5202
+ onChildRunning
5203
+ });
5204
+ }
4474
5205
  registerDefaultRoutes() {
4475
5206
  this.app.get("/health", (_req, res) => {
4476
5207
  res.json(this.health());
@@ -4604,12 +5335,19 @@ var Agent = class {
4604
5335
  this.app.post("/execute/:name", (req, res) => this.executeServerlessHttp(req, res, req.params.name));
4605
5336
  }
4606
5337
  async executeReasoner(req, res, name) {
5338
+ const metadata = this.buildMetadata(req);
5339
+ const reasoner = this.reasoners.get(name);
5340
+ if (reasoner && this.shouldRunAsync(req)) {
5341
+ res.status(202).json({ status: "processing", execution_id: metadata.executionId });
5342
+ void this.runReasonerAsync(reasoner, { targetName: name, input: req.body, metadata });
5343
+ return;
5344
+ }
4607
5345
  try {
4608
5346
  await this.executeInvocation({
4609
5347
  targetName: name,
4610
5348
  targetType: "reasoner",
4611
5349
  input: req.body,
4612
- metadata: this.buildMetadata(req),
5350
+ metadata,
4613
5351
  req,
4614
5352
  res,
4615
5353
  respond: true
@@ -4914,6 +5652,121 @@ var Agent = class {
4914
5652
  }
4915
5653
  throw new TargetNotFoundError(`Reasoner not found: ${params.targetName}`);
4916
5654
  }
5655
+ /**
5656
+ * True when an incoming reasoner dispatch should run detached (202-ack).
5657
+ *
5658
+ * Requires async execution to be enabled AND the request to carry BOTH
5659
+ * `X-Execution-ID` and `X-Run-ID`. The `X-Run-ID` header is the marker that
5660
+ * the control plane dispatched this via an async-aware path — the workflow
5661
+ * execute paths (`/execute`, `/execute/async`, agent-to-agent calls, triggers)
5662
+ * always set it (see control-plane callAgent), and those paths wait for the
5663
+ * out-of-band `/status` result. The legacy synchronous invoke endpoint
5664
+ * (`POST /api/v1/reasoners/{node}.{reasoner}`) omits `X-Run-ID` for
5665
+ * long-running agents and forwards the agent's HTTP response verbatim; it
5666
+ * cannot handle a 202, so we must run synchronously there and return the
5667
+ * result inline. Direct HTTP callers / tests without these headers likewise
5668
+ * keep the synchronous request/response contract.
5669
+ */
5670
+ shouldRunAsync(req) {
5671
+ if (this.config.asyncExecution === false) return false;
5672
+ return this.hasHeader(req, "x-execution-id") && this.hasHeader(req, "x-run-id");
5673
+ }
5674
+ /** True when the request carries a non-empty value for the given header. */
5675
+ hasHeader(req, name) {
5676
+ const raw = req.headers[name];
5677
+ const value = Array.isArray(raw) ? raw[0] : raw;
5678
+ return typeof value === "string" && value.trim().length > 0;
5679
+ }
5680
+ /**
5681
+ * Run a reasoner detached after its dispatch was 202-acked, delivering the
5682
+ * terminal status out-of-band via `POST /executions/{id}/status`.
5683
+ *
5684
+ * A pause-clock is registered for the execution so `ctx.pause()` (and any
5685
+ * awaited paused descendant) can exclude its wait from the active wall-clock
5686
+ * budget. A watchdog aborts the reasoner if active time exceeds the budget,
5687
+ * guaranteeing the control plane always sees a terminal status even if the
5688
+ * reasoner hangs. Mirrors the Python SDK's `_execute_async_with_callback`.
5689
+ */
5690
+ async runReasonerAsync(reasoner, params) {
5691
+ const executionId = params.metadata.executionId;
5692
+ const reasonerName = params.metadata.reasonerId ?? params.targetName;
5693
+ const start = Date.now();
5694
+ const budgetMs = this.config.executionBudgetMs ?? 72e5;
5695
+ const pauseClock = new PauseClock();
5696
+ this.pauseClocks.set(executionId, pauseClock);
5697
+ const controller = new AbortController();
5698
+ let watchdogTimer;
5699
+ const watchdog = new Promise((_, reject) => {
5700
+ const checkInterval = Math.min(5e3, Math.max(100, budgetMs / 4));
5701
+ watchdogTimer = setInterval(() => {
5702
+ const activeElapsed = Date.now() - start - pauseClock.totalPaused();
5703
+ if (activeElapsed > budgetMs) {
5704
+ pauseClock.timedOut = true;
5705
+ this.cancelRegistry.cancel(executionId, "reasoner_timeout");
5706
+ reject(
5707
+ new Error(
5708
+ `reasoner '${reasonerName}' timed out after ${Math.round(budgetMs / 1e3)}s of active time`
5709
+ )
5710
+ );
5711
+ }
5712
+ }, checkInterval);
5713
+ });
5714
+ watchdog.catch(() => {
5715
+ });
5716
+ const completedAt = () => (/* @__PURE__ */ new Date()).toISOString();
5717
+ try {
5718
+ const result = await Promise.race([
5719
+ this.runReasoner(reasoner, {
5720
+ targetName: params.targetName,
5721
+ input: params.input,
5722
+ metadata: params.metadata,
5723
+ respond: false,
5724
+ controller
5725
+ }),
5726
+ watchdog
5727
+ ]);
5728
+ await this.agentFieldClient.reportExecutionResult(executionId, {
5729
+ status: "succeeded",
5730
+ result,
5731
+ durationMs: Date.now() - start,
5732
+ completedAt: completedAt(),
5733
+ reasoner: reasonerName
5734
+ });
5735
+ } catch (err) {
5736
+ const durationMs = Date.now() - start;
5737
+ if (pauseClock.timedOut) {
5738
+ await this.agentFieldClient.reportExecutionResult(executionId, {
5739
+ status: "failed",
5740
+ error: err?.message ?? `reasoner '${reasonerName}' timed out`,
5741
+ errorDetails: { reason: "reasoner_timeout" },
5742
+ durationMs,
5743
+ completedAt: completedAt(),
5744
+ reasoner: reasonerName
5745
+ });
5746
+ } else if (controller.signal.aborted) {
5747
+ await this.agentFieldClient.reportExecutionResult(executionId, {
5748
+ status: "cancelled",
5749
+ error: "cancelled_by_control_plane",
5750
+ errorDetails: { reason: "cancelled" },
5751
+ durationMs,
5752
+ completedAt: completedAt(),
5753
+ reasoner: reasonerName
5754
+ });
5755
+ } else {
5756
+ await this.agentFieldClient.reportExecutionResult(executionId, {
5757
+ status: "failed",
5758
+ error: err?.message ?? "Execution failed",
5759
+ errorDetails: err?.responseData,
5760
+ durationMs,
5761
+ completedAt: completedAt(),
5762
+ reasoner: reasonerName
5763
+ });
5764
+ }
5765
+ } finally {
5766
+ if (watchdogTimer) clearInterval(watchdogTimer);
5767
+ this.pauseClocks.delete(executionId);
5768
+ }
5769
+ }
4917
5770
  async runReasoner(reasoner, params) {
4918
5771
  const req = params.req ?? {};
4919
5772
  const res = params.res ?? {};
@@ -4930,7 +5783,8 @@ var Agent = class {
4930
5783
  agent: this
4931
5784
  });
4932
5785
  const { controller, release } = this.cancelRegistry.register(
4933
- executionMetadata.executionId
5786
+ executionMetadata.executionId,
5787
+ params.controller
4934
5788
  );
4935
5789
  return ExecutionContext.run(execCtx, async () => {
4936
5790
  this.executionLogger.system("execution.started", "Execution started", {
@@ -5153,9 +6007,7 @@ var Agent = class {
5153
6007
  try {
5154
6008
  const reasoners = this.reasonerDefinitions();
5155
6009
  const skills = this.skillDefinitions();
5156
- const port = this.config.port ?? 8001;
5157
- const hostForUrl = this.config.publicUrl ? void 0 : this.config.host && this.config.host !== "0.0.0.0" ? this.config.host : "127.0.0.1";
5158
- const publicUrl = this.config.publicUrl ?? `http://${hostForUrl ?? "127.0.0.1"}:${port}`;
6010
+ const publicUrl = this.resolvePublicUrl();
5159
6011
  const agentTags = this.config.tags ?? [];
5160
6012
  const regResponse = await this.agentFieldClient.register({
5161
6013
  id: this.config.nodeId,
@@ -6288,7 +7140,7 @@ var OpenRouterMediaProvider = class {
6288
7140
  while (true) {
6289
7141
  const remaining = deadline - Date.now();
6290
7142
  if (remaining <= 0) break;
6291
- await sleep(Math.min(pollInterval, remaining));
7143
+ await sleep3(Math.min(pollInterval, remaining));
6292
7144
  if (Date.now() >= deadline) break;
6293
7145
  const pollRes = await this.get(pollEndpoint);
6294
7146
  if (!pollRes.ok) {
@@ -6646,7 +7498,7 @@ var OpenRouterMediaProvider = class {
6646
7498
  });
6647
7499
  }
6648
7500
  };
6649
- function sleep(ms) {
7501
+ function sleep3(ms) {
6650
7502
  return new Promise((resolve2) => setTimeout(resolve2, ms));
6651
7503
  }
6652
7504
 
@@ -6655,167 +7507,6 @@ init_types();
6655
7507
  init_factory();
6656
7508
  init_runner();
6657
7509
 
6658
- // src/status/ExecutionStatus.ts
6659
- var ExecutionStatus = {
6660
- PENDING: "pending",
6661
- QUEUED: "queued",
6662
- WAITING: "waiting",
6663
- RUNNING: "running",
6664
- SUCCEEDED: "succeeded",
6665
- FAILED: "failed",
6666
- CANCELLED: "cancelled",
6667
- TIMEOUT: "timeout",
6668
- UNKNOWN: "unknown"
6669
- };
6670
- var CANONICAL_STATUSES = new Set(
6671
- Object.values(ExecutionStatus)
6672
- );
6673
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
6674
- ExecutionStatus.SUCCEEDED,
6675
- ExecutionStatus.FAILED,
6676
- ExecutionStatus.CANCELLED,
6677
- ExecutionStatus.TIMEOUT
6678
- ]);
6679
- var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
6680
- ExecutionStatus.PENDING,
6681
- ExecutionStatus.QUEUED,
6682
- ExecutionStatus.WAITING,
6683
- ExecutionStatus.RUNNING
6684
- ]);
6685
- var STATUS_ALIASES = {
6686
- success: "succeeded",
6687
- successful: "succeeded",
6688
- completed: "succeeded",
6689
- complete: "succeeded",
6690
- done: "succeeded",
6691
- ok: "succeeded",
6692
- error: "failed",
6693
- failure: "failed",
6694
- errored: "failed",
6695
- canceled: "cancelled",
6696
- cancel: "cancelled",
6697
- timed_out: "timeout",
6698
- wait: "queued",
6699
- awaiting_approval: "waiting",
6700
- awaiting_human: "waiting",
6701
- approval_pending: "waiting",
6702
- in_progress: "running",
6703
- processing: "running"
6704
- };
6705
- function normalizeStatus(status) {
6706
- if (status == null) return "unknown";
6707
- const normalized = status.trim().toLowerCase();
6708
- if (!normalized) return "unknown";
6709
- if (CANONICAL_STATUSES.has(normalized)) return normalized;
6710
- return STATUS_ALIASES[normalized] ?? "unknown";
6711
- }
6712
- function isTerminal(status) {
6713
- return TERMINAL_STATUSES.has(normalizeStatus(status));
6714
- }
6715
- function isActive(status) {
6716
- return ACTIVE_STATUSES.has(normalizeStatus(status));
6717
- }
6718
- var ApprovalClient = class {
6719
- http;
6720
- nodeId;
6721
- headers;
6722
- constructor(opts) {
6723
- this.http = axios4.create({
6724
- baseURL: opts.baseURL.replace(/\/$/, ""),
6725
- timeout: 3e4,
6726
- httpAgent,
6727
- httpsAgent
6728
- });
6729
- this.nodeId = opts.nodeId;
6730
- const merged = { ...opts.headers ?? {} };
6731
- if (opts.apiKey) {
6732
- merged["X-API-Key"] = opts.apiKey;
6733
- }
6734
- this.headers = merged;
6735
- }
6736
- /**
6737
- * Request human approval, transitioning the execution to `waiting`.
6738
- *
6739
- * Calls `POST /api/v1/agents/{node}/executions/{id}/request-approval`.
6740
- */
6741
- async requestApproval(executionId, payload) {
6742
- const body = {
6743
- approval_request_id: payload.approvalRequestId,
6744
- expires_in_hours: payload.expiresInHours ?? 72
6745
- };
6746
- if (payload.approvalRequestUrl) {
6747
- body.approval_request_url = payload.approvalRequestUrl;
6748
- }
6749
- if (payload.callbackUrl) {
6750
- body.callback_url = payload.callbackUrl;
6751
- }
6752
- const res = await this.http.post(
6753
- `/api/v1/agents/${encodeURIComponent(this.nodeId)}/executions/${encodeURIComponent(executionId)}/request-approval`,
6754
- body,
6755
- { headers: { ...this.headers, "Content-Type": "application/json" } }
6756
- );
6757
- return {
6758
- approvalRequestId: res.data.approval_request_id ?? "",
6759
- approvalRequestUrl: res.data.approval_request_url ?? ""
6760
- };
6761
- }
6762
- /**
6763
- * Get the current approval status for an execution.
6764
- *
6765
- * Calls `GET /api/v1/agents/{node}/executions/{id}/approval-status`.
6766
- */
6767
- async getApprovalStatus(executionId) {
6768
- const res = await this.http.get(
6769
- `/api/v1/agents/${encodeURIComponent(this.nodeId)}/executions/${encodeURIComponent(executionId)}/approval-status`,
6770
- { headers: this.headers }
6771
- );
6772
- const data = res.data;
6773
- return {
6774
- status: data.status ?? "pending",
6775
- response: data.response,
6776
- requestUrl: data.request_url,
6777
- requestedAt: data.requested_at,
6778
- respondedAt: data.responded_at
6779
- };
6780
- }
6781
- /**
6782
- * Poll approval status with exponential backoff until resolved.
6783
- *
6784
- * Returns once the status is no longer `pending` (i.e. approved, rejected,
6785
- * or expired).
6786
- */
6787
- async waitForApproval(executionId, opts) {
6788
- const pollInterval = opts?.pollIntervalMs ?? 5e3;
6789
- const maxInterval = opts?.maxIntervalMs ?? 6e4;
6790
- const timeout = opts?.timeoutMs;
6791
- const backoffFactor = 2;
6792
- const startTime = Date.now();
6793
- let interval = pollInterval;
6794
- while (true) {
6795
- if (timeout != null && Date.now() - startTime >= timeout) {
6796
- throw new Error(
6797
- `Approval for execution ${executionId} timed out after ${timeout}ms`
6798
- );
6799
- }
6800
- await sleep2(interval);
6801
- let data;
6802
- try {
6803
- data = await this.getApprovalStatus(executionId);
6804
- } catch {
6805
- interval = Math.min(interval * backoffFactor, maxInterval);
6806
- continue;
6807
- }
6808
- if (data.status !== "pending") {
6809
- return data;
6810
- }
6811
- interval = Math.min(interval * backoffFactor, maxInterval);
6812
- }
6813
- }
6814
- };
6815
- function sleep2(ms) {
6816
- return new Promise((resolve2) => setTimeout(resolve2, ms));
6817
- }
6818
-
6819
- export { ACTIVE_STATUSES, AIClient, Agent, AgentRouter, ApprovalClient, Audio, CANONICAL_STATUSES, DIDAuthenticator, DidClient, DidInterface, DidManager, ExecutionContext, ExecutionLogger, ExecutionStatus, File, HEADER_CALLER_DID, HEADER_DID_NONCE, HEADER_DID_SIGNATURE, HEADER_DID_TIMESTAMP, HarnessRunner, Image, JWE_ALG, JWE_ENC, KEY_AGREEMENT_TYPE, MediaProviderError, MediaRouter, MemoryClient, MemoryClientBase, MemoryEventClient, MemoryInterface, MultimodalResponse, OpenRouterMediaProvider, PayloadEncryptionError, RateLimitError, RealtimeSession, ReasonerContext, SUPPORTED_PROVIDERS, SUPPORTED_SESSION_TRANSPORTS, SessionTransportError, SkillContext, StatelessRateLimiter, TERMINAL_STATUSES, Text, Video, WorkflowReporter, audioFromBase64, audioFromBuffer, audioFromFile, audioFromUrl, buildProvider, buildSessionDefinition, buildToolConfig, capabilitiesToTools, capabilityToMetadataTool, capabilityToTool, createExecutionLogger, createHarnessResult, createMetrics, createMultimodalResponse, createRawResult, decrypt, decryptToString, encryptForDid, encryptToJwk, executeToolCallLoop, extractKeyAgreementJwk, fileFromBase64, fileFromBuffer, fileFromPath, fileFromUrl, generateX25519KeyPair, getCurrentContext, getCurrentSkillContext, imageFromBase64, imageFromBuffer, imageFromFile, imageFromUrl, isActive, isExecutionLogBatchPayload, isTerminal, normalizeExecutionLogEntry, normalizeSessionTransportValue, normalizeStatus, serializeExecutionLogEntry, text, validateSessionTransport, videoFromBase64, videoFromBuffer, videoFromFile, videoFromUrl };
7510
+ export { ACTIVE_STATUSES, AIClient, Agent, AgentRouter, ApprovalClient, ApprovalResult, Audio, CANONICAL_STATUSES, DIDAuthenticator, DidClient, DidInterface, DidManager, ExecutionContext, ExecutionLogger, ExecutionStatus, File, HEADER_CALLER_DID, HEADER_DID_NONCE, HEADER_DID_SIGNATURE, HEADER_DID_TIMESTAMP, HarnessRunner, Image, JWE_ALG, JWE_ENC, KEY_AGREEMENT_TYPE, MediaProviderError, MediaRouter, MemoryClient, MemoryClientBase, MemoryEventClient, MemoryInterface, MultimodalResponse, OpenRouterMediaProvider, PauseClock, PauseManager, PayloadEncryptionError, RateLimitError, RealtimeSession, ReasonerContext, SUPPORTED_PROVIDERS, SUPPORTED_SESSION_TRANSPORTS, SessionTransportError, SkillContext, StatelessRateLimiter, TERMINAL_STATUSES, Text, Video, WorkflowReporter, audioFromBase64, audioFromBuffer, audioFromFile, audioFromUrl, buildProvider, buildSessionDefinition, buildToolConfig, capabilitiesToTools, capabilityToMetadataTool, capabilityToTool, createExecutionLogger, createHarnessResult, createMetrics, createMultimodalResponse, createRawResult, decrypt, decryptToString, encryptForDid, encryptToJwk, executeToolCallLoop, extractKeyAgreementJwk, fileFromBase64, fileFromBuffer, fileFromPath, fileFromUrl, generateX25519KeyPair, getCurrentContext, getCurrentSkillContext, imageFromBase64, imageFromBuffer, imageFromFile, imageFromUrl, installApprovalWebhookRoute, isActive, isExecutionLogBatchPayload, isTerminal, normalizeExecutionLogEntry, normalizeSessionTransportValue, normalizeStatus, serializeExecutionLogEntry, text, validateSessionTransport, videoFromBase64, videoFromBuffer, videoFromFile, videoFromUrl };
6820
7511
  //# sourceMappingURL=index.js.map
6821
7512
  //# sourceMappingURL=index.js.map