@sema-agent/core 5.53.0 → 5.55.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 (80) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/dist/agents/cumulative-stats.d.ts +26 -0
  3. package/dist/agents/cumulative-stats.js +56 -0
  4. package/dist/agents/observer.d.ts +11 -7
  5. package/dist/agents/observer.js +2 -4
  6. package/dist/agents/subagent.d.ts +4 -0
  7. package/dist/agents/subagent.js +1 -1
  8. package/dist/agents/verify.d.ts +27 -3
  9. package/dist/agents/verify.js +7 -2
  10. package/dist/core/governance-codes.js +14 -0
  11. package/dist/core/hooks.js +1 -1
  12. package/dist/core/lsp-diagnostics.d.ts +19 -17
  13. package/dist/core/lsp-diagnostics.js +11 -5
  14. package/dist/core/mcp.d.ts +214 -5
  15. package/dist/core/mcp.js +315 -30
  16. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  17. package/dist/core/memory-engine/consolidation.js +342 -0
  18. package/dist/core/memory-engine/dual-root.js +3 -0
  19. package/dist/core/memory-engine/engine.d.ts +237 -4
  20. package/dist/core/memory-engine/engine.js +1111 -4
  21. package/dist/core/memory-engine/export-bundle.js +9 -0
  22. package/dist/core/memory-engine/file-backend.js +27 -1
  23. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  24. package/dist/core/memory-engine/frontmatter.js +111 -0
  25. package/dist/core/memory-engine/index.d.ts +4 -2
  26. package/dist/core/memory-engine/index.js +3 -1
  27. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  28. package/dist/core/memory-engine/sync-client.js +26 -0
  29. package/dist/core/memory-engine/tools.d.ts +9 -0
  30. package/dist/core/memory-engine/tools.js +57 -13
  31. package/dist/core/memory-engine/types.d.ts +99 -0
  32. package/dist/core/memory-recall.js +4 -3
  33. package/dist/core/memory.d.ts +33 -3
  34. package/dist/core/memory.js +6 -4
  35. package/dist/core/permission-rule-model.d.ts +140 -21
  36. package/dist/core/permission-rule-model.js +76 -17
  37. package/dist/core/permission-rule-org.d.ts +4 -3
  38. package/dist/core/permission-rule-org.js +12 -3
  39. package/dist/core/permission-rules.d.ts +22 -0
  40. package/dist/core/permission-rules.js +60 -6
  41. package/dist/core/reminder-disclosure.d.ts +29 -4
  42. package/dist/core/reminder-disclosure.js +60 -12
  43. package/dist/core/runner/prepare-memory.js +7 -2
  44. package/dist/core/runner/prepare-task.d.ts +31 -1
  45. package/dist/core/runner/prepare-task.js +31 -14
  46. package/dist/core/runner/runtask.d.ts +8 -1
  47. package/dist/core/runner/runtask.js +12 -10
  48. package/dist/core/runner/session-rule-policy.js +5 -3
  49. package/dist/core/runner/synthetic-tools.js +4 -2
  50. package/dist/core/runner/turn-attachments.d.ts +16 -6
  51. package/dist/core/runner/turn-attachments.js +34 -20
  52. package/dist/core/tool-policy.d.ts +18 -0
  53. package/dist/core/tool-policy.js +19 -8
  54. package/dist/core/types.d.ts +89 -6
  55. package/dist/core/untrusted-egress.js +12 -2
  56. package/dist/core/untrusted-text.d.ts +189 -3
  57. package/dist/core/untrusted-text.js +416 -6
  58. package/dist/engine/loop/types.d.ts +7 -0
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +2 -2
  61. package/dist/orchestration/workflow-types.d.ts +16 -6
  62. package/dist/orchestration/workflow-types.js +10 -4
  63. package/dist/orchestration/workflow.js +32 -6
  64. package/dist/prompts/default.d.ts +12 -2
  65. package/dist/stores/file/background-agent-store.js +1 -0
  66. package/dist/stores/file/checkpoint-store.d.ts +6 -2
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/fs-atomic.d.ts +151 -10
  69. package/dist/stores/file/fs-atomic.js +208 -32
  70. package/dist/stores/file/index.d.ts +26 -3
  71. package/dist/stores/file/index.js +25 -2
  72. package/dist/stores/file/shared-ledger.d.ts +40 -5
  73. package/dist/stores/file/shared-ledger.js +24 -8
  74. package/dist/stores/file/workflow-run-store.d.ts +8 -1
  75. package/dist/stores/file/workflow-run-store.js +1 -0
  76. package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
  77. package/dist/tools/fs/bash-readonly-classifier.js +58 -47
  78. package/dist/tools/fs/index.d.ts +3 -1
  79. package/package.json +1 -1
  80. package/test/export-surface.snapshot.json +30 -1
