@sema-agent/core 7.35.1 → 7.36.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 (71) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/brain/anthropic.js +6 -6
  3. package/dist/brain/errors.d.ts +1 -14
  4. package/dist/brain/errors.js +2 -6
  5. package/dist/brain/open-responses.js +7 -7
  6. package/dist/brain/openai.js +7 -7
  7. package/dist/brain/provider-echo-shape.d.ts +26 -0
  8. package/dist/brain/provider-echo-shape.js +12 -0
  9. package/dist/brain/stream-engine.d.ts +2 -1
  10. package/dist/brain/stream-engine.js +28 -14
  11. package/dist/brain/tool-call-id.d.ts +27 -15
  12. package/dist/brain/tool-call-id.js +43 -0
  13. package/dist/core/ask-question.d.ts +0 -30
  14. package/dist/core/ask-question.js +3 -2
  15. package/dist/core/checkpoint-store.d.ts +5 -3
  16. package/dist/core/checkpoint-store.js +2 -1
  17. package/dist/core/context-edit.d.ts +4 -0
  18. package/dist/core/context-edit.js +5 -2
  19. package/dist/core/engine-notice.d.ts +7 -0
  20. package/dist/core/gate-lanes.js +12 -10
  21. package/dist/core/governance-codes.d.ts +1 -1
  22. package/dist/core/governance-codes.js +2 -0
  23. package/dist/core/hooks.d.ts +3 -0
  24. package/dist/core/hooks.js +4 -2
  25. package/dist/core/mode-policy.js +2 -1
  26. package/dist/core/permission-rule-model.d.ts +2 -2
  27. package/dist/core/permission-rule-model.js +2 -2
  28. package/dist/core/permission-rules.js +2 -5
  29. package/dist/core/runner/active-skill-scope.js +5 -5
  30. package/dist/core/runner/decide-continuation.js +2 -1
  31. package/dist/core/runner/permission-rule-lanes.d.ts +3 -0
  32. package/dist/core/runner/permission-rule-lanes.js +7 -6
  33. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  34. package/dist/core/runner/prepare-context-lane.js +7 -5
  35. package/dist/core/runner/prepare-gate-stations.js +17 -12
  36. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  37. package/dist/core/runner/prepare-park-ask.js +7 -6
  38. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  39. package/dist/core/runner/prepare-policy-chain.js +10 -9
  40. package/dist/core/runner/prepare-safety-scan.js +9 -2
  41. package/dist/core/runner/prepare-task.js +3 -3
  42. package/dist/core/runner/resume-apply.js +2 -1
  43. package/dist/core/runner/run-attachment-seats.js +3 -1
  44. package/dist/core/runner/run-harness-handlers.js +6 -1
  45. package/dist/core/runner/run-leg.js +11 -9
  46. package/dist/core/runner/run-recovery-lanes.js +2 -0
  47. package/dist/core/runner/session-rule-policy.js +5 -4
  48. package/dist/core/runner/tool-call-id-fold-seat.d.ts +13 -0
  49. package/dist/core/runner/tool-call-id-fold-seat.js +31 -0
  50. package/dist/core/sensitive-path-policy.js +2 -2
  51. package/dist/core/side-query.d.ts +5 -1
  52. package/dist/core/side-query.js +5 -1
  53. package/dist/core/task-event.d.ts +2 -1
  54. package/dist/core/tool-name-readings.d.ts +366 -0
  55. package/dist/core/tool-name-readings.js +136 -0
  56. package/dist/core/tool-policy.d.ts +5 -5
  57. package/dist/core/tool-policy.js +15 -13
  58. package/dist/core/tool-registry.d.ts +1 -31
  59. package/dist/core/tool-registry.js +2 -22
  60. package/dist/core/tool-result-budget.d.ts +3 -0
  61. package/dist/core/tool-result-budget.js +5 -3
  62. package/dist/core/tool-roster.d.ts +30 -9
  63. package/dist/core/tool-roster.js +45 -5
  64. package/dist/engine/llm/types.d.ts +4 -0
  65. package/dist/tools/fs/file-read-check.d.ts +1 -1
  66. package/dist/tools/fs/file-read-check.js +4 -3
  67. package/dist/tools/fs/shell-read-check.d.ts +15 -1
  68. package/dist/tools/fs/shell-read-check.js +5 -2
  69. package/package.json +2 -1
  70. package/wire-changes/core@7.35.2.json +61 -0
  71. package/wire-changes/core@7.36.0.json +288 -0
