@botbotgo/agent-harness 0.0.268 → 0.0.270

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +3 -2
  2. package/README.zh.md +3 -2
  3. package/dist/acp.js +2 -2
  4. package/dist/api.d.ts +6 -11
  5. package/dist/api.js +20 -23
  6. package/dist/contracts/runtime.d.ts +42 -70
  7. package/dist/flow/build-flow-graph.js +29 -45
  8. package/dist/flow/types.d.ts +0 -6
  9. package/dist/package-version.d.ts +1 -1
  10. package/dist/package-version.js +1 -1
  11. package/dist/persistence/file-store.js +24 -17
  12. package/dist/persistence/sqlite-run-context-store.js +2 -2
  13. package/dist/persistence/sqlite-store.js +18 -16
  14. package/dist/protocol/a2a/http.js +69 -46
  15. package/dist/protocol/ag-ui/http.js +9 -9
  16. package/dist/runtime/adapter/invocation-result.js +2 -2
  17. package/dist/runtime/adapter/tool/tool-hitl.d.ts +3 -1
  18. package/dist/runtime/adapter/tool/tool-hitl.js +75 -6
  19. package/dist/runtime/harness/events/listener-runtime.d.ts +2 -2
  20. package/dist/runtime/harness/events/streaming.d.ts +8 -8
  21. package/dist/runtime/harness/events/streaming.js +10 -10
  22. package/dist/runtime/harness/events/timeline.js +4 -4
  23. package/dist/runtime/harness/run/governance.js +33 -4
  24. package/dist/runtime/harness/run/helpers.js +2 -2
  25. package/dist/runtime/harness/run/operator-overview.js +6 -0
  26. package/dist/runtime/harness/run/recovery.js +20 -20
  27. package/dist/runtime/harness/run/resume.js +3 -3
  28. package/dist/runtime/harness/run/run-lifecycle.js +5 -5
  29. package/dist/runtime/harness/run/run-operations.d.ts +2 -2
  30. package/dist/runtime/harness/run/run-operations.js +21 -21
  31. package/dist/runtime/harness/run/start-run.d.ts +3 -3
  32. package/dist/runtime/harness/run/start-run.js +3 -3
  33. package/dist/runtime/harness/run/startup-runtime.js +1 -1
  34. package/dist/runtime/harness/run/stream-run.js +37 -27
  35. package/dist/runtime/harness/run/thread-records.js +12 -33
  36. package/dist/runtime/harness/system/mem0-ingestion-sync.js +2 -2
  37. package/dist/runtime/harness/system/runtime-memory-manager.js +4 -4
  38. package/dist/runtime/harness/system/runtime-memory-records.js +6 -6
  39. package/dist/runtime/harness/system/runtime-memory-sync.js +6 -4
  40. package/dist/runtime/harness/system/thread-memory-sync.js +7 -5
  41. package/dist/runtime/harness.d.ts +2 -2
  42. package/dist/runtime/harness.js +161 -156
  43. package/dist/runtime/support/harness-support.js +4 -4
  44. package/package.json +1 -1
@@ -11,6 +11,10 @@ function writeJson(response, statusCode, payload) {
11
11
  response.setHeader("content-type", "application/json; charset=utf-8");
12
12
  response.end(JSON.stringify(payload));
13
13
  }
