@sema-agent/core 7.6.3 → 7.8.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 (164) hide show
  1. package/CHANGELOG.md +70 -10
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/peer-admission.d.ts +1 -1
  6. package/dist/agents/send-message-tool.js +2 -1
  7. package/dist/agents/subagent-steps.js +5 -4
  8. package/dist/agents/subagent.js +2 -3
  9. package/dist/brain/anthropic.js +8 -2
  10. package/dist/brain/open-responses.js +5 -3
  11. package/dist/brain/openai.js +31 -8
  12. package/dist/brain/reasoning.d.ts +32 -0
  13. package/dist/brain/reasoning.js +18 -0
  14. package/dist/core/ask-question.js +2 -2
  15. package/dist/core/auto-mode-defaults.d.ts +16 -0
  16. package/dist/core/auto-mode-defaults.js +1 -0
  17. package/dist/core/auto-mode.d.ts +19 -0
  18. package/dist/core/auto-mode.js +74 -56
  19. package/dist/core/checkpoint-execution-record.d.ts +110 -0
  20. package/dist/core/checkpoint-execution-record.js +49 -0
  21. package/dist/core/checkpoint-store.d.ts +114 -2
  22. package/dist/core/checkpoint-store.js +35 -2
  23. package/dist/core/context-edit.d.ts +0 -22
  24. package/dist/core/context-edit.js +2 -11
  25. package/dist/core/engine-notice.d.ts +17 -0
  26. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  27. package/dist/core/fs-write-gate-policy.js +7 -7
  28. package/dist/core/gate-fold.js +2 -2
  29. package/dist/core/gate-lanes.js +4 -3
  30. package/dist/core/governance-codes.d.ts +1 -1
  31. package/dist/core/governance-codes.js +8 -0
  32. package/dist/core/hooks.d.ts +8 -2
  33. package/dist/core/hooks.js +1 -1
  34. package/dist/core/lsp.js +2 -2
  35. package/dist/core/mcp-server-spec.d.ts +17 -0
  36. package/dist/core/mcp.js +3 -1
  37. package/dist/core/memory-engine/tools.js +4 -12
  38. package/dist/core/permission-rules.d.ts +32 -2
  39. package/dist/core/permission-rules.js +43 -37
  40. package/dist/core/present-plan-tool.js +3 -4
  41. package/dist/core/runner/active-skill-scope.js +7 -4
  42. package/dist/core/runner/clock-and-limits.d.ts +117 -0
  43. package/dist/core/runner/clock-and-limits.js +118 -0
  44. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  45. package/dist/core/runner/contracts.d.ts +37 -3
  46. package/dist/core/runner/decide-continuation.d.ts +98 -0
  47. package/dist/core/runner/decide-continuation.js +133 -0
  48. package/dist/core/runner/execution-record.d.ts +26 -0
  49. package/dist/core/runner/execution-record.js +19 -0
  50. package/dist/core/runner/git-leg-delivery.d.ts +28 -0
  51. package/dist/core/runner/git-leg-delivery.js +94 -0
  52. package/dist/core/runner/initial-run-state.d.ts +14 -0
  53. package/dist/core/runner/initial-run-state.js +11 -0
  54. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  55. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  56. package/dist/core/runner/prepare-caps-and-workflow.js +33 -11
  57. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  58. package/dist/core/runner/prepare-defer-classify.js +10 -10
  59. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  60. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  61. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  62. package/dist/core/runner/prepare-gate-stations.js +3 -1
  63. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  64. package/dist/core/runner/prepare-hands-readface.js +3 -2
  65. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  66. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  67. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  68. package/dist/core/runner/prepare-lsp.js +3 -2
  69. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  70. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  71. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  72. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  73. package/dist/core/runner/prepare-park-ask.js +4 -1
  74. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  75. package/dist/core/runner/prepare-project-context.js +7 -5
  76. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  77. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  78. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  79. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  80. package/dist/core/runner/prepare-question-face.js +5 -3
  81. package/dist/core/runner/prepare-safety-scan.js +42 -2
  82. package/dist/core/runner/prepare-task.js +37 -19
  83. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  84. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  85. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  86. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  87. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  88. package/dist/core/runner/run-harness-handlers.js +345 -0
  89. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  90. package/dist/core/runner/run-turn-boundary.js +694 -0
  91. package/dist/core/runner/runtask.d.ts +0 -68
  92. package/dist/core/runner/runtask.js +66 -1486
  93. package/dist/core/runner/session-rule-policy.js +9 -4
  94. package/dist/core/runner/steer-admission.d.ts +17 -0
  95. package/dist/core/runner/steer-admission.js +17 -0
  96. package/dist/core/runner/synthetic-tools.js +5 -8
  97. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  98. package/dist/core/runner/tool-disclosure.js +9 -5
  99. package/dist/core/runner/tool-end-body.d.ts +71 -0
  100. package/dist/core/runner/tool-end-body.js +75 -0
  101. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  102. package/dist/core/runner/tool-face-overlay.js +125 -0
  103. package/dist/core/runner/tool-output-projection.d.ts +10 -0
  104. package/dist/core/runner/tool-output-projection.js +33 -10
  105. package/dist/core/sensitive-path-policy.d.ts +3 -0
  106. package/dist/core/sensitive-path-policy.js +6 -5
  107. package/dist/core/shared-memory/tools.js +3 -6
  108. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
  109. package/dist/core/store-contracts/checkpoint-store-contract.js +108 -0
  110. package/dist/core/task-event.d.ts +10 -0
  111. package/dist/core/task-registry.js +4 -5
  112. package/dist/core/task-spec.d.ts +8 -0
  113. package/dist/core/tool-catalog-entries.d.ts +48 -0
  114. package/dist/core/tool-catalog-entries.js +174 -0
  115. package/dist/core/tool-conformance.d.ts +22 -0
  116. package/dist/core/tool-conformance.js +37 -0
  117. package/dist/core/tool-face.d.ts +171 -0
  118. package/dist/core/tool-face.js +25 -0
  119. package/dist/core/tool-policy.d.ts +24 -0
  120. package/dist/core/tool-policy.js +4 -2
  121. package/dist/core/tool-registry.d.ts +114 -0
  122. package/dist/core/tool-registry.js +114 -0
  123. package/dist/core/tool-result-budget.d.ts +0 -3
  124. package/dist/core/tool-result-budget.js +4 -3
  125. package/dist/core/tool-result-store.d.ts +0 -18
  126. package/dist/core/tool-result-store.js +2 -18
  127. package/dist/core/tool-roster.d.ts +450 -0
  128. package/dist/core/tool-roster.js +497 -0
  129. package/dist/core/tool-spec.d.ts +54 -3
  130. package/dist/core/tools.js +7 -0
  131. package/dist/core/trace.d.ts +24 -0
  132. package/dist/core/wiring-manifest.d.ts +13 -0
  133. package/dist/core/wiring-manifest.js +2 -1
  134. package/dist/core/write-protect.d.ts +2 -1
  135. package/dist/core/write-protect.js +5 -4
  136. package/dist/engine/compaction/utils.d.ts +6 -0
  137. package/dist/engine/compaction/utils.js +19 -19
  138. package/dist/engine/loop/types.d.ts +6 -0
  139. package/dist/index.d.ts +20 -8
  140. package/dist/index.js +19 -7
  141. package/dist/orchestration/goal.js +2 -1
  142. package/dist/orchestration/run-spec.js +3 -5
  143. package/dist/orchestration/run-workflow-tool.js +2 -2
  144. package/dist/prompt-assembly/event-registry.js +3 -3
  145. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  146. package/dist/stores/file/checkpoint-store.d.ts +7 -0
  147. package/dist/stores/file/checkpoint-store.js +20 -1
  148. package/dist/tools/fs/fs-bash.d.ts +1 -1
  149. package/dist/tools/fs/fs-bash.js +7 -11
  150. package/dist/tools/fs/fs-read.js +2 -2
  151. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  152. package/dist/tools/fs/fs-search-tools.js +3 -16
  153. package/dist/tools/fs/fs-write.js +4 -6
  154. package/dist/tools/fs/repo-map.js +2 -2
  155. package/dist/tools/fs/safety.d.ts +2 -1
  156. package/dist/tools/fs/safety.js +6 -5
  157. package/dist/tools/monitor.js +2 -2
  158. package/dist/tools/scheduler-tools.js +5 -8
  159. package/dist/tools/task-list.js +5 -8
  160. package/dist/tools/todo.js +2 -2
  161. package/dist/tools/web.js +3 -6
  162. package/dist/tools/worktree.js +3 -4
  163. package/package.json +4 -1
  164. package/test/export-surface.snapshot.json +183 -7