@@ -52,36 +52,6 @@ export interface ToolCallLike {
52
52
  } | undefined;
53
53
  }
54
54
  type CallPathTarget = ToolCallPathTarget;
55
- /**
56
- * The names a tool-name rule may spell this call's tool by — the face's `names` when they include the call's name,
57
- * else the call's name alone (a bare request, or a face minted for another name). Every name-keyed matcher and the
58
- * protective path floor read a rule's tool name against THIS list, which is the roster's one canonicalization seen
59
- * from the call's side: a rule written against an alias and one written against the wire name reach the same calls.
60
- */
61
- export declare function ruleNamesOf(req: Pick<ToolCallLike, "toolName" | "face">): readonly string[];
62
- /** Does the call answer to `name` — is it one of the call's names ({@link ruleNamesOf})? The engine's TIGHTENS keyed on
63
- * one of its own tool names read this, so a tool that wears the name under any of its names is tightened (the
64
- * fail-safe direction); a grant or a routing keyed on an engine tool name reads the wire name. */
65
- export declare function callAnswersTo(req: Pick<ToolCallLike, "toolName" | "face">, name: string): boolean;
66
- /** Does a set of rule tool names name this call's tool ({@link ruleNamesOf})? */
67
- export declare function toolNameSetHas(set: {
68
- has(name: string): boolean;
69
- }, req: Pick<ToolCallLike, "toolName" | "face">): boolean;
70
- /** Does `set` hold any of the call's names ({@link ruleNamesOf})? The read of a MEMBERSHIP tighten keyed on the engine's
71
- * own tool names (the egress set, a read station's tools, the compliance veto): a tool is held under every name it
72
- * answers to. A grant or a routing keyed on an engine tool name reads the wire name instead. */
73
- export declare function anyNamed(set: {
74
- has(name: string): boolean;
75
- }, names: readonly string[]): boolean;
76
- /** The first of the call's names ({@link ruleNamesOf}, the spelling first) that `map` holds an entry for, with the entry.
77
- * The read of a SEAT keyed on the engine's own tool names (a tier, a mark's sources): every key of one seat is read under
78
- * the ONE name this returns — a seat is never assembled from two names' entries. */
79
- export declare function firstNamed<V>(map: {
80
- get(name: string): V | undefined;
81
- }, names: readonly string[]): {
82
- readonly name: string;
83
- readonly value: V;
84
- } | undefined;
85
55
  /**
86
56
  * design/388 B6/B17 — the path target a PROTECTIVE fence reads (a deny-list judging a write's target: sensitive paths,
87
57
  * frozen paths, write protection, the transcript gate; edit tracking): the object face's own declaration first, the
@@ -89,7 +59,7 @@ export declare function firstNamed<V>(map: {
89
59
  * face that declares none. Reading the catalog for an undeclared same-name tool can only TIGHTEN a deny-list (it adds
90
60
  * a slot to judge — the pre-388 posture, never wider), which is exactly why this floor is wrong for a grant.
91
61
  *
92
- * The floor reads EVERY name the call answers to (`ruleNamesOf`: the face's `names`, else the call's name alone) in
62
+ * The floor is a `member` seat over every name the call answers to (the face's `names`, else the call's name alone) in
93
63
  * the face's order — the name the call was made under first, then the wire name, then the other aliases — and the
94
64
  * first name the catalog declares a target for wins: a call made under a catalog name and resolved to a tool that
95
65
  * declares none keeps THAT name's slot, also when the tool answers to a second catalog name with another slot.
@@ -1,5 +1,6 @@
1
1
  import { TOOL_MOUNT_TAGS, pathTargetSlotOnly } from "./tool-face.js";
2
2
  import { ENGINE_CARDS, TOOL_CATALOG_ENTRIES } from "./tool-catalog-entries.js";
3
+ import { seatOf } from "./tool-name-readings.js";
3
4
  export function describeToolCatalog() {
4
5
  return TOOL_CATALOG_ENTRIES;
5
6
  }
@@ -51,31 +52,10 @@ export function pathTargetValue(args, pt) {
51
52
  }
52
53
  return undefined;
53
54
  }
54
- export function ruleNamesOf(req) {
55
- const names = req.face?.names;
56
- return names !== undefined && names.includes(req.toolName) ? names : [req.toolName];
57
- }
58
- export function callAnswersTo(req, name) {
59
- return ruleNamesOf(req).includes(name);
60
- }
61
- export function toolNameSetHas(set, req) {
62
- return anyNamed(set, ruleNamesOf(req));
63
- }
64
- export function anyNamed(set, names) {
65
- return names.some((n) => set.has(n));
66
- }
67
- export function firstNamed(map, names) {
68
- for (const name of names) {
69
- const value = map.get(name);
70
- if (value !== undefined)
71
- return { name, value };
72
- }
73
- return undefined;
74
- }
75
55
  export function protectivePathTargetOf(call) {
76
56
  if (call.face?.pathTarget !== undefined)
77
57
  return call.face.pathTarget;
78
- const floor = ruleNamesOf(call).map((n) => pathTargetOf(n)).find((pt) => pt !== undefined);
58
+ const floor = seatOf("floor.protectivePathTarget", { get: pathTargetOf }, call)?.value;
79
59
  return floor === undefined || call.face === undefined ? floor : pathTargetSlotOnly(floor);
80
60
  }
81
61
  export const BARE_REQUEST_CODE = "policy.bare_request";
@@ -23,6 +23,9 @@ export interface AggregateBudgetOptions {
23
23
  budgetChars?: number;
24
24
  /** Tool names exempt from the budget (their own caps bound them) — e.g. read_file, read_tool_result. */
