@sema-agent/core 5.17.0-pre.0 → 5.18.0

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 (81) hide show
  1. package/CHANGELOG.md +186 -1
  2. package/dist/agents/subagent.d.ts +8 -0
  3. package/dist/agents/subagent.js +93 -2
  4. package/dist/core/ask-question.js +6 -1
  5. package/dist/core/auto-compaction.d.ts +6 -0
  6. package/dist/core/auto-compaction.js +15 -1
  7. package/dist/core/canonical-json.js +176 -14
  8. package/dist/core/checkpoint-store.d.ts +15 -0
  9. package/dist/core/checkpoint-store.js +73 -0
  10. package/dist/core/governance-codes.js +1 -0
  11. package/dist/core/hooks.d.ts +12 -1
  12. package/dist/core/hooks.js +41 -6
  13. package/dist/core/mcp.d.ts +1 -0
  14. package/dist/core/mcp.js +18 -3
  15. package/dist/core/memory-engine/content-origin.d.ts +27 -0
  16. package/dist/core/memory-engine/content-origin.js +38 -0
  17. package/dist/core/memory-engine/engine.d.ts +12 -2
  18. package/dist/core/memory-engine/engine.js +172 -12
  19. package/dist/core/memory-engine/file-backend.d.ts +4 -0
  20. package/dist/core/memory-engine/file-backend.js +25 -3
  21. package/dist/core/memory-engine/index.d.ts +2 -1
  22. package/dist/core/memory-engine/index.js +2 -1
  23. package/dist/core/memory-engine/layout.d.ts +16 -0
  24. package/dist/core/memory-engine/layout.js +90 -2
  25. package/dist/core/memory-engine/sync-client.d.ts +1 -0
  26. package/dist/core/memory-engine/sync-client.js +23 -5
  27. package/dist/core/memory-engine/tools.d.ts +55 -0
  28. package/dist/core/memory-engine/tools.js +307 -0
  29. package/dist/core/memory-engine/types.d.ts +1 -1
  30. package/dist/core/memory.d.ts +4 -0
  31. package/dist/core/memory.js +15 -2
  32. package/dist/core/permission-rule-consent.d.ts +131 -0
  33. package/dist/core/permission-rule-consent.js +307 -0
  34. package/dist/core/permission-rule-model.d.ts +66 -0
  35. package/dist/core/permission-rule-model.js +135 -0
  36. package/dist/core/permission-rule-store.d.ts +89 -0
  37. package/dist/core/permission-rule-store.js +145 -0
  38. package/dist/core/permission-rules.d.ts +3 -2
  39. package/dist/core/permission-rules.js +9 -4
  40. package/dist/core/runner/prepare-memory.d.ts +3 -1
  41. package/dist/core/runner/prepare-memory.js +54 -14
  42. package/dist/core/runner/prepare-task.d.ts +12 -0
  43. package/dist/core/runner/prepare-task.js +270 -32
  44. package/dist/core/runner/runtask.js +27 -1
  45. package/dist/core/runner/tool-output-projection.js +1 -1
  46. package/dist/core/runner/turn-attachments.d.ts +2 -1
  47. package/dist/core/runner/turn-attachments.js +9 -6
  48. package/dist/core/session-reconcile.js +19 -1
  49. package/dist/core/task-registry-agent.js +1 -0
  50. package/dist/core/tool-policy.d.ts +17 -1
  51. package/dist/core/tool-policy.js +64 -8
  52. package/dist/core/tools.js +1 -0
  53. package/dist/core/trace.d.ts +20 -0
  54. package/dist/core/types.d.ts +15 -0
  55. package/dist/core/wiring-manifest.d.ts +5 -1
  56. package/dist/core/wiring-manifest.js +4 -2
  57. package/dist/engine/llm/validation.js +121 -5
  58. package/dist/engine/loop/agent-loop.d.ts +2 -0
  59. package/dist/engine/loop/agent-loop.js +17 -4
  60. package/dist/index.d.ts +7 -2
  61. package/dist/index.js +6 -1
  62. package/dist/prompts/supervisor.d.ts +1 -1
  63. package/dist/prompts/supervisor.js +1 -1
  64. package/dist/stores/file/checkpoint-store.d.ts +1 -0
  65. package/dist/stores/file/checkpoint-store.js +1 -0
  66. package/dist/stores/file/permission-rule-store.d.ts +32 -0
  67. package/dist/stores/file/permission-rule-store.js +213 -0
  68. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  69. package/dist/tools/fs/bash-readonly-classifier.js +11 -10
  70. package/dist/tools/fs/fs-bash.js +17 -10
  71. package/dist/tools/fs/fs-read.d.ts +1 -1
  72. package/dist/tools/fs/fs-read.js +4 -3
  73. package/dist/tools/fs/fs-shared.d.ts +15 -0
  74. package/dist/tools/fs/fs-shared.js +73 -2
  75. package/dist/tools/fs/fs-write.js +8 -8
  76. package/dist/tools/fs/index.d.ts +1 -0
  77. package/dist/tools/fs/index.js +1 -1
  78. package/dist/tools/fs/safety.d.ts +1 -0
  79. package/dist/tools/fs/safety.js +9 -2
  80. package/dist/tools/web.js +2 -0
  81. package/package.json +1 -1
