@sema-agent/core 7.3.0 → 7.4.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 (72) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/agents/peer-admission.d.ts +18 -3
  3. package/dist/agents/peer-admission.js +79 -4
  4. package/dist/agents/peer-held-queue.d.ts +101 -0
  5. package/dist/agents/peer-held-queue.js +229 -0
  6. package/dist/agents/peer-idle.d.ts +109 -0
  7. package/dist/agents/peer-idle.js +240 -0
  8. package/dist/agents/peer-notice-route.d.ts +33 -0
  9. package/dist/agents/peer-notice-route.js +46 -0
  10. package/dist/agents/peer-notices.d.ts +103 -0
  11. package/dist/agents/peer-notices.js +206 -0
  12. package/dist/agents/peer-session-drain.d.ts +39 -4
  13. package/dist/agents/peer-session-drain.js +248 -42
  14. package/dist/agents/send-message-tool.d.ts +8 -1
  15. package/dist/agents/send-message-tool.js +96 -30
  16. package/dist/agents/subagent.js +1 -0
  17. package/dist/brain/status-sink.d.ts +10 -0
  18. package/dist/brain/status-sink.js +13 -4
  19. package/dist/brain/stream-engine.d.ts +11 -0
  20. package/dist/brain/stream-engine.js +39 -3
  21. package/dist/core/arg-summary.d.ts +13 -3
  22. package/dist/core/arg-summary.js +138 -7
  23. package/dist/core/auto-mode-defaults.d.ts +11 -0
  24. package/dist/core/auto-mode-defaults.js +2 -0
  25. package/dist/core/auto-mode.d.ts +59 -0
  26. package/dist/core/auto-mode.js +57 -1
  27. package/dist/core/checkpoint-store.js +2 -2
  28. package/dist/core/governance-codes.d.ts +1 -1
  29. package/dist/core/governance-codes.js +8 -0
  30. package/dist/core/hooks.d.ts +30 -0
  31. package/dist/core/hooks.js +43 -8
  32. package/dist/core/mailbox-store.d.ts +33 -1
  33. package/dist/core/mailbox-store.js +42 -2
  34. package/dist/core/runner/assemble-result.d.ts +5 -0
  35. package/dist/core/runner/assemble-result.js +1 -1
  36. package/dist/core/runner/denial-limit-arms.d.ts +149 -0
  37. package/dist/core/runner/denial-limit-arms.js +91 -0
  38. package/dist/core/runner/edited-files-ledger.d.ts +33 -0
  39. package/dist/core/runner/edited-files-ledger.js +14 -0
  40. package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
  41. package/dist/core/runner/prepare-hands-readface.js +1 -0
  42. package/dist/core/runner/prepare-task.d.ts +62 -1
  43. package/dist/core/runner/prepare-task.js +135 -89
  44. package/dist/core/runner/runtask.js +12 -0
  45. package/dist/core/sensitive-path-policy.d.ts +27 -6
  46. package/dist/core/sensitive-path-policy.js +57 -2
  47. package/dist/core/task-notification.d.ts +24 -2
  48. package/dist/core/task-notification.js +6 -1
  49. package/dist/core/tool-policy.d.ts +55 -4
  50. package/dist/core/tool-policy.js +28 -5
  51. package/dist/core/tools.js +1 -0
  52. package/dist/core/types.d.ts +251 -15
  53. package/dist/core/wiring-manifest.d.ts +41 -5
  54. package/dist/core/wiring-manifest.js +8 -0
  55. package/dist/engine/harness/agent-harness.d.ts +1 -0
  56. package/dist/engine/harness/agent-harness.js +3 -0
  57. package/dist/engine/harness/types.d.ts +3 -0
  58. package/dist/engine/loop/agent-loop.d.ts +7 -0
  59. package/dist/engine/loop/agent-loop.js +79 -0
  60. package/dist/engine/loop/types.d.ts +42 -0
  61. package/dist/index.d.ts +12 -6
  62. package/dist/index.js +10 -4
  63. package/dist/internal/harness-types.d.ts +1 -1
  64. package/dist/orchestration/workflow.js +7 -3
  65. package/dist/tools/fs/fs-write.d.ts +4 -4
  66. package/dist/tools/fs/fs-write.js +99 -14
  67. package/dist/tools/fs/index.d.ts +7 -1
  68. package/dist/tools/fs/index.js +1 -1
  69. package/dist/tools/fs/safety.d.ts +29 -8
  70. package/dist/tools/fs/safety.js +11 -1
  71. package/package.json +1 -1
  72. package/test/export-surface.snapshot.json +181 -1