@@ -0,0 +1,497 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Type } from "typebox";
3
+ import { getToolContract } from "../prompt-assembly/tool-catalog.js";
4
+ import { protocolOf } from "./protocol-table.js";
5
+ import { RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_KEYS_MAX, TOOL_KEY_MAX_CHARS, TOOL_MOUNT_TAGS, } from "./tool-face.js";
6
+ function lit(values) {
7
+ return Type.Union(values.map((v) => Type.Literal(v)));
8
+ }
9
+ const Name = Type.String({ minLength: 1, maxLength: 128 });
10
+ const Short = Type.String({ maxLength: 64 });
11
+ const Key = Type.String({ maxLength: TOOL_KEY_MAX_CHARS });
12
+ const Digest16 = Type.String({ minLength: 16, maxLength: 16 });
13
+ export const ROSTER_SOURCES = ["builtin", "caller", "host", "mcp", "a2a", "external", "synthetic"];
14
+ export const ToolRosterRenderHints = Type.Object({
15
+ userFacingName: Type.Optional(Type.String({ maxLength: RENDER_HINT_MAX_CHARS })),
16
+ activity: Type.Optional(Type.String({ maxLength: RENDER_HINT_ACTIVITY_MAX_CHARS })),
17
+ summaryParams: Type.Optional(Type.Array(Key, { maxItems: RENDER_HINT_MAX_LIST })),
18
+ resultCards: Type.Optional(Type.Array(Key, { maxItems: RENDER_HINT_MAX_LIST })),
19
+ approvalCard: Type.Optional(lit(TOOL_APPROVAL_CARDS)),
20
+ }, { additionalProperties: false });
21
+ export const ToolRosterEntry = Type.Object({
22
+ name: Name,
23
+ aliases: Type.Array(Name, { uniqueItems: true }),
24
+ source: lit(ROSTER_SOURCES),
25
+ origin: Type.Optional(Type.Object({ peer: Name, declaredBy: Type.Optional(lit(["center", "file", "request"])) }, { additionalProperties: false })),
26
+ contract: Type.Object({ contractId: Short, implementationRevision: Short }, { additionalProperties: false }),
27
+ shapeDigest: Digest16,
28
+ wireSchemaDigest: Digest16,
29
+ cardId: Type.String({ maxLength: 160 }),
30
+ capabilityId: Digest16,
31
+ inputKeys: Type.Array(Type.Object({ name: Key, type: lit(["string", "number", "integer", "boolean", "object", "array", "null", "mixed"]) }, { additionalProperties: false }), { maxItems: TOOL_KEYS_MAX }),
32
+ effect: lit(["read", "write", "idempotent"]),
33
+ effectProvenance: lit(["declared", "caller", "synthetic", "default"]),
34
+ offload: Type.Boolean(),
35
+ offloadThresholdChars: Type.Optional(Type.Integer({ minimum: 0 })),
36
+ compactable: Type.Boolean(),
37
+ budgetExempt: Type.Boolean(),
38
+ egress: Type.Boolean(),
39
+ egressProvenance: lit(["declared", "caller", "default"]),
40
+ irreversibility: Type.Optional(lit(["never", "maybe", "always"])),
41
+ reversibilityProbe: Type.Boolean(),
42
+ contentOrigin: lit(["local", "execution", "external"]),
43
+ contentOriginProvenance: lit(["declared", "server", "exempted", "default"]),
44
+ family: lit(TOOL_FAMILIES),
45
+ pathTarget: Type.Optional(Type.Object({ param: Key, aliases: Type.Array(Key), access: lit(["read", "create", "edit"]), skillScopeEligible: Type.Boolean() }, { additionalProperties: false })),
46
+ ruleFace: Type.Object({ primaryParams: Type.Array(Key), params: Type.Array(Key) }, { additionalProperties: false }),
47
+ approvalPreview: Type.Boolean(),
48
+ validateInput: Type.Boolean(),
49
+ deferred: Type.Boolean(),
50
+ alwaysLoad: Type.Boolean(),
51
+ modelGate: Type.Optional(Short),
52
+ renderHints: Type.Optional(ToolRosterRenderHints),
53
+ mountedBy: lit(TOOL_MOUNT_TAGS),
54
+ }, { additionalProperties: false });
55
+ export const ToolRoster = Type.Object({
56
+ schemaVersion: Type.Literal(1),
57
+ count: Type.Integer({ minimum: 0 }),
58
+ entries: Type.Array(ToolRosterEntry, { maxItems: 2048 }),
59
+ digest: Digest16,
60
+ }, { additionalProperties: false });
61
+ export const ToolRosterDelta = Type.Object({
62
+ fromDigest: Digest16,
63
+ roster: ToolRoster,
64
+ summary: Type.Object({ added: Type.Array(Name), removed: Type.Array(Name), changed: Type.Array(Name) }, { additionalProperties: false }),
65
+ }, { additionalProperties: false });
66
+ export function rosterEntryNamed(roster, toolName) {
67
+ return roster.entries.find((r) => r.name === toolName) ?? roster.entries.find((r) => r.aliases.includes(toolName));
68
+ }
69
+ export function toolCallFaceOf(roster, toolName) {
70
+ if (roster === undefined)
71
+ return undefined;
72
+ const e = rosterEntryNamed(roster, toolName);
73
+ if (e === undefined)
74
+ return undefined;
75
+ return {
76
+ family: e.family,
77
+ ...(e.pathTarget !== undefined ? { pathTarget: e.pathTarget } : {}),
78
+ capabilityId: e.capabilityId,
79
+ contractId: e.contract.contractId,
80
+ shapeDigest: e.shapeDigest,
81
+ implementationRevision: e.contract.implementationRevision,
82
+ };
83
+ }
84
+ export function parkedToolIdentity(roster, toolName) {
85
+ if (roster === undefined)
86
+ return {};
87
+ const e = rosterEntryNamed(roster, toolName);
88
+ if (e === undefined)
89
+ return {};
90
+ return { capabilityId: e.capabilityId, contractId: e.contract.contractId, shapeDigest: e.shapeDigest, implementationRevision: e.contract.implementationRevision, rosterDigest: roster.digest };
91
+ }
92
+ export function parkedToolFace(roster, toolName) {
93
+ if (roster === undefined)
94
+ return {};
95
+ const e = rosterEntryNamed(roster, toolName);
96
+ if (e === undefined)
97
+ return {};
98
+ return { family: e.family, ...(e.pathTarget !== undefined ? { pathTarget: e.pathTarget } : {}) };
99
+ }
100
+ export function parkedCallFaceOf(pending) {
101
+ if (pending.family === undefined || pending.capabilityId === undefined || pending.contractId === undefined || pending.shapeDigest === undefined || pending.implementationRevision === undefined)
102
+ return undefined;
103
+ return {
104
+ family: pending.family,
105
+ ...(pending.pathTarget !== undefined ? { pathTarget: pending.pathTarget } : {}),
106
+ capabilityId: pending.capabilityId,
107
+ contractId: pending.contractId,
108
+ shapeDigest: pending.shapeDigest,
109
+ implementationRevision: pending.implementationRevision,
110
+ };
111
+ }
112
+ export function callFaceSeat(face) {
113
+ return face !== undefined ? { face } : {};
114
+ }
115
+ export function toolIdentityOf(entry) {
116
+ return { capabilityId: entry.capabilityId, contractId: entry.contract.contractId, shapeDigest: entry.shapeDigest, implementationRevision: entry.contract.implementationRevision };
117
+ }
118
+ function canonical(node) {
119
+ if (Array.isArray(node))
120
+ return `[${node.map(canonical).join(",")}]`;
121
+ if (node === null || typeof node !== "object")
122
+ return JSON.stringify(node) ?? "null";
123
+ const keys = Object.keys(node).sort();
124
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(node[k])}`).join(",")}}`;
125
+ }
126
+ function sha16(text) {
127
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
128
+ }
129
+ export function mintCapabilityId(t) {
130
+ return sha16(canonical({ source: t.source, peer: t.peer, name: t.name, contractId: t.contractId }));
131
+ }
132
+ export function rosterDigestOf(entries) {
133
+ return sha16(canonical(entries));
134
+ }
135
+ const SCALAR_TYPES = new Set(["string", "number", "integer", "boolean"]);
136
+ function schemaTypeOf(node) {
137
+ if (node === null || typeof node !== "object")
138
+ return { type: "mixed", scalar: false };
139
+ const s = node;
140
+ const branches = s["anyOf"] ?? s["oneOf"];
141
+ if (Array.isArray(branches) && branches.length > 0) {
142
+ const parts = branches.map(schemaTypeOf);
143
+ const scalar = parts.every((p) => p.scalar);
144
+ const first = parts[0].type;
145
+ return parts.every((p) => p.type === first) ? { type: first, scalar } : { type: "mixed", scalar };
146
+ }
147
+ if (s["const"] !== undefined) {
148
+ const t = typeof s["const"];
149
+ return t === "string" || t === "number" || t === "boolean" ? { type: t, scalar: true } : { type: "mixed", scalar: false };
150
+ }
151
+ const en = s["enum"];
152
+ if (Array.isArray(en) && en.length > 0) {
153
+ const kinds = new Set(en.map((v) => typeof v));
154
+ if (kinds.size === 1) {
155
+ const t = [...kinds][0];
156
+ if (t === "string" || t === "number" || t === "boolean")
157
+ return { type: t, scalar: true };
158
+ }
159
+ return { type: "mixed", scalar: [...kinds].every((k) => k === "string" || k === "number" || k === "boolean") };
160
+ }
161
+ const t = s["type"];
162
+ if (typeof t === "string") {
163
+ if (SCALAR_TYPES.has(t))
164
+ return { type: t, scalar: true };
165
+ if (t === "object" || t === "array" || t === "null")
166
+ return { type: t, scalar: false };
167
+ }
168
+ if (Array.isArray(t))
169
+ return { type: "mixed", scalar: t.every((x) => typeof x === "string" && SCALAR_TYPES.has(x)) };
170
+ if (s["properties"] !== undefined)
171
+ return { type: "object", scalar: false };
172
+ if (s["items"] !== undefined)
173
+ return { type: "array", scalar: false };
174
+ return { type: "mixed", scalar: false };
175
+ }
176
+ export function inputKeysOf(schema) {
177
+ const keys = [];
178
+ const scalar = new Set();
179
+ const props = schema?.properties;
180
+ if (props !== null && typeof props === "object" && !Array.isArray(props)) {
181
+ for (const [name, sub] of Object.entries(props)) {
182
+ const t = schemaTypeOf(sub);
183
+ keys.push({ name, type: t.type });
184
+ if (t.scalar)
185
+ scalar.add(name);
186
+ }
187
+ }
188
+ return { keys, scalar };
189
+ }
190
+ export function schemaKeyBoundProblem(schema) {
191
+ const props = schema?.properties;
192
+ if (props === null || typeof props !== "object" || Array.isArray(props))
193
+ return undefined;
194
+ const names = Object.keys(props);
195
+ if (names.length > TOOL_KEYS_MAX)
196
+ return `the schema declares ${names.length} top-level properties; a roster row carries at most ${TOOL_KEYS_MAX}`;
197
+ const long = names.find((n) => n.length > TOOL_KEY_MAX_CHARS);
198
+ if (long !== undefined)
199
+ return `the schema property ${JSON.stringify(long.slice(0, 40) + "…")} is ${long.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`;
200
+ return undefined;
201
+ }
202
+ export function ruleFaceOfInstance(schema, declared, pathTarget) {
203
+ const { scalar } = inputKeysOf(schema);
204
+ const derived = new Set(scalar);
205
+ if (pathTarget !== undefined) {
206
+ derived.add(pathTarget.param);
207
+ for (const a of pathTarget.aliases ?? [])
208
+ derived.add(a);
209
+ }
210
+ const primary = declared?.primaryParams !== undefined ? [...declared.primaryParams] : pathTarget !== undefined ? [pathTarget.param, ...(pathTarget.aliases ?? [])] : [];
211
+ for (const p of primary)
212
+ derived.add(p);
213
+ const params = declared?.params !== undefined ? declared.params.filter((p) => derived.has(p)) : [...derived];
214
+ const ordered = [...new Set([...params, ...primary.filter((p) => params.includes(p) || declared?.params === undefined)])];
215
+ return { primaryParams: primary, params: ordered };
216
+ }
217
+ function capRenderHints(h) {
218
+ const out = {};
219
+ if (typeof h.userFacingName === "string")
220
+ out.userFacingName = h.userFacingName;
221
+ if (typeof h.activity === "string")
222
+ out.activity = h.activity;
223
+ if (Array.isArray(h.summaryParams))
224
+ out.summaryParams = h.summaryParams.filter((s) => typeof s === "string");
225
+ if (Array.isArray(h.resultCards))
226
+ out.resultCards = h.resultCards.filter((s) => typeof s === "string");
227
+ if (typeof h.approvalCard === "string" && TOOL_APPROVAL_CARDS.includes(h.approvalCard))
228
+ out.approvalCard = h.approvalCard;
229
+ return Object.keys(out).length > 0 ? out : undefined;
230
+ }
231
+ export const faceOf = (t) => t;
232
+ function configError(code, message) {
233
+ const e = new Error(message);
234
+ e.code = code;
235
+ return e;
236
+ }
237
+ export class RosterBuilder {
238
+ #tools = [];
239
+ #engineLane = [];
240
+ #meta = new WeakMap();
241
+ #effects;
242
+ constructor(effects) {
243
+ this.#effects = effects;
244
+ }
245
+ get tools() {
246
+ return this.#tools;
247
+ }
248
+ get length() {
249
+ return this.#tools.length;
250
+ }
251
+ metaOf(tool) {
252
+ return this.#meta.get(tool);
253
+ }
254
+ mount(tool, meta) {
255
+ this.#admit(tool, meta);
256
+ this.#tools.push(tool);
257
+ }
258
+ mountEngineLane(tool, meta) {
259
+ this.#admit(tool, meta);
260
+ this.#engineLane.push(tool);
261
+ }
262
+ #admit(tool, meta) {
263
+ if ((meta.source === "mcp" || meta.source === "a2a" || meta.source === "external") !== (meta.origin !== undefined)) {
264
+ throw configError("config.tool_roster_origin", `tool ${JSON.stringify(tool.name)}: a ${meta.source} mount ${meta.origin === undefined ? "requires" : "must not carry"} an origin`);
265
+ }
266
+ if (!TOOL_MOUNT_TAGS.includes(meta.mountedBy))
267
+ throw configError("config.tool_roster_mount_tag", `tool ${JSON.stringify(tool.name)}: mount tag ${JSON.stringify(meta.mountedBy)} is outside the closed set`);
268
+ this.#meta.set(tool, meta);
269
+ const eff = faceOf(tool).effect;
270
+ if (eff !== undefined && !(meta.mountedBy === "scenario" && this.#effects.has(tool.name)))
271
+ this.#effects.set(tool.name, eff);
272
+ }
273
+ mountAll(tools, meta) {
274
+ for (const t of tools)
275
+ this.mount(t, meta);
276
+ }
277
+ unmountWhere(pred) {
278
+ const removed = [];
279
+ for (let i = this.#tools.length - 1; i >= 0; i--) {
280
+ const t = this.#tools[i];
281
+ if (pred(t, i)) {
282
+ this.#tools.splice(i, 1);
283
+ removed.unshift(t);
284
+ }
285
+ }
286
+ return removed;
287
+ }
288
+ replaceAt(index, tool) {
289
+ const prior = this.#tools[index];
290
+ if (prior === undefined)
291
+ throw new Error(`roster: no tool at index ${index}`);
292
+ const meta = this.#meta.get(prior);
293
+ this.#tools[index] = tool;
294
+ if (meta !== undefined)
295
+ this.#meta.set(tool, meta);
296
+ }
297
+ replaceDomain(prefix, fresh, meta) {
298
+ let anchor = -1;
299
+ for (let i = this.#tools.length - 1; i >= 0; i--) {
300
+ if (this.#tools[i].name.startsWith(prefix)) {
301
+ anchor = i;
302
+ this.#tools.splice(i, 1);
303
+ }
304
+ }
305
+ for (const t of fresh) {
306
+ this.#meta.set(t, meta);
307
+ const eff = faceOf(t).effect;
308
+ if (eff !== undefined)
309
+ this.#effects.set(t.name, eff);
310
+ }
311
+ if (anchor >= 0)
312
+ this.#tools.splice(anchor, 0, ...fresh);
313
+ else
314
+ this.#tools.push(...fresh);
315
+ return { anchor };
316
+ }
317
+ snapshot(facts) {
318
+ const all = [...this.#tools, ...this.#engineLane];
319
+ const winner = new Map();
320
+ for (const t of all)
321
+ winner.set(t.name, t);
322
+ const entries = [];
323
+ const emitted = new Set();
324
+ for (const t of all) {
325
+ if (emitted.has(t.name))
326
+ continue;
327
+ emitted.add(t.name);
328
+ const survivor = winner.get(t.name);
329
+ const meta = this.#meta.get(survivor) ?? { source: "caller", mountedBy: "scenario" };
330
+ entries.push(mintRosterEntry(survivor, meta, facts));
331
+ }
332
+ return { schemaVersion: 1, count: entries.length, entries, digest: rosterDigestOf(entries) };
333
+ }
334
+ }
335
+ export function callerMountSource(tool) {
336
+ return getToolContract(tool, "caller").origin === "core" ? "builtin" : "caller";
337
+ }
338
+ export function mintRosterEntry(tool, meta, facts) {
339
+ const face = faceOf(tool);
340
+ const fallbackOrigin = meta.source === "builtin" ? "core" : meta.source === "synthetic" ? "synthetic" : meta.source === "mcp" || meta.source === "a2a" ? meta.source : "caller";
341
+ const d = getToolContract(tool, fallbackOrigin);
342
+ const peer = meta.origin?.peer ?? null;
343
+ const protocolTool = peer !== null ? protocolOf(tool.name)?.parse(tool.name)?.tool ?? tool.name : tool.name;
344
+ const cardId = meta.source === "builtin" || meta.source === "synthetic"
345
+ ? `builtin:${tool.name}`
346
+ : meta.source === "mcp" || meta.source === "a2a"
347
+ ? `${meta.source}:${peer}/${protocolTool}`
348
+ : `${meta.source === "external" ? "external" : "caller"}:${d.contractId}`;
349
+ const { keys: inputKeys } = inputKeysOf(tool.parameters);
350
+ const declaredEffect = face.effect;
351
+ const foldedEffect = facts.effects.get(tool.name);
352
+ const effect = declaredEffect ?? foldedEffect ?? "write";
353
+ const effectProvenance = declaredEffect !== undefined ? (meta.source === "synthetic" ? "synthetic" : "declared") : foldedEffect !== undefined && (meta.source === "mcp" || meta.source === "a2a") ? "caller" : "default";
354
+ const egress = face.egress === true || facts.egress.has(tool.name);
355
+ const egressProvenance = face.egress === true ? "declared" : egress ? "caller" : "default";
356
+ const irreversibility = face.irreversibility ?? facts.irreversibilityTier.get(tool.name);
357
+ const reversibilityProbe = face.reversibilityProbe !== undefined || facts.reversibilityProbes.has(tool.name);
358
+ const contentOrigin = face.contentOrigin ?? (facts.trustedTools.has(tool.name) ? "local" : "external");
359
+ const contentOriginProvenance = face.contentOrigin !== undefined ? (meta.source === "mcp" || meta.source === "a2a" ? "server" : "declared") : facts.trustedTools.has(tool.name) ? "exempted" : "default";
360
+ const pathTarget = face.pathTarget;
361
+ const rf = ruleFaceOfInstance(tool.parameters, face.ruleFace, pathTarget);
362
+ const hints = face.renderHints !== undefined ? capRenderHints(face.renderHints) : undefined;
363
+ return {
364
+ name: tool.name,
365
+ aliases: [...new Set(tool.aliases ?? [])],
366
+ source: meta.source,
367
+ ...(meta.origin !== undefined ? { origin: { peer: meta.origin.peer, ...(meta.origin.declaredBy !== undefined ? { declaredBy: meta.origin.declaredBy } : {}) } } : {}),
368
+ contract: { contractId: d.contractId.slice(0, 64), implementationRevision: d.implementationRevision.slice(0, 64) },
369
+ shapeDigest: d.shapeDigest,
370
+ wireSchemaDigest: d.wireSchemaDigest,
371
+ cardId: cardId.slice(0, 160),
372
+ capabilityId: mintCapabilityId({ source: meta.source, peer, name: tool.name, contractId: d.contractId }),
373
+ inputKeys,
374
+ effect,
375
+ effectProvenance,
376
+ offload: face.offload !== false,
377
+ ...(typeof face.offloadThresholdChars === "number" && Number.isFinite(face.offloadThresholdChars) && face.offloadThresholdChars >= 0 ? { offloadThresholdChars: Math.floor(face.offloadThresholdChars) } : {}),
378
+ compactable: face.compactable === true,
379
+ budgetExempt: face.budgetExempt === true,
380
+ egress,
381
+ egressProvenance,
382
+ ...(irreversibility !== undefined ? { irreversibility } : {}),
383
+ reversibilityProbe,
384
+ contentOrigin,
385
+ contentOriginProvenance,
386
+ family: face.family !== undefined && TOOL_FAMILIES.includes(face.family) ? face.family : "other",
387
+ ...(pathTarget !== undefined
388
+ ? { pathTarget: { param: pathTarget.param, aliases: [...(pathTarget.aliases ?? [])], access: pathTarget.access, skillScopeEligible: pathTarget.skillScopeEligible === true } }
389
+ : {}),
390
+ ruleFace: rf,
391
+ approvalPreview: typeof tool.approvalPreview === "function",
392
+ validateInput: typeof tool.validateInput === "function",
393
+ deferred: facts.deferred.has(tool.name),
394
+ alwaysLoad: face.alwaysLoad === true || face.mcpAlwaysLoad === true,
395
+ ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate.slice(0, 64) } : {}),
396
+ ...(hints !== undefined ? { renderHints: hints } : {}),
397
+ mountedBy: meta.mountedBy,
398
+ };
399
+ }
400
+ export class ToolRosterDeltaSeat {
401
+ #current;
402
+ #snapshot;
403
+ #pending = [];
404
+ #sink;
405
+ arm(current, snapshot) {
406
+ this.#current = current;
407
+ this.#snapshot = snapshot;
408
+ }
409
+ get current() {
410
+ return this.#current;
411
+ }
412
+ republish() {
413
+ if (this.#current === undefined || this.#snapshot === undefined)
414
+ return undefined;
415
+ const next = this.#snapshot();
416
+ const delta = diffToolRoster(this.#current, next);
417
+ this.#current = next;
418
+ if (this.#sink !== undefined)
419
+ this.#sink(delta);
420
+ else {
421
+ this.#pending.push(delta);
422
+ if (this.#pending.length > 64)
423
+ this.#pending.shift();
424
+ }
425
+ return delta;
426
+ }
427
+ subscribe(sink) {
428
+ this.#sink = sink;
429
+ const pending = this.#pending;
430
+ this.#pending = [];
431
+ for (const d of pending)
432
+ sink(d);
433
+ }
434
+ drain() {
435
+ const pending = this.#pending;
436
+ this.#pending = [];
437
+ return pending;
438
+ }
439
+ }
440
+ export function ruleFacesFromRoster(roster) {
441
+ const byName = new Map(roster.entries.map((e) => [e.name, e]));
442
+ for (const e of roster.entries)
443
+ for (const a of e.aliases)
444
+ if (!byName.has(a))
445
+ byName.set(a, e);
446
+ return (toolName) => {
447
+ const e = byName.get(toolName);
448
+ if (e === undefined)
449
+ return undefined;
450
+ const params = new Set(e.ruleFace.params);
451
+ return {
452
+ primaryParams: e.ruleFace.primaryParams,
453
+ params: e.ruleFace.params,
454
+ nonScalar: e.inputKeys.filter((k) => !params.has(k.name) && (k.type === "object" || k.type === "array")).map((k) => k.name),
455
+ pathTargeting: e.pathTarget !== undefined,
456
+ };
457
+ };
458
+ }
459
+ export function diffToolRoster(prev, next) {
460
+ const before = new Map(prev.entries.map((e) => [e.name, canonical(e)]));
461
+ const after = new Map(next.entries.map((e) => [e.name, canonical(e)]));
462
+ const added = [];
463
+ const removed = [];
464
+ const changed = [];
465
+ for (const [name, row] of after) {
466
+ const old = before.get(name);
467
+ if (old === undefined)
468
+ added.push(name);
469
+ else if (old !== row)
470
+ changed.push(name);
471
+ }
472
+ for (const name of before.keys())
473
+ if (!after.has(name))
474
+ removed.push(name);
475
+ return { fromDigest: prev.digest, roster: next, summary: { added, removed, changed } };
476
+ }
477
+ export function judgeParkedToolIdentity(pending, row) {
478
+ const carried = pending.capabilityId !== undefined || pending.contractId !== undefined || pending.shapeDigest !== undefined || pending.implementationRevision !== undefined;
479
+ if (row === undefined)
480
+ return "absent";
481
+ if (!carried)
482
+ return "name_only";
483
+ const id = toolIdentityOf(row);
484
+ return pending.capabilityId === id.capabilityId && pending.contractId === id.contractId && pending.shapeDigest === id.shapeDigest && pending.implementationRevision === id.implementationRevision ? "match" : "mismatch";
485
+ }
486
+ export function offendingResultCard(hints, cardId, engineCards) {
487
+ for (const c of hints?.resultCards ?? []) {
488
+ if (typeof c !== "string")
489
+ continue;
490
+ if (c.startsWith(`${cardId}/`))
491
+ continue;
492
+ if (c.startsWith("builtin:") || engineCards.has(c) || !c.includes("/"))
493
+ return c;
494
+ return c;
495
+ }
496
+ return undefined;
497
+ }
@@ -14,6 +14,7 @@ import type { AgentDefinition } from "./agent-definition.js";
14
14
  import type { TaskEvent } from "./task-event.js";
15
15
  import type { TaskSpec } from "./task-spec.js";
16
16
  import type { BackgroundChildEvent } from "./delegation-frames.js";
17
+ import type { ToolFamily, ToolPathTarget, ToolRenderHints, ToolRuleFace } from "./tool-face.js";
17
18
  /**
18
19
  * Side-effect class of a tool. Used by wake/resume reconciliation: when a call was interrupted
19
20
  * before its result was recorded, the outcome is unknown, so core must decide whether re-doing it
@@ -403,9 +404,10 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
403
404
  * (`config.tool_model_gate_unknown_class`). Restore channels: explicit composition (no tag),
404
405
  * {@link TaskSpec.restoreGatedTools}, env `SEMA_TOOL_MODEL_GATE=off`,
405
406
  * `RunnerDeps.toolModelGate: false`; {@link TaskSpec.excludeTools} always wins regardless.
406
- * The materialized `AgentTool` face does NOT carry this field (`defineTool` is a whitelist
407
- * constructor) the decision completes on the ToolSpec face inside `prepareConfigDoors`,
408
- * before any conversion.
407
+ * The gate DECISION completes on the ToolSpec face inside `prepareConfigDoors`, before any
408
+ * conversion; `defineTool` then forwards the class onto the materialized face (design/388: the
409
+ * effect roster reports `modelGate` on a row that survived the gate, so an operator can read which
410
+ * class a mounted scaffold belongs to — the forwarded value is a read face, never a second gate).
409
411
  */
410
412
  modelGate?: string;
411
413
  /**
@@ -421,6 +423,55 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
421
423
  /** Declared behavior revision — bump when execute-visible behavior changes. */
422
424
  implementationRevision: string;
423
425
  };
426
+ /**
427
+ * design/388 §2.2 ① — the DISPLAY / DISPATCH family of this tool (closed set {@link TOOL_FAMILIES}).
428
+ * Input to grouping, card pre-selection and the rule editor's completion — never an authorization
429
+ * source: no gate reads it. Absent on a caller/MCP tool ⇒ the roster reports `"other"`; every
430
+ * built-in definition in the static catalog declares one (the catalog completeness gate reds a
431
+ * missing declaration rather than defaulting it).
432
+ */
433
+ family?: ToolFamily;
434
+ /**
435
+ * design/388 §2.2 ② — the parameter that names this tool's filesystem TARGET and how it is touched.
436
+ * Present ⇔ the tool can be confirmed BY PATH: write confinement (`allowDirs`, the fs-write gate,
437
+ * the sensitive-path guard, frozen-path denies), edited-file tracking and the transcript-directory
438
+ * gate all read it off the call's face instead of a name table. ABSENT ⇒ the tool is handled as a
439
+ * write whose target cannot be confirmed (fail-closed under every path-confining face) — exactly how
440
+ * a tool outside the retired name tables was handled. `param` outranks `aliases` (NotebookEdit's
441
+ * `notebook_path` beats a decoy `file_path`). `skillScopeEligible` is a SEPARATE grant: only a tool
442
+ * that sets it enters the active-skill `allowPaths` face (Write/Edit do; NotebookEdit deliberately
443
+ * does not; a caller tool that merely takes a built-in's NAME inherits nothing).
444
+ */
445
+ pathTarget?: ToolPathTarget;
446
+ /**
447
+ * design/388 §2.2 ③ — the parameter vocabulary a permission rule (`Tool(param:pattern)`) may name.
448
+ * DERIVED from the schema when absent (the scalar top-level parameters plus the path target's
449
+ * aliases; object/array parameters never enter — a rule on them can never match). A declaration can
450
+ * only NARROW the derived set. A tool whose `prepareArguments` renames keys must declare its face
451
+ * explicitly (the schema keys are not what a call carries).
452
+ */
453
+ ruleFace?: ToolRuleFace;
454
+ /**
455
+ * design/388 §2.2 ④ — declarative render hints for a shell that has no local implementation of
456
+ * this tool (an MCP / caller / external tool). Every string is UNTRUSTED display input: the
457
+ * roster caps lengths, the shell strips control and bidi characters. `approvalCard` is a
458
+ * pre-filter, never a compatibility proof — the shell validates the call's args against its own
459
+ * card schema before mounting a specialised card. `resultCards` may only name this tool's own
460
+ * `cardId` namespace; a declaration naming an engine card (`builtin:*`, `mcp`) is refused at prepare.
461
+ */
462
+ renderHints?: ToolRenderHints;
463
+ /**
464
+ * design/388 §2.2 ⑤ — this tool's results are CLEARABLE by the context-edit micro-compaction
465
+ * (the CC `microCompact` whitelist, previously the name table `COMPACTABLE_TOOLS`). Default
466
+ * false: an undeclared tool's results survive to the harder defenses.
467
+ */
468
+ compactable?: true;
469
+ /**
470
+ * design/388 §2.2 ⑤ — this tool's results are EXEMPT from the per-batch aggregate result budget
471
+ * because they are self-bounded (Read pages at its own cap; the offload reader pages small
472
+ * slices). Default false: every undeclared tool counts against the budget.
473
+ */
474
+ budgetExempt?: true;
424
475
  /**
425
476
  * Execute the tool. Return a string (becomes text content) or a structured result.
426
477
  * Throw to signal failure (the loop encodes it as an error tool result).
@@ -54,6 +54,13 @@ export function defineTool(spec, options) {
54
54
  ...(spec.defer !== undefined ? { defer: spec.defer } : {}),
55
55
  ...(spec.alwaysLoad !== undefined ? { alwaysLoad: spec.alwaysLoad } : {}),
56
56
  ...(spec.prepareArguments ? { prepareArguments: spec.prepareArguments } : {}),
57
+ ...(spec.family !== undefined ? { family: spec.family } : {}),
58
+ ...(spec.pathTarget !== undefined ? { pathTarget: spec.pathTarget } : {}),
59
+ ...(spec.ruleFace !== undefined ? { ruleFace: spec.ruleFace } : {}),
60
+ ...(spec.renderHints !== undefined ? { renderHints: spec.renderHints } : {}),
61
+ ...(spec.compactable === true ? { compactable: true } : {}),
62
+ ...(spec.budgetExempt === true ? { budgetExempt: true } : {}),
63
+ ...(spec.modelGate !== undefined ? { modelGate: spec.modelGate } : {}),
57
64
  ...(spec.approvalPreview ? { approvalPreview: spec.approvalPreview } : {}),
58
65
  ...(spec.validateInput ? { validateInput: spec.validateInput } : {}),
59
66
  execute: async (toolCallId, rawParams, signal) => {
@@ -511,6 +511,30 @@ export type TraceEvent = {
511
511
  /** The boundary classification the admission asserted. */
512
512
  boundary: "sandbox_internal";
513
513
  ts: number;
514
+ } | {
515
+ /**
516
+ * The auto-mode classifier decided (or declined to decide) one pending ask — one frame per
517
+ * `decide` call, including the breaker-open short-circuit, so a consumer can read the gate's
518
+ * wall-clock cost per tool call and the failure cause behind an ask that reached a person. A
519
+ * TRACE frame rather than a task event: it fires once per gated call, it is an operator's read
520
+ * (why did that ask take four seconds / why did it fall back), and the session-level breaker
521
+ * read face a shell renders is a separate, additive wire face. `ms` is the wall time from the
522
+ * decide call to its verdict (timeout included: a timed-out round reads the deadline).
523
+ */
524
+ kind: "auto_mode.classified";
525
+ version: 1;
526
+ taskId: string;
527
+ /** The gated call the classifier judged. */
528
+ toolCallId: string;
529
+ /** The classifier model (the resolved `classifier` role, or the main model it fell back to). */
530
+ model: string;
531
+ /** Wall-clock milliseconds the gate waited on this decision (integer, ≥ 0). */
532
+ ms: number;
533
+ /** The verdict kind the gate acted on (`allow` / `block` / `unavailable` / `parse_error`). */
534
+ verdict: "allow" | "block" | "unavailable" | "parse_error";
535
+ /** Present iff `verdict === "unavailable"`: `error` / `timeout` / `breaker_open`. */
536
+ cause?: "error" | "timeout" | "breaker_open";
537
+ ts: number;
514
538
  } | {
515
539
  /** C1 — the failover brain served this call from a FALLBACK entry (`createFailoverBrain`): the
516
540
  * primary (and possibly earlier hops) failed cleanly upfront. Without this, same-model gateway
@@ -262,6 +262,17 @@ export interface WiringManifest {
262
262
  * assembly, and legs of one assembly must fingerprint alike whether or not a server was up.
263
263
  */
264
264
  mcp?: readonly WiringManifestMcpEntry[];
265
+ /**
266
+ * design/388 §2.5 — EFFECTIVE half only, and ALWAYS present on an engine-minted effective manifest:
267
+ * the leg's TOOL ROSTER — one row per mounted instance in wire order, every declared axis, the
268
+ * engine-minted identity (`capabilityId`), the schema-derived `inputKeys` / rule vocabulary and the
269
+ * roster `digest`. The static half has no roster to report (it mounts nothing). NOT part of
270
+ * {@link configFingerprint}: a roster is per-leg by construction (a child leg mounts a different set
271
+ * under the same assembly), so hashing it would break the cross-leg equality the fingerprint
272
+ * advertises — the roster carries its own digest for that. A run-time change to the set arrives as a
273
+ * `tool_roster_delta` event, never as a re-emitted manifest.
274
+ */
275
+ tools?: import("./tool-roster.js").ToolRoster;
265
276
  /** EFFECTIVE half only — a short, non-sensitive fingerprint (sha256 prefix over the canonical
266
277
  * JSON of this manifest's own resolved facts; every field here is an enum/boolean/count, no
267
278
  * secrets) so an operator can correlate legs that ran under the same resolved assembly. */
@@ -333,6 +344,8 @@ export interface WiringFacts {
333
344
  /** Effective half only — see {@link WiringManifest.mcp}; the static half materializes nothing.
334
345
  * Copied entry-wise onto the manifest (the caller's array is never aliased). */
335
346
  mcp?: readonly WiringManifestMcpEntry[];
347
+ /** Effective half only — the leg's minted tool roster (see {@link WiringManifest.tools}). */
348
+ tools?: import("./tool-roster.js").ToolRoster;
336
349
  }
337
350
  /** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
338
351
  * single-source shapes, no parallel hand-copied interface). */
@@ -127,9 +127,10 @@ export function deriveWiringManifest(facts) {
127
127
  ...(facts.half === "effective" && facts.modelGate !== undefined ? { modelGate: { class: facts.modelGate.class, removed: [...facts.modelGate.removed], restore: facts.modelGate.restore } } : {}),
128
128
  ...(facts.half === "effective" && facts.autoMode !== undefined ? { autoMode: readAutoModeFact(facts.autoMode) } : {}),
129
129
  ...(facts.half === "effective" && facts.mcp !== undefined ? { mcp: facts.mcp.map((e) => ({ ...e })) } : {}),
130
+ ...(facts.half === "effective" && facts.tools !== undefined ? { tools: facts.tools } : {}),
130
131
  };
131
132
  if (facts.half === "effective") {
132
- const { leg: _leg, mcp: _mcp, ...assembly } = manifest;
133
+ const { leg: _leg, mcp: _mcp, tools: _tools, ...assembly } = manifest;
133
134
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
134
135
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
135
136
  manifest.configFingerprint = createHash("sha256")