@@ -85,6 +85,39 @@ export interface McpDroppedTool {
85
85
  tool: string;
86
86
  reason: string;
87
87
  }
88
+ /**
89
+ * The completeness verdict of a walked STANDARD MCP list method (`tools/list`, `resources/list` — see
90
+ * {@link walkMcpListPages}). ABSENT means the walk reached the end of pagination and the aggregated
91
+ * listing IS the server's full list. PRESENT means it is not, and the listing must not be presented —
92
+ * or DIFFED — as if it were: an entry missing from a truncated listing is unproven-absent, not
93
+ * withdrawn (that inference is exactly what made a paginating server's second-page tools read as
94
+ * `removed`).
95
+ * - `page_cap` — {@link MAX_MCP_LIST_PAGES} pages were walked with a cursor still pending;
96
+ * - `cursor_loop` — the server handed back a cursor already used in this walk (a paginator that does
97
+ * not advance); the walk stops rather than spinning to the cap re-appending the same page;
98
+ * - `cursor_invalid` — the server sent a `nextCursor` that is not a string (`null` excepted: that is
99
+ * JSON's absent optional). It cannot be followed and it is not proof of the end, so the walk stops
100
+ * and says so instead of certifying a short listing as complete;
101
+ * - `page_error` — a CONTINUATION page failed. The pages already retrieved are kept and the failure
102
+ * text carried. A FIRST-page failure is never this: it still throws (a caller's "no listing at all"
103
+ * must not degrade into "an empty listing");
104
+ * - `budget_exhausted` — CORE stopped the walk: the time this listing is allowed to spend
105
+ * ({@link walkMcpListPages}'s `budgetMs`) had too little left to fund another request
106
+ * ({@link MIN_MCP_PAGE_BUDGET_MS}), so none was sent. Distinct from `page_error` on purpose — the
107
+ * server neither failed nor was asked. Issuing that request anyway would hand the SDK a
108
+ * near-zero timeout, and its -32001 would arrive here wearing the server's failure clothes.
109
+ * `error` is SERVER-authored text — neutralize it before it rides any model/operator face.
110
+ */
111
+ export interface McpListingIncomplete {
112
+ reason: "page_cap" | "cursor_loop" | "cursor_invalid" | "page_error" | "budget_exhausted";
113
+ /** Pages actually retrieved before the walk stopped (≥ 1 — a zero-page walk throws instead). */
114
+ pages: number;
115
+ error?: string;
116
+ /** The walk's total time budget in ms. Present ONLY on `budget_exhausted`, where it is the whole
117
+ * actionable content of the verdict (which bound to raise); every other reason leaves it absent
118
+ * rather than reporting a budget that had nothing to do with the stop. */
119
+ budgetMs?: number;
120
+ }
88
121
  /** Tools materialized from one or more MCP servers, plus a disposer to disconnect them. */