@@ -0,0 +1,91 @@
1
+ import { createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence } from "../auto-mode.js";
2
+ import { deliverEngineNotice } from "../types.js";
3
+ import { inlineUntrusted } from "../untrusted-text.js";
4
+ export function attachRebuiltDenialTrackers(entries, denialLimit) {
5
+ const rebuiltTrackers = new Map();
6
+ const trackerForRebuilt = (decider) => {
7
+ let t = rebuiltTrackers.get(decider);
8
+ if (t === undefined) {
9
+ t = createAutoModeDenialTracker(denialLimit);
10
+ rebuiltTrackers.set(decider, t);
11
+ }
12
+ return t;
13
+ };
14
+ return entries?.map((pc) => pc.autoMode !== undefined && pc.autoMode.denialTracking === undefined
15
+ ? { ...pc, autoMode: { ...pc.autoMode, denialTracking: trackerForRebuilt(pc.autoMode.decider) } }
16
+ : pc);
17
+ }
18
+ export function createDenialLimitStop(opts) {
19
+ const gateStopRef = {};
20
+ const stopForDenialLimit = (info) => {
21
+ if (gateStopRef.terminal !== undefined)
22
+ return;
23
+ const message = `too many classifier denials in headless mode — ${denialLimitSentence(info.fallback)} The run was stopped: ` +
24
+ `the auto-mode classifier's denial limit falls back to a person, and no approver is wired to fall back to ` +
25
+ `(latest blocked action: "${info.toolName}").`;
26
+ const terminal = Object.assign(new Error(message), { code: "classifier.denial_limit" });
27
+ gateStopRef.terminal = terminal;
28
+ deliverEngineNotice(opts.onNotice, {
29
+ code: "classifier.denial_limit",
30
+ message,
31
+ detail: { sessionId: opts.sessionId, runId: opts.runId, toolName: info.toolName, toolCallId: info.toolCallId, consecutive: info.fallback.consecutive, total: info.fallback.total, limit: info.fallback.limit },
32
+ });
33
+ opts.abort();
34
+ };
35
+ return { gateStopRef, stopForDenialLimit };
36
+ }
37
+ export function frozenClassifierExcluded(d) {
38
+ return d.decisionReason === "hook" || d.matchedAskRule !== undefined || d.denialLimitFallback !== undefined;
39
+ }
40
+ export async function judgeInheritedClassifier(opts) {
41
+ const { autoMode, ask, req } = opts;
42
+ if (autoMode === undefined || frozenClassifierExcluded(ask))
43
+ return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false };
44
+ const verdict = await autoMode.decider
45
+ .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
46
+ .catch(() => ({ kind: "unavailable", cause: "error" }));
47
+ if (verdict.kind === "allow") {
48
+ autoMode.denialTracking?.recordAllow();
49
+ return { kind: "allow" };
50
+ }
51
+ if (verdict.kind !== "block")
52
+ return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false };
53
+ const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
54
+ const category = verdict.category ? inlineUntrusted(verdict.category) : "";
55
+ const tracked = autoMode.denialTracking?.recordBlock();
56
+ if (tracked?.limitReached !== true) {
57
+ return {
58
+ kind: "deny",
59
+ result: {
60
+ action: "deny",
61
+ message: `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
62
+ decisionReason: "classifier",
63
+ },
64
+ };
65
+ }
66
+ const fallback = tracked.fallback;
67
+ return {
68
+ kind: "resolve",
69
+ ask: { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback },
70
+ fallback,
71
+ mintedHere: true,
72
+ };
73
+ }
74
+ export function headlessDenyAtFold(r) {
75
+ return r.resolution === "no_approver" || r.resolution === "blanket_allow_refused";
76
+ }
77
+ export function headlessDenyAtRecheck(r) {
78
+ return headlessDenyAtFold(r) || r.resolution === "approver_unavailable" || r.approverUnavailable === true;
79
+ }
80
+ export function settleDenialLimitFallback(opts) {
81
+ const { fallback, resolved } = opts;
82
+ if (fallback === undefined)
83
+ return;
84
+ if (resolved.action === "allow") {
85
+ if (opts.mintedHere)
86
+ opts.tracker?.recordAllow();
87
+ }
88
+ else if (resolved.action === "deny" && opts.headless(resolved)) {
89
+ opts.stop({ toolName: opts.toolName, toolCallId: opts.toolCallId, fallback });
90
+ }
91
+ }
@@ -0,0 +1,33 @@
1
+ import type { FileEditedHook, TaskResult } from "../types.js";
2
+ /** The ceiling on DISTINCT FILES in a run's edited-file ledger (`TaskResult.editedFiles`). A result
3
+ * seat must be bounded by something other than how long the model keeps going; a single host-side
4
+ * user message that touches a thousand distinct files is already past what that observation is for.
5
+ * Far above the delegated-child projection's own cap, which bounds a NOTIFICATION preview rather
6
+ * than a per-message ledger — different job, different ceiling. The field's contract states this
7
+ * number and the saturation behaviour, so a consumer can tell a full list from a clipped one. */
8
+ export declare const RUN_EDITED_FILES_CAP = 1000;
9
+ /** A run's OWN edited-file ledger — the observation behind `TaskResult.editedFiles`. */
10
+ export interface EditedFilesLedger {
11
+ /** The hands band's post-write seat feeds this (the other end of the same mutation lane the
12
+ * first-touch hook sits at). */
13
+ note: FileEditedHook;
14
+ /** The read face: the ledger as `TaskResult.editedFiles`, or `undefined` when this run's hands
15
+ * landed nothing (the key is ABSENT then, never an empty array). A LIVE reader, read at result
16
+ * assembly — so the throw-path backstop terminal sees the same ledger the ordinary assembly does. */
17
+ snapshot: () => TaskResult["editedFiles"];
18
+ }
19
+ /**
20
+ * Build the ledger. Built UNCONDITIONALLY by prepare — unlike the first-touch history hook, which
21
+ * exists only when a history store is wired, this answers "what did this run change", a fact about
22
+ * the run and not about the store. Insertion order = order of first edit; the cap keeps a result seat
23
+ * bounded by something other than the model's persistence (past it, listed paths keep counting and
24
+ * new ones are dropped — see the field's own contract).
25
+ *
26
+ * Identity is the CANONICAL key, display is the model's own spelling. The two differ, and both halves
27
+ * matter: the band resolves a relative argument against the LIVE cwd, so one spelling can name two
28
+ * different files across a `cd` (keying on the spelling would merge them and drop a real file from
29
+ * the ledger), and two spellings can name one file (keying on the spelling would split one file into
30
+ * two rows). The first spelling that reached a given file is what the row shows — the coordinate the
31
+ * transcript used, which is the seat's stated path form.
32
+ */
33
+ export declare function createEditedFilesLedger(cap?: number): EditedFilesLedger;
@@ -0,0 +1,14 @@
1
+ export const RUN_EDITED_FILES_CAP = 1000;
2
+ export function createEditedFilesLedger(cap = RUN_EDITED_FILES_CAP) {
3
+ const counts = new Map();
4
+ return {
5
+ note: (n) => {
6
+ const seen = counts.get(n.key);
7
+ if (seen !== undefined)
8
+ seen.edits += 1;
9
+ else if (counts.size < cap)
10
+ counts.set(n.key, { path: n.path, edits: 1 });
11
+ },
12
+ snapshot: () => (counts.size > 0 ? [...counts.values()].map(({ path, edits }) => ({ path, edits })) : undefined),
13
+ };
14
+ }
@@ -204,6 +204,11 @@ export interface PrepareHandsReadFaceInput {
204
204
  /** design/381 — the first-touch history hook prepare built (present iff a fileHistoryStore is
205
205
  * wired and the run mounts a real fs env); threaded to the write band's trackFileEdit seat. */
206
206
  trackFileEdit?: import("../types.js").TrackFileEditHook;
207
+ /** borrowed — the mutation lane's LANDED observation seat (the accumulator behind
208
+ * `TaskResult.editedFiles`); threaded to the write band's onFileEdited seat. Unlike
209
+ * `trackFileEdit` it does NOT depend on a wired history store: it observes what this run's hands
210
+ * did, so it is present on every run that mounts the band. */
211
+ onFileEdited?: import("../types.js").FileEditedHook;
207
212
  }
208
213
  /** The phase's outputs (相 API 规则件 four-class form) — ALL settled before the return; the driver
209
214
  * binds them as fresh consts (R-5) except the inverted-closure trio and the two shellGated bits,
@@ -313,6 +313,7 @@ export async function prepareHandsMount(input) {
313
313
  reminderMark: input.reminderMark,
314
314
  reminderDisclosureCounts: input.reminderDisclosureCounts,
315
315
  ...(input.trackFileEdit !== undefined ? { trackFileEdit: input.trackFileEdit } : {}),
316
+ ...(input.onFileEdited !== undefined ? { onFileEdited: input.onFileEdited } : {}),
316
317
  includeShell: handsIncludeShell,
317
318
  readOnly: handsReadOnly,
318
319
  ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
@@ -1,7 +1,7 @@
1
1
  import { AgentHarness, type ThinkingLevel } from "../../internal/harness.js";
2
2
  import type { Model } from "../../internal/llm.js";
3
3
  import { type CompactionForkContext } from "../auto-compaction.js";
4
- import { type AutoModeDecider } from "../auto-mode.js";
4
+ import { type AutoModeDecider, type AutoModeDenialTracker } from "../auto-mode.js";
5
5
  import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
6
6
  import { type MaterializedMcp } from "../mcp.js";
7
7
  import { type MaterializedA2a } from "../a2a.js";
@@ -214,6 +214,24 @@ export interface FileHistoryBoundarySeat {
214
214
  begin(entryId: string): void;
215
215
  settle(): Promise<void>;
216
216
  }
217
+ /** The ONE reading of {@link RunInternals.fileHistoryLineage} (see {@link resolveFileHistoryCoordinates}):
218
+ * the lineage's scope iff BOTH tree coordinates match; otherwise the run's own session. */
219
+ export declare function resolveFileHistoryScope(lineage: RunInternals["fileHistoryLineage"], historyRoot: string, historyFs: string, sessionId: string): string;
220
+ /**
221
+ * WHICH filesystem an env is a view of — the second coordinate of tree identity for the file-history
222
+ * lineage, read from what the env's MINTER states rather than inferred from a path:
223
+ * · a remote workspace names itself through its {@link WorkspaceHandle} (provider + sandbox, and the
224
+ * device lane's id when stamped) — two runs on one sandbox share a tree, two sandboxes never do;
225
+ * · an env that DECLARES its paths host-local (`hostLocalPaths: true`), or is the host adapter itself
226
+ * (a {@link NodeExecutionEnv}, which the #211 seam names as the host-local default), is the
227
+ * control-plane host's filesystem — every such env is one tree;
228
+ * · anything else — an env declaring `hostLocalPaths: false`, or an undeclared custom adapter — is
229
+ * attested to nothing, so it is its OWN tree: a per-instance token, which still matches when the
230
+ * child literally holds the parent's env object and never matches a fresh per-task mint.
231
+ * A remote handle that cannot be read (an env not yet connected) falls to the per-instance arm for
232
+ * the same reason: unattested is not shared.
233
+ */
234
+ export declare function fileHistoryFilesystemIdentity(env: ExecutionEnv): string;
217
235
  export interface Prepared {
218
236
  harness: AgentHarness;
219
237
  /** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
@@ -320,6 +338,11 @@ export interface Prepared {
320
338
  /** RB-430-a: prepare-time rewind disclosures (conversation-only branch / no snapshot backend / no file
321
339
  * env), echoed verbatim onto `TaskResult.rewindNotes`. Present only when there is something to say. */
322
340
  rewindNotes?: NonNullable<TaskResult["rewindNotes"]>;
341
+ /** The run's edited-file ledger read face — what its OWN hands landed, as `TaskResult.editedFiles`
342
+ * (undefined when nothing landed: the key is absent, never an empty array). A LIVE reader rather
343
+ * than a snapshot, so the throw-path backstop terminal reports the same ledger the ordinary
344
+ * assembly would. Always present on Prepared; independent of whether a fileHistoryStore is wired. */
345
+ editedFilesSnapshot: () => TaskResult["editedFiles"];
323
346
  /** design/381 — the run's turn-start boundary seat (present iff a fileHistoryStore is wired and
324
347
  * the run mounts a real fs env). runtask calls begin() at the first committed user entry and
325
348
  * awaits settle() at the lease close + the finish tail. */
@@ -725,6 +748,19 @@ export interface Prepared {
725
748
  * the task the typed terminal — the harness turns a loop throw into an error assistant message, so
726
749
  * without this the cause would reach the caller only as the generic `provider.error`. */
727
750
  brainCallGuardrailRef: BrainCallGuardrailRef;
751
+ /**
752
+ * #548 — the tool gate's own TYPED STOP: set (once) when the classifier denial limit was reached with
753
+ * no approver to fall back to (headless), together with the run abort. The run loop adopts it as the
754
+ * terminal `threw` (`TaskResult.errorCode` = the error's `code`, `errorMessage` = its sentence) the
755
+ * same way it adopts the brain-call guardrail's — a loop that ended because THIS lane aborted it must
756
+ * report the cause, not the consequence. The abort-result details seam reads it too, so the aborted
757
+ * call's own `tool_end` carries the code. `undefined` ⇒ no gate stop happened.
758
+ */
759
+ gateStopRef: {
760
+ terminal?: Error & {
761
+ code: string;
762
+ };
763
+ };
728
764
  /** design/80 D-B — set by a tool calling `ctx.requestReview()` (the first-party `present_plan` tool, CC
729
765
  * ExitPlanMode parity): the run loop honors it at the next CLEAN turn boundary by minting a `plan_review`
730
766
  * checkpoint. `{ pending }` is set (with an optional reason) the moment a tool requests review; the boundary
@@ -1269,6 +1305,13 @@ export interface InheritedGate {
1269
1305
  */
1270
1306
  autoMode?: {
1271
1307
  decider: AutoModeDecider;
1308
+ /**
1309
+ * #548 — the ancestor's per-run DENIAL-LIMIT tracker, frozen beside its decider (same owner). The
1310
+ * wrapper arms count the frozen classifier's blocks on it and, at a bound, resolve the fallback
1311
+ * ask at the frozen approver instead of denying (`requiresRealApproval` set, sandbox admission
1312
+ * excluded). Live-only, like the decider: a cross-process redemption starts a fresh count.
1313
+ */
1314
+ denialTracking?: AutoModeDenialTracker;
1272
1315
  /**
1273
1316
  * #503 — the SERIALIZABLE criteria half of this classifier (assembly inputs + knobs + the
1274
1317
  * deployment's settings epoch), present when the arming deployment opted in
@@ -1801,6 +1844,24 @@ export interface RunInternals {
1801
1844
  * parent's cwd instead of an empty per-task sandbox. `isolation: "worktree"` wins over this when both set.
1802
1845
  */
1803
1846
  parentCwd?: string;
1847
+ /**
1848
+ * The spawning run's file-history LINEAGE — the scope it records first-touch edits into and the
1849
+ * TREE those records' keys are minted against (the canonical root spelling + the filesystem
1850
+ * identity of {@link fileHistoryFilesystemIdentity}) — threaded VERBATIM by core delegation
1851
+ * callers from {@link import("../types.js").ToolExecuteContext.fileHistoryLineage} (NEVER a
1852
+ * {@link TaskSpec} field). {@link resolveFileHistoryScope} is the ONE reading: this run records
1853
+ * into the lineage's scope iff its own tree coordinates BOTH equal the lineage's, and then
1854
+ * re-exposes the SAME triple on its own ctx, so every same-tree descendant of a root session — at
1855
+ * any depth — lands in the root session's scope (the fixed point), while a descendant on another
1856
+ * tree (worktree isolation, explicit `cwd`, a fresh per-task sandbox) becomes the root of its own
1857
+ * subtree's history. Absent on a top-level run, on a run with no live history store, and on a
1858
+ * tier-3 revival (the reviver's lineage says nothing about the revived row's tree).
1859
+ */
1860
+ fileHistoryLineage?: {
1861
+ scope: string;
1862
+ root: string;
1863
+ fs: string;
1864
+ };
1804
1865
  /**
1805
1866
  * [c209-D] — the EXPLICIT Agent.cwd request, distinct from the best-effort `parentCwd`
1806
1867
  * inheritance hint above: inheritance may be silently ignored by a factory (or absent without one),