@arnilo/prism 0.0.1 → 0.0.2

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 (120) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/README.md +17 -7
  3. package/dist/agent-definitions.d.ts +12 -0
  4. package/dist/agent-definitions.js +131 -0
  5. package/dist/agent-loops.d.ts +14 -0
  6. package/dist/agent-loops.js +161 -0
  7. package/dist/agents.js +263 -76
  8. package/dist/cache-helpers.d.ts +28 -0
  9. package/dist/cache-helpers.js +73 -0
  10. package/dist/cli-runner.d.ts +38 -2
  11. package/dist/cli-runner.js +167 -5
  12. package/dist/compaction.js +2 -0
  13. package/dist/config.js +47 -12
  14. package/dist/contracts.d.ts +581 -6
  15. package/dist/contracts.js +41 -1
  16. package/dist/contribution-parsing.d.ts +19 -0
  17. package/dist/contribution-parsing.js +124 -0
  18. package/dist/contributions.d.ts +13 -3
  19. package/dist/contributions.js +96 -20
  20. package/dist/extensions.js +3 -0
  21. package/dist/index.d.ts +19 -9
  22. package/dist/index.js +10 -4
  23. package/dist/input.d.ts +7 -1
  24. package/dist/input.js +52 -11
  25. package/dist/instruction-injection.d.ts +28 -0
  26. package/dist/instruction-injection.js +55 -0
  27. package/dist/manifests.d.ts +1 -1
  28. package/dist/manifests.js +3 -3
  29. package/dist/models.d.ts +4 -1
  30. package/dist/models.js +5 -2
  31. package/dist/node/agent-definitions.d.ts +98 -0
  32. package/dist/node/agent-definitions.js +389 -0
  33. package/dist/node/contribution-discovery.d.ts +17 -0
  34. package/dist/node/contribution-discovery.js +163 -0
  35. package/dist/node/instruction-injectors.d.ts +32 -0
  36. package/dist/node/instruction-injectors.js +72 -0
  37. package/dist/node/session-store-jsonl.d.ts +1 -1
  38. package/dist/node/session-store-jsonl.js +42 -4
  39. package/dist/node/system-project-prompts.d.ts +30 -0
  40. package/dist/node/system-project-prompts.js +53 -0
  41. package/dist/provider-events.d.ts +3 -1
  42. package/dist/provider-events.js +34 -0
  43. package/dist/provider-request-policy.js +15 -1
  44. package/dist/providers/openai-compatible.js +1 -1
  45. package/dist/providers.d.ts +6 -2
  46. package/dist/providers.js +15 -1
  47. package/dist/redaction.d.ts +2 -1
  48. package/dist/redaction.js +3 -0
  49. package/dist/registry-options.d.ts +5 -0
  50. package/dist/registry-options.js +5 -0
  51. package/dist/rpc.d.ts +6 -2
  52. package/dist/rpc.js +71 -13
  53. package/dist/session-stores.d.ts +3 -1
  54. package/dist/session-stores.js +67 -6
  55. package/dist/skills.d.ts +4 -1
  56. package/dist/skills.js +3 -1
  57. package/dist/system-prompts.js +6 -2
  58. package/dist/testing/compaction-conformance.d.ts +17 -0
  59. package/dist/testing/compaction-conformance.js +61 -0
  60. package/dist/testing/extension-conformance.d.ts +26 -0
  61. package/dist/testing/extension-conformance.js +55 -0
  62. package/dist/testing/provider-conformance.d.ts +7 -0
  63. package/dist/testing/provider-conformance.js +18 -31
  64. package/dist/testing/session-store-conformance.d.ts +20 -0
  65. package/dist/testing/session-store-conformance.js +92 -0
  66. package/dist/testing/tool-conformance.d.ts +39 -0
  67. package/dist/testing/tool-conformance.js +79 -0
  68. package/dist/tools.d.ts +7 -2
  69. package/dist/tools.js +50 -13
  70. package/docs/agent-definitions.md +251 -0
  71. package/docs/agent-events.md +199 -0
  72. package/docs/agent-loops.md +217 -0
  73. package/docs/agent-session-runtime.md +20 -8
  74. package/docs/cli-rpc.md +39 -4
  75. package/docs/compaction-and-retry.md +2 -2
  76. package/docs/compaction-conformance.md +76 -0
  77. package/docs/compaction-llm.md +6 -3
  78. package/docs/compaction-observational-memory.md +4 -4
  79. package/docs/configuration-and-manifests.md +6 -1
  80. package/docs/context-and-skills.md +79 -6
  81. package/docs/contribution-discovery.md +149 -0
  82. package/docs/contribution-registries.md +9 -6
  83. package/docs/credentials-and-redaction.md +2 -0
  84. package/docs/customization.md +191 -0
  85. package/docs/database-persistence.md +407 -0
  86. package/docs/extension-authoring.md +193 -0
  87. package/docs/extension-conformance.md +80 -0
  88. package/docs/extensions.md +6 -0
  89. package/docs/host-security.md +141 -0
  90. package/docs/index.md +40 -19
  91. package/docs/input-and-prompt-assembly.md +19 -3
  92. package/docs/instruction-injection.md +183 -0
  93. package/docs/migration.md +201 -0
  94. package/docs/model-registry.md +122 -0
  95. package/docs/node-jsonl-session-store.md +5 -4
  96. package/docs/performance.md +127 -0
  97. package/docs/provider-caching.md +206 -0
  98. package/docs/provider-conformance.md +32 -5
  99. package/docs/provider-layer.md +51 -11
  100. package/docs/provider-packages.md +65 -5
  101. package/docs/provider-request-policies.md +113 -0
  102. package/docs/providers/kimi.md +22 -0
  103. package/docs/providers/neuralwatt.md +388 -0
  104. package/docs/providers/openai-compatible.md +1 -0
  105. package/docs/providers/openai.md +21 -0
  106. package/docs/providers/opencode-go.md +31 -3
  107. package/docs/providers/openrouter.md +29 -0
  108. package/docs/providers/zai.md +17 -0
  109. package/docs/public-contracts.md +87 -12
  110. package/docs/release-and-install.md +76 -26
  111. package/docs/runs-and-usage.md +236 -0
  112. package/docs/session-store-conformance.md +78 -0
  113. package/docs/session-stores-and-branching.md +10 -6
  114. package/docs/session-stores.md +126 -0
  115. package/docs/settings-auth-trust-security.md +18 -4
  116. package/docs/structured-output.md +247 -0
  117. package/docs/system-prompts.md +104 -2
  118. package/docs/tool-conformance.md +87 -0
  119. package/docs/tools.md +64 -8
  120. package/package.json +35 -2