@@ -62,7 +62,7 @@ const CC_DETAIL_TYPES = new Set([
62
62
  "agent", "task", "task-list", "task-output", "workflow-run",
63
63
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
64
64
  "task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
65
- "monitor-start", "path_not_in_root", "readonly_out_of_root",
65
+ "monitor-start", "path_not_in_root", "readonly_out_of_root", "bash_invalid_timeout",
66
66
  "report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
67
67
  ]);
68
68
  export const structuredFrom = (result) => {
@@ -94,6 +94,7 @@ export interface AttachmentInputs {
94
94
  mcpInstructions?: boolean;
95
95
  };
96
96
  undiscoveredTools?: readonly string[];
97
+ deferDirectCall?: boolean;
97
98
  changedFiles?: ReadonlyArray<{
98
99
  path: string;
99
100
  mtimeMs: number;
@@ -118,7 +119,7 @@ export interface AttachmentInputs {
118
119
  mcpDroppedTools?: ReadonlyArray<McpDroppedTool>;
119
120
  }
120
121
  export declare function collectDueAttachments(state: AttachmentState, inp: AttachmentInputs): readonly TurnAttachment[];
121
- export declare function renderToolSearchUsageReminder(undiscovered: readonly string[]): string;
122
+ export declare function renderToolSearchUsageReminder(undiscovered: readonly string[], directCallEnabled?: boolean): string;
122
123
  export declare function renderDateChange(newDate: string): string;
123
124
  export declare function collectDateChange(state: DateChangeState, today: string): TurnAttachment | undefined;
124
125
  export declare function collectInstructionsChange(state: InstructionsChangeState, probed: ReadonlyArray<{
@@ -145,7 +145,7 @@ export function collectDueAttachments(state, inp) {
145
145
  const sinceUse = state.toolSearchLastUseTurn === undefined ? Number.POSITIVE_INFINITY : cadenceNow - state.toolSearchLastUseTurn;
146
146
  if (sinceUse >= TOOL_SEARCH_REMINDER_CONFIG.EVERY_N_TURNS && cadenceNow - state.lastToolSearchReminderTurn >= TOOL_SEARCH_REMINDER_CONFIG.EVERY_N_TURNS) {
147
147
  state.lastToolSearchReminderTurn = cadenceNow;
148
- (out ??= []).push({ source: "tool_search_usage_reminder", body: renderToolSearchUsageReminder(inp.undiscoveredTools) });
148
+ (out ??= []).push({ source: "tool_search_usage_reminder", body: renderToolSearchUsageReminder(inp.undiscoveredTools, inp.deferDirectCall !== false) });
149
149
  }
150
150
  }
151
151
  if (inp.config.planModeReminder && inp.planActive) {
@@ -248,14 +248,17 @@ function renderTaskReminder(state) {
248
248
  }
249
249
  return message;
250
250
  }
251
- export function renderToolSearchUsageReminder(undiscovered) {
251
+ export function renderToolSearchUsageReminder(undiscovered, directCallEnabled = true) {
252
252
  const shown = undiscovered.slice(0, TOOL_SEARCH_REMINDER_CONFIG.MAX_NAMES);
253
253
  const more = undiscovered.length - shown.length;
254
254
  const names = `${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
255
255
  return (`Some available tools' schemas are not loaded in this conversation yet: ${names}. Before concluding a ` +
256
256
  `capability is missing or building a workaround, use ${TOOL_SEARCH_TOOL_NAME} to find and load relevant tools — ` +
257
- `keywords to search, or query "select:<name>[,<name>...]" for specific tools. Calling a tool before its schema ` +
258
- `is loaded will fail. This is just a gentle reminder - ignore if not applicable to the current work.`);
257
+ `keywords to search, or query "select:<name>[,<name>...]" for specific tools. ` +
258
+ (directCallEnabled
259
+ ? `You do not have these tools' parameters, so activate one rather than guessing its arguments. `
260
+ : `Calling a tool before its schema is loaded will fail. `) +
261
+ `This is just a gentle reminder - ignore if not applicable to the current work.`);
259
262
  }
260
263
  export function renderDateChange(newDate) {
261
264
  return `The date has changed. Today's date is now ${newDate}. DO NOT mention this to the user explicitly because they are already aware.`;
@@ -400,8 +403,8 @@ export function renderToolsDelta(input) {
400
403
  if (added.length > 0) {
401
404
  const swapped = input.staticFace === true ? (input.swappedUnderStatic ?? []).filter((n) => added.includes(n)) : [];
402
405
  blocks.push((input.staticFace === true
403
- ? "The following deferred tools are now active — call them directly. Their parameter schemas were " +
404
- "provided in the ToolSearch result (the tools list itself keeps compact placeholder entries):\n"
406
+ ? "The following deferred tools are now active — call them directly. The tools list keeps compact " +
407
+ 'placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema:\n'
405
408
  : "The following deferred tools are now available. Their full schemas are loaded — call them " +
406
409
  "directly like any other tool:\n") +
407
410
  added.map((n) => `- ${n}`).join("\n") +
@@ -10,6 +10,20 @@ const INTERRUPTED_IDEMPOTENT = "[INTERRUPTED] The previous run ended before this
10
10
  "what you intended. If you decide not to re-issue it, verify the current state first.";
11
11
  const INTERRUPTED_NEVER_STARTED = "[INTERRUPTED] The run was aborted before this tool call started. It was never executed and had " +
12
12
  "no side effects — it is safe to re-issue this call if you still need it.";
13
+ const INTERRUPTED_REPEAT_UNKNOWN = "[INTERRUPTED] Same as an earlier interrupted call in this batch: no result was recorded and the outcome is " +
14
+ "UNKNOWN — verify before relying on it.";
15
+ const INTERRUPTED_REPEAT_SAFE = "[INTERRUPTED] Same as an earlier interrupted call in this batch: this tool is read-only/idempotent, so just " +
16
+ "call it again if you still need the result.";
17
+ const INTERRUPTED_REPEAT_IDEMPOTENT = "[INTERRUPTED] Same as an earlier interrupted call in this batch: it is safe to REPLAY — re-issue it, or verify " +
18
+ "the current state if you decide not to.";
19
+ const INTERRUPTED_REPEAT_NEVER_STARTED = "[INTERRUPTED] Same as an earlier interrupted call in this batch: it never started and had no side effects — " +
20
+ "safe to re-issue.";
21
+ const REPEAT_TEXT = {
22
+ never_started: INTERRUPTED_REPEAT_NEVER_STARTED,
23
+ read: INTERRUPTED_REPEAT_SAFE,
24
+ idempotent: INTERRUPTED_REPEAT_IDEMPOTENT,
25
+ unknown: INTERRUPTED_REPEAT_UNKNOWN,
26
+ };
13
27
  export function findOrphanToolCalls(messages, suspendedBatch) {
14
28
  const resolvedAt = new Map();
15
29
  const callSitesAt = new Map();
@@ -70,11 +84,15 @@ export async function reconcileInterruptedSession(session, toolEffects, suspende
70
84
  const { messages } = await session.buildContext();
71
85
  const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
72
86
  const recovered = [];
87
+ const statedInFull = new Set();
73
88
  for (const orphan of orphans) {
74
89
  const effect = toolEffects?.get(orphan.toolName) ?? "write";
75
90
  const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
76
91
  const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(orphan.toolCallId);
77
- const text = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
92
+ const fullText = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
93
+ const cls = neverStarted ? "never_started" : effect === "read" ? "read" : effect === "idempotent" ? "idempotent" : "unknown";
94
+ const text = statedInFull.has(cls) ? REPEAT_TEXT[cls] : fullText;
95
+ statedInFull.add(cls);
78
96
  const errorKind = neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown";
79
97
  const entryId = await session.appendMessage({
80
98
  role: "toolResult",
@@ -982,6 +982,7 @@ export async function reviveBackgroundAgentLane(core, id, access, abort) {
982
982
  handle.errorRetryAfterMs = undefined;
983
983
  handle.resultIsPartial = undefined;
984
984
  handle.stopSource = undefined;
985
+ handle.stoppedBy = undefined;
985
986
  handle.completionId = undefined;
986
987
  handle.terminalNotified = undefined;
987
988
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
@@ -8,7 +8,7 @@ export interface ToolCallRequest {
8
8
  suspendCount: number;
9
9
  };
10
10
  }
11
- export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier";
11
+ export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier" | "persisted_rule";
12
12
  export type PermissionResult = {
13
13
  action: "allow";
14
14
  updatedInput?: unknown;
@@ -43,6 +43,12 @@ export declare function toolPolicyNameSets(p: ToolPolicy | undefined): readonly
43
43
  export declare function createAllowDenyPolicy(opts: {
44
44
  allow?: string[];
45
45
  deny?: string[];
46
+ onInvalidName?: "throw" | "skip";
47
+ onInvalidNameIssue?: (issue: {
48
+ entry: string;
49
+ list: "allow" | "deny";
50
+ message: string;
51
+ }) => void;
46
52
  }): NamedToolPolicy;
47
53
  export declare function createApprovalPolicy(opts: {
48
54
  requireApproval: string[];
@@ -81,6 +87,7 @@ export interface AskRequest {
81
87
  args: unknown;
82
88
  readonly preview?: unknown;
83
89
  readonly boundInputHash?: string;
90
+ readonly ruleSuggestions?: readonly import("./permission-rule-model.js").RuleSuggestion[];
84
91
  message: string;
85
92
  readonly principal?: string;
86
93
  readonly sourceTaskId?: string;
@@ -100,6 +107,15 @@ export type AskOutcome = boolean | "unavailable" | {
100
107
  };
101
108
  export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDelegationProvenance): OnAsk;
102
109
  export declare function askApproverIdentity(onAsk: unknown): unknown;
110
+ export declare function tryCloneArgs<T>(v: T): {
111
+ ok: true;
112
+ value: T;
113
+ } | {
114
+ ok: false;
115
+ reason: string;
116
+ cause?: unknown;
117
+ };
118
+ export declare function describeThrown(err: unknown): string;
103
119
  export type ResolvedAsk = PermissionResult & {
104
120
  approverUnavailable?: true;
105
121
  presentedInput?: unknown;
@@ -2,6 +2,8 @@ import { homedir } from "node:os";
2
2
  import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
4
  import { boundInputHashOf } from "./canonical-json.js";
5
+ import { inlineUntrusted } from "./untrusted-text.js";
6
+ import { parsePermissionRule } from "./permission-rules.js";
5
7
  import { writeTargetPath } from "../tools/fs/safety.js";
6
8
  export function decisionText(d) {
7
9
  return d.message;
@@ -43,6 +45,41 @@ export function toolPolicyNameSets(p) {
43
45
  return Array.isArray(sets) ? sets : [];
44
46
  }
45
47
  export function createAllowDenyPolicy(opts) {
48
+ const invalid = [];
49
+ const screen = (entries, list) => {
50
+ if (entries === undefined)
51
+ return undefined;
52
+ const kept = [];
53
+ for (const entry of entries) {
54
+ if (parsePermissionRule(entry).ruleContent === undefined) {
55
+ kept.push(entry);
56
+ continue;
57
+ }
58
+ invalid.push({
59
+ entry,
60
+ list,
61
+ message: `"${entry}" is a rule CONTENT form, not a tool name — a name set matches raw tool names, so this entry ` +
62
+ `can never match any mounted tool (in an allow list it removes the tool entirely). Use the tool NAME here, ` +
63
+ `and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
64
+ `createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`,
65
+ });
66
+ }
67
+ return kept;
68
+ };
69
+ const screenedAllow = screen(opts.allow, "allow");
70
+ const screenedDeny = screen(opts.deny, "deny");
71
+ if (invalid.length > 0) {
72
+ if ((opts.onInvalidName ?? "throw") === "throw") {
73
+ const e = new Error(`createAllowDenyPolicy: ${invalid.length} entr${invalid.length === 1 ? "y is" : "ies are"} not tool name(s):\n` +
74
+ invalid.map((i) => ` [${i.list}] ${i.message}`).join("\n"));
75
+ e.code = "config.invalid_tool_name_set";
76
+ e.issues = invalid;
77
+ throw e;
78
+ }
79
+ for (const issue of invalid)
80
+ opts.onInvalidNameIssue?.(issue);
81
+ }
82
+ opts = { ...opts, ...(screenedAllow ? { allow: screenedAllow } : {}), ...(screenedDeny ? { deny: screenedDeny } : {}) };
46
83
  const allow = opts.allow ? new Set(opts.allow) : undefined;
47
84
  const deny = new Set(opts.deny ?? []);
48
85
  return {
@@ -90,7 +127,7 @@ export function createApprovalPolicy(opts) {
90
127
  catch (err) {
91
128
  return {
92
129
  action: "deny",
93
- message: `approval errored for "${req.toolName}": ${err instanceof Error ? err.message : String(err)}`,
130
+ message: `approval errored for "${req.toolName}": ${describeThrown(err)}`,
94
131
  };
95
132
  }
96
133
  const okRaw = ok;
@@ -528,17 +565,36 @@ export function withDelegationProvenance(onAsk, delegation) {
528
565
  export function askApproverIdentity(onAsk) {
529
566
  return typeof onAsk === "function" ? (delegatedApproverRoot.get(onAsk) ?? onAsk) : onAsk;
530
567
  }
531
- function tryCloneArgs(v) {
568
+ export function tryCloneArgs(v) {
532
569
  try {
533
570
  const value = structuredClone(v);
534
571
  if (containsSharedMemory(value))
535
- return { ok: false };
572
+ return { ok: false, reason: "the value carries shared memory (SharedArrayBuffer), which a clone still aliases" };
536
573
  return { ok: true, value };
537
574
  }
575
+ catch (err) {
576
+ return { ok: false, reason: describeThrown(err), cause: err };
577
+ }
578
+ }
579
+ export function describeThrown(err) {
580
+ try {
581
+ if (typeof err === "string")
582
+ return containThrownText(err);
583
+ if (err instanceof Error) {
584
+ const message = err.message;
585
+ if (typeof message === "string" && message !== "")
586
+ return containThrownText(message);
587
+ }
588
+ return `a non-Error value was thrown (${err === null ? "null" : typeof err})`;
589
+ }
538
590
  catch {
539
- return { ok: false };
591
+ return "the value could not be described";
540
592
  }
541
593
  }
594
+ const THROWN_TEXT_MAX = 300;
595
+ function containThrownText(text) {
596
+ return inlineUntrusted(text, THROWN_TEXT_MAX);
597
+ }
542
598
  function containsSharedMemory(v, seen = new Set()) {
543
599
  if (typeof SharedArrayBuffer !== "undefined" && v instanceof SharedArrayBuffer)
544
600
  return true;
@@ -593,7 +649,7 @@ export async function resolveAsk(req, onAsk, signal) {
593
649
  if (!presented.ok) {
594
650
  return {
595
651
  action: "deny",
596
- message: `approval for "${req.toolName}" could not present the args safely (unclonable value) — denied fail-closed`,
652
+ message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${presented.reason}) — denied fail-closed`,
597
653
  decisionReason: "mode",
598
654
  };
599
655
  }
@@ -603,7 +659,7 @@ export async function resolveAsk(req, onAsk, signal) {
603
659
  if (!approverView.ok) {
604
660
  return {
605
661
  action: "deny",
606
- message: `approval for "${req.toolName}" could not present the args safely (unclonable value) — denied fail-closed`,
662
+ message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${approverView.reason}) — denied fail-closed`,
607
663
  decisionReason: "mode",
608
664
  };
609
665
  }
@@ -612,7 +668,7 @@ export async function resolveAsk(req, onAsk, signal) {
612
668
  catch (err) {
613
669
  return {
614
670
  action: "deny",
615
- message: `approval errored for "${req.toolName}": ${err instanceof Error ? err.message : String(err)}`,
671
+ message: `approval errored for "${req.toolName}": ${describeThrown(err)}`,
616
672
  decisionReason: "mode",
617
673
  };
618
674
  }
@@ -635,7 +691,7 @@ export async function resolveAsk(req, onAsk, signal) {
635
691
  if (!edit.ok) {
636
692
  return {
637
693
  action: "deny",
638
- message: `the approved edit for "${req.toolName}" is not safely clonable — denied fail-closed`,
694
+ message: `the approved edit for "${req.toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`,
639
695
  decisionReason: "mode",
640
696
  };
641
697
  }
@@ -45,6 +45,7 @@ export function defineTool(spec, options) {
45
45
  executionMode,
46
46
  ...(spec.isConcurrencySafe ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
47
47
  ...(spec.effect ? { effect: spec.effect } : {}),
48
+ ...(spec.contentOrigin ? { contentOrigin: spec.contentOrigin } : {}),
48
49
  ...(spec.prepareArguments ? { prepareArguments: spec.prepareArguments } : {}),
49
50
  ...(spec.approvalPreview ? { approvalPreview: spec.approvalPreview } : {}),
50
51
  execute: async (toolCallId, rawParams, signal) => {
@@ -181,6 +181,20 @@ export type TraceEvent = {
181
181
  site: string;
182
182
  message: string;
183
183
  ts: number;
184
+ } | {
185
+ kind: "permission.persisted_rule_allowed";
186
+ version: 1;
187
+ taskId: string;
188
+ toolName: string;
189
+ toolCallId: string;
190
+ rule: string;
191
+ ts: number;
192
+ } | {
193
+ kind: "permission.rule_store_unreadable";
194
+ version: 1;
195
+ taskId: string;
196
+ message: string;
197
+ ts: number;
184
198
  } | {
185
199
  kind: "brain.failover";
186
200
  version: 1;
@@ -217,6 +231,12 @@ export type TraceEvent = {
217
231
  estTokens: number;
218
232
  floor: number;
219
233
  ts: number;
234
+ } | {
235
+ kind: "compaction.unevaluable";
236
+ version: 1;
237
+ taskId: string;
238
+ estTokens: number;
239
+ ts: number;
220
240
  } | {
221
241
  kind: "compaction.blocked";
222
242
  version: 1;
@@ -20,6 +20,7 @@ export interface Brain {
20
20
  complete?: CompleteSimpleFn;
21
21
  }
22
22
  export type ToolEffect = "read" | "write" | "idempotent";
23
+ export type ToolContentOrigin = "external" | "execution" | "local";
23
24
  export interface ToolSpec<TParams extends TSchema = TSchema> {
24
25
  name: string;
25
26
  aliases?: string[];
@@ -30,6 +31,17 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
30
31
  name: string;
31
32
  description: string;
32
33
  }>;
34
+ agentToolFaces?: ReadonlyArray<{
35
+ name: string;
36
+ allowTools?: readonly string[];
37
+ denyTools?: readonly string[];
38
+ canRedelegate?: boolean;
39
+ }>;
40
+ agentToolPool?: ReadonlyArray<{
41
+ name: string;
42
+ aliases?: readonly string[];
43
+ contentOrigin?: ToolContentOrigin;
44
+ }>;
33
45
  approvalPreview?: (args: unknown) => unknown;
34
46
  agentModels?: readonly string[];
35
47
  withAgents?: (agents: ReadonlyArray<AgentDefinition>) => ToolSpec;
@@ -42,6 +54,7 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
42
54
  } | Promise<{
43
55
  reversible: boolean;
44
56
  }>;
57
+ contentOrigin?: ToolContentOrigin;
45
58
  executionMode?: "sequential" | "parallel";
46
59
  isConcurrencySafe?: (args: unknown) => boolean;
47
60
  parameters: TParams;
@@ -174,6 +187,7 @@ export interface HandsBandOptions {
174
187
  readImageDownsampler?: ((input: Buffer, mimeType: string) => Promise<import("./mcp.js").DownsampledImage | undefined>) | false;
175
188
  beforeWrite?: BeforeWriteHook;
176
189
  autoBackgroundOnTimeout?: boolean;
190
+ readCyberReminder?: boolean;
177
191
  }
178
192
  export interface AgentDefinition {
179
193
  name: string;
@@ -771,6 +785,7 @@ export interface RunnerDeps {
771
785
  checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
772
786
  fileSnapshotStore?: import("./file-snapshot-store.js").FileSnapshotStore;
773
787
  sessionPolicyStore?: import("./session-policy-store.js").SessionPolicyStore;
788
+ permissionRuleStore?: import("./permission-rule-store.js").PermissionRuleStoreProvider;
774
789
  runtimeCapsResolver?: (principal: string | undefined) => RuntimeCaps | undefined | Promise<RuntimeCaps | undefined>;
775
790
  lockedConfig?: import("./locked-config.js").LockedConfig;
776
791
  compliancePostureResolver?: (principal: string | undefined) => import("./compliance.js").CompliancePosture | undefined | Promise<import("./compliance.js").CompliancePosture | undefined>;
@@ -43,6 +43,9 @@ export interface WiringManifest {
43
43
  backgroundAgentStore: boolean;
44
44
  hostChildEventSink: boolean;
45
45
  };
46
+ permissionRules: {
47
+ storeWired: boolean;
48
+ };
46
49
  governance: {
47
50
  audience: "operator";
48
51
  lockedConfig: boolean;
@@ -71,13 +74,14 @@ export interface WiringFacts {
71
74
  checkpointDurability?: StoreDurability;
72
75
  sessionDurability: StoreDurability;
73
76
  backgroundAgentStoreWired: boolean;
77
+ permissionRuleStoreWired: boolean;
74
78
  hostChildEventSinkWired: boolean;
75
79
  lockedConfigWired: boolean;
76
80
  complianceWired: boolean;
77
81
  memoryAdmissionWired: boolean;
78
82
  retentionPolicyWired: boolean;
79
83
  }
80
- export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy">;
84
+ export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore">;
81
85
  export type StaticWiringSpec = Pick<TaskSpec, "onAsk" | "onQuestion" | "checkpointStore" | "durableApproval" | "mcp" | "interactiveTools" | "interactionPosture">;
82
86
  export declare function resolveDeclaredDurability(store: {
83
87
  readonly durability?: StoreDurability;
@@ -19,9 +19,9 @@ export function deriveAskEffective(form, parkEffective) {
19
19
  case "callback":
20
20
  return "human_reachable";
21
21
  case "allow":
22
- return "auto_allow";
22
+ return parkEffective === true ? "park_only" : parkEffective === "unresolved" ? "unresolved" : "auto_allow";
23
23
  case "deny":
24
- return "auto_deny";
24
+ return parkEffective === true ? "park_only" : parkEffective === "unresolved" ? "unresolved" : "auto_deny";
25
25
  case "absent":
26
26
  return parkEffective === true ? "park_only" : parkEffective === "unresolved" ? "unresolved" : "auto_deny";
27
27
  default: {
@@ -86,6 +86,7 @@ export function deriveWiringManifest(facts) {
86
86
  parkLane,
87
87
  session: { store: manifestDurabilityOf(facts.sessionDurability) },
88
88
  fleet: { backgroundAgentStore: facts.backgroundAgentStoreWired, hostChildEventSink: facts.hostChildEventSinkWired },
89
+ permissionRules: { storeWired: facts.permissionRuleStoreWired },
89
90
  governance: {
90
91
  audience: "operator",
91
92
  lockedConfig: facts.lockedConfigWired,
@@ -177,6 +178,7 @@ export function describeStaticWiring(deps, spec = {}) {
177
178
  ...(capable ? { checkpointDurability: resolveDeclaredDurability(checkpointStore, "checkpointStore") } : {}),
178
179
  sessionDurability: resolveDeclaredDurability(deps.sessionStore, "sessionStore"),
179
180
  backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
181
+ permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
180
182
  hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
181
183
  lockedConfigWired: deps.lockedConfig !== undefined,
182
184
  complianceWired: deps.compliancePostureResolver !== undefined,
@@ -1,5 +1,6 @@
1
1
  import { Compile } from "typebox/compile";
2
2
  import { Value } from "typebox/value";
3
+ import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
3
4
  const validatorCache = new WeakMap();
4
5
  const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
5
6
  function isRecord(value) {
@@ -253,6 +254,123 @@ function formatValidationPath(error) {
253
254
  const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
254
255
  return path || "root";
255
256
  }
257
+ const INSTANCE_PATH_MAX_BRANCHES = 256;
258
+ function resolveInstancePath(root, instancePath) {
259
+ if (instancePath === "")
260
+ return { found: true, value: root, segments: [] };
261
+ const body = instancePath.startsWith("/") ? instancePath.slice(1) : instancePath;
262
+ const found = [];
263
+ let budget = INSTANCE_PATH_MAX_BRANCHES;
264
+ const walk = (cursor, remaining, segments) => {
265
+ if (found.length > 1 || budget <= 0)
266
+ return;
267
+ if (cursor === null || typeof cursor !== "object")
268
+ return;
269
+ const container = cursor;
270
+ for (let cut = remaining.indexOf("/");; cut = remaining.indexOf("/", cut + 1)) {
271
+ const key = cut === -1 ? remaining : remaining.slice(0, cut);
272
+ if (budget <= 0)
273
+ return;
274
+ budget--;
275
+ if (Object.prototype.hasOwnProperty.call(container, key)) {
276
+ if (cut === -1)
277
+ found.push({ value: container[key], segments: [...segments, key] });
278
+ else
279
+ walk(container[key], remaining.slice(key.length + 1), [...segments, key]);
280
+ if (found.length > 1)
281
+ return;
282
+ }
283
+ if (cut === -1)
284
+ return;
285
+ }
286
+ };
287
+ walk(root, body, []);
288
+ if (found.length !== 1 || budget <= 0)
289
+ return { found: false };
290
+ return { found: true, value: found[0].value, segments: found[0].segments };
291
+ }
292
+ function accessorFrom(segments, property) {
293
+ const all = property === undefined ? [...segments] : [...segments, property];
294
+ return all.join(".");
295
+ }
296
+ function jsonTypeNameOf(value) {
297
+ if (value === null)
298
+ return "null";
299
+ if (value === undefined)
300
+ return "undefined";
301
+ if (Array.isArray(value))
302
+ return "array";
303
+ const t = typeof value;
304
+ return t === "object" ? "object" : t;
305
+ }
306
+ function synthesizeValidationIssues(errors, checkedValue) {
307
+ const sentences = [];
308
+ const rest = [];
309
+ for (const error of errors) {
310
+ const resolved = resolveInstancePath(checkedValue, error.instancePath);
311
+ const segments = resolved.found ? resolved.segments : undefined;
312
+ if (segments !== undefined && error.keyword === "required") {
313
+ const properties = error.params.requiredProperties ?? [];
314
+ for (const property of properties) {
315
+ sentences.push(`The required parameter \`${accessorFrom(segments, property)}\` is missing`);
316
+ }
317
+ continue;
318
+ }
319
+ if (segments !== undefined && error.keyword === "additionalProperties") {
320
+ const properties = error.params.additionalProperties ?? [];
321
+ for (const property of properties) {
322
+ sentences.push(`An unexpected parameter \`${accessorFrom(segments, property)}\` was provided`);
323
+ }
324
+ continue;
325
+ }
326
+ if (segments !== undefined && error.keyword === "type" && resolved.found) {
327
+ const expected = error.params.type;
328
+ const expectedText = Array.isArray(expected) ? expected.join(" | ") : (expected ?? "the declared type");
329
+ const provided = jsonTypeNameOf(resolved.value);
330
+ const accessor = accessorFrom(segments) || "root";
331
+ sentences.push(`The parameter \`${accessor}\` type is expected as \`${expectedText}\` but provided as \`${provided}\``);
332
+ continue;
333
+ }
334
+ rest.push(` - ${formatValidationPath(error)}: ${error.message}`);
335
+ }
336
+ let restText = "";
337
+ if (rest.length > 0) {
338
+ const marker = (n) => ` … ${n} more issue${n === 1 ? "" : "s"} omitted`;
339
+ const kept = [];
340
+ let used = 0;
341
+ for (const line of rest) {
342
+ const cost = kept.length === 0 ? line.length : line.length + 1;
343
+ if (used + cost > VALIDATION_ERROR_ISSUES_MAX_CHARS)
344
+ break;
345
+ kept.push(line);
346
+ used += cost;
347
+ }
348
+ if (kept.length === rest.length) {
349
+ restText = kept.join("\n");
350
+ }
351
+ else {
352
+ while (kept.length > 0 && used + marker(rest.length - kept.length).length + 1 > VALIDATION_ERROR_ISSUES_MAX_CHARS) {
353
+ const removed = kept.pop();
354
+ used -= kept.length === 0 ? removed.length : removed.length + 1;
355
+ }
356
+ if (kept.length === 0) {
357
+ const clip = "… (issue truncated)";
358
+ const others = rest.length - 1;
359
+ const markerCost = others > 0 ? marker(others).length + 1 : 0;
360
+ const room = VALIDATION_ERROR_ISSUES_MAX_CHARS - markerCost - clip.length;
361
+ if (room > 0) {
362
+ kept.push(`${sliceHeadSafe(rest[0], room)}${clip}`);
363
+ }
364
+ }
365
+ const omitted = rest.length - kept.length;
366
+ if (omitted > 0)
367
+ kept.push(marker(omitted));
368
+ restText = kept.join("\n");
369
+ }
370
+ }
371
+ const all = [...sentences, ...(restText ? [restText] : [])];
372
+ return all.join("\n") || "Unknown validation error";
373
+ }
256
374
  export function validateToolCall(tools, toolCall) {
257
375
  const tool = findToolByName(tools, toolCall.name);
258
376
  if (!tool) {
@@ -284,10 +402,7 @@ export function validateToolArguments(tool, toolCall) {
284
402
  if (validator.Check(args)) {
285
403
  return args;
286
404
  }
287
- const errors = validator
288
- .Errors(args)
289
- .map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
290
- .join("\n") || "Unknown validation error";
405
+ const errors = synthesizeValidationIssues([...validator.Errors(args)], args);
291
406
  const schemaJson = (() => {
292
407
  try {
293
408
  const s = JSON.stringify(tool.parameters);
@@ -297,6 +412,7 @@ export function validateToolArguments(tool, toolCall) {
297
412
  return "(schema not serializable)";
298
413
  }
299
414
  })();
300
- throw new Error(`Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}\n\nExpected parameter schema:\n${schemaJson}`);
415
+ throw new Error(`Validation failed for tool "${toolCall.name}":\n${errors}\n\nExpected parameter schema:\n${schemaJson}`);
301
416
  }
302
417
  const VALIDATION_ERROR_SCHEMA_MAX_CHARS = 4_000;
418
+ const VALIDATION_ERROR_ISSUES_MAX_CHARS = 2_000;
@@ -12,3 +12,5 @@ export type LoopStep = {
12
12
  };
13
13
  export type LoopTraceSink = (step: LoopStep) => void;
14
14
  export declare function runAgentLoop(prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, emit: AgentEventSink, signal?: AbortSignal, streamFn?: StreamFn, runtime?: AgentCoreStreamRuntimeDeps, trace?: LoopTraceSink): Promise<AgentMessage[]>;
15
+ export declare const ROSTER_LISTING_MAX = 25;
16
+ export declare const ROSTER_SEARCH_HINT_NAME = "ToolSearch";
@@ -1,4 +1,5 @@
1
1
  import { findToolByName, validateToolArguments } from "../llm/index.js";
2
+ import { truncateError } from "../../core/tool-errors.js";
2
3
  import { resolveAgentCoreStreamFn } from "./runtime-deps.js";
3
4
  function appendTextDeltaToAssistantMessage(message, contentIndex, delta) {
4
5
  const content = [...message.content];
@@ -831,14 +832,26 @@ function prepareToolCallArguments(tool, toolCall) {
831
832
  arguments: preparedArguments,
832
833
  };
833
834
  }
835
+ export const ROSTER_LISTING_MAX = 25;
836
+ export const ROSTER_SEARCH_HINT_NAME = "ToolSearch";
837
+ function formatRosterRecovery(availableTools) {
838
+ if (availableTools.length === 0)
839
+ return "";
840
+ const shown = availableTools.slice(0, ROSTER_LISTING_MAX);
841
+ const withheld = availableTools.length - shown.length;
842
+ const listing = withheld > 0 ? `${shown.join(", ")} … and ${withheld} more` : shown.join(", ");
843
+ const hint = availableTools.includes(ROSTER_SEARCH_HINT_NAME)
844
+ ? `. Use ${ROSTER_SEARCH_HINT_NAME} to look up a tool by name.`
845
+ : "";
846
+ return ` Available tools: ${listing}${hint}`;
847
+ }
834
848
  async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
835
849
  const tool = findToolByName(currentContext.tools, toolCall.name);
836
850
  if (!tool) {
837
851
  const availableTools = (currentContext.tools ?? []).map((t) => t.name);
838
- const available = availableTools.join(", ");
839
852
  return {
840
853
  kind: "immediate",
841
- result: createErrorToolResult(`Tool ${toolCall.name} not found.${available ? ` Available tools: ${available}` : ""}`, { details: { code: "tool.not_found", toolName: toolCall.name, availableTools } }),
854
+ result: createErrorToolResult(`Tool ${toolCall.name} not found.${formatRosterRecovery(availableTools)}`, { details: { code: "tool.not_found", toolName: toolCall.name, availableTools } }),
842
855
  isError: true,
843
856
  };
844
857
  }
@@ -1043,7 +1056,7 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
1043
1056
  }
1044
1057
  }
1045
1058
  catch (error) {
1046
- const note = `[post-tool processing failed (the tool already executed): ${error instanceof Error ? error.message : String(error)}]`;
1059
+ const note = truncateError(`[post-tool processing failed (the tool already executed): ${error instanceof Error ? error.message : String(error)}]`);
1047
1060
  result = { ...result, content: [...result.content, { type: "text", text: note }] };
1048
1061
  }
1049
1062
  if (deliveryMark !== undefined && readDeliveryFailureMark(result.details) === undefined) {
@@ -1095,7 +1108,7 @@ function createErrorToolResult(message, source) {
1095
1108
  }
1096
1109
  }
1097
1110
  return {
1098
- content: [{ type: "text", text: message }],
1111
+ content: [{ type: "text", text: truncateError(message) }],
1099
1112
  details,
1100
1113
  };
1101
1114
  }