14
+ function writeA2aDiscoveryHeaders(response) {
15
+ response.setHeader("A2A-Version", "1.0");
16
+ response.setHeader("A2A-Supported-Versions", SUPPORTED_A2A_VERSIONS.join(", "));
17
+ }
14
18
  function writeSseHeaders(response) {
15
19
  response.statusCode = 200;
16
20
  response.setHeader("content-type", "text/event-stream; charset=utf-8");
@@ -356,18 +360,19 @@ function toSessionRecord(session) {
356
360
  if (!session) {
357
361
  return null;
358
362
  }
363
+ const typed = session;
359
364
  return {
360
- sessionId: session.threadId,
361
- entryAgentId: session.entryAgentId,
362
- currentAgentId: session.currentAgentId,
363
- currentState: session.currentState,
364
- latestRequestId: session.latestRunId,
365
- createdAt: session.createdAt,
366
- updatedAt: session.updatedAt,
367
- messages: session.messages,
368
- requests: session.runs.map((run) => ({
369
- requestId: run.runId,
370
- sessionId: run.threadId,
365
+ sessionId: typed.sessionId ?? typed.threadId ?? "",
366
+ entryAgentId: typed.entryAgentId,
367
+ currentAgentId: typed.currentAgentId,
368
+ currentState: typed.currentState,
369
+ latestRequestId: typed.latestRequestId ?? typed.latestRunId ?? "",
370
+ createdAt: typed.createdAt,
371
+ updatedAt: typed.updatedAt,
372
+ messages: typed.messages,
373
+ requests: (typed.requests ?? typed.runs ?? []).map((run) => ({
374
+ requestId: run.requestId,
375
+ sessionId: run.sessionId,
371
376
  agentId: run.agentId,
372
377
  executionMode: run.executionMode,
373
378
  adapterKind: run.adapterKind,
@@ -392,25 +397,26 @@ function toRequestRecord(request) {
392
397
  if (!request) {
393
398
  return null;
394
399
  }
400
+ const typed = request;
395
401
  return {
396
- requestId: request.runId,
397
- sessionId: request.threadId,
398
- agentId: request.agentId,
399
- executionMode: request.executionMode,
400
- adapterKind: request.adapterKind,
401
- createdAt: request.createdAt,
402
- updatedAt: request.updatedAt,
403
- state: request.state,
404
- checkpointRef: request.checkpointRef,
405
- resumable: request.resumable,
406
- startedAt: request.startedAt,
407
- endedAt: request.endedAt,
408
- lastActivityAt: request.lastActivityAt,
409
- currentAgentId: request.currentAgentId,
410
- delegationChain: request.delegationChain,
411
- runtimeSnapshot: request.runtimeSnapshot,
412
- traceItems: request.traceItems,
413
- runtimeTimeline: request.runtimeTimeline,
402
+ requestId: typed.requestId ?? typed.runId ?? "",
403
+ sessionId: typed.sessionId ?? typed.threadId ?? "",
404
+ agentId: typed.agentId,
405
+ executionMode: typed.executionMode,
406
+ adapterKind: typed.adapterKind,
407
+ createdAt: typed.createdAt,
408
+ updatedAt: typed.updatedAt,
409
+ state: typed.state,
410
+ checkpointRef: typed.checkpointRef,
411
+ resumable: typed.resumable,
412
+ startedAt: typed.startedAt,
413
+ endedAt: typed.endedAt,
414
+ lastActivityAt: typed.lastActivityAt,
415
+ currentAgentId: typed.currentAgentId,
416
+ delegationChain: typed.delegationChain,
417
+ runtimeSnapshot: typed.runtimeSnapshot,
418
+ traceItems: typed.traceItems,
419
+ runtimeTimeline: typed.runtimeTimeline,
414
420
  };
415
421
  }
416
422
  function buildTaskFromSessionAndRequest(session, request, approvals, output, failureMessage) {
@@ -473,8 +479,8 @@ async function buildTaskFromRuntime(runtime, requestId) {
473
479
  if (!request) {
474
480
  return null;
475
481
  }
476
- const session = await runtime.getThread(request.threadId);
477
- const approvals = await runtime.listApprovals({ threadId: request.threadId, runId: request.runId });
482
+ const session = await runtime.getThread(request.sessionId);
483
+ const approvals = await runtime.listApprovals({ sessionId: request.sessionId, requestId: request.requestId });
478
484
  return buildTaskFromSessionAndRequest(toSessionRecord(session), toRequestRecord(request), approvals);
479
485
  }
480
486
  async function hydrateTaskStateFromEvents(runtime, task) {
@@ -508,7 +514,7 @@ async function listTasksFromRuntime(runtime, params) {
508
514
  });
509
515
  const startIndex = params.cursor ? Number.parseInt(Buffer.from(params.cursor, "base64url").toString("utf8"), 10) || 0 : 0;
510
516
  const page = runs.slice(startIndex, startIndex + params.limit);
511
- const tasks = (await Promise.all(page.map((run) => buildTaskFromRuntime(runtime, run.runId)))).filter((task) => Boolean(task));
517
+ const tasks = (await Promise.all(page.map((run) => buildTaskFromRuntime(runtime, run.requestId)))).filter((task) => Boolean(task));
512
518
  const nextIndex = startIndex + page.length;
513
519
  const nextPageToken = nextIndex < runs.length ? Buffer.from(String(nextIndex), "utf8").toString("base64url") : "";
514
520
  return {
@@ -718,12 +724,12 @@ async function* streamSendMessageTaskUpdates(runtime, options, method) {
718
724
  let emittedInitialTask = false;
719
725
  for await (const item of runtime.streamEvents(options)) {
720
726
  if (item.type === "event") {
721
- runId = runId ?? item.event.runId;
727
+ runId = runId ?? item.event.requestId;
722
728
  if (item.event.eventType === "output.delta" && typeof item.event.payload.content === "string") {
723
729
  streamedText += item.event.payload.content;
724
730
  }
725
731
  if (item.event.eventType === "run.created" || item.event.eventType === "run.state.changed" || item.event.eventType === "output.delta" || item.event.eventType === "approval.requested") {
726
- const task = withStreamingStatusText(await buildTaskFromRuntime(runtime, item.event.runId), streamedText || undefined);
732
+ const task = withStreamingStatusText(await buildTaskFromRuntime(runtime, item.event.requestId), streamedText || undefined);
727
733
  if (task) {
728
734
  yield { task, mode: emittedInitialTask ? "status-update" : "task" };
729
735
  emittedInitialTask = true;
@@ -732,8 +738,8 @@ async function* streamSendMessageTaskUpdates(runtime, options, method) {
732
738
  continue;
733
739
  }
734
740
  if (item.type === "result") {
735
- runId = item.result.runId;
736
- const task = withRunResult(await buildTaskFromRuntime(runtime, item.result.runId), item.result, streamedText || item.result.output);
741
+ runId = item.result.requestId;
742
+ const task = withRunResult(await buildTaskFromRuntime(runtime, item.result.requestId), item.result, streamedText || item.result.output);
737
743
  yield { task, mode: emittedInitialTask ? "status-update" : "task" };
738
744
  return;
739
745
  }
@@ -810,7 +816,7 @@ async function streamExistingTaskUpdates(runtime, response, id, method, taskId)
810
816
  }
811
817
  };
812
818
  const unsubscribe = runtime.subscribe((event) => {
813
- if (event.runId !== taskId) {
819
+ if (event.requestId !== taskId) {
814
820
  return;
815
821
  }
816
822
  if (event.eventType !== "run.state.changed"
@@ -893,10 +899,10 @@ export async function serveA2aOverHttp(runtime, options = {}) {
893
899
  };
894
900
  const pushRelevantEventTypes = new Set(["run.state.changed", "approval.requested", "approval.resolved"]);
895
901
  const unsubscribePushNotifications = runtime.subscribe((event) => {
896
- if (!pushRelevantEventTypes.has(event.eventType) || !pushConfigsByTask.has(event.runId)) {
902
+ if (!pushRelevantEventTypes.has(event.eventType) || !pushConfigsByTask.has(event.requestId)) {
897
903
  return;
898
904
  }
899
- void dispatchPushNotificationsForTask(event.runId);
905
+ void dispatchPushNotificationsForTask(event.requestId);
900
906
  });
901
907
  const httpServer = createServer(async (request, response) => {
902
908
  try {
@@ -911,6 +917,19 @@ export async function serveA2aOverHttp(runtime, options = {}) {
911
917
  });
912
918
  return;
913
919
  }
920
+ writeA2aDiscoveryHeaders(response);
921
+ if (request.method === "OPTIONS" && (requestUrl.pathname === rpcPath || requestUrl.pathname === agentCardPath || requestUrl.pathname === "/.well-known/agent.json")) {
922
+ response.statusCode = 204;
923
+ response.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
924
+ response.end();
925
+ return;
926
+ }
927
+ if (request.method === "HEAD" && (requestUrl.pathname === agentCardPath || requestUrl.pathname === "/.well-known/agent.json")) {
928
+ response.statusCode = 200;
929
+ response.setHeader("content-type", "application/json; charset=utf-8");
930
+ response.end();
931
+ return;
932
+ }
914
933
  if (request.method === "GET" && (requestUrl.pathname === agentCardPath || requestUrl.pathname === "/.well-known/agent.json")) {
915
934
  writeJson(response, 200, buildAgentCard(runtime, buildCardOptions()));
916
935
  return;
@@ -934,12 +953,12 @@ export async function serveA2aOverHttp(runtime, options = {}) {
934
953
  const result = await runtime.run({
935
954
  agentId: parsed.agentId ?? options.defaultAgentId,
936
955
  input: parsed.input,
937
- ...(parsed.sessionId ? { threadId: parsed.sessionId } : {}),
956
+ ...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
938
957
  invocation: attachServiceParameters(parsed.invocation, serviceParameters),
939
958
  });
940
- const session = await runtime.getThread(result.threadId);
941
- const requestRecord = await runtime.getRun(result.runId);
942
- const approvals = await runtime.listApprovals({ threadId: result.threadId, runId: result.runId });
959
+ const session = await runtime.getThread(result.sessionId);
960
+ const requestRecord = await runtime.getRun(result.requestId);
961
+ const approvals = await runtime.listApprovals({ sessionId: result.sessionId, requestId: result.requestId });
943
962
  const task = buildTaskFromSessionAndRequest(toSessionRecord(session), toRequestRecord(requestRecord), approvals, result.output);
944
963
  if (task && parsed.pushNotificationConfig) {
945
964
  registerPushNotificationConfig(task.id, parsed.pushNotificationConfig);
@@ -948,6 +967,10 @@ export async function serveA2aOverHttp(runtime, options = {}) {
948
967
  writeJson(response, 200, toSuccess(payload.id ?? null, toSendMessageResult(payload.method, task)));
949
968
  return;
950
969
  }
970
+ if (payload.method === "GetAgentCard") {
971
+ writeJson(response, 200, toSuccess(payload.id ?? null, buildAgentCard(runtime, buildCardOptions())));
972
+ return;
973
+ }
951
974
  if (payload.method === "message/stream" || payload.method === "SendStreamingMessage") {
952
975
  if (!acceptsSse(request)) {
953
976
  writeJson(response, 406, toError(payload.id ?? null, -32004, "A2A streaming requires `Accept: text/event-stream`."));
@@ -958,7 +981,7 @@ export async function serveA2aOverHttp(runtime, options = {}) {
958
981
  const sourceIterator = streamSendMessageTaskUpdates(runtime, {
959
982
  agentId: parsed.agentId ?? options.defaultAgentId,
960
983
  input: parsed.input,
961
- ...(parsed.sessionId ? { threadId: parsed.sessionId } : {}),
984
+ ...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
962
985
  invocation: attachServiceParameters(parsed.invocation, serviceParameters),
963
986
  }, payload.method);
964
987
  const iterator = (async function* () {
@@ -991,8 +1014,8 @@ export async function serveA2aOverHttp(runtime, options = {}) {
991
1014
  }
992
1015
  if (payload.method === "tasks/cancel" || payload.method === "CancelTask") {
993
1016
  const { taskId } = parseTaskLocatorParams(payload.params);
994
- const result = await runtime.cancelRun({ runId: taskId, reason: "Cancelled via A2A bridge." });
995
- const task = await buildTaskFromRuntime(runtime, result.runId);
1017
+ const result = await runtime.cancelRun({ requestId: taskId, reason: "Cancelled via A2A bridge." });
1018
+ const task = await buildTaskFromRuntime(runtime, result.requestId);
996
1019
  writeJson(response, 200, toSuccess(payload.id ?? null, task));
997
1020
  return;
998
1021
  }
@@ -51,8 +51,8 @@ function toRunStarted(event, input) {
51
51
  return {
52
52
  type: "RUN_STARTED",
53
53
  timestamp: createTimestamp(),
54
- threadId: event.threadId,
55
- runId: event.runId,
54
+ threadId: event.sessionId,
55
+ runId: event.requestId,
56
56
  input,
57
57
  };
58
58
  }
@@ -187,19 +187,19 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
187
187
  const result = await runtime.run({
188
188
  agentId: input.agentId,
189
189
  input: input.input,
190
- ...(input.sessionId ? { threadId: input.sessionId } : {}),
190
+ ...(input.sessionId ? { sessionId: input.sessionId } : {}),
191
191
  ...(input.invocation ? { invocation: input.invocation } : {}),
192
192
  listeners: {
193
193
  onEvent: async (event) => {
194
194
  if (event.eventType === "run.created") {
195
- runId = event.runId;
196
- threadId = event.threadId;
195
+ runId = event.requestId;
196
+ threadId = event.sessionId;
197
197
  await writeSseEvent(response, toRunStarted(event, input.input));
198
198
  return;
199
199
  }
200
200
  if (event.eventType === "output.delta") {
201
- runId = runId ?? event.runId;
202
- threadId = threadId ?? event.threadId;
201
+ runId = runId ?? event.requestId;
202
+ threadId = threadId ?? event.sessionId;
203
203
  const delta = typeof event.payload.content === "string" ? event.payload.content : "";
204
204
  if (!delta) {
205
205
  return;
@@ -301,8 +301,8 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
301
301
  },
302
302
  },
303
303
  });
304
- runId = runId ?? result.runId;
305
- threadId = threadId ?? result.threadId;
304
+ runId = runId ?? result.requestId;
305
+ threadId = threadId ?? result.sessionId;
306
306
  if (!textMessageStarted && result.output) {
307
307
  await ensureTextStart();
308
308
  await writeSseEvent(response, {
@@ -20,8 +20,8 @@ export function finalizeInvocationResult(params) {
20
20
  const stateSnapshot = buildStateSnapshot(result);
21
21
  const memoryCandidates = executedToolResults.flatMap((toolResult) => toolResult.memoryCandidates ?? []);
22
22
  return {
23
- threadId,
24
- runId,
23
+ sessionId: threadId,
24
+ requestId: runId,
25
25
  agentId: bindingAgentId,
26
26
  state: Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? "waiting_for_approval" : "completed",
27
27
  interruptContent,
@@ -1,7 +1,9 @@
1
1
  import type { CompiledTool } from "../../../contracts/types.js";
2
2
  import type { CompiledAgentBinding } from "../../../contracts/types.js";
3
+ import type { RuntimeGovernanceDecisionMode } from "../../../contracts/types.js";
3
4
  type InterruptFn = <I = unknown, R = unknown>(value: I) => R;
5
+ export declare function resolveToolApprovalDecisionMode(compiledTool: CompiledTool, binding?: CompiledAgentBinding): RuntimeGovernanceDecisionMode;
4
6
  export declare function toolRequiresRuntimeApproval(compiledTool: CompiledTool): boolean;
5
- export declare function wrapToolForHumanInTheLoop<T>(resolvedTool: T, compiledTool: CompiledTool, interruptFn?: InterruptFn): T;
7
+ export declare function wrapToolForHumanInTheLoop<T>(resolvedTool: T, compiledTool: CompiledTool, binding?: CompiledAgentBinding, interruptFn?: InterruptFn): T;
6
8
  export declare function wrapToolForExecution<T>(resolvedTool: T, compiledTool: CompiledTool, binding?: CompiledAgentBinding, interruptFn?: InterruptFn): T;
7
9
  export {};
@@ -14,6 +14,16 @@ function asRecord(value) {
14
14
  function asString(value) {
15
15
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
16
16
  }
17
+ function readApprovalDecisionMode(value) {
18
+ if (value === "none" ||
19
+ value === "manual" ||
20
+ value === "auto-approve" ||
21
+ value === "auto-reject" ||
22
+ value === "deny-and-continue") {
23
+ return value;
24
+ }
25
+ return undefined;
26
+ }
17
27
  function readApprovalOverride(compiledTool) {
18
28
  if (compiledTool.config?.requireApproval === true) {
19
29
  return true;
@@ -59,11 +69,62 @@ function requiresApprovalForHighRiskMcpWrite(compiledTool) {
59
69
  .join(" ");
60
70
  return WRITE_LIKE_REMOTE_TOOL_PATTERN.test(targetText);
61
71
  }
62
- export function toolRequiresRuntimeApproval(compiledTool) {
72
+ function matchesGovernanceToolPolicy(rule, compiledTool) {
73
+ const match = asRecord(rule.match) ?? rule;
74
+ const toolName = asString(match.toolName);
75
+ const category = asString(match.category);
76
+ const toolType = asString(match.toolType);
77
+ const compiledCategory = compiledTool.type === "mcp"
78
+ ? "mcp"
79
+ : compiledTool.type === "backend"
80
+ ? "backend"
81
+ : compiledTool.type === "provider"
82
+ ? "provider-native"
83
+ : "local";
84
+ return (!toolName || toolName === compiledTool.name)
85
+ && (!category || category === compiledCategory)
86
+ && (!toolType || toolType === compiledTool.type);
87
+ }
88
+ function readToolApprovalModeFromGovernance(compiledTool, binding) {
89
+ const governance = asRecord(binding?.harnessRuntime.governance);
90
+ const overrides = Array.isArray(governance?.toolPolicies) ? governance.toolPolicies : [];
91
+ for (const rule of overrides) {
92
+ const typedRule = asRecord(rule);
93
+ if (!typedRule || !matchesGovernanceToolPolicy(typedRule, compiledTool)) {
94
+ continue;
95
+ }
96
+ const overrideMode = readApprovalDecisionMode(typedRule.decisionMode ?? typedRule.approvalMode);
97
+ if (overrideMode) {
98
+ return overrideMode;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+ function readToolApprovalModeFromConfig(compiledTool) {
104
+ const approval = asRecord(compiledTool.config?.approval);
105
+ return readApprovalDecisionMode(approval?.mode ??
106
+ approval?.decisionMode ??
107
+ compiledTool.config?.approvalMode);
108
+ }
109
+ export function resolveToolApprovalDecisionMode(compiledTool, binding) {
63
110
  if (compiledTool.hitl?.enabled === true) {
64
- return true;
111
+ return "manual";
112
+ }
113
+ const governanceMode = readToolApprovalModeFromGovernance(compiledTool, binding);
114
+ if (governanceMode) {
115
+ return governanceMode;
116
+ }
117
+ const configuredMode = readToolApprovalModeFromConfig(compiledTool);
118
+ if (configuredMode) {
119
+ return configuredMode;
65
120
  }
66
- return readApprovalOverride(compiledTool) || requiresApprovalForSensitiveMemoryWrite(compiledTool) || requiresApprovalForHighRiskMcpWrite(compiledTool);
121
+ if (readApprovalOverride(compiledTool) || requiresApprovalForSensitiveMemoryWrite(compiledTool) || requiresApprovalForHighRiskMcpWrite(compiledTool)) {
122
+ return "manual";
123
+ }
124
+ return "none";
125
+ }
126
+ export function toolRequiresRuntimeApproval(compiledTool) {
127
+ return resolveToolApprovalDecisionMode(compiledTool) === "manual";
67
128
  }
68
129
  function toolApprovalReason(compiledTool) {
69
130
  if (compiledTool.hitl?.enabled === true) {
@@ -101,17 +162,25 @@ function resolveApprovedInput(originalInput, resumeValue) {
101
162
  }
102
163
  return originalInput;
103
164
  }
104
- export function wrapToolForHumanInTheLoop(resolvedTool, compiledTool, interruptFn = interrupt) {
105
- if (!toolRequiresRuntimeApproval(compiledTool) || typeof resolvedTool !== "object" || resolvedTool === null) {
165
+ export function wrapToolForHumanInTheLoop(resolvedTool, compiledTool, binding, interruptFn = interrupt) {
166
+ const decisionMode = resolveToolApprovalDecisionMode(compiledTool, binding);
167
+ if (decisionMode === "none" || typeof resolvedTool !== "object" || resolvedTool === null) {
106
168
  return resolvedTool;
107
169
  }
108
170
  const target = resolvedTool;
109
171
  const runWithApproval = async (input, config, invokeOriginal) => {
172
+ if (decisionMode === "auto-approve") {
173
+ return invokeOriginal(input, config);
174
+ }
175
+ if (decisionMode === "auto-reject" || decisionMode === "deny-and-continue") {
176
+ return "Tool execution denied by runtime policy.";
177
+ }
110
178
  const resumed = interruptFn({
111
179
  toolName: compiledTool.name,
112
180
  toolId: compiledTool.id,
113
181
  allowedDecisions: compiledTool.hitl?.allow ?? ["approve", "edit", "reject"],
114
182
  inputPreview: toInputPreview(input),
183
+ decisionMode,
115
184
  ...(toolApprovalReason(compiledTool) ? { approvalReason: toolApprovalReason(compiledTool) } : {}),
116
185
  });
117
186
  const approvedInput = resolveApprovedInput(input, resumed);
@@ -238,5 +307,5 @@ export function wrapToolForExecution(resolvedTool, compiledTool, binding, interr
238
307
  },
239
308
  })
240
309
  : resolvedTool;
241
- return wrapToolWithDedupe(wrapToolForHumanInTheLoop(guardedTool, compiledTool, interruptFn), compiledTool);
310
+ return wrapToolWithDedupe(wrapToolForHumanInTheLoop(guardedTool, compiledTool, binding, interruptFn), compiledTool);
242
311
  }
@@ -4,10 +4,10 @@ export declare function createListenerDispatchRuntime(input: {
4
4
  notifyListener: <T>(listener: ((value: T) => void | Promise<void>) | undefined, value: T) => Promise<void>;
5
5
  getThread: (threadId: string) => Promise<{
6
6
  currentState: RunResult["state"];
7
- latestRunId: string;
7
+ latestRequestId: string;
8
8
  entryAgentId: string;
9
9
  currentAgentId?: string;
10
- runs: Array<{
10
+ requests: Array<{
11
11
  agentId: string;
12
12
  }>;
13
13
  pendingDecision?: {
@@ -1,20 +1,20 @@
1
1
  import type { HarnessEvent, HarnessStreamItem, RunListeners, RunResult } from "../../../contracts/types.js";
2
2
  export type InternalHarnessStreamItem = HarnessStreamItem | {
3
3
  type: "content";
4
- threadId: string;
5
- runId: string;
4
+ sessionId: string;
5
+ requestId: string;
6
6
  agentId: string;
7
7
  content: string;
8
8
  } | {
9
9
  type: "content-blocks";
10
- threadId: string;
11
- runId: string;
10
+ sessionId: string;
11
+ requestId: string;
12
12
  agentId: string;
13
13
  contentBlocks: unknown[];
14
14
  } | {
15
15
  type: "tool-result";
16
- threadId: string;
17
- runId: string;
16
+ sessionId: string;
17
+ requestId: string;
18
18
  agentId: string;
19
19
  toolName: string;
20
20
  output: unknown;
@@ -27,10 +27,10 @@ export declare function dispatchRunListeners(stream: AsyncGenerator<InternalHarn
27
27
  notifyListener: <T>(listener: ((value: T) => void | Promise<void>) | undefined, value: T) => Promise<void>;
28
28
  getThread: (threadId: string) => Promise<{
29
29
  currentState: RunResult["state"];
30
- latestRunId: string;
30
+ latestRequestId: string;
31
31
  entryAgentId: string;
32
32
  currentAgentId?: string;
33
- runs: Array<{
33
+ requests: Array<{
34
34
  agentId: string;
35
35
  }>;
36
36
  pendingDecision?: {
@@ -5,8 +5,8 @@ export async function emitOutputDeltaAndCreateItem(emit, threadId, runId, agentI
5
5
  });
6
6
  return {
7
7
  type: "content",
8
- threadId,
9
- runId,
8
+ sessionId: threadId,
9
+ requestId: runId,
10
10
  agentId,
11
11
  content,
12
12
  };
@@ -14,8 +14,8 @@ export async function emitOutputDeltaAndCreateItem(emit, threadId, runId, agentI
14
14
  export function createContentBlocksItem(threadId, runId, agentId, contentBlocks) {
15
15
  return {
16
16
  type: "content-blocks",
17
- threadId,
18
- runId,
17
+ sessionId: threadId,
18
+ requestId: runId,
19
19
  agentId,
20
20
  contentBlocks,
21
21
  };
@@ -44,8 +44,8 @@ export async function dispatchRunListeners(stream, listeners, options) {
44
44
  await options.notifyListener(listeners.onUpstreamEvent, item.event);
45
45
  if (item.surfaceItem) {
46
46
  await options.notifyListener(listeners.onTraceItem, {
47
- threadId: item.threadId,
48
- runId: item.runId,
47
+ sessionId: item.sessionId,
48
+ requestId: item.requestId,
49
49
  surfaceItem: item.surfaceItem,
50
50
  event: item.event,
51
51
  });
@@ -67,16 +67,16 @@ export async function dispatchRunListeners(stream, listeners, options) {
67
67
  if (latestResult) {
68
68
  return mergeRunResultOutput(latestResult, output);
69
69
  }
70
- const thread = await options.getThread(latestEvent.threadId);
70
+ const thread = await options.getThread(latestEvent.sessionId);
71
71
  if (!thread) {
72
- throw new Error(`Unknown thread ${latestEvent.threadId}`);
72
+ throw new Error(`Unknown session ${latestEvent.sessionId}`);
73
73
  }
74
74
  return createFallbackRunResultFromLatestEvent({
75
75
  latestEvent,
76
76
  currentState: thread.currentState,
77
- latestRunId: thread.latestRunId,
77
+ latestRunId: thread.latestRequestId,
78
78
  entryAgentId: thread.entryAgentId,
79
- latestAgentId: thread.currentAgentId ?? thread.runs[0]?.agentId,
79
+ latestAgentId: thread.currentAgentId ?? thread.requests[0]?.agentId,
80
80
  approvalId: thread.pendingDecision?.approvalId,
81
81
  pendingActionId: thread.pendingDecision?.pendingActionId,
82
82
  output,
@@ -19,10 +19,10 @@ function classifyTimelineItem(event) {
19
19
  }
20
20
  export function projectRuntimeTimeline(events, options = {}) {
21
21
  const filtered = events.filter((event) => {
22
- if (options.threadId && event.threadId !== options.threadId) {
22
+ if (options.sessionId && event.sessionId !== options.sessionId) {
23
23
  return false;
24
24
  }
25
- if (options.runId && event.runId !== options.runId) {
25
+ if (options.requestId && event.requestId !== options.requestId) {
26
26
  return false;
27
27
  }
28
28
  return true;
@@ -40,8 +40,8 @@ export function projectRuntimeTimeline(events, options = {}) {
40
40
  })
41
41
  .map((event) => ({
42
42
  eventId: event.eventId,
43
- threadId: event.threadId,
44
- runId: event.runId,
43
+ sessionId: event.sessionId,
44
+ requestId: event.requestId,
45
45
  eventType: event.eventType,
46
46
  timestamp: event.timestamp,
47
47
  sequence: event.sequence,
@@ -1,5 +1,5 @@
1
1
  import { getBindingPrimaryTools } from "../../support/compiled-binding.js";
2
- import { toolRequiresRuntimeApproval } from "../../adapter/tool/tool-hitl.js";
2
+ import { resolveToolApprovalDecisionMode } from "../../adapter/tool/tool-hitl.js";
3
3
  import { compiledToolHasInputSchema } from "../tool-schema.js";
4
4
  const WRITE_LIKE_PATTERN = /\b(write|edit|delete|create|update|append|insert|push|commit|publish|send|post|apply|merge|sync|upload|save)\b/i;
5
5
  function inputHints(binding, tool) {
@@ -64,6 +64,11 @@ function readRisk(value) {
64
64
  function readApprovalPolicy(value) {
65
65
  return value === "explicit-hitl" || value === "runtime-default" || value === "none" ? value : undefined;
66
66
  }
67
+ function readDecisionMode(value) {
68
+ return value === "none" || value === "manual" || value === "auto-approve" || value === "auto-reject" || value === "deny-and-continue"
69
+ ? value
70
+ : undefined;
71
+ }
67
72
  function normalizeServerRef(value) {
68
73
  if (typeof value !== "string" || value.trim().length === 0) {
69
74
  return undefined;
@@ -123,18 +128,32 @@ function applyGovernanceOverrides(binding, policies) {
123
128
  const overrideRisk = readRisk(typedRule.risk);
124
129
  const overrideApprovalPolicy = readApprovalPolicy(typedRule.approvalPolicy);
125
130
  const overrideRequiresApproval = typeof typedRule.requiresApproval === "boolean" ? typedRule.requiresApproval : undefined;
131
+ const overrideDecisionMode = readDecisionMode(typedRule.decisionMode ?? typedRule.approvalMode);
126
132
  if (overrideRisk) {
127
133
  merged.risk = overrideRisk;
128
134
  }
129
135
  if (overrideRequiresApproval !== undefined) {
130
136
  merged.requiresApproval = overrideRequiresApproval;
131
137
  }
138
+ if (overrideDecisionMode) {
139
+ merged.decisionMode = overrideDecisionMode;
140
+ merged.requiresApproval = overrideDecisionMode === "manual";
141
+ merged.approvalPolicy =
142
+ overrideDecisionMode === "none"
143
+ ? "none"
144
+ : merged.approvalPolicy === "explicit-hitl"
145
+ ? "explicit-hitl"
146
+ : "runtime-default";
147
+ }
132
148
  if (overrideApprovalPolicy) {
133
149
  merged.approvalPolicy = overrideApprovalPolicy;
134
150
  }
135
151
  else if (overrideRequiresApproval === true && merged.approvalPolicy === "none") {
136
152
  merged.approvalPolicy = "runtime-default";
137
153
  }
154
+ else if (overrideRequiresApproval === false && !overrideDecisionMode) {
155
+ merged.approvalPolicy = "none";
156
+ }
138
157
  const extraHints = readStringArray(typedRule.inputRiskHints);
139
158
  if (extraHints.length > 0) {
140
159
  merged.inputRiskHints = Array.from(new Set([...merged.inputRiskHints, ...extraHints]));
@@ -160,6 +179,9 @@ function applyRemoteMcpGovernance(binding, policies) {
160
179
  const transport = merged.mcpTransport;
161
180
  if (transport && requireApprovalTransports.has(transport)) {
162
181
  merged.requiresApproval = true;
182
+ if (merged.decisionMode === "none") {
183
+ merged.decisionMode = "manual";
184
+ }
163
185
  if (merged.approvalPolicy === "none") {
164
186
  merged.approvalPolicy = "runtime-default";
165
187
  }
@@ -179,11 +201,15 @@ export function buildRuntimeGovernanceBundles(binding) {
179
201
  const toolPolicies = applyGovernanceOverrides(binding, applyRemoteMcpGovernance(binding, getBindingPrimaryTools(binding).map((tool) => {
180
202
  const remoteMcp = readRemoteMcpMetadata(tool);
181
203
  const writeLikeRemoteMcp = tool.type === "mcp" && (remoteMcp.access === "read-write" || WRITE_LIKE_PATTERN.test(`${tool.name} ${tool.description}`));
182
- const requiresApproval = toolRequiresRuntimeApproval(tool) ||
204
+ const derivedDecisionMode = resolveToolApprovalDecisionMode(tool, binding);
205
+ const requiresApproval = derivedDecisionMode === "manual" ||
183
206
  remoteMcp.trustTier === "untrusted" ||
184
207
  remoteMcp.approvalPolicy === "always" ||
185
208
  (remoteMcp.approvalPolicy === "write" && writeLikeRemoteMcp) ||
186
209
  remoteMcp.tenantScope === "cross-tenant";
210
+ const decisionMode = requiresApproval
211
+ ? (derivedDecisionMode === "none" ? "manual" : derivedDecisionMode)
212
+ : derivedDecisionMode;
187
213
  const approvalReason = remoteMcp.trustTier === "untrusted"
188
214
  ? "untrusted-mcp-server"
189
215
  : remoteMcp.tenantScope === "cross-tenant"
@@ -230,7 +256,8 @@ export function buildRuntimeGovernanceBundles(binding) {
230
256
  promptInjectionRisk: remoteMcp.promptInjectionRisk,
231
257
  }),
232
258
  requiresApproval,
233
- approvalPolicy: tool.hitl?.enabled === true ? "explicit-hitl" : requiresApproval ? "runtime-default" : "none",
259
+ approvalPolicy: tool.hitl?.enabled === true ? "explicit-hitl" : decisionMode === "none" ? "none" : "runtime-default",
260
+ decisionMode,
234
261
  hasInputSchema: compiledToolHasInputSchema(tool),
235
262
  inputRiskHints: Array.from(new Set(inputRiskHints)),
236
263
  };
@@ -241,7 +268,9 @@ export function buildRuntimeGovernanceBundles(binding) {
241
268
  return [{
242
269
  bundleId: `governance/${binding.agent.id}`,
243
270
  title: "Runtime tool governance",
244
- summary: `${toolPolicies.filter((tool) => tool.requiresApproval).length} of ${toolPolicies.length} tool(s) require approval`,
271
+ summary: `${toolPolicies.filter((tool) => tool.requiresApproval).length} of ${toolPolicies.length} tool(s) require approval; ` +
272
+ `auto-approved=${toolPolicies.filter((tool) => tool.decisionMode === "auto-approve").length}; ` +
273
+ `auto-rejected=${toolPolicies.filter((tool) => tool.decisionMode === "auto-reject" || tool.decisionMode === "deny-and-continue").length}`,
245
274
  toolPolicies,
246
275
  }];
247
276
  }
@@ -57,8 +57,8 @@ export function mergeRunResultOutput(result, streamedOutput) {
57
57
  }
58
58
  export function createFallbackRunResultFromLatestEvent(input) {
59
59
  return {
60
- threadId: input.latestEvent.threadId,
61
- runId: input.latestRunId,
60
+ sessionId: input.latestEvent.sessionId,
61
+ requestId: input.latestRunId,
62
62
  agentId: input.latestAgentId ?? input.entryAgentId,
63
63
  state: input.currentState,
64
64
  output: input.output,