@@ -0,0 +1,76 @@
1
+ # Compaction conformance
2
+
3
+ ## What it does
4
+
5
+ Compaction conformance helpers are dependency-free assertions for `CompactionStrategy` adapter tests. They exercise the summary-result shape, secret redaction, and abort observation of any `CompactionStrategy` without network or credentials.
6
+
7
+ Exported from `@arnilo/prism/testing/compaction-conformance`:
8
+
9
+ - `assertCompactionStrategyConforms(strategy, options?)`
10
+ - `CompactionConformanceOptions`
11
+
12
+ ## When to use it
13
+
14
+ Use this helper when implementing a custom `CompactionStrategy` (the core default strategy and the first-party LLM compaction package both conform). It asserts:
15
+
16
+ - `compact()` returns a `CompactionResult` with a non-empty string `summary`
17
+ - known secrets are redacted from the summary and any returned `entries`
18
+ - (when `exerciseAbort: true`) an already-aborted `signal` is observed
19
+
20
+ ## Inputs / request
21
+
22
+ ```ts
23
+ import { assertCompactionStrategyConforms } from "@arnilo/prism/testing/compaction-conformance";
24
+ import type { CompactionStrategy } from "@arnilo/prism";
25
+
26
+ const { summary } = await assertCompactionStrategyConforms(myStrategy, {
27
+ secrets: ["api-key-value"],
28
+ exerciseAbort: true,
29
+ });
30
+ ```
31
+
32
+ `CompactionConformanceOptions`:
33
+ - `secrets?: readonly string[]` — secrets that must not appear in the summary or returned entries
34
+ - `exerciseAbort?: boolean` — assert the strategy observes an already-aborted `signal`
35
+
36
+ ## Outputs / response / events
37
+
38
+ Returns `Promise<{ summary: string }>`; throws a plain `Error` on the first contract violation. No events, no runner.
39
+
40
+ ## Request/response example
41
+
42
+ ```ts
43
+ import { assertCompactionStrategyConforms } from "@arnilo/prism/testing/compaction-conformance";
44
+
45
+ await assertCompactionStrategyConforms(myStrategy, { secrets: ["secret-value"] });
46
+ // throws if the summary is empty, a secret leaks into the summary, or a
47
+ // secret leaks into the returned entries.
48
+ ```
49
+
50
+ ## Implementation example
51
+
52
+ ```ts
53
+ import { assertCompactionStrategyConforms } from "@arnilo/prism/testing/compaction-conformance";
54
+ import { createDefaultCompactionStrategy } from "@arnilo/prism";
55
+
56
+ await assertCompactionStrategyConforms(
57
+ createDefaultCompactionStrategy({ keepRecentEntries: 1, secrets: ["secret-value"] }),
58
+ { secrets: ["secret-value"] },
59
+ );
60
+ ```
61
+
62
+ ## Extension and configuration notes
63
+
64
+ - The helper builds a tiny two-message fixture; it does not call your strategy with production entries.
65
+ - Abort observation is optional because not every strategy performs async work that can be aborted.
66
+
67
+ ## Security and performance notes
68
+
69
+ - No credentials, no network, no real secrets required; pass fake secret strings.
70
+ - The helper asserts redaction of exactly the secrets you supply (mirroring `createSecretRedactor`'s exact-match behavior); it does not detect arbitrary secret patterns.
71
+
72
+ ## Related APIs
73
+
74
+ - [Compaction and retry](compaction-and-retry.md)
75
+ - [Provider conformance](provider-conformance.md)
76
+ - [Session store conformance](session-store-conformance.md)
@@ -33,7 +33,7 @@ Key exports:
33
33
  | `thinkingLevel` | Passed as `ProviderRequest.options.extra.thinkingLevel`. |
34
34
  | `reserveTokens` | Output budget basis; defaults to `16384`. |
35
35
  | `keepRecentTokens` | Approximate recent-token budget; defaults to `20000`. |
36
- | `maxSummaryTokens` / `maxOutputTokens` | Sets generic `model.parameters.maxTokens` and truncates oversized collected summaries. |
36
+ | `maxSummaryTokens` / `maxOutputTokens` | Computes the summary output budget, writes it to `summaryModel/model.parameters.maxTokens`, and truncates oversized collected summaries. First-party providers serialize that generic field to their real request field (`max_output_tokens` for OpenAI Responses, `max_tokens` for OpenAI-compatible/Anthropic-style providers). |
37
37
  | `maxToolResultChars` | Tool-result JSON truncation limit; defaults to `2000`. |
38
38
  | `trackFileOperations`, `includeFileOperations` | Control file path extraction and final summary blocks. |
39
39
  | `secrets` | Exact strings to redact from serialized prompts and final summaries. |
@@ -61,12 +61,15 @@ import { createLlmCompactionStrategy } from "@arnilo/prism-compaction-llm";
61
61
 
62
62
  const strategy = createLlmCompactionStrategy({
63
63
  provider: summaryProvider,
64
- model: { provider: "mock", model: "cheap-summary" },
64
+ model: { provider: "openai", model: "gpt-4.1-mini" },
65
65
  keepRecentTokens: 20_000,
66
66
  reserveTokens: 16_384,
67
+ maxOutputTokens: 800,
67
68
  providerOptions: { cacheRetention: "short" },
68
69
  customInstructions: "Focus on current files and failing tests.",
69
70
  });
71
+ // Provider request model includes: { parameters: { maxTokens: 800 } }.
72
+ // First-party serializers map it to max_output_tokens/max_tokens on the wire.
70
73
 
71
74
  await session.compact({ strategy, secrets: [apiKey] });
72
75
  ```
@@ -99,7 +102,7 @@ const agent = createAgent({ model, provider, compaction: { strategy, thresholdEn
99
102
  Registration only contributes an inert strategy. The host must resolve and pass it to runtime config.
100
103
 
101
104
  ## Security and performance notes
102
- Preparation is O(n) over branch entries and uses only arrays, strings, and JSON serialization. The strategy makes only the needed provider call(s): one history summary plus one split-turn prefix summary when needed. It does not discover credentials, read files, start background jobs, or add provider SDK dependencies. Redaction is exact-string only; pass every known secret that may appear in history or provider output.
105
+ Preparation is O(n) over branch entries and uses only arrays, strings, and JSON serialization. Output-budget calculation is O(1) and does not add an extra summarization call. The strategy makes only the needed provider call(s): one history summary plus one split-turn prefix summary when needed. It does not discover credentials, read files, start background jobs, or add provider SDK dependencies. Redaction is exact-string only; pass every known secret that may appear in history or provider output.
103
106
 
104
107
  ## Related APIs
105
108
  - [Compaction and retry policies](compaction-and-retry.md): core compaction strategy surface.
@@ -38,7 +38,7 @@ Key exports:
38
38
  | `recallObservationalMemory()` | Recover source evidence for a known observation/reflection id from supplied current-branch entries. |
39
39
  | `createMemoryId()` / `isMemoryId()` | Create/check 12-character ids. |
40
40
  | `resolveObservationalMemorySettings()` | Merge `observational-memory` settings with defaults and overrides. |
41
- | `createObservationalMemoryRuntime()` | Explicitly run observer/reflector/dropper workers for a supplied session/store/provider. |
41
+ | `createObservationalMemoryRuntime()` | Explicitly run observer/reflector/dropper workers for a supplied session, owned append callback, and provider. |
42
42
  | `createObservationalMemoryCompactionStrategy()` | Render existing folded memory as a standard Prism compaction summary with `data.memory`. |
43
43
  | `createObservationalMemoryExtension()` | Inert extension helper that registers the strategy contribution unless disabled. |
44
44
  | `createRecallMemoryTool()` | Optional exact-id `recall` tool factory backed by host-supplied current-branch entries. |
@@ -74,7 +74,7 @@ const evidence = recallObservationalMemory(entries, "aaaaaaaaaaaa");
74
74
 
75
75
  const memory = createObservationalMemoryRuntime({
76
76
  session,
77
- store,
77
+ appendEntry: (entry) => store.append(entry),
78
78
  workerProvider,
79
79
  workerModel: { provider: "mock", model: "memory" },
80
80
  });
@@ -92,7 +92,7 @@ await kernel.load([createObservationalMemoryExtension({ recallTool: { getEntries
92
92
 
93
93
  Settings are read from the `observational-memory` key only when a host calls `resolveObservationalMemorySettings()` or `runtime.flush()`. Defaults are `observeAfterTokens: 10000`, `reflectAfterTokens: 20000`, `compactAfterTokens: 81000`, `observationsPoolMaxTokens: 20000`, `observationsPoolTargetTokens: 10000`, `agentMaxTurns: 16`, `passive: false`, and `debugLog: false`.
94
94
 
95
- The runtime requires host-supplied `session`, matching `store`, `workerProvider`, and `workerModel`. Optional credential resolution is explicit; missing requested credentials skip worker execution.
95
+ The runtime requires host-supplied `session`, an `appendEntry` callback bound to that session's owning store/branch, `workerProvider`, and `workerModel`. It no longer accepts a separate `store` option because mismatched session/store pairs can append memory entries outside the active branch. After each memory append, the runtime checks the appended entry is visible at the session leaf and fails closed/restores the previous checkout if the callback points elsewhere. Optional credential resolution is explicit; missing requested credentials skip worker execution.
96
96
 
97
97
  `createObservationalMemoryCompactionStrategy()` keeps recent message entries like the default compaction strategy, renders existing observations/reflections as the summary, and returns a standard Prism compaction entry. Its `data` includes `throughEntryId`, `keepEntryIds`, `strategy`, `trigger`, and `memory: { type: "om.folded", version: 1, fullFold, observations, reflections, droppedObservationIds }`. When active observations exceed `observationsPoolMaxTokens`, it performs a full fold into `data.memory`.
98
98
 
@@ -108,7 +108,7 @@ The runtime requires host-supplied `session`, matching `store`, `workerProvider`
108
108
  - Recall tool and commands only see current-branch entries supplied by the host callback.
109
109
  - Invalid or missing ids fail closed; invalid recall tool ids skip entry lookup.
110
110
  - Utilities and fast compaction are O(n) over supplied entries and use no provider, network, filesystem, timer, worker, credential, or settings access.
111
- - Workers serialize only supplied branch entries, enforce `agentMaxTurns`, and run one consolidation pipeline at a time per runtime.
111
+ - Workers serialize only supplied branch entries, enforce `agentMaxTurns`, and run one consolidation pipeline at a time per runtime. Worker transcripts replay assistant `tool_call` messages before matching role `tool` `tool_result` messages so provider requests stay valid for call/result-pairing providers.
112
112
  - Compaction preserves raw history; Prism appends one standard compaction entry and rebuilds provider context from its summary plus kept recent messages.
113
113
  - Pass known secrets to render/recall/runtime/tool/command helpers to redact exact values from prompts, records, structured results, and text output.
114
114
  - Live tests are opt-in with `PRISM_LIVE_OBSERVATIONAL_MEMORY_TESTS=1`.
@@ -69,6 +69,7 @@ parsePrismManifest(value: unknown): PrismManifest
69
69
  | `authMethod` | `authMethods` | Auth method descriptor; uses `credentialName`, never a resolved credential value. |
70
70
  | `providerRequestPolicy` | `providerRequestPolicies` | Provider request policy declaration. |
71
71
  | `systemPromptContribution` | `systemPromptContributions` | System prompt contribution declaration. |
72
+ | `instructionInjector` | `instructionInjectors` | Instruction injector declaration; selected on `AgentConfig`/`RunOptions.instructionInjectors` (Phase 30). |
72
73
 
73
74
  ## Outputs / response / events
74
75
 
@@ -76,6 +77,7 @@ parsePrismManifest(value: unknown): PrismManifest
76
77
  - Later config layers override earlier layers.
77
78
  - Nested plain objects merge recursively.
78
79
  - Arrays and primitives replace previous values.
80
+ - Config and manifest JSON object keys named `__proto__`, `prototype`, or `constructor` are rejected at any depth before merge/clone output is built.
79
81
  - `parsePrismManifest()` returns a validated manifest or throws a field-specific validation error.
80
82
  - No events are emitted and no registries are modified by these helpers.
81
83
 
@@ -106,6 +108,7 @@ const manifest = definePrismManifest({
106
108
  { kind: "authMethod", name: "demo.api-key", metadata: { credentialName: "apiKey" } },
107
109
  { kind: "providerRequestPolicy", name: "demo.cache" },
108
110
  { kind: "systemPromptContribution", name: "demo.prompt" },
111
+ { kind: "instructionInjector", name: "demo.injector" },
109
112
  ],
110
113
  resources: [{ uri: "package://demo/prompt.md", purpose: "prompt" }],
111
114
  });
@@ -123,13 +126,14 @@ console.log(config.demo);
123
126
 
124
127
  - Hosts choose the layer order. Prism documents `built-in -> manifest defaults -> host app -> optional user/global -> runtime overrides` but does not load those layers automatically.
125
128
  - Manifest contribution declarations are data. Hosts may later choose to import the declared module/export and register it, but parsing the manifest never does that.
126
- - Contribution `kind` values match `createContributionRegistries()` categories, including the Phase 14 provider primitives `providerPackage`, `authMethod`, `providerRequestPolicy`, and `systemPromptContribution`.
129
+ - Contribution `kind` values match `createContributionRegistries()` categories, including the Phase 14 provider primitives `providerPackage`, `authMethod`, `providerRequestPolicy`, `systemPromptContribution`, and the Phase 30 `instructionInjector`.
127
130
  - Filesystem config loading is intentionally outside the root API and belongs to the optional [`@arnilo/prism/node/config`](node-filesystem-config.md) subpath.
128
131
  - Manifest `resources` entries are URI declarations; use [resource loading](resource-loading.md) helpers with a host-provided loader to fetch them.
129
132
 
130
133
  ## Security and performance notes
131
134
 
132
135
  - Config and manifest values must be JSON-compatible data.
136
+ - Config/manifest JSON rejects `__proto__`, `prototype`, and `constructor` keys at every depth to block prototype pollution rather than silently dropping unsafe input.
133
137
  - Do not put resolved credential values, tokens, headers, or executable code in config defaults, manifests, or metadata.
134
138
  - Manifest parsing does not execute package code, dynamically import modules, resolve credentials, call providers/tools, or read resources.
135
139
  - Config merging is dependency-free and proportional to the total number of JSON fields.
@@ -140,3 +144,4 @@ console.log(config.demo);
140
144
  - [Extension kernel and event bus](extensions.md): hosts can load extensions after they decide to execute package code.
141
145
  - [Resource loading](resource-loading.md): load manifest, prompt, skill, and package resources through host-provided loaders.
142
146
  - [Credentials and redaction](credentials-and-redaction.md): credential values stay out of manifests and config layers.
147
+ - [Contribution discovery (workspace)](contribution-discovery.md): opt-in scanner that resolves non-skill on-disk entries into `ManifestContributionDeclaration` envelopes.
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## When to use it
8
8
 
9
- Use context resolution when a host wants project/session/context blocks resolved before prompt composition. Use the skill registry when a host wants explicit progressive skill disclosure.
9
+ Use context resolution when a host wants project/session/context blocks resolved before prompt composition. Use the skill registry when a host wants explicit progressive skill disclosure. Declarative `AgentDefinition.skills` are inactive unless listed; omitted skills means none unless the host uses the migration-only `activateAllCapabilities: true` option.
10
10
 
11
11
  Do not use these helpers as an agent loop, package discovery mechanism, context cache, token budgeter, retrier, credential resolver, semantic skill ranker, tool activator, or permission system.
12
12
 
@@ -32,7 +32,10 @@ Skill selection:
32
32
  ```ts
33
33
  import { createSkillRegistry, resolveActiveSkills } from "@arnilo/prism";
34
34
 
35
- const registry = createSkillRegistry([{ name: "brief", instructions: "Answer briefly.", toolNames: ["echo"] }]);
35
+ const registry = createSkillRegistry(
36
+ [{ name: "brief", instructions: "Answer briefly.", toolNames: ["echo"] }],
37
+ { duplicate: "error" },
38
+ );
36
39
  const active = resolveActiveSkills({
37
40
  registry,
38
41
  names: ["brief"],
@@ -40,13 +43,15 @@ const active = resolveActiveSkills({
40
43
  });
41
44
  ```
42
45
 
46
+ `createSkillRegistry(skills?, options?)` stores skills by `skill.name`. Duplicate names replace deterministically by default for compatibility. Pass `{ duplicate: "error" }` to throw `Duplicate skill: <name>` and prevent silent shadowing.
47
+
43
48
  `ResolveActiveSkillsOptions` accepts a `SkillRegistry`, requested skill names, and host-active `ToolDefinition[]`.
44
49
 
45
50
  ## Outputs / response / events
46
51
 
47
52
  `resolveContextProviders()` returns `readonly ContextBlock[]` in provider order. If a middleware registry is supplied, the `context` hook can transform the final block array.
48
53
 
49
- `resolveActiveSkills()` returns requested skills in requested order. Unknown skills and skills that reference inactive tools throw before prompt composition.
54
+ `resolveActiveSkills()` returns requested skills in requested order. Unknown skills, duplicate skill registrations in strict mode, and skills that reference inactive tools throw before prompt composition.
50
55
 
51
56
  ## Request/response example
52
57
 
@@ -85,7 +90,7 @@ const request = await assembleProviderInput({
85
90
 
86
91
  ## Extension and configuration notes
87
92
 
88
- Extensions can contribute context providers and skills with `registerContextProvider()` and `registerSkill()`, but those contributions stay inert until the host selects providers or registers/selects skills. The agent/session runtime uses the `context` and selected `skills` arrays passed on `AgentConfig`; it does not auto-select contributions.
93
+ Extensions can contribute context providers and skills with `registerContextProvider()` and `registerSkill()`, but those contributions stay inert until the host selects providers or registers/selects skills. `resolveAgentDefinition()` only selects skills named in `AgentDefinition.skills` by default; omitted declarative skills activate none. The agent/session runtime uses the `context` and selected `skills` arrays passed on `AgentConfig`; it does not auto-select contributions.
89
94
 
90
95
  ```ts
91
96
  const providers = [kernel.registries.contextProviders.resolve("project")];
@@ -95,19 +100,87 @@ const skills = resolveActiveSkills({ registry: skillRegistry, names: ["brief"],
95
100
 
96
101
  `context` middleware runs only when a middleware registry is supplied to the helper. Middleware transforms context data; it does not grant tool access. Skills can reference tool names, but only host-active tools satisfy those references.
97
102
 
103
+ ## Runtime skill selection and activation
104
+
105
+ The agent/session runtime resolves skills per run and wires each active skill's `context` into the assembled provider input. Runtime `AgentConfig.skills` and declarative `AgentDefinition.skills` have different defaults:
106
+
107
+ | Surface | Config shape | Run override | Active skills |
108
+ | --- | --- | --- | --- |
109
+ | Runtime agent | `AgentConfig.skills: SkillRegistry` | `RunOptions.activeSkills: ["brief"]` | Named skills only, resolved with `resolveActiveSkills({ registry, names, tools })`. |
110
+ | Runtime agent | `AgentConfig.skills: SkillRegistry` | no `activeSkills` / no `skills` | All registry skills (`SkillRegistry.list()`). |
111
+ | Runtime agent | `AgentConfig.skills: Skill[]` | `RunOptions.skills: [...]` | Override array only. |
112
+ | Runtime agent | `AgentConfig.skills: Skill[]` | no `RunOptions.skills` | All configured array skills. |
113
+ | Declarative definition | `AgentDefinition.skills: ["brief"]` | later runtime `activeSkills` optional | Listed names only. |
114
+ | Declarative definition | omitted `AgentDefinition.skills` | `activateAllCapabilities` false/default | No skills active. |
115
+ | Declarative definition | omitted `AgentDefinition.skills` | `activateAllCapabilities: true` | All registry skills, migration-only. |
116
+
117
+ Runtime selection precedence mirrors the other `RunOptions` overrides (`redactor`, `validate`):
118
+
119
+ 1. `AgentConfig.skills` is a `SkillRegistry` and `RunOptions.activeSkills: readonly string[]` (names) is set → the runtime calls `resolveActiveSkills({ registry, names, tools })`.
120
+ 2. `RunOptions.skills: readonly Skill[]` is set → that array replaces `AgentConfig.skills` for the run. This override exists for the case where `AgentConfig.skills` is a plain `Skill[]` (no registry), so name resolution is impossible.
121
+ 3. Neither set → all configured runtime skills are active (current behavior; `SkillRegistry.list()` or the plain array as-is). This is not the declarative default.
122
+
123
+ names win when a registry exists. `RunOptions.activeSkills` cannot be used against a plain-array `AgentConfig.skills` — use `RunOptions.skills` instead. Use `RunOptions.skills: []` for an explicit no-skills runtime run.
124
+
125
+ Each active skill contributes two things the runtime now wires together:
126
+
127
+ - `Skill.instructions` → rendered as system messages by `skillMessages()` (active set only).
128
+ - `Skill.context: ContextProvider[]` → collected across active skills (`activeSkills.flatMap(s => s.context ?? [])`), resolved through the existing `resolveContextProviders(...)`, and merged into the request's `context` **after** host `AgentConfig.context` blocks. Inactive skills contribute neither instructions nor context.
129
+
130
+ `toolNames` enforcement is live: because selection routes through `resolveActiveSkills()`, a skill demanding a host-inactive tool throws with `Skill ${name} requires inactive tool: ${missing}` **before the first provider turn** — no provider call, no store write, no partial side effect. This is the fail-fast contract the docs already claimed; the runtime now honors it.
131
+
132
+ ```ts
133
+ import { createAgent, createSkillRegistry, type ContextProvider } from "@arnilo/prism";
134
+
135
+ const schema: ContextProvider = { name: "schema", resolve: () => [{ title: "Schema", content: "selected schema" }] };
136
+ const skills = createSkillRegistry([
137
+ { name: "summarize", instructions: "Summarize.", context: [schema], toolNames: ["echo"] },
138
+ { name: "translate", instructions: "Translate." },
139
+ ]);
140
+
141
+ const agent = createAgent({ model, provider, skills, tools: [echo] });
142
+
143
+ // Only summarize this run: its instructions render and schema context resolves;
144
+ // translate stays inactive and contributes neither. If `echo` were not in the
145
+ // active tool set, this run would throw before the first provider turn.
146
+ await agent.createSession().run(input, { activeSkills: ["summarize"] });
147
+
148
+ // Same config, different active skills on the next run:
149
+ await agent.createSession().run(input, { activeSkills: ["translate"] });
150
+
151
+ // Plain-array override (no registry on AgentConfig.skills):
152
+ await session.run(input, { skills: [{ name: "verbose", instructions: "Be verbose." }] });
153
+ await session.run(input, { skills: [] }); // explicit no skills for this run
154
+ ```
155
+
156
+ Skill selection grants no tool access and cannot bypass permissions — a skill's `toolNames` can only *require* host-active tools, never activate or grant them. Declarative skills also do not activate themselves by presence in a registry; list names on `AgentDefinition.skills` (or pass runtime `activeSkills`) when wanted. Per-skill token budgeting is deferred; the merge order (host context, then skill context) is the only priority knob today.
157
+
158
+ ### Migration note
159
+
160
+ For declarative agents, old configs that omitted `skills` should now add explicit names:
161
+
162
+ ```ts
163
+ // New safe default: no skill activates by omission.
164
+ resolveAgentDefinition({ name: "doc", model, skills: ["brief"] }, context);
165
+ ```
166
+
167
+ Use `activateAllCapabilities: true` only as a temporary all-skills/all-tools compatibility opt-in during migration. Runtime `RunOptions.activeSkills` remains the per-run narrowing tool after an agent has a skill registry configured.
168
+
98
169
  ## Security and performance notes
99
170
 
100
171
  - Context providers run sequentially and deterministically in caller order.
101
- - Skill registry lookup is `Map`-backed, and selection is linear in requested skills plus active tools.
172
+ - Skill registry lookup is `Map`-backed, and selection is linear in requested skills plus active tools. Strict duplicate mode adds one O(1) `Map.has()` check during registration only.
102
173
  - These helpers perform no provider calls, tool execution, resource loading, package discovery, filesystem/network access, retries, timers, or watchers by themselves.
103
174
  - Context and skill output is host/extension data. Do not include secrets unless the host explicitly accepts that prompt exposure.
104
- - Active tools remain host-supplied; skills and middleware do not activate tools or grant permissions.
175
+ - Active tools remain host-supplied; skills and middleware do not activate tools or grant permissions. Use `duplicate: "error"` when loading third-party skills to prevent silent name shadowing.
105
176
 
106
177
  ## Related APIs
107
178
 
108
179
  - [Agent/session runtime](agent-session-runtime.md): consumes host-selected context providers and skills from explicit agent config.
109
180
  - [Input and prompt assembly](input-and-prompt-assembly.md): default prompt builder and provider-input assembly helper.
181
+ - [Instruction injection](instruction-injection.md): package injectors contribute `contextBlocks` that merge after host+skill provider blocks.
110
182
  - [Public contracts](public-contracts.md): `ContextProvider`, `ContextResolutionContext`, `ContextBlock`, `Skill`, `SkillRegistry`, `PromptBuilder`, and `PromptBuildRequest`.
111
183
  - [Middleware hooks](middleware-hooks.md): `context` and `prompt_build` hooks.
112
184
  - [Contribution registries](contribution-registries.md): inert context provider and skill contributions.
185
+ - [Contribution discovery (workspace)](contribution-discovery.md): opt-in filesystem scanner that turns `SKILL.md`/`manifest.json` into registered skills and descriptor stubs. (Per-agent `AGENT.md` bundles live under an app-controlled `configRoot`; see [Agent definitions](agent-definitions.md).)
113
186
  - [Tools](tools.md): host-owned active tools and permissions.
@@ -0,0 +1,149 @@
1
+ # Contribution discovery (workspace)
2
+
3
+ ## What it does
4
+
5
+ Discovery scans the filesystem for workspace contribution directories and turns their on-disk files into inert `DiscoveredContribution` envelopes the host then registers. It reads text only — it never `import()`s, `require()`s, or otherwise executes contribution code, and it never grants tools, permissions, credentials, or provider slots.
6
+
7
+ APIs (Node subpath `@arnilo/prism/node/contribution-discovery`, parsers and registrar on the main barrel):
8
+
9
+ - `discoverContributions(options)` / `DiscoveryOptions`: directory-walking scanner for the workspace `.agents/` tree. One `readdir` per kind-root.
10
+ - `parseSkillFile(text, path)`: stdlib-only frontmatter parser for `SKILL.md` (no YAML dependency, no `node:*` import). Re-exported from `@arnilo/prism`.
11
+ - `registerDiscoveredContributions(registries, contributions)`: registers realized `Skill` objects for the `skill` kind and descriptor-only stubs for other kinds. Re-exported from `@arnilo/prism`.
12
+ - `ContributionFileKind`, `DiscoveredContribution`: contract types re-exported from `@arnilo/prism`.
13
+ - `createPathTrustPolicy` / `isPathInsideReal` (from `@arnilo/prism/node/trust`): realpath-resolved, fail-closed containment used to gate workspace roots.
14
+
15
+ ## When to use it
16
+
17
+ Use discovery when a host or the `prism` CLI wants to honor a project-local contribution layout without wiring every contribution by hand. Typical hosts pass `--discover` on the CLI or call `discoverContributions()` on startup, then feed the result through `registerDiscoveredContributions()` into their existing `ContributionRegistries`.
18
+
19
+ Do not use it to discover providers — provider/model packages stay config/package-driven (Phase 24; see [Provider packages](provider-packages.md)). Do not use it to auto-activate anything: discovery registers, never activates. Skills become selectable only when a run passes `RunOptions.activeSkills`, and `toolNames` is still enforced against the resolved tool set at activation time.
20
+
21
+ ## Inputs / request
22
+
23
+ ### Directory layout
24
+
25
+ | Origin | Path | Scanned by |
26
+ | --- | --- | --- |
27
+ | Workspace | `<workspace>/.agents/{skills,tools,context,instructions}/<name>/` | `workspaceRoot`, gated by `trust` |
28
+
29
+ ### Per-kind entry file
30
+
31
+ | Kind | Entry file | Parsed into |
32
+ | --- | --- | --- |
33
+ | `skill` | `<dir>/SKILL.md` | A realized `Skill` (placed in `DiscoveredContribution.skill`) |
34
+ | `tool` / `context` / `instructions` | `<dir>/manifest.json` | A `ManifestContributionDeclaration` |
35
+
36
+ ### Frontmatter keys
37
+
38
+ `SKILL.md` frontmatter (YAML-like, parsed by a stdlib-only parser — no YAML dependency):
39
+
40
+ | Key | Meaning |
41
+ | --- | --- |
42
+ | `name` | Required if you want an explicit name; otherwise the parent directory name is used. |
43
+ | `description` | Optional skill description. |
44
+ | `toolNames` | Optional comma list (`[a, b]`) or YAML block list (`- a`). Enforced at activation, not at discovery. |
45
+
46
+ The markdown body below the front fence becomes `Skill.instructions`. Unknown frontmatter keys are tolerated and collected into `Skill.metadata` (never fatal).
47
+
48
+ `manifest.json` entries follow the [`ManifestContributionDeclaration`](configuration-and-manifests.md) shape: `kind`, `name`, and optional `module`/`exportName`/`resource`/`metadata`.
49
+
50
+ ### `DiscoveryOptions`
51
+
52
+ | Field | Meaning |
53
+ | --- | --- |
54
+ | `kinds` | Which `ContributionFileKind` values to scan (`skill`, `tool`, `context`, `instructions`). |
55
+ | `workspaceRoot` | Workspace root. Gated by `trust`; untrusted roots are skipped silently. |
56
+ | `permission?` | `PermissionPolicy` asserting each directory read (`assertPermission`). |
57
+ | `trust?` | `TrustPolicy` (e.g. `createPathTrustPolicy`) fail-closed against the workspace root. |
58
+
59
+ ### CLI flags
60
+
61
+ | Flag | Meaning |
62
+ | --- | --- |
63
+ | `--discover` | Enable workspace contribution discovery. Opt-in; default runs never touch the filesystem. |
64
+ | `--discover-kinds <csv>` | Kinds to scan. Defaults to `skill`. Accepts `skill,tool,context,instructions`. |
65
+ | `--no-discovery` | Hard-disable discovery even if `--discover` is set. |
66
+
67
+ ## Outputs / response / events
68
+
69
+ `discoverContributions()` returns `readonly DiscoveredContribution[]`. Each envelope has `kind`, `name`, `origin` (`"global"` | `"workspace"`), `path`, and either `skill` (for the `skill` kind) or `declaration` (a `ManifestContributionDeclaration` for other kinds), plus optional `metadata`. The envelope is inert: it contains no executable code, no credential, and no resolved provider/model/tool reference.
70
+
71
+ `registerDiscoveredContributions(registries, contributions)` writes into the existing `ContributionRegistries`: `skills` get full `Skill` objects; `tool`/`context` get descriptor-only stubs whose `execute`/`resolve` throw (executable behavior is host-owned); `instructions` get descriptors with empty `text` (Phase 30 lifts `declaration.resource` into actual text). The host retains the original `DiscoveredContribution[]` for provenance — tool and context descriptors carry no `metadata.discovered` slot because `ToolDefinition` and `ContextProvider` have no metadata field. Phase 30 adds a host-owned `loadInstructionInjector` adapter (see [Instruction injection](instruction-injection.md)) that turns a discovered `kind: "instructions"` contribution into a live `InstructionInjector` — markdown-only → static `every_turn` injector; module-referenced → resolved through a host-supplied `moduleLoader` (core never auto-`import()`s).
72
+
73
+ ## Request/response example
74
+
75
+ Discovering a workspace skill:
76
+
77
+ ```json
78
+ [
79
+ { "kind": "skill", "name": "greeter", "origin": "workspace", "path": "/proj/.agents/skills/greeter/SKILL.md", "skill": { "name": "greeter", "description": "g", "instructions": "..." } }
80
+ ]
81
+ ```
82
+
83
+ ## Implementation example
84
+
85
+ ```ts
86
+ import {
87
+ createContributionRegistries,
88
+ registerDiscoveredContributions,
89
+ createSkillRegistry,
90
+ createAgent,
91
+ createMockProvider,
92
+ providerDone,
93
+ } from "@arnilo/prism";
94
+ import { discoverContributions } from "@arnilo/prism/node/contribution-discovery";
95
+ import { createPathTrustPolicy } from "@arnilo/prism/node/trust";
96
+
97
+ // 1. Discover — workspace gated by trust.
98
+ const trust = createPathTrustPolicy({ trustedRoots: [workspaceRoot] });
99
+ const discovered = await discoverContributions({
100
+ kinds: ["skill"],
101
+ workspaceRoot,
102
+ trust,
103
+ });
104
+
105
+ // 2. Register — skills become full Skill objects; other kinds register as stubs.
106
+ // Discovery never imports or executes contribution code.
107
+ const registries = createContributionRegistries();
108
+ registerDiscoveredContributions(registries, discovered);
109
+
110
+ // 3. Run — discovery did NOT activate anything. The run explicitly selects skills.
111
+ const session = createAgent({
112
+ model: { provider: "mock", model: "demo" },
113
+ provider: createMockProvider([providerDone()]),
114
+ skills: createSkillRegistry(registries.skills.list()),
115
+ }).createSession();
116
+
117
+ await session.run("Hi", { activeSkills: ["greeter"] });
118
+ ```
119
+
120
+ A complete runnable example lives at `examples/discover-skills.ts`.
121
+
122
+ ## Extension and configuration notes
123
+
124
+ - Discovery is loader-driven, not module-driven. The on-disk contribution is a data file (`SKILL.md`/`manifest.json`); the host decides whether, when, and how to register and activate it. There is no `import()` of untrusted modules.
125
+ - `ContributionFileKind` is the discovery-side kind name (`skill`/`tool`/`context`/`instructions`). The manifest-side kind is `ManifestContributionKind` ([Configuration and manifests](configuration-and-manifests.md)); the node scanner maps between them.
126
+ - The `--discover-kinds` CSV lets a host scan a subset. The CLI default is `skill`; other kinds register descriptor stubs only (host-owned execution).
127
+ - First-party skills are expected to ship as installable packages (separate request); Phase 29 ships discovery infrastructure only and adds no first-party skill package.
128
+
129
+ ## Security and performance notes
130
+
131
+ - **Workspace gating**: workspace roots are checked through `createPathTrustPolicy` + `isPathInsideReal`, which resolve symlinks and fail closed (return false) if either root or target cannot be resolved. Untrusted workspace roots are skipped silently, never thrown over. Permission is asserted per directory read via `assertPermission`.
132
+ - **Symlink handling**: symlinked entries that escape the kind root after realpath resolution are excluded. `SKILL.md` and `manifest.json` are also realpath-checked against their contribution directory before read, so an entry-file symlink cannot escape to another path.
133
+ - **Opt-in**: discovery is opt-in — it runs only when the host passes `--discover` or calls `discoverContributions()` explicitly. Default runs perform no filesystem I/O.
134
+ - **No auto-execute**: discovery reads text. It does not `import()`, `require()`, or run contribution code. `registerDiscoveredContributions` registers descriptor stubs whose execution methods throw — the host lifts them into live tools/providers itself.
135
+ - **No auto-activate**: discovery registers skills; it does not select them. Activation requires explicit `RunOptions.activeSkills`, and `toolNames` is still validated against the resolved tool set. Discovery grants no tools, permissions, or provider slots.
136
+ - **No provider scanning**: provider/model discovery stays config/package-driven (Phase 24). See [Provider packages](provider-packages.md).
137
+ - **AGENTS.md / SYSTEM.md are not discovery kinds**: the root-level `AGENTS.md` (workspace) prompt file does not fit the named-subdir scanner and is loaded by a sibling Node loader, `loadSystemPromptFiles` from `@arnilo/prism/node/system-prompts`. See [System prompts](system-prompts.md). The CLI auto-loads `AGENTS.md` in print/json modes; RPC mode does not (host-owned).
138
+ - **Secrets**: the secrets-redaction path is unaffected — discovery reads contribution files, not provider request content. Skill `instructions` flow through normal input assembly and the same redaction pipeline as any system message.
139
+ - **Performance**: one `readdir` per kind-root per origin; missing kind directories are normal (ENOENT-tolerant). Default runs perform no discovery I/O at all. The merged output is inert.
140
+
141
+ ## Related APIs
142
+
143
+ - [Contribution registries](contribution-registries.md): where discovered envelopes are registered.
144
+ - [Context and skills](context-and-skills.md): `createSkillRegistry` / `resolveActiveSkills` and the `RunOptions.activeSkills` activation that consumes discovered skills.
145
+ - [Extensions](extensions.md): host-provided extensions still register contributions programmatically; discovery is the filesystem-driven complement.
146
+ - [Configuration and manifests](configuration-and-manifests.md): `ManifestContributionDeclaration` / `ManifestContributionKind` shapes used by non-skill entries.
147
+ - [CLI/RPC](cli-rpc.md): the `--discover`, `--discover-kinds`, and `--no-discovery` flags.
148
+ - [System prompts](system-prompts.md): `loadSystemPromptFiles` loads root-level `AGENTS.md` / `SYSTEM.md` — a sibling loader, not a scanner kind.
149
+ - [Security/auth/trust](settings-auth-trust-security.md): `createPathTrustPolicy`, `assertPermission`, and the trust model.
@@ -18,15 +18,15 @@ Do not use them as a dependency injection container, manifest loader, settings l
18
18
  ## Inputs / request
19
19
 
20
20
  ```ts
21
- createContributionRegistry<T>(options?: { label?: string }): ContributionRegistry<T>
22
- createContributionRegistries(): ContributionRegistries
21
+ createContributionRegistry<T>(options?: { label?: string; duplicate?: "replace" | "error" }): ContributionRegistry<T>
22
+ createContributionRegistries(options?: { duplicate?: "replace" | "error" }): ContributionRegistries
23
23
  ```
24
24
 
25
25
  `ContributionRegistry<T>` methods:
26
26
 
27
27
  | Method | Input | Result |
28
28
  | --- | --- | --- |
29
- | `register(key, contribution)` | string key and contribution | Stores or replaces the contribution for that key. |
29
+ | `register(key, contribution)` | string key and contribution | Stores/replaces the contribution for that key; throws `Duplicate <label>: <key>` when `duplicate: "error"`. |
30
30
  | `get(key)` | string key | Returns the contribution or `undefined`. |
31
31
  | `resolve(key)` | string key | Returns the contribution or throws `Unknown <label>: <key>`. |
32
32
  | `list()` | none | Returns contributions in insertion order. |
@@ -37,7 +37,7 @@ createContributionRegistries(): ContributionRegistries
37
37
 
38
38
  Registry calls return plain contribution objects. Unknown `resolve()` calls throw before provider, model, tool, credential, prompt, resource, or session behavior can run. `systemPromptContributions` are inert until a host passes selected values to `AgentConfig.systemPrompt` or `RunOptions.systemPrompt`.
39
39
 
40
- Registering the same key replaces the contribution deterministically. Registries do not emit events by themselves; the extension kernel may emit events when it uses them.
40
+ Registering the same key replaces the contribution deterministically by default. Passing `duplicate: "error"` adds one `Map.has()` check before `set()` and throws `Duplicate <label>: <key>` instead of silently shadowing. Registries do not emit events by themselves; the extension kernel may emit events when it uses them.
41
41
 
42
42
  ## Request/response example
43
43
 
@@ -60,7 +60,7 @@ const tool: ToolDefinition = {
60
60
  },
61
61
  };
62
62
 
63
- const registries = createContributionRegistries();
63
+ const registries = createContributionRegistries({ duplicate: "error" });
64
64
  registries.tools.register(tool.name, tool);
65
65
  registries.agents.register("demo", {
66
66
  name: "demo",
@@ -87,6 +87,8 @@ void skill;
87
87
  - Hosts can use contribution registries directly and skip extension loading entirely.
88
88
  - Extension packages should register contributions through the host-provided extension API once the extension kernel is in use.
89
89
  - Registry keys are explicit strings. Prefer stable ids/names such as `provider.id`, `tool.name`, `skill.name`, or package-qualified names when collisions matter.
90
+ - Default duplicate policy is `"replace"` for compatibility and deterministic last-write-wins behavior. External apps that load third-party contributions should prefer `duplicate: "error"` to prevent silent shadowing.
91
+ - Migration safety: when moving from legacy all-in-scope capability activation to named `AgentDefinition.tools` / `skills`, enable strict registries first so duplicate third-party names fail during registration instead of changing which capability a name resolves to.
90
92
  - Manifest contribution `kind` values match these registry keys. For example, `authMethods` accepts `authMethod` manifest declarations, `providerPackages` accepts `providerPackage`, `providerRequestPolicies` accepts `providerRequestPolicy`, and `systemPromptContributions` accepts `systemPromptContribution`.
91
93
  - Manifest and configuration loading are separate APIs; this page only covers in-memory registration.
92
94
  - Tool contributions are inert. They are not executable until the host registers selected definitions in an active tool registry and passes that registry to `dispatchToolCall()`.
@@ -96,7 +98,7 @@ void skill;
96
98
 
97
99
  ## Security and performance notes
98
100
 
99
- - Generic registries are `Map`-backed with O(1) lookup.
101
+ - Generic registries are `Map`-backed with O(1) lookup; strict duplicate checks add one O(1) `Map.has()` during registration only.
100
102
  - Registries are explicit objects returned by factories. Prism does not create hidden global contribution registries.
101
103
  - Registries must not store resolved credential values, tokens, headers, or secret-bearing settings.
102
104
  - `credentialResolvers` may store resolver objects, but resolved credentials must stay at the edge that needs them.
@@ -116,3 +118,4 @@ void skill;
116
118
  - [Compaction and retry policies](compaction-and-retry.md): selected compaction strategy and retry policy behavior.
117
119
  - [Public contracts](public-contracts.md): contribution contract types stored in these registries.
118
120
  - [Credentials and redaction](credentials-and-redaction.md): credential resolver and secret-redaction rules.
121
+ - [Contribution discovery (workspace)](contribution-discovery.md): opt-in directory scanner that fills these registries from `SKILL.md`/`manifest.json`.
@@ -95,6 +95,7 @@ console.log(error.message);
95
95
  ## Extension and configuration notes
96
96
 
97
97
  - Hosts and extension packages can implement `CredentialResolver` and pass it explicitly to code that needs credentials.
98
+ - `AgentConfig.credentials` is host-owned metadata for compatibility; `createAgent()` / `session.run()` do not call `credentials.resolve()`. Provider adapters, compaction workers, or request policies should receive and resolve credentials at the provider edge.
98
99
  - Use `createExplicitCredentialResolver()` when documenting a fixed order such as runtime override, stored credential, caller-provided env object, then fallback resolver.
99
100
  - Use `createEnvCredentialResolver()` only with an object supplied by the host; Prism does not read `process.env` for you.
100
101
  - Provider adapters should resolve credentials as late as possible, per request.
@@ -109,6 +110,7 @@ console.log(error.message);
109
110
  - Cycle and non-JSON value handling: `redactSecrets()` is cycle-safe via a `WeakSet` visited-set. Self-referential or mutually referenced objects render `"[Circular]"` at the back-reference instead of throwing. `Date` and `RegExp` values are passed through unchanged; `ArrayBuffer` and typed arrays are passed through unchanged; `Map` is normalized to a plain object and `Set` to an array so the output stays JSON-compatible. `errorToErrorInfo()` tolerates a cyclic `error.cause` (rendered via `String()`).
110
111
  - Use placeholders in tests and docs. Never commit real tokens.
111
112
  - Live provider/worker tests are gated behind explicit environment variables and skipped by default: `PRISM_LIVE_PROVIDER_TESTS`, `PRISM_LIVE_COMPACTION_TESTS`, `PRISM_LIVE_OBSERVATIONAL_MEMORY_TESTS`. Default `npm test` is network-free; do not add ungated network calls to default tests.
113
+ - `AgentConfig.credentials` is not eagerly resolved, serialized into provider requests/events/stores, or passed to loops/compaction by the core runtime.
112
114
  - `resolveCredentialValue()` and `createExplicitCredentialResolver()` do not cache values. Add host-side caching only if a real credential source needs it.
113
115
  - `refreshOAuthCredential()` only calls the supplied OAuth provider and optional store; it has no built-in persistence or retry loop.
114
116