agentfootprint 9.31.0 → 9.33.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 (144) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +4 -1
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/hosting/firestoreSessions.js +809 -0
  5. package/dist/adapters/hosting/firestoreSessions.js.map +1 -0
  6. package/dist/adapters/identity/jwks.js +18 -0
  7. package/dist/adapters/identity/jwks.js.map +1 -1
  8. package/dist/adapters/types.js.map +1 -1
  9. package/dist/artifacts/gcsArtifacts.js +20 -0
  10. package/dist/artifacts/gcsArtifacts.js.map +1 -1
  11. package/dist/esm/adapters/hosting/firestoreSessions.d.ts +470 -0
  12. package/dist/esm/adapters/hosting/firestoreSessions.js +799 -0
  13. package/dist/esm/adapters/hosting/firestoreSessions.js.map +1 -0
  14. package/dist/esm/adapters/identity/jwks.d.ts +18 -0
  15. package/dist/esm/adapters/identity/jwks.js +18 -0
  16. package/dist/esm/adapters/identity/jwks.js.map +1 -1
  17. package/dist/esm/adapters/types.d.ts +30 -2
  18. package/dist/esm/adapters/types.js.map +1 -1
  19. package/dist/esm/artifacts/gcsArtifacts.d.ts +20 -0
  20. package/dist/esm/artifacts/gcsArtifacts.js +20 -0
  21. package/dist/esm/artifacts/gcsArtifacts.js.map +1 -1
  22. package/dist/esm/events/payloads.d.ts +36 -0
  23. package/dist/esm/events/registry.d.ts +3 -1
  24. package/dist/esm/events/registry.js +2 -0
  25. package/dist/esm/events/registry.js.map +1 -1
  26. package/dist/esm/hosting/admission.d.ts +25 -0
  27. package/dist/esm/hosting/admission.js +25 -0
  28. package/dist/esm/hosting/admission.js.map +1 -1
  29. package/dist/esm/hosting/envelope.d.ts +10 -0
  30. package/dist/esm/hosting/envelope.js +10 -0
  31. package/dist/esm/hosting/envelope.js.map +1 -1
  32. package/dist/esm/hosting/index.d.ts +1 -0
  33. package/dist/esm/hosting/index.js.map +1 -1
  34. package/dist/esm/hosting/ingressRecord.d.ts +177 -0
  35. package/dist/esm/hosting/ingressRecord.js +246 -0
  36. package/dist/esm/hosting/ingressRecord.js.map +1 -0
  37. package/dist/esm/hosting/standingAgent.js +50 -1
  38. package/dist/esm/hosting/standingAgent.js.map +1 -1
  39. package/dist/esm/hosting/types.d.ts +31 -0
  40. package/dist/esm/hosting/types.js.map +1 -1
  41. package/dist/esm/hosting-providers.d.ts +22 -0
  42. package/dist/esm/hosting-providers.js +27 -0
  43. package/dist/esm/hosting-providers.js.map +1 -1
  44. package/dist/esm/lib/mcp/gatewayTransport.d.ts +52 -0
  45. package/dist/esm/lib/mcp/gatewayTransport.js +1 -0
  46. package/dist/esm/lib/mcp/gatewayTransport.js.map +1 -1
  47. package/dist/esm/lib/mcp/mcpClient.js +8 -1
  48. package/dist/esm/lib/mcp/mcpClient.js.map +1 -1
  49. package/dist/esm/lib/mcp/types.d.ts +10 -0
  50. package/dist/esm/observe.js +2 -1
  51. package/dist/esm/observe.js.map +1 -1
  52. package/dist/esm/recorders/core/ResilienceRecorder.d.ts +8 -7
  53. package/dist/esm/recorders/core/ResilienceRecorder.js +9 -7
  54. package/dist/esm/recorders/core/ResilienceRecorder.js.map +1 -1
  55. package/dist/esm/recorders/core/resilienceHooks.d.ts +1 -0
  56. package/dist/esm/recorders/core/resilienceHooks.js +13 -0
  57. package/dist/esm/recorders/core/resilienceHooks.js.map +1 -1
  58. package/dist/esm/resilience/withCircuitBreaker.d.ts +27 -7
  59. package/dist/esm/resilience/withCircuitBreaker.js +53 -19
  60. package/dist/esm/resilience/withCircuitBreaker.js.map +1 -1
  61. package/dist/esm/resilience/withFallback.d.ts +11 -0
  62. package/dist/esm/resilience/withFallback.js +11 -0
  63. package/dist/esm/resilience/withFallback.js.map +1 -1
  64. package/dist/esm/resilience/withRetry.d.ts +12 -0
  65. package/dist/esm/resilience/withRetry.js +12 -0
  66. package/dist/esm/resilience/withRetry.js.map +1 -1
  67. package/dist/esm/security/PermissionPolicy.d.ts +22 -0
  68. package/dist/esm/security/PermissionPolicy.js +22 -0
  69. package/dist/esm/security/PermissionPolicy.js.map +1 -1
  70. package/dist/events/registry.js +2 -0
  71. package/dist/events/registry.js.map +1 -1
  72. package/dist/hosting/admission.js +25 -0
  73. package/dist/hosting/admission.js.map +1 -1
  74. package/dist/hosting/envelope.js +10 -0
  75. package/dist/hosting/envelope.js.map +1 -1
  76. package/dist/hosting/index.js.map +1 -1
  77. package/dist/hosting/ingressRecord.js +250 -0
  78. package/dist/hosting/ingressRecord.js.map +1 -0
  79. package/dist/hosting/standingAgent.js +50 -1
  80. package/dist/hosting/standingAgent.js.map +1 -1
  81. package/dist/hosting/types.js.map +1 -1
  82. package/dist/hosting-providers.js +34 -1
  83. package/dist/hosting-providers.js.map +1 -1
  84. package/dist/lib/mcp/gatewayTransport.js +1 -0
  85. package/dist/lib/mcp/gatewayTransport.js.map +1 -1
  86. package/dist/lib/mcp/mcpClient.js +8 -1
  87. package/dist/lib/mcp/mcpClient.js.map +1 -1
  88. package/dist/observe.js +2 -1
  89. package/dist/observe.js.map +1 -1
  90. package/dist/recorders/core/ResilienceRecorder.js +9 -7
  91. package/dist/recorders/core/ResilienceRecorder.js.map +1 -1
  92. package/dist/recorders/core/resilienceHooks.js +13 -0
  93. package/dist/recorders/core/resilienceHooks.js.map +1 -1
  94. package/dist/resilience/withCircuitBreaker.js +53 -19
  95. package/dist/resilience/withCircuitBreaker.js.map +1 -1
  96. package/dist/resilience/withFallback.js +11 -0
  97. package/dist/resilience/withFallback.js.map +1 -1
  98. package/dist/resilience/withRetry.js +12 -0
  99. package/dist/resilience/withRetry.js.map +1 -1
  100. package/dist/security/PermissionPolicy.js +22 -0
  101. package/dist/security/PermissionPolicy.js.map +1 -1
  102. package/dist/types/adapters/hosting/firestoreSessions.d.ts +471 -0
  103. package/dist/types/adapters/hosting/firestoreSessions.d.ts.map +1 -0
  104. package/dist/types/adapters/identity/jwks.d.ts +18 -0
  105. package/dist/types/adapters/identity/jwks.d.ts.map +1 -1
  106. package/dist/types/adapters/types.d.ts +30 -2
  107. package/dist/types/adapters/types.d.ts.map +1 -1
  108. package/dist/types/artifacts/gcsArtifacts.d.ts +20 -0
  109. package/dist/types/artifacts/gcsArtifacts.d.ts.map +1 -1
  110. package/dist/types/events/payloads.d.ts +36 -0
  111. package/dist/types/events/payloads.d.ts.map +1 -1
  112. package/dist/types/events/registry.d.ts +3 -1
  113. package/dist/types/events/registry.d.ts.map +1 -1
  114. package/dist/types/hosting/admission.d.ts +25 -0
  115. package/dist/types/hosting/admission.d.ts.map +1 -1
  116. package/dist/types/hosting/envelope.d.ts +10 -0
  117. package/dist/types/hosting/envelope.d.ts.map +1 -1
  118. package/dist/types/hosting/index.d.ts +1 -0
  119. package/dist/types/hosting/index.d.ts.map +1 -1
  120. package/dist/types/hosting/ingressRecord.d.ts +178 -0
  121. package/dist/types/hosting/ingressRecord.d.ts.map +1 -0
  122. package/dist/types/hosting/standingAgent.d.ts.map +1 -1
  123. package/dist/types/hosting/types.d.ts +31 -0
  124. package/dist/types/hosting/types.d.ts.map +1 -1
  125. package/dist/types/hosting-providers.d.ts +22 -0
  126. package/dist/types/hosting-providers.d.ts.map +1 -1
  127. package/dist/types/lib/mcp/gatewayTransport.d.ts +52 -0
  128. package/dist/types/lib/mcp/gatewayTransport.d.ts.map +1 -1
  129. package/dist/types/lib/mcp/types.d.ts +10 -0
  130. package/dist/types/lib/mcp/types.d.ts.map +1 -1
  131. package/dist/types/observe.d.ts.map +1 -1
  132. package/dist/types/recorders/core/ResilienceRecorder.d.ts +8 -7
  133. package/dist/types/recorders/core/ResilienceRecorder.d.ts.map +1 -1
  134. package/dist/types/recorders/core/resilienceHooks.d.ts +1 -0
  135. package/dist/types/recorders/core/resilienceHooks.d.ts.map +1 -1
  136. package/dist/types/resilience/withCircuitBreaker.d.ts +27 -7
  137. package/dist/types/resilience/withCircuitBreaker.d.ts.map +1 -1
  138. package/dist/types/resilience/withFallback.d.ts +11 -0
  139. package/dist/types/resilience/withFallback.d.ts.map +1 -1
  140. package/dist/types/resilience/withRetry.d.ts +12 -0
  141. package/dist/types/resilience/withRetry.d.ts.map +1 -1
  142. package/dist/types/security/PermissionPolicy.d.ts +22 -0
  143. package/dist/types/security/PermissionPolicy.d.ts.map +1 -1
  144. package/package.json +5 -1