89
122
  export interface MaterializedMcp {
90
123
  tools: AgentTool[];
@@ -125,10 +158,17 @@ export interface MaterializedMcp {
125
158
  pendingRemovals: string[];
126
159
  };
127
160
  /**
128
- * One entry per server that failed to connect / list its tools and was SKIPPED (fail-open,
129
- * design/29). The task proceeds with the healthy servers' tools a single bad server (missing
130
- * stdio command, unreachable URL) must never brick every task in the scenario. The Runner forwards
131
- * these to `onError(phase:"mcp")`. Empty when every server connected.
161
+ * The OPERATOR warning lane for this materialization (the Runner forwards each entry to
162
+ * `onError(phase:"mcp")`). Empty when every server connected and listed completely. Two populations,
163
+ * distinguishable by the `code` each Error carries a consumer must NOT read mere presence as "this
164
+ * server was skipped":
165
+ * - `mcp.server_unavailable` — a server that failed to connect / list its tools and was SKIPPED
166
+ * (fail-open, design/29): the task proceeds with the healthy servers' tools, because a single bad
167
+ * server (missing stdio command, unreachable URL) must never brick every task in the scenario;
168
+ * - `mcp.listing_incomplete` — a server that DID connect but whose `tools/list` pagination walk did
169
+ * not reach the end ({@link McpListingIncomplete}): its tools past the stopping point are not
170
+ * mounted this task, which is a capability loss the operator must see rather than infer from a
171
+ * short tool list.
132
172
  */
133
173
  warnings: Error[];
134
174
  /**
@@ -186,7 +226,13 @@ export interface MaterializedMcp {
186
226
  dispose: () => Promise<void>;
187
227
  }
188
228
  /** [1605] One per-server entry of {@link MaterializedMcp.refresh}. `added`/`removed` are namespaced
189
- * (`mcp__<server>__<tool>`) names; `tools`/`axes`/`dropped` are present only on `"refreshed"`. */
229
+ * (`mcp__<server>__<tool>`) names; `tools`/`axes`/`dropped` are present only on `"refreshed"`.
230
+ * The model-facing refresh receipt renders each `dropped` entry with ITS OWN `reason` (schema
231
+ * gate, same-listing name collision, …) — never a blanket label; a consumer matching that receipt
232
+ * text should key on the tool name, not on any fixed reason wording. Note the startup-time
233
+ * sibling: two DECLARED servers whose names normalize to one prefix refuse the whole mount at
234
+ * connect (`config.mcp_server_name_collision`, both original spellings named) — a deployment
235
+ * carrying such a pair learns at the first materialize, with the rename spelled out in the error. */
190
236
  export interface McpRefreshResult {
191
237
  server: string;
192
238
  /** The server's namespaced-name prefix (`mcp__<normalized-server>__`) — the consumer's SPLICE
@@ -199,11 +245,37 @@ export interface McpRefreshResult {
199
245
  status: "refreshed" | "not_connected" | "failed" | "revoked";
200
246
  toolCount: number;
201
247
  added: string[];
248
+ /**
249
+ * Names that were mounted before this refresh and are no longer in the roster it returns — i.e. an
250
+ * assertion that the server withdrew them. It is therefore EMPTY whenever {@link listingIncomplete}
251
+ * is present, and empty as a FACT rather than a policy: an incomplete walk cannot prove absence, so
252
+ * it retains what it could not prove gone (see `listingIncomplete`) and nothing leaves. `added` is
253
+ * unaffected — a name that IS in the partial listing was really listed, whether or not the walk
254
+ * finished (#381).
255
+ */
202
256
  removed: string[];
203
257
  tools?: AgentTool[];
204
258
  axes?: McpToolAxis[];
205
259
  dropped?: McpDroppedTool[];
260
+ /**
261
+ * Set on `not_connected` / `failed` / `revoked`; ALSO set on a `"refreshed"` entry whose listing walk
262
+ * did not finish (see {@link listingIncomplete}) — a refresh that produced usable tools is not a
263
+ * failure, so the status stays `refreshed` and the incompleteness rides here.
264
+ */
206
265
  error?: string;
266
+ /**
267
+ * #381 — present when this entry's `tools/list` walk did NOT reach the end of pagination. `tools` is
268
+ * then a MERGE: everything the partial listing did return, plus the previously-mounted entries whose
269
+ * absence it cannot prove (so the consumer's replace-the-whole-prefix swap withdraws nothing, and
270
+ * `removed: []` describes a roster that really lost nothing). Absent on a complete walk and on every
271
+ * non-`refreshed` status.
272
+ *
273
+ * RESIDUAL, honest: the model-facing `RefreshMcpTools` receipt renders a refreshed entry's line from
274
+ * added/removed/excluded/dropped only, so this fact reaches the STRUCTURED face (`details.results`)
275
+ * and the operator, not the receipt text — rendering it there is a prepare-task change, filed rather
276
+ * than smuggled in here.
277
+ */
278
+ listingIncomplete?: McpListingIncomplete;
207
279
  }
208
280
  /** design/99 §E9 — projected per-server MCP status (see {@link MaterializedMcp.statuses}). NOTE: `serverInfo`
209
281
  * and `error` are SERVER-controlled strings (verbatim from the remote) — UNTRUSTED; a consumer rendering them
@@ -416,6 +488,30 @@ interface McpContentItem {
416
488
  };
417
489
  }
418
490
  export declare function mapContent(content: Array<McpContentItem>, serverName?: string, imageResizer?: McpImageResizer): Promise<Array<TextContent | ImageContent>>;
491
+ /**
492
+ * Connect to each MCP server, list its tools, and wrap them as AgentTools.
493
+ * Tools are namespaced `mcp__<server>__<tool>` (CC parity) to avoid collisions. The model sees each
494
+ * tool's real JSON-Schema (`inputSchema`), which the agent loop validates natively.
495
+ *
496
+ * Fail-open (design/29): a server that fails to connect or list tools is SKIPPED — its error is
497
+ * returned in `warnings`, the healthy servers still materialize. A single misconfigured server
498
+ * (bad stdio command, unreachable URL) never bricks the whole task.
499
+ *
500
+ * Call `dispose()` when the task finishes — connections are task-scoped, never persisted.
501
+ */
502
+ /**
503
+ * Invisible / format / private-use / unassigned characters stripped out of every model-facing string a
504
+ * server advertises — CC `MZg` :144247. CC spells this as the property class BELOW followed by five
505
+ * explicit ranges (zero-width U+200B–U+200F, bidi embedding U+202A–U+202E, bidi isolates U+2066–U+2069,
506
+ * the BOM, and the BMP private-use area); on this engine's Unicode data every one of those code points
507
+ * is already `Cf` or `Co`, so the class alone is byte-equivalent to CC's expression and the source stays
508
+ * free of literal invisible characters (a file carrying them is a grep blind spot, and this repo has
509
+ * paid for that once already). The equivalence is asserted by a pin, not assumed — a Unicode-data change
510
+ * that moved any of those ranges out of the class would open exactly the hole this strips.
511
+ *
512
+ * `\p{Cc}` is deliberately NOT here: newlines and tabs are legitimate description formatting.
513
+ */
514
+ export declare const MCP_INVISIBLE_TEXT_RE: RegExp;
419
515
  /** One tool's schema-normalization outcome (CC 220 `zyo` @336616-336654's return shape). */
420
516
  export type McpSchemaNormalizeResult = {
421
517
  outcome: "unchanged";
@@ -465,6 +561,28 @@ export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormal
465
561
  * to catch, and this gate alone does not catch it (no type key ⇒ passes the root-type check; the
466
562
  * combinator's own structure is legal JSON Schema ⇒ passes validateJsonSchemaShape too).
467
563
  */
564
+ /**
565
+ * A schema defect the operator must HEAR about but that must not cost the tool its mount — returns a
566
+ * core-authored sentence, or `undefined` when there is nothing to say.
567
+ *
568
+ * Today that is exactly one thing: a TOP-LEVEL parameter name outside `[a-zA-Z0-9_.-]{1,64}`. A name
569
+ * like `"user name"` is rejected at the Anthropic wire, and because the tool table rides EVERY request
570
+ * it takes the whole request down with it — while the operator previously got nothing at all pointing
571
+ * at the cause (no warning, no drop record, just a task that stopped working). CC checks the identical
572
+ * names against the identical expression (`EX_`/`_xo` :325793/:325824, reported as `check:"propertyKey"`).
573
+ *
574
+ * WHY AN ADVISORY AND NOT A DROP, stated because the sibling structural gate above does drop: CC's own
575
+ * disposition here is keep-and-warn — its drop arm sits behind a rollout flag that ships OFF (`Xyd` /
576
+ * `Yyd` :354641/:354622, an empty remote-config list ⇒ false), so CC's shipped behavior is to keep the
577
+ * tool and warn "requests that include it may fail". The difference from the structural half is real
578
+ * and not a technicality: a schema with no object root is unusable everywhere, whereas this charset is
579
+ * ONE provider's rule, and this engine is bring-your-own-model. Dropping here would delete a working
580
+ * tool from a deployment whose provider accepts the name — a Chinese- or Japanese-named parameter is
581
+ * the ordinary case, not a hostile one. Detection was the gap; removal was never the mandate.
582
+ *
583
+ * TOP-LEVEL only, like CC: a nested property name is not what the provider validates.
584
+ */
585
+ export declare function mcpToolSchemaAdvisory(schema: unknown): string | undefined;
468
586
  export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
469
587
  export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer, // design/116 CONFIRM-1 seam: deployment-injected; default = auto-detected sharp
470
588
  reminderDisclosure?: {
@@ -490,6 +608,97 @@ export declare function applyCallerAxisOverride(name: string, hint: McpToolAxis
490
608
  * for the discrimination nails; the message is SERVER-authored and is only ever classified here, never
491
609
  * trusted as instructions. */
492
610
  export declare function classifyDirReadInvalidParams(message: string): "not_found" | "not_directory";
611
+ /**
612
+ * The content key {@link walkMcpListPages}'s cursor-loop arm uses to recognize an entry it already
613
+ * collected: the entry's own JSON, which is what the server sent and what a re-served page repeats
614
+ * verbatim. `undefined` for anything unserializable — an entry that cannot be keyed is never treated as
615
+ * a repeat, so the failure direction is "kept, and disclosed by the lane that judges it" rather than
616
+ * "silently gone".
617
+ *
618
+ * NOT a semantic identity: two spellings of the same entry (different key order, a description the
619
+ * server re-rendered) read as different entries here and both survive. That is the safe direction —
620
+ * this key only ever authorizes a DELETION, so it must be exact and never clever.
621
+ *
622
+ * Exported for the walker's own unit pins (same reason {@link walkMcpListPages} is): its bound is a
623
+ * behavior, and a pin that re-declared the bound locally would pass while the real one drifted.
624
+ */
625
+ export declare function listEntryFingerprint(entry: unknown): string | undefined;
626
+ /**
627
+ * #381 — walk a paginated MCP list method to the END (`{cursor?}` → `{items, nextCursor?}`), bounded.
628
+ *
629
+ * Every spec-standard MCP list method is paginated the same way: absence of `nextCursor` means "that
630
+ * was the last page". A single request therefore returns the WHOLE list only for servers that choose
631
+ * not to paginate — and a server may start paginating at any time, with no protocol change and no
632
+ * malice, the moment its list outgrows its own page size. Treating page one as the complete list is
633
+ * both a capability loss (the rest is never mounted) and, at any call site that DIFFS two listings, an
634
+ * active falsehood about the server's inventory.
635
+ *
636
+ * Contract:
637
+ * - page ONE is fetched with the caller's own historic parameters (`cursor === undefined`), so a
638
+ * non-paginating server's wire traffic is byte-identical to before;
639
+ * - a FIRST-page failure propagates unchanged — every caller's failure shape (skipped server,
640
+ * `failed` refresh, per-server attributed aggregate error) is built on that throw;
641
+ * - a CONTINUATION failure keeps the pages already retrieved and reports {@link McpListingIncomplete}
642
+ * (`page_error`) — fail-open WITH disclosure, the same law {@link readDirViaExtension} follows —
643
+ * except an ABORT, which still propagates (a cancelled walk is not a short listing);
644
+ * - a repeated cursor stops the walk (`cursor_loop`) instead of spinning to the cap, AND undoes the
645
+ * entries that page re-served (a paginator that does not advance is typically one answering with a
646
+ * page already collected, and keeping it is how one entry gets listed twice) — judged by
647
+ * `identityOf` where the caller can key its entries, whole-page otherwise; an unreadable cursor
648
+ * stops the walk as `cursor_invalid` rather than passing for the end of the listing;
649
+ * - {@link MAX_MCP_LIST_PAGES} pages with a cursor still pending stops it too (`page_cap`);
650
+ * - a remainder too short to fund a real request stops it as `budget_exhausted` — core's own doing,
651
+ * named as such (see {@link MIN_MCP_PAGE_BUDGET_MS}).
652
+ * SIZE, stated rather than assumed: a single page has never been bounded here (a server could always
653
+ * answer with an arbitrarily long list), and walking multiplies that existing exposure by at most the
654
+ * page cap — a bounded multiplier, not a new unbounded surface. Per-entry admission stays where it
655
+ * already is (the intake schema gate, which also drops a name a later page repeats).
656
+ *
657
+ * TIME, the reason `budgetMs` exists (codex review, confirmed): the page cap bounds REQUESTS, not
658
+ * latency. Giving every page its own full request timeout would let a server that stalls each page hold
659
+ * a connect (or a refresh) for up to twenty timeouts — twenty minutes at the SDK's 60s default, where
660
+ * an unpaginated listing cost one. So the WALK gets the budget a single listing used to have: page one
661
+ * is issued with the full amount (byte-identical to the pre-walk request) and each continuation gets
662
+ * only what is left. ACCEPTED COST, stated: a genuinely slow server whose pages together outlast that
663
+ * bound is truncated where a single-request listing would not have been — but that truncation is
664
+ * DISCLOSED, while the alternative is an undisclosed twenty-fold latency ceiling on the task's startup
665
+ * path. The bound is the caller's own configured request timeout, not a new knob, so a deployment that
666
+ * needs more already has the dial. `fetchPage` receives the remaining budget and MUST spend it as that
667
+ * request's timeout — the deadline is cooperative, since only the caller can pass a timeout to its own
668
+ * transport — and must REPORT that same number if the request then times out: a page cut off at 3ms
669
+ * whose failure text names the whole listing budget describes a wait that never happened.
670
+ *
671
+ * NOT merged with {@link readDirViaExtension}, which walks the same way but owns CC-anchored
672
+ * first-page `InvalidParams` discrimination (not-a-directory vs not-found) and its own three-state
673
+ * flags; folding it in here would put that CC semantics into the standard-method path where it has no
674
+ * meaning.
675
+ *
676
+ * Exported for the walker's own unit pins — its arms are failure/deadline states that no fixture server
677
+ * can drive deterministically. Every production consumer goes through the three call sites in this file.
678
+ */
679
+ export declare function walkMcpListPages<T>(fetchPage: (cursor: string | undefined, remainingMs: number) => Promise<{
680
+ items: T[];
681
+ nextCursor?: string;
682
+ cursorInvalid?: boolean;
683
+ }>, opts: {
684
+ budgetMs: number;
685
+ onPage?: () => void;
686
+ signal?: AbortSignal;
687
+ /** A key for one entry's CONTENT — equal keys mean the same listing entry, served again.
688
+ * `undefined` for an entry the caller cannot key. Used ONLY by the cursor-loop arm, to tell a
689
+ * re-served page from a page that merely arrived with a broken cursor; absent ⇒ that arm cannot
690
+ * tell them apart and falls back to dropping the whole page (see it for the trade).
691
+ *
692
+ * CONTENT, not name: an entry's name is what the CALLERS' admission gates key on, and they judge
693
+ * each entry on its own (a tool whose schema they refuse is dropped, and a later entry of the same
694
+ * name is then free to mount). Keying this on the name would let this walk delete the second
695
+ * entry before any of that ran — the only admissible definition of a tool, removed on the theory
696
+ * that a name cannot appear twice, which is a rule the layer below owns and applies differently. */
697
+ fingerprintOf?: (item: T) => string | undefined;
698
+ }): Promise<{
699
+ items: T[];
700
+ incomplete?: McpListingIncomplete;
701
+ }>;
493
702
  /**
494
703
  * RB-408 G12 — `tools/call` result parse that accepts the MCP `2026-07-28` widening of
495
704
  * `structuredContent` from "a JSON object" to ANY JSON value (the same revision that relaxes