25
25
  exemptTools?: ReadonlySet<string>;
26
+ /** The wire name a result's recorded tool name resolves to on the leg's roster (an exemption is judged on the tool the
27
+ * call ran, not the spelling the model used). Absent ⇒ the recorded name. */
28
+ toolNameOf?: (recorded: string) => string;
26
29
  /** design/30 store for offload+read-back. Absent ⇒ self-contained truncation preview. */
27
30
  store?: ToolResultStore;
28
31
  /** Namespaces the stable ref so an offloaded result is globally addressable. */
@@ -1,6 +1,7 @@
1
1
  import { isToolResult } from "./message-utils.js";
2
2
  import { buildPreview, buildToolResultRef, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "./tool-result-store.js";
3
3
  import { budgetExemptToolNames } from "./tool-registry.js";
4
+ import { wireOf } from "./tool-name-readings.js";
4
5
  export const AGGREGATE_TOOL_RESULT_BUDGET_CHARS = 200_000;
5
6
  const HEAD = 1_000;
6
7
  const TAIL = 1_000;
@@ -38,6 +39,7 @@ const CATALOG_BUDGET_EXEMPT_TOOLS = budgetExemptToolNames();
38
39
  export async function capAggregateToolResults(messages, opts) {
39
40
  const budget = opts.budgetChars ?? AGGREGATE_TOOL_RESULT_BUDGET_CHARS;
40
41
  const exempt = opts.exemptTools ?? CATALOG_BUDGET_EXEMPT_TOOLS;
42
+ const exemptResult = (m) => m.toolName !== undefined && exempt.has(wireOf("budget.exemptResult", { toolName: opts.toolNameOf !== undefined ? opts.toolNameOf(m.toolName) : m.toolName }));
41
43
  let out;
42
44
  let i = 0;
43
45
  while (i < messages.length) {
@@ -52,7 +54,7 @@ export async function capAggregateToolResults(messages, opts) {
52
54
  let total = 0;
53
55
  for (let k = i; k < j; k++) {
54
56
  const m = arr[k];
55
- if (m.toolName && exempt.has(m.toolName))
57
+ if (exemptResult(m))
56
58
  continue;
57
59
  total += textChars(m.content);
58
60
  }
@@ -94,7 +96,7 @@ export async function capAggregateToolResults(messages, opts) {
94
96
  const cand = [];
95
97
  for (let k = i; k < j; k++) {
96
98
  const m = out[k];
97
- if (m.toolName && exempt.has(m.toolName))
99
+ if (exemptResult(m))
98
100
  continue;
99
101
  if (isOffloadedPreview(m.content))
100
102
  continue;
@@ -112,7 +114,7 @@ export async function capAggregateToolResults(messages, opts) {
112
114
  const cand2 = [];
113
115
  for (let k = i; k < j; k++) {
114
116
  const m = out[k];
115
- if (m.toolName && exempt.has(m.toolName))
117
+ if (exemptResult(m))
116
118
  continue;
117
119
  if (isOffloadedPreview(messages[k].content))
118
120
  continue;
@@ -257,11 +257,11 @@ export declare function rosterEntryNamed(roster: ToolRoster, toolName: string):
257
257
  * an alias and one written against the wire name are one rule.
258
258
  */
259
259
  export declare function rosterToolName(roster: ToolRoster | undefined, name: string): string;
260
- export { anyNamed, callAnswersTo, firstNamed, ruleNamesOf, toolNameSetHas } from "./tool-registry.js";
260
+ export { ruleNamesOf } from "./tool-name-readings.js";
261
261
  /** design/388 B6 — the per-call face a gated request carries, read off a roster snapshot by name (aliases resolve).
262
262
  * `names` leads with the name the face was minted under (the model's spelling at the gate), then the wire name and the
263
- * other aliases: a reader that must pick ONE of the call's names (the protective path floor) picks the spelling first,
264
- * as the same call was judged before name resolution. */
263
+ * other aliases — the ONE order of a call's names: a reader that must pick one of them by position (a `member` seat)
264
+ * picks the spelling first, as the same call was judged before name resolution. */
265
265
  export declare function toolCallFaceOf(roster: ToolRoster | undefined, toolName: string): import("./tool-policy.js").ToolCallFace | undefined;
266
266
  /** design/388 B18 — the identity members a park row carries, read off a roster snapshot by name; an empty object when
267
267
  * the name is not on the snapshot (the row then reads pre-identity and the resume takes the name-only path). */
@@ -272,16 +272,30 @@ export declare function parkedToolIdentity(roster: ToolRoster | undefined, toolN
272
272
  implementationRevision?: string;
273
273
  rosterDigest?: string;
274
274
  };
275
- /** design/388 B6 × B18 (merge-seam adversarial round H1) — the FACE members a park row carries beside its identity:
276
- * the display family and the declared path target the parked call was judged under. A recheck that runs before
277
- * the leg's roster exists (the pre-CAS resume-edit re-adjudication) rebuilds the call face from the row through
278
- * {@link parkedCallFaceOf}, so it judges the same object the live gate judged. An empty object when the name is
279
- * not on the snapshot (the row then reads pre-face and the recheck falls to the catalog for the name). */
280
- export declare function parkedToolFace(roster: ToolRoster | undefined, toolName: string): {
275
+ /** design/388 B6 × B18 — the FACE members a park row carries beside its identity: the display family, the declared
276
+ * path target and the names the parked call was judged under. `names` is copied from the face the gate stamped on the
277
+ * request (the model's spelling first — the row carries the judged fact, it is not re-derived); a request with no face
278
+ * takes the roster's names for its row. A recheck that runs before the leg's roster exists (the pre-CAS resume-edit
279
+ * re-adjudication) rebuilds the call face from the row through {@link parkedCallFaceOf}, so it judges the same object
280
+ * the live gate judged. An empty object when the name is not on the snapshot (the row then reads pre-face and the
281
+ * recheck falls to the catalog for the name). */
282
+ export declare function parkedToolFace(roster: ToolRoster | undefined, req: {
283
+ readonly toolName: string;
284
+ readonly face?: {
285
+ readonly names?: readonly string[];
286
+ } | undefined;
287
+ }): {
281
288
  family?: ToolFamily;
282
289
  pathTarget?: ToolRosterEntry["pathTarget"];
283
290
  names?: readonly string[];
284
291
  };
292
+ /** The live face a resumed park row is re-judged under: minted under the row's recorded spelling (`names[0]`) when that
293
+ * spelling still resolves to the row's own tool on this roster, else under the row's wire name — a spelling that no
294
+ * longer resolves, or that another row now owns, never lends the row another tool's face. */
295
+ export declare function parkedRowLiveFace(roster: ToolRoster | undefined, row: {
296
+ readonly toolName: string;
297
+ readonly names?: readonly string[];
298
+ }): import("./tool-policy.js").ToolCallFace | undefined;
285
299
  /** The call face a park row carries — present iff the row was minted with BOTH its face members and its four
286
300
  * identity members (one mint writes all of them; a row missing any is pre-388 and yields undefined, never a
287
301
  * partial face). This is the face every recheck of the row judges under, roster or no roster. */
@@ -443,6 +457,13 @@ export declare class RosterBuilder {
443
457
  * yielded instance's position and never a row per object. */
444
458
  snapshot(facts: RosterSnapshotFacts): ToolRoster;
445
459
  }
460
+ /**
461
+ * The roster's one declaration rule on engine names: a row whose names (its wire name and the aliases that resolve to it,
462
+ * {@link namesResolvingTo}) belong to TWO or more engine tools is refused — one tool answering as two engine tools would
463
+ * have every per-call seat keyed on engine names choose between two definitions of the call. A row answering to one engine
464
+ * tool under several of that tool's names, and an alias another row owns, are not counted twice. Undefined ⇒ none.
465
+ */
466
+ export declare function rosterEngineNamesProblem(roster: ToolRoster): string | undefined;
446
467
  /** Which source a `TaskSpec.tools` entry mounts as: a definition carrying a CORE contract is a built-in the host chose
447
468
  * to mount; anything else is the caller's own tool. Identity follows the object, never the name. */
448
469
  export declare function callerMountSource(tool: AgentTool): RosterSource;
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { getToolContract } from "../prompt-assembly/tool-catalog.js";
4
4
  import { protocolOf } from "./protocol-table.js";
5
+ import { describeToolCatalog } from "./tool-registry.js";
5
6
  import { RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_APPROVAL_CARDS, TOOL_CARD_ID_MAX_CHARS, TOOL_CONTRACT_MAX_CHARS, TOOL_FAMILIES, TOOL_KEYS_MAX, TOOL_KEY_MAX_CHARS, TOOL_MOUNT_TAGS, TOOL_WIRE_NAME_MAX_CHARS, TOOL_PATH_ABSENCES, TOOL_PATH_BASES, } from "./tool-face.js";
6
7
  function lit(values) {
7
8
  return Type.Union(values.map((v) => Type.Literal(v)));
@@ -80,7 +81,7 @@ export function rosterToolName(roster, name) {
80
81
  function namesResolvingTo(roster, e) {
81
82
  return [e.name, ...e.aliases.filter((a) => a !== e.name && rosterToolName(roster, a) === e.name)];
82
83
  }
83
- export { anyNamed, callAnswersTo, firstNamed, ruleNamesOf, toolNameSetHas } from "./tool-registry.js";
84
+ export { ruleNamesOf } from "./tool-name-readings.js";
84
85
  export function toolCallFaceOf(roster, toolName) {
85
86
  if (roster === undefined)
86
87
  return undefined;
@@ -106,13 +107,19 @@ export function parkedToolIdentity(roster, toolName) {
106
107
  return {};
107
108
  return { capabilityId: e.capabilityId, contractId: e.contract.contractId, shapeDigest: e.shapeDigest, implementationRevision: e.contract.implementationRevision, rosterDigest: roster.digest };
108
109
  }
109
- export function parkedToolFace(roster, toolName) {
110
+ export function parkedToolFace(roster, req) {
110
111
  if (roster === undefined)
111
112
  return {};
112
- const e = rosterEntryNamed(roster, toolName);
113
+ const e = rosterEntryNamed(roster, req.toolName);
113
114
  if (e === undefined)
114
115
  return {};
115
- return { family: e.family, ...(e.pathTarget !== undefined ? { pathTarget: e.pathTarget } : {}), names: namesResolvingTo(roster, e) };
116
+ const judged = req.face?.names;
117
+ return { family: e.family, ...(e.pathTarget !== undefined ? { pathTarget: e.pathTarget } : {}), names: judged !== undefined && judged.includes(req.toolName) ? [...judged] : namesResolvingTo(roster, e) };
118
+ }
119
+ export function parkedRowLiveFace(roster, row) {
120
+ const spelled = row.names?.[0];
121
+ const under = spelled !== undefined && roster !== undefined && rosterToolName(roster, spelled) === row.toolName ? spelled : row.toolName;
122
+ return toolCallFaceOf(roster, under);
116
123
  }
117
124
  export function parkedCallFaceOf(pending) {
118
125
  if (pending.family === undefined || pending.capabilityId === undefined || pending.contractId === undefined || pending.shapeDigest === undefined || pending.implementationRevision === undefined)
@@ -369,8 +376,41 @@ export class RosterBuilder {
369
376
  const meta = this.#meta.get(survivor) ?? { source: "caller", mountedBy: "scenario" };
370
377
  entries.push(mintRosterEntry(survivor, meta, facts));
371
378
  }
372
- return { schemaVersion: 1, count: entries.length, entries, digest: rosterDigestOf(entries) };
379
+ const roster = { schemaVersion: 1, count: entries.length, entries, digest: rosterDigestOf(entries) };
380
+ const worn = rosterEngineNamesProblem(roster);
381
+ if (worn !== undefined)
382
+ throw configError("config.tool_face_invalid", worn);
383
+ return roster;
384
+ }
385
+ }
386
+ let catalogOwners;
387
+ function catalogOwnerOf(name) {
388
+ if (catalogOwners === undefined) {
389
+ const owners = new Map();
390
+ for (const e of describeToolCatalog()) {
391
+ owners.set(e.name, e.name);
392
+ for (const a of e.face.aliases ?? [])
393
+ if (!owners.has(a))
394
+ owners.set(a, e.name);
395
+ }
396
+ catalogOwners = owners;
373
397
  }
398
+ return catalogOwners.get(name);
399
+ }
400
+ export function rosterEngineNamesProblem(roster) {
401
+ for (const e of roster.entries) {
402
+ const resolving = namesResolvingTo(roster, e);
403
+ const worn = new Map();
404
+ for (const n of resolving) {
405
+ const owner = catalogOwnerOf(n);
406
+ if (owner !== undefined && !worn.has(owner))
407
+ worn.set(owner, n);
408
+ }
409
+ if (worn.size >= 2) {
410
+ return `tool ${JSON.stringify(e.name)} answers to the names of ${worn.size} engine tools (${[...worn.values()].map((n) => JSON.stringify(n)).join(", ")}): a tool may answer to at most one engine tool's names — keep one of those aliases`;
411
+ }
412
+ }
413
+ return undefined;
374
414
  }
375
415
  export function callerMountSource(tool) {
376
416
  return getToolContract(tool, "caller").origin === "core" ? "builtin" : "caller";
@@ -242,6 +242,10 @@ export interface DocumentContent {
242
242
  export interface ToolCall {
243
243
  type: "toolCall";
244
244
  id: string;
245
+ /** The provider's own spelling of {@link id}, present only when the brain folded it (`brain/tool-call-id.ts`);
246
+ * handed back to the provider for the call and its result, persisted with the message. Upstream bytes: render
247
+ * {@link id}. */
248
+ wireId?: string;
245
249
  name: string;
246
250
  arguments: Record<string, unknown>;
247
251
  executionMode?: "sequential" | "parallel";
@@ -15,7 +15,7 @@
15
15
  import type { ExecutionEnv } from "../../internal/harness-types.js";
16
16
  import type { ContainmentFace } from "./containment-face.js";
17
17
  import { type ReadDenyMatcher } from "./read-deny.js";
18
- import type { ShellReadCheck } from "./shell-read-check.js";
18
+ import { type ShellReadCheck } from "./shell-read-check.js";
19
19
  export interface FileReadStation {
20
20
  readonly check: ShellReadCheck;
21
21
  /** The station's membership: the tool names whose declared read target it judges. */
@@ -1,7 +1,8 @@
1
1
  import { effectivePathSpellingOf } from "../../core/effective-path-target.js";
2
- import { firstNamed, pathTargetOf, ruleNamesOf } from "../../core/tool-registry.js";
2
+ import { pathTargetOf } from "../../core/tool-registry.js";
3
3
  import { fitMiddle, READ_DENY_SENTENCE_PART_MAX } from "./read-deny.js";
4
4
  import { resolveKey, violationDetails } from "./safety.js";
5
+ import { readStationMemberOf } from "./shell-read-check.js";
5
6
  import { grantOfferFor, parentDirLexically } from "./shell-read-judge.js";
6
7
  const ADMISSION_MAX = 1024;
7
8
  const VOUCH = { disposition: "vouch", finding: "none" };
@@ -17,7 +18,7 @@ export function createFileReadStation(opts) {
17
18
  };
18
19
  const check = {
19
20
  judge(call, args, cwd, signal, callOpts) {
20
- const member = ruleNamesOf(call).find((n) => opts.tools.has(n));
21
+ const member = readStationMemberOf(call, opts.tools)?.name;
21
22
  if (member === undefined)
22
23
  return null;
23
24
  const toolName = call.toolName;
@@ -71,7 +72,7 @@ export function composeReadChecks(stations) {
71
72
  for (const n of s.tools)
72
73
  if (!byName.has(n))
73
74
  byName.set(n, s);
74
- const stationOf = (call) => firstNamed(byName, ruleNamesOf(call))?.value;
75
+ const stationOf = (call) => readStationMemberOf(call, byName)?.value;
75
76
  return {
76
77
  judge: (call, args, cwd, signal, callOpts) => stationOf(call)?.check.judge(call, args, cwd, signal, callOpts) ?? null,
77
78
  unanswered: (call, cause) => (stationOf(call) ?? stations[0]).check.unanswered(call, cause),
@@ -22,7 +22,7 @@ import { violationDetails } from "./safety.js";
22
22
  import { type ShellReadPlan } from "./shell-read-plan.js";
23
23
  import { type EstablishedRead, type ShellReadBoundary, type ShellReadDisposition, type ShellReadFinding, type ShellReadSeat } from "./shell-read-judge.js";
24
24
  import type { ReadRootGrantCandidate } from "../../core/tool-spec.js";
25
- import { type ToolCallLike } from "../../core/tool-registry.js";
25
+ import type { ToolCallLike } from "../../core/tool-registry.js";
26
26
  /** The ONE mint of the containment card — the targets a finding established OUTSIDE the roots (an out-of-root read, a
27
27
  * target that resolves elsewhere by the spawn). */
28
28
  declare function containmentDetails(paths: readonly string[]): {
@@ -105,6 +105,20 @@ export interface ShellReadCheck {
105
105
  }
106
106
  /** The call a read station judges: its wire name and the face the gate stamped (whose `names` the station reads). */
107
107
  export type ReadStationCall = Pick<ToolCallLike, "toolName" | "face">;
108
+ /** The read station a call belongs to — the first of its names (the spelling first) a station's membership holds; every
109
+ * station reads the call's slot and seat under that one name. */
110
+ export declare function readStationMemberOf<V>(call: ReadStationCall, members: {
111
+ get(name: string): V | undefined;
112
+ }): {
113
+ readonly name: string;
114
+ readonly value: V;
115
+ } | undefined;
116
+ export declare function readStationMemberOf(call: ReadStationCall, members: {
117
+ has(name: string): boolean;
118
+ }): {
119
+ readonly name: string;
120
+ readonly value: true;
121
+ } | undefined;
108
122
  /** The pre-spawn admission the shell tools call: re-establishes what the station established for this
109
123
  * call and refuses when any canonical target moved. `undefined` ⇒ spawn. A call the station never judged
110
124
  * (no gate ran) has nothing to re-establish and spawns. */
@@ -8,7 +8,7 @@ import { planShellReads } from "./shell-read-plan.js";
8
8
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
9
9
  import { programArgs, spellsOption } from "./shell-read-write-targets.js";
10
10
  import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
11
- import { ruleNamesOf } from "../../core/tool-registry.js";
11
+ import { seatOf } from "../../core/tool-name-readings.js";
12
12
  import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
13
13
  import { argvOf, lexShellText } from "../../shell/lex.js";
14
14
  function containmentDetails(paths) {
@@ -178,6 +178,9 @@ async function writeGuardsOf(judged, boundary, placed, env, signal) {
178
178
  }
179
179
  return { unplaced, writeProtected };
180
180
  }
181
+ export function readStationMemberOf(call, members) {
182
+ return seatOf("fold.readStation", members, call);
183
+ }
181
184
  const ADMISSION_MAX = 1024;
182
185
  function shellCommandOf(args) {
183
186
  const command = args?.command;
@@ -360,7 +363,7 @@ export function createShellReadStation(opts) {
360
363
  }
361
364
  admissions.set(toolCallId, a);
362
365
  };
363
- const memberOf = (call) => ruleNamesOf(call).find((n) => opts.tools.has(n));
366
+ const memberOf = (call) => readStationMemberOf(call, opts.tools)?.name;
364
367
  const check = {
365
368
  judge(call, args, cwd, signal, callOpts) {
366
369
  const member = memberOf(call);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.35.1",
3
+ "version": "7.36.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -83,6 +83,7 @@
83
83
  "gate:contract-sentences": "node scripts/verify-contract-sentences.mjs",
84
84
  "gate:tool-catalog": "node scripts/verify-tool-catalog.mjs",
85
85
  "gate:tool-names": "node scripts/verify-tool-name-tables.mjs",
86
+ "gate:name-readings": "node scripts/verify-name-readings.mjs",
86
87
  "gate:tool-docs": "node scripts/gen-tool-spec-fields.mjs --check",
87
88
  "gate:backlog-ledger": "node scripts/verify-backlog-ledger.mjs",
88
89
  "gate:child-clamp-seats": "node scripts/verify-child-clamp-seats.mjs",
@@ -0,0 +1,61 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.35.2",
4
+ "from": "47ed594ee8d729368c4be8d8a189d908b33e15e5",
5
+ "to": "v7.35.2",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [],
13
+ "typeKeysUnreadableArms": [],
14
+ "typeKeysFlattenedCount": 219,
15
+ "declarationMoves": [],
16
+ "mintMoves": [],
17
+ "criteriaObserves": [
18
+ {
19
+ "kind": "typeMember",
20
+ "name": "BrainStatus.elapsedMs",
21
+ "assertions": [
22
+ "G1.a",
23
+ "G1.c"
24
+ ]
25
+ },
26
+ {
27
+ "kind": "typeMember",
28
+ "name": "BrainStatus.errClass",
29
+ "assertions": [
30
+ "G1.b"
31
+ ]
32
+ },
33
+ {
34
+ "kind": "typeMember",
35
+ "name": "BrainStatus.phase",
36
+ "assertions": [
37
+ "G1.a",
38
+ "G1.b",
39
+ "G1.c"
40
+ ]
41
+ },
42
+ {
43
+ "kind": "typeMember",
44
+ "name": "BrainStatus.timeoutMs",
45
+ "assertions": [
46
+ "G1.a",
47
+ "G1.b",
48
+ "G1.c"
49
+ ]
50
+ }
51
+ ],
52
+ "consumersHint": {
53
+ "cli": [],
54
+ "server": [],
55
+ "sdk": [],
56
+ "note": "scanned cli@6b6025f7 server@25d6b7a6 sdk@e8f6f0d — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
57
+ },
58
+ "breakingCount": 0,
59
+ "breakingItems": [],
60
+ "toResolvedAt": "8a69688057ed91863f6574e06b9f4f83dfdcb7a6"
61
+ }