package/AGENTS.md CHANGED
@@ -475,7 +475,7 @@ Resilience decorators live on the `agentfootprint/resilience` subpath
475
475
  (not the main barrel). Each preserves the `LLMProvider` interface and
476
476
  stacks freely.
477
477
 
478
- ### Observability — 91 typed events across 21 domains
478
+ ### Observability — 92 typed events across 21 domains
479
479
 
480
480
  ```typescript
481
481
  agent.on('agentfootprint.context.injected', (e) =>
package/CLAUDE.md CHANGED
@@ -45,6 +45,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
45
45
  - **Durable store** (8.9.0): `sqliteVectorStore` follows `hosting/sqliteSessions` line for line — lazy `node:sqlite`, WAL read-back on `journalMode`, STRICT tables, schema-identity + schema-version refusals, `':memory:'` refused. TWO things it adds that have no precedent there: `putMany`/`putIfVersion`/`forget` wrap in a transaction (sqliteSessions has none), and the EMBEDDER FINGERPRINT (`'<id>@<dims>'`, one per namespace in `af_index_meta`) is refused at write AND query. `SqliteUnavailableError` is now ONE class in `lib/sqliteUnavailable.ts` re-exported by both doors — a second class of that name is a duplicate type the build refuses.
46
46
  - **Retrieval rule** (8.8.0): implement `RetrievalStrategy` (memory/retrieval/types.ts) — `select(pool) → verdict[]`, one verdict per candidate, order preserved; it never touches the store and never embeds. Pass as `defineRAG({retrieval})`. `topK()` is the only shipped one; rerank/MMR are named-but-deferred adapters behind the same interface. `TopKStrategy` is a UNION whose arms exclude (`{topK,threshold}` vs `{retrieval}`) — refused in the type AND at runtime, because two spellings of one rule can disagree.
47
47
  - **Injection/skill**: `Injection = {id, flavor, trigger, inject}` (lib/injection-engine/types.ts:161); trigger is a closed 4-variant union (:30 — new kind edits evaluator.ts:40-74 switch). Factories defineSkill etc.; skill graph via `skillGraph()` (skillGraph.ts:392) with pluggable `EntryScorer` (entryScorer.ts:64). `SkillGraphConfig` is a UNION (flat arm `start`/`steps` vs tree arm) — the contradictions it encodes are ALSO refused at build (`.tree()` + `.entry()`/`.route()`, a non-leaf in `skills[]` under a tree, two skills claiming one id, a second `.skillGraph()` on one agent).
48
+ - **MCP gateway fetch seam (9.32.0)**: `GatewayTransportOptions.fetch?: FetchLike` → `McpGatewayTransport.fetch` → `createVendingFetch(t, t.fetch)` (mcpClient.ts). Composition ORDER is the feature: the credential is vended and applied FIRST, then the consumer's fetch runs — so an mTLS agent / DPoP signer sees the final headers and has the last word while per-request vending survives. Zero vendor code lands here, ever; the secrecy-invariant test extends over the injected fetch. Absent ⇒ `createVendingFetch`'s default global fetch, byte-identical.
48
49
  - **Ports table** (wired via AgentOptions): PermissionChecker (adapters/types.ts:403), PricingTable (:412), CredentialProvider (identity/types.ts:89), CacheStrategy (cache/types.ts:151, registerCacheStrategy), ThinkingHandler (thinking/types.ts:114 — auto-wire scans HARDCODED SHIPPED_THINKING_HANDLERS, registry.ts:26), ReliabilityConfig (reliability/types.ts:183), OutputSchemaParser (core/outputSchema.ts:62, duck-typed).
49
50
  - **Run input** (8.18.0): `src/core/runInput.ts` is THE door — `normalizeRunInput(input, 'Agent.run')` adapts a bare string to `{ message }` and refuses everything else with `InvalidRunInputError` (incl. empty/whitespace). Called at the top of every runner's `run()` (Agent, LLMCall, Sequence, Parallel, Conditional, Loop, LlmRouter); `Runner.run`/`RunnerBase.run` declare `TIn | string` so implementations stay assignable (a widened param on a METHOD breaks the bivariance that made `Runner<object,…>` accept them). A new runner adds one line, not a second policy.
50
51
  - **Message content integrity** (8.18.0): a turn with no text is stopped at its SOURCE, never at the crash line (`buildMessagesSlot` `truncate`). Sources: `validators.safeStringify` (now total; `undefined`→`NO_TOOL_VALUE`), toolCalls' pauseHere-resume branch (`PauseAnswerRequiredError`), `runMessageChain` (non-text `allow` AND `ask` → denial), `Agent.assertDeliverableRoles` (declared injections), `validateCheckpoint` (per-message), `callLLM` (provider chunk contract). `buildMessagesSlot.assertComposable` is the NET — it names position/role/origin and deliberately does not coerce.
@@ -54,6 +55,8 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
54
55
  - **Identity verifier (9.26.0)**: `IdentityVerifier` = `{ verify(token) → VerifiedIdentity }` (hosting/identityVerification.ts) wired at `standingAgent({ identity: { verify, allowAnonymous? } })`. Runs ONCE at the top of the handler, BEFORE `identityForRequest` composes scope, and its answer is shared by all three doors (turn / artifact / session-op) — a later door cannot be the lenient one. Extraction is `bearerToken(headers)` (the ONE reader; every dialect normalizes onto `authorization: Bearer`). Refusals carry `IdentityFailureClass` and NEVER the token (`sdkFailure` law); a key set that could not be FETCHED is `VerifierUnavailableError`/503, not 401. One adapter: `jwksIdentity` (adapters/identity/jwks.ts) over `jose` (optional peer, ESM-only ⇒ dynamic-import-then-lazyRequire, structural `JoseBackend` seam), mapping by `err.code` STRING not `instanceof` (the classes are not on jose's main entry; codes are pinned by test/adapters/identity/jwks.test.ts against the real package).
55
56
  - **Admission policy (9.26.0)**: `AdmissionPolicy` = `{ decide(ctx) → 'allow' | {queue:true} | {refuse} }` (hosting/admission.ts) at `standingAgent({ admission })`, consulted once per turn BEFORE lane selection. Fed by `spendLedger()` — an in-process rolling window keyed by `spendKeyFor(identity)`, filled from `stream.llm_end` (tokens) + `cost.tick` (usd, ABSENT when no pricing table). Turns counted at ADMISSION, not completion. `{queue:true}` threads `queueAnyway` into `serialize` (bypasses the `'reject'` collision check only). Built-in: `turnsPerHour`. Absent ⇒ no ledger, no listeners, zero delta.
56
57
  - **Session-history ops (9.26.0)**: a SECOND domain on the wire's `op` field. `hosting/wireOps.ts` is the ONE list (`WIRE_OPS`/`isWireOp`/`refuseUnknownWireOp`); each domain reader DECLINES the other's ops and both raise the same unknown-op refusal, so ordering between `readArtifactWireOp` and `readSessionWireOp` is not load-bearing. `SessionLifecycle` gained OPTIONAL `listByUser?`/`ownerOf?` (feature-detected; absent ⇒ `SessionIndexUnavailableError`, never an empty list). Ownership is DERIVED by `envelopeOwner(envelope)` at persist time — `persist` takes no owner — and WRITE-ONCE in both stores (memory: `!owners.has(id)`; sqlite: `owner = COALESCE(<table>.owner, excluded.owner)` — reversing that argument order transfers sessions by writing). Cross-user transcript = `SessionNotFoundError`, identical to missing. Both ops REQUIRE `identity` configured. **The ORDINARY TURN door asks the same question** (`mayOpenSession` in standingAgent, right after `hydrate` and before the strict readers, covering the `decision`/resume path too): with a verifier configured, `envelopeOwner(stored) === verified?.userId` or the same `SessionNotFoundError` — anything less made the transcript 404 decoration in front of a door that hydrated the same conversation into a model AND re-stamped its owner. No verifier ⇒ the check does not run (zero delta). Consequences stated in the option doc: pre-verification sessions name no owner and cannot be continued at a verifying door, and a refused turn does reveal that an id is taken. sqliteSessions added `owner`/`message_count` columns + an index WITHOUT a schema bump (older readers name their four columns explicitly; `SESSIONS_COLUMNS` stays the original four so a 9.25 file still opens, migrated by ALTER TABLE).
58
+ - **Ingress record (9.32.0)**: `standingAgent({ onIngressDecision })` — ONE record per DOOR decision, filed when the reply reaches its terminal. The funnel is ONE place: `handler` wraps the host's reply once (`beginIngress`/`IngressNote.watch`, hosting/ingressRecord.ts) and `answerRequest` is the old body verbatim, so no exit path reports and none added later can forget. Optional terminals are spread CONDITIONALLY on the wrapper (`awaiting`/`artifact`/`sessions`) — defining one the host lacks would tell the composer it can describe a pause it cannot and kill the `PauseNotCarriedError` path. The record carries classes + identifiers only: no token, no header, no claim set, **no error message** (only `code`/`name` — the `sdkFailure` law). `userId` is the PROVEN id, never a claimed one. It covers what a consumer's own `verify`/`decide` cannot see (missing bearer, `VerifierUnavailableError`, cross-owner 404) and deliberately NOT a body the transport refused first (`InvalidWireOpError` is raised inside `httpHost`'s `readRequest`, before the handler exists — stated on the type, never implied). **It is a STREAM, not `auditExport()`'s chain**, and saying otherwise would make it the failure it closes. Zero-cost unset: no wrapper object, the host's own reply.
59
+
57
60
  - **Recordings as artifacts (9.26.0)**: `artifacts: { store, recordings: true | { label } }`. `Agent.startRunRecording()` calls the SAME `recordRun` (before `createExecutor` — `attach()` collects for the executor not yet built); `fileRunRecording` mints AFTER `finalizeResult`, awaited, every failure contained to `artifacts.refused`. Pure half in artifacts/recordingArtifact.ts; payload is the recording's JSON TEXT (a live snapshot handed to an in-process store would be a live view into a finished run). No new wire op — `artifact-get` serves it.
58
61
  - **Code staging-in (9.26.0)**: `CodeSession.stageInputs?(inputs) → StagedCodeInput[]` — OPTIONAL, feature-detected via `canStageCodeInputs`, and its contract is TWO promises: the payloads are readable at the returned paths, AND every later `execute` exposes the manifest as `STAGED_INPUTS_ENV` (`AF_STAGED_INPUTS`, `name → path`). `CodeInput.name` is the MANIFEST KEY (the wants arg name, so a static description can name it) and `fileName` is the on-disk name — separate fields so the two cannot drift. `codeRunnerTool({ wants })` composes the schema properties + the description clause and refuses BY NAME on a non-staging runner. Implemented by localCodeRunner only.
59
62
  - **Repeated-call nudge (9.26.0)**: `core/agent/repeatedCall.ts` (pure `noteRepeatedCall` + `repeatedCallLedgers()`) + the ONE batch-loop hook in toolCalls. Fingerprints (FNV-1a) of stable-stringified args and the tool's OWN delivered result — never values. **The counters are NOT tracked state**: they live in a bounded run-keyed map held by `buildToolCallsHandler` and keyed by `deps.currentRun().runId`, so a turn that repeats nothing is byte-identical in state, commit log, narrative and recordings (a scope key would have changed all four for every agent that merely upgraded); the repeat itself rides `agentfootprint.tools.repeated_call`, the emit channel per-attempt facts belong on. A resume mints a new runId ⇒ counting restarts. Fires at the SECOND identical landing, once. `AgentOptions.repeatedCallNudge: false` disables (threaded value-conditionally; ON is the default). Deliberately NOT applied on the pause-resume dispatch paths.
@@ -66,7 +69,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
66
69
  - **Embedder fingerprint** (8.9.0) → `Embedder.id` (optional; every shipped embedder sets one, and NONE include dims — the store appends `@<dims>` itself, so an id carrying its own size double-stamps) + `indexDocuments` defaulting `embedderId` to it + `SqliteVectorStore.reconcileFingerprint` (the only comparison site). Rule: dimensions ALWAYS decide, model ids decide only when BOTH sides named themselves — refusing on an absent name would block the majority of callers who never pass `embedderId`.
67
70
  - **Retrieval record** (8.8.0) → FOUR stages write one object in sequence: `loadRelevant` (candidates+scores+threshold verdicts) → `pickByBudget` (re-marks admitted→over-budget/over-max-entries) → `formatDefault` (`promptFragment` + `promptPosition`) → the read mount's outputMapper lifts it to root as `retrievalEvidence_<id>`. `memoryRecallInjections` then splits ONE recall into one ActiveInjection PER CHUNK — guarded by a byte-equality check (`fragments.join('\n\n') === systemContent`) that falls back to the single injection rather than change the prompt. `rank` (score order) and `promptPosition` (picker order) are DIFFERENT and both load-bearing: joining fragments in rank order reproduces the right bytes in a sequence the model never saw.
68
71
  - **AgentState** → all 8 stages/ files, both builders' mappers, memory-wire STRING-TYPED keys ('runIdentity'/'turnNumber'/… buildAgentChart.ts:177-180 — not refactor-safe), finalizeResult's `reliabilityFail*`/`policyHalt*` reads (rename silently kills the typed errors).
69
- - **events/** → 91 typed events across 21 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
72
+ - **events/** → 92 typed events across 21 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
70
73
  - **adapters/types.ts LLMMessage/LLMRequest** → 62 importers: tool_use round-trip (toolCalls.ts:115-135), wire assembly (callLLM.ts:150-160), providers, cache strategies, security/extractSequence, reliability loop.
71
74
  - **Cache** → strategy registration is a MODULE SIDE EFFECT (src/index.ts:15-17); an entry point skipping that import silently falls back to NoOp. Resolved once per Agent at construction (Agent.ts:347).
72
75
  - **Injection engine eval semantics** → Evaluate stage cursor keystone (buildInjectionEngineSubflow.ts:214-222), Route stage MIRRORS slot filters (keep in sync with buildSystemPromptSlot), read_skill gate (toolCalls.ts:747). The gate's allowed set is TWO kinds of id (8.4.0): a HOP (`deps.allowedSkillIds` = `graph.reachableSkills(cursor)` — activates AND moves the cursor) ∪ an OPEN skill (`deps.openSkillIds` = `Agent.openSkillIds()`, Agent.ts:1052 — a registered skill whose trigger is `llm-activated` AND that the graph declares no incoming edge to; activates, never moves the cursor). Both clauses are load-bearing: the trigger test is "read_skill can really activate this", the edge test keeps a bare model edge `.route(a,m)` from-gated. **8.5.0** extends the SAME trigger test three ways: a decision `tree()` returns `[]` from `reachableSkills` (leaves compile to `rule` triggers, so a pick could only be accepted then dropped — the gate refuses with a tree-specific message, `deps.skillGraphIsTree`, derived in AgentBuilder from `graph.nodes.some(kind==='predicate')`); `surfaceMode:'tool-only'` is refused at agent build for any skill whose trigger is not `llm-activated` (skillBodyDelivery.ts — the read_skill tool result is the only channel that mode has, and the graph never calls read_skill); and `read_skill`'s DESCRIPTION is rebuilt per iteration from `reachableSkills(cursor) ∪ open` (Agent.readSkillOfferFor → buildToolsSlot's `readSkillFor`). **The read_skill ENUM must stay the full catalog** — `toolArgValidation` defaults to `'enforce'` and runs at toolCalls.ts:570, BEFORE the gate at :765, and sets `error = true` which the gate skips; narrowing the enum silently retires the teaching refusal, `skill.rejected`, routeRecorder's rejection hops and the rejected-cap governor.
@@ -379,7 +379,7 @@ if (isPaused(result)) {
379
379
  }
380
380
  ```
381
381
 
382
- ## Observability — 91 typed events × 21 domains
382
+ ## Observability — 92 typed events × 21 domains
383
383
 
384
384
  ```typescript
385
385
  agent.on('agentfootprint.context.injected', (e) =>