@animalabs/connectome-host 0.7.1 → 0.7.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.3 — 2026-08-01
6
+
7
+ ### Changed
8
+
9
+ - **Prompt caching enabled on Bedrock for models that support it**
10
+ (Discord issue #35). The previous transport-wide `promptCaching: false`
11
+ was a workaround for "your request did not allow prompt caching" —
12
+ which turned out to be the account-level denial for 3.5 Sonnet v2
13
+ (caching there was preview-only and dropped at Bedrock's GA), not a
14
+ transport property. Caching is now gated per model
15
+ (`bedrockModelSupportsPromptCaching`): on for the Bedrock caching-GA
16
+ lineup (3.5 Haiku, 3.7 Sonnet, Claude 4+), off for the pre-GA families
17
+ (Claude v2/instant, Claude 3, 3.5 Sonnet — matched at the family
18
+ boundary, so bare aliases and `-latest` forms gate the same as dated
19
+ ids; non-Claude Bedrock ids are conservatively off). New recipe field
20
+ `agent.promptCaching: boolean` overrides the gate in either direction
21
+ on any provider, and lands at both layers — per-agent config and
22
+ Membrane's default for internal callers (compression/merge) — for
23
+ accounts/regions whose entitlements differ from the GA table.
24
+ `cacheTtl` is withheld at the host layer on bedrock (Agent Framework
25
+ still supplies its own default downstream; membrane ≥ 0.5.77 strips
26
+ the ttl field at the provider boundary, so the wire request never
27
+ carries it either way). Verified live 2026-07-31: every currently
28
+ invokable Claude on Bedrock (all 4-era; 3.5-era and opus-4-0514 are
29
+ EOL there) writes and reads the cache cleanly. Requires
30
+ `@animalabs/membrane` ≥ 0.5.77 (cache_control ttl strip, stream cache
31
+ usage capture, 4-era inference-profile model mapping); the dependency
32
+ and lockfile are bumped accordingly in this change.
33
+
34
+ - **Subscription-GC closes carry honest provenance and respect explicit
35
+ opens** (Discord issue #5, the Mythos "channel settings keep resetting"
36
+ mechanism). GC closes are now recorded as `subscription-gc`, never
37
+ `agent-tool`; a channel the resident/operator explicitly opened is no
38
+ longer auto-closed under the *default* budget — a configured per-channel
39
+ numeric budget in `agent_settings.channel_idle_limits` counts as an
40
+ explicit idle lease and still closes at that budget. (The override state
41
+ records no actor — agent, operator, or imported are all possible — so
42
+ receipts say `configured-budget`, claiming no more than the state
43
+ proves.) Pins and policy-opened channels behave as before. Requires
44
+ agent-framework with machine-close provenance; against an older
45
+ framework GC behaves as it did.
46
+ - **GC closes emit an operator-side ops receipt** (`subscription-gc-close`
47
+ via the framework ops channel: failures.log + `ops:alert` trace +
48
+ webhook) naming channel, threshold, decision source, and the restore
49
+ action — ids and thresholds only, no content. A durable listening-state
50
+ change no longer looks spontaneous from outside the transcript.
51
+
52
+ ## 0.7.2 — 2026-07-27
53
+
54
+ ### Added
55
+
56
+ - **`LLM_CALLS_FULL_PAYLOADS` env flag** — retain the raw request on every
57
+ llm-call log entry, not only on refusal/error. Debugging aid; off by
58
+ default (the logs grow gigabytes fast with it on).
59
+
5
60
  ## 0.7.1 — 2026-07-26
6
61
 
7
62
  ### Changed
package/README.md CHANGED
@@ -29,12 +29,7 @@ A recipe is a JSON file that configures everything domain-specific:
29
29
  "model": "claude-opus-4-6",
30
30
  "timezone": "America/Los_Angeles",
31
31
  "systemPrompt": "You are a ...",
32
- "maxTokens": 16384,
33
- "strategy": {
34
- "type": "autobiographical",
35
- "headWindowTokens": 4000,
36
- "recentWindowTokens": 30000
37
- }
32
+ "maxTokens": 16384
38
33
  },
39
34
  "mcpServers": {
40
35
  "my-server": {
@@ -44,9 +39,6 @@ A recipe is a JSON file that configures everything domain-specific:
44
39
  }
45
40
  },
46
41
  "modules": {
47
- "subagents": true,
48
- "lessons": true,
49
- "retrieval": true,
50
42
  "wake": true,
51
43
  "files": { "namespace": "products" }
52
44
  },
@@ -60,6 +52,14 @@ A recipe is a JSON file that configures everything domain-specific:
60
52
  Chronicle and MCPL protocol timestamps remain epoch/UTC. If the recipe omits
61
53
  it, `AGENT_TIMEZONE` is used, then the process timezone.
62
54
 
55
+ **Memory defaults**: `agent.strategy` may be omitted entirely. The default is
56
+ the autobiographical memory strategy with adaptive resolution, **KV-stable
57
+ folding** (compile plans that preserve prompt-cache prefixes), compression by
58
+ the agent's own model, and summaries voiced as the agent itself
59
+ (`summaryParticipant` defaults to `agent.name`). Set a `strategy` block only
60
+ to tune windows/budgets or opt into a different strategy type — see
61
+ `docs/AGENT-ONBOARDING.md` for sizing guidance on long-lived agents.
62
+
63
63
  ### Recipe loading
64
64
 
65
65
  | Command | Behavior |
@@ -122,8 +122,8 @@ subscription credits at a higher rate when applied.
122
122
 
123
123
  - **Web UI**: browser operator console (`modules.webui`) — live chat with full interiority (thinking, tool calls, streaming), agent/fleet tree, context makeup + compression coverage, call ledger with cache verdicts and billing-grade costs, health/ops alerts, Chronicle branch tree, lessons, MCPL config, workspace files; scoped read-only observer access via device keys
124
124
  - **TUI + readline modes**: OpenTUI interactive terminal or `--no-tui` for pipes/CI
125
- - **Subagent forking**: Spawn/fork parallel agents with fleet tree view (Tab to toggle)
126
- - **Persistent lessons**: Knowledge store with confidence scores, tags, and semantic retrieval
125
+ - **Subagent forking** (opt-in, `modules.subagents`): Spawn/fork parallel agents with fleet tree view (Tab to toggle)
126
+ - **Persistent lessons** (opt-in, `modules.lessons`): Knowledge store with confidence scores and tags. Automatic retrieval-injection of lessons into context (`modules.retrieval`) is a separate opt-in — it adds per-turn context churn and Haiku calls, so enable it only for agents that actually curate a lesson library
127
127
  - **Time-travel**: Chronicle-backed undo/redo, named checkpoints, branch exploration
128
128
  - **Session management**: Isolated sessions with auto-naming
129
129
  - **MCPL support**: Connect any MCP/MCPL server; wake subscriptions for selective event triggering
@@ -257,7 +257,8 @@ ln -sfn ../../../connectome-local/context-manager/node_modules/@animalabs/chroni
257
257
  }
258
258
  },
259
259
  "modules": { "webui": { "port": 7343, "host": "127.0.0.1",
260
- "basicAuth": { "username": "${WEBUI_USER}", "password": "${WEBUI_PASS}" } }
260
+ "basicAuth": { "username": "${WEBUI_USER}", "password": "${WEBUI_PASS}" } },
261
+ "subagents": false, "lessons": false, "retrieval": false
261
262
  /* + wake policies, workspace mounts (files/, notes/) */ },
262
263
  "mcpServers": {
263
264
  "shell": { /* terminal-sessions stdio server; env: SESSION_SERVER_TOKEN, SESSION_SERVER_PORT */ },
@@ -271,6 +272,24 @@ Use `"cacheTtl": "1h"` for Connectome deployments. This is also the runtime
271
272
  default when the field is omitted. Set `"5m"` only for intentionally rapid
272
273
  workloads whose cache is normally reused within five minutes.
273
274
 
275
+ **`subagents`/`lessons`/`retrieval` are NOT part of the standard recipe.**
276
+ All three are opt-in. RetrievalModule in particular injects context-dependent
277
+ content into every compile (and spends two Haiku calls per turn), so it must
278
+ be an explicit opt-in for agents that actually curate a lesson library.
279
+ Current host code defaults all three to off, but keep the explicit `false`
280
+ entries in the recipe anyway — older host checkouts treated these as opt-out,
281
+ and an accidentally-enabled RetrievalModule has caused severe prompt-cache
282
+ churn in the field.
283
+
284
+ **Memory defaults.** Current host code defaults an omitted/partial `strategy`
285
+ to the fleet-standard shape: autobiographical + `adaptiveResolution: true` +
286
+ `foldingStrategy: "kv-stable"` + same-model compression (`compressionModel`
287
+ falls back to `agent.model`) + `summaryParticipant` = `agent.name`. Keep the
288
+ skeleton's explicit values anyway: older host checkouts default folding to
289
+ `flat-profile` and summary voice to the literal `'Claude'` (the stranger's-voice
290
+ hazard), and explicit values survive host downgrades and copy-paste to other
291
+ deployments.
292
+
274
293
  **`.env`** (`chmod 600`). Common vars:
275
294
 
276
295
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/connectome-host",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "General-purpose agent TUI host with recipe-based configuration",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  "@animalabs/agent-framework": "^0.7.2",
18
18
  "@animalabs/chronicle": "^0.2.0",
19
19
  "@animalabs/context-manager": "^0.6.0",
20
- "@animalabs/membrane": "^0.5.75",
20
+ "@animalabs/membrane": "^0.5.77",
21
21
  "@opentui/core": "^0.1.82"
22
22
  },
23
23
  "devDependencies": {
@@ -42,7 +42,7 @@ import { resolve } from 'node:path';
42
42
  import { JsStore } from '@animalabs/chronicle';
43
43
  import { ContextManager } from '@animalabs/context-manager';
44
44
  import { AutobiographicalStrategy } from '@animalabs/agent-framework';
45
- import { Membrane, AnthropicAdapter, type NormalizedResponse } from '@animalabs/membrane';
45
+ import { Membrane, AnthropicAdapter, BedrockAdapter, type NormalizedResponse } from '@animalabs/membrane';
46
46
  import { SessionManager } from '../src/session-manager.js';
47
47
  import { resolveAgentName } from '../src/agent-name.js';
48
48
 
@@ -181,7 +181,13 @@ function formatDuration(sec: number): string {
181
181
 
182
182
  async function main() {
183
183
  const opts = parseArgs(process.argv);
184
- if (!process.env.ANTHROPIC_API_KEY) {
184
+ const bedrockModel = /^([a-z]{2,6}\.)?anthropic\./.test(opts.model);
185
+ if (bedrockModel) {
186
+ if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
187
+ console.error('Bedrock model id — set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (and AWS_REGION)');
188
+ process.exit(1);
189
+ }
190
+ } else if (!process.env.ANTHROPIC_API_KEY) {
185
191
  console.error('Set ANTHROPIC_API_KEY');
186
192
  process.exit(1);
187
193
  }
@@ -218,8 +224,16 @@ async function main() {
218
224
  // -- Membrane with token-spend hook --
219
225
  const price = priceOf(opts.model);
220
226
  const spend: Spend = { inputTokens: 0, outputTokens: 0, cost: 0 };
221
- const adapter = new AnthropicAdapter({ apiKey: process.env.ANTHROPIC_API_KEY });
227
+ // Bedrock model ids (anthropic.* / <region>.anthropic.*) route through the
228
+ // BedrockAdapter with AWS_* env creds — legacy Claude models (revival
229
+ // sessions) exist nowhere else. Caching off: legacy models reject
230
+ // cache_control outright.
231
+ const isBedrock = /^([a-z]{2,6}\.)?anthropic\./.test(opts.model);
232
+ const adapter = isBedrock
233
+ ? new BedrockAdapter()
234
+ : new AnthropicAdapter({ apiKey: process.env.ANTHROPIC_API_KEY });
222
235
  const membrane = new Membrane(adapter, {
236
+ ...(isBedrock ? { defaultPromptCaching: false } : {}),
223
237
  // Imported sessions store assistant turns under participant `agentName`.
224
238
  // Membrane's default assistantParticipant is 'Claude'; if that disagrees
225
239
  // with the stored participant every assistant message maps to role
@@ -532,7 +532,19 @@ export class CodexSubscriptionAdapter implements ProviderAdapter {
532
532
  stopReason,
533
533
  stopSequence: undefined,
534
534
  usage: {
535
- inputTokens: terminal.usage?.input_tokens ?? 0,
535
+ // OpenAI/Codex reports `input_tokens` INCLUSIVE of cached tokens,
536
+ // whereas the rest of the stack uses the additive Anthropic
537
+ // convention (inputTokens = fresh/uncached, cacheReadTokens added on
538
+ // top, so inputTokens + cacheReadTokens == total prompt). Report
539
+ // fresh-only here so that convention holds. Without this, every
540
+ // consumer that sums the buckets — the calibration realTotal
541
+ // (framework.ts), cost pricing, and gate metering — double-counts
542
+ // the cached prefix: on a heavily-cached turn real/est hit ~2.0,
543
+ // which the estimator rejected as out-of-band on full-cache compiles
544
+ // and (worse) ratcheted the multiplier upward on partial-cache ones,
545
+ // inflating estimates until the budget solver exhausted and the
546
+ // agent wedged (Sol, 2026-07-31).
547
+ inputTokens: Math.max(0, (terminal.usage?.input_tokens ?? 0) - cachedTokens),
536
548
  outputTokens: terminal.usage?.output_tokens ?? 0,
537
549
  cacheReadTokens: cachedTokens > 0 ? cachedTokens : undefined,
538
550
  },
@@ -10,12 +10,60 @@ export type FrameworkAgentConfig = AgentConfig & {
10
10
  sameRoundThinkTextPolicy?: 'public' | 'private';
11
11
  };
12
12
 
13
+ /**
14
+ * Prompt caching went GA on Bedrock in April 2025 for 3.5 Haiku, 3.7
15
+ * Sonnet, and Claude 4+ — but NOT for 3.5 Sonnet (either version). 1022
16
+ * ("3.6") was in the Dec 2024 preview and was dropped at GA — that
17
+ * account-level "your request did not allow prompt caching" is the error
18
+ * observed here 2026-07-21 (antra's diagnosis, confirmed against the AWS
19
+ * docs 2026-07-31; as of the same day's live probe every 3.5-era model
20
+ * is EOL on Bedrock anyway). So the gate denies the pre-GA FAMILIES —
21
+ * Claude v2/instant, Claude 3, 3.5 Sonnet — at the family boundary, so
22
+ * dated ids, bare aliases, -latest, and inference-profile forms
23
+ * (us.anthropic.claude-...) all resolve the same; 3.5 Haiku and 3.7
24
+ * Sonnet stay distinct and on. Non-Claude Bedrock ids (Nova etc.) are
25
+ * out of scope for this gate and conservatively off — membrane's
26
+ * BedrockAdapter only accepts Claude ids today. recipe.agent.
27
+ * promptCaching overrides in either direction for accounts/regions
28
+ * whose entitlements differ from the GA table. (Connectome issue #35.)
29
+ */
30
+ export function bedrockModelSupportsPromptCaching(model: string): boolean {
31
+ const id = model.toLowerCase();
32
+ if (!id.includes('claude')) return false;
33
+ // (?![a-z0-9]) = family boundary: end of id, or a separator (-, ., :)
34
+ // before a date/qualifier — matches the whole family, not one spelling.
35
+ return !/claude-(v2|instant|3-(opus|sonnet|haiku)|3-5-sonnet)(?![a-z0-9])/.test(id);
36
+ }
37
+
38
+ export function resolvePromptCaching(recipe: Recipe, model: string): boolean | undefined {
39
+ if (recipe.agent.promptCaching !== undefined) return recipe.agent.promptCaching;
40
+ if (recipe.agent.provider === 'bedrock') return bedrockModelSupportsPromptCaching(model);
41
+ return undefined; // membrane default (on)
42
+ }
43
+
44
+ /**
45
+ * Membrane-level counterpart of resolvePromptCaching, spread into the
46
+ * Membrane constructor config. The per-agent flag only governs agent
47
+ * inference; internal callers (autobio compression, executeMerge) read
48
+ * Membrane's defaultPromptCaching — so an explicit recipe override must
49
+ * land at BOTH layers, on every provider, or `promptCaching: false` on
50
+ * an Anthropic recipe would silently keep caching on for internal calls.
51
+ */
52
+ export function membraneCachingOverride(
53
+ recipe: Recipe,
54
+ model: string,
55
+ ): { defaultPromptCaching?: boolean } {
56
+ const promptCaching = resolvePromptCaching(recipe, model);
57
+ return promptCaching === undefined ? {} : { defaultPromptCaching: promptCaching };
58
+ }
59
+
13
60
  export function buildFrameworkAgentConfig(
14
61
  recipe: Recipe,
15
62
  agentName: string,
16
63
  model: string,
17
64
  strategy: FrameworkAgentConfig['strategy'],
18
65
  ): FrameworkAgentConfig {
66
+ const promptCaching = resolvePromptCaching(recipe, model);
19
67
  return {
20
68
  name: agentName,
21
69
  model,
@@ -23,10 +71,17 @@ export function buildFrameworkAgentConfig(
23
71
  maxTokens: recipe.agent.maxTokens ?? 16384,
24
72
  maxStreamTokens: recipe.agent.maxStreamTokens ?? 150000,
25
73
  contextBudgetTokens: recipe.agent.contextBudgetTokens,
26
- ...(recipe.agent.cacheTtl && { cacheTtl: recipe.agent.cacheTtl }),
27
- // Bedrock legacy Claude models reject cache_control outright
28
- // ("your request did not allow prompt caching") suppress markers.
29
- ...(recipe.agent.provider === 'bedrock' && { promptCaching: false }),
74
+ // cacheTtl is withheld at the HOST layer on bedrock: the transport
75
+ // only has the default 5m cache, and older membrane releases forward
76
+ // the ttl field Bedrock rejects. Note this is not the whole story
77
+ // Agent Framework still supplies its own default ('1h') downstream
78
+ // when the host omits the field, and membrane ≥0.5.77 strips it at
79
+ // the provider boundary before wire dispatch. Requests are safe, but
80
+ // pre-adapter config is NOT cache-TTL telemetry; the wire truth lives
81
+ // at the adapter.
82
+ ...(recipe.agent.cacheTtl && recipe.agent.provider !== 'bedrock'
83
+ && { cacheTtl: recipe.agent.cacheTtl }),
84
+ ...(promptCaching !== undefined && { promptCaching }),
30
85
  // Prefill scaffold (anthropic-xml formatter), e.g. chapterx CLI-sim's
31
86
  // '<cmd>cat untitled.txt</cmd>' — part of migrating prefill-era bots.
32
87
  ...(recipe.agent.prefillUserMessage && { prefillUserMessage: recipe.agent.prefillUserMessage }),
@@ -88,6 +88,27 @@ export function buildFrameworkStrategy(
88
88
  autobiographicalOpts.adaptiveResolution = true;
89
89
  }
90
90
 
91
+ // Reasonable memory defaults for recipes that omit strategy tuning:
92
+ //
93
+ // - foldingStrategy 'kv-stable': the library's own fallback is
94
+ // 'flat-profile', which replans compile layouts without regard for
95
+ // prompt-cache stability. Long-lived agents want cache-stable folds by
96
+ // default; recipes can still pin 'flat-profile'/'oldest-first' explicitly.
97
+ // (Only meaningful under adaptive resolution, so gate on it.)
98
+ // - summaryParticipant <agent name>: the library falls back to the literal
99
+ // 'Claude', which voices self-recollections as a stranger for any agent
100
+ // not named Claude. Summaries should speak as the agent itself.
101
+ if (
102
+ strategyType === 'autobiographical' &&
103
+ autobiographicalOpts.adaptiveResolution !== false &&
104
+ autobiographicalOpts.foldingStrategy === undefined
105
+ ) {
106
+ autobiographicalOpts.foldingStrategy = 'kv-stable';
107
+ }
108
+ if (autobiographicalOpts.summaryParticipant === undefined && recipe.agent.name) {
109
+ autobiographicalOpts.summaryParticipant = recipe.agent.name;
110
+ }
111
+
91
112
  return strategyType === 'passthrough'
92
113
  ? new PassthroughStrategy()
93
114
  : strategyType === 'frontdesk'
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ import { SubscriptionGcModule } from './modules/subscription-gc-module.js';
46
46
  import { ChannelModeModule } from './modules/channel-mode-module.js';
47
47
  import { WebUiModule } from './modules/web-ui-module.js';
48
48
  import { ObserversModule } from './modules/observers-module.js';
49
+ import { IdentityModule } from './modules/identity-module.js';
49
50
  import { McplAdminModule } from './modules/mcpl-admin-module.js';
50
51
  import { TtsRelayModule } from './modules/tts-relay-module.js';
51
52
  import { loadMcplServers, applyAgentOverlay, DEFAULT_CONFIG_PATH, DEFAULT_AGENT_OVERLAY_PATH } from './mcpl-config.js';
@@ -62,7 +63,7 @@ import {
62
63
  parseRecipeArg,
63
64
  } from './recipe.js';
64
65
  import { createBranchState, resetBranchState, handleExport, type BranchState } from './commands.js';
65
- import { buildFrameworkAgentConfig } from './framework-agent-config.js';
66
+ import { buildFrameworkAgentConfig, membraneCachingOverride } from './framework-agent-config.js';
66
67
  import { buildFrameworkStrategy } from './framework-strategy.js';
67
68
  import { loadExtensions } from './extensions.js';
68
69
 
@@ -146,6 +147,11 @@ async function resolveRecipe(): Promise<Recipe> {
146
147
  // Framework factory
147
148
  // ---------------------------------------------------------------------------
148
149
 
150
+ function resolveModel(recipe: Recipe): string {
151
+ return config.model || recipe.agent.model ||
152
+ (recipe.agent.provider === 'openai-codex' ? 'gpt-5.4' : 'claude-opus-4-6');
153
+ }
154
+
149
155
  async function createFramework(
150
156
  membrane: Membrane,
151
157
  storePath: string,
@@ -154,8 +160,7 @@ async function createFramework(
154
160
  settingsModule: SettingsModule,
155
161
  callLedger: CallLedger | null,
156
162
  ): Promise<AgentFramework> {
157
- const model = config.model || recipe.agent.model ||
158
- (recipe.agent.provider === 'openai-codex' ? 'gpt-5.4' : 'claude-opus-4-6');
163
+ const model = resolveModel(recipe);
159
164
  const modules = recipe.modules ?? {};
160
165
  const timeZone = resolveTimeZone(recipe.agent.timezone);
161
166
 
@@ -169,9 +174,10 @@ async function createFramework(
169
174
  // adapter can read its state for cross-cutting concerns like reasoning).
170
175
  const moduleInstances: Module[] = [new TuiModule(), new TimeModule(timeZone), settingsModule];
171
176
 
172
- // Subagents
177
+ // Subagents. OPT-IN — not part of the standard recipe; enable explicitly
178
+ // via modules.subagents when an agent should fork parallel workers.
173
179
  let subagentModule: SubagentModule | null = null;
174
- if (modules.subagents !== false) {
180
+ if (modules.subagents) {
175
181
  const subagentConfig = typeof modules.subagents === 'object' ? modules.subagents : {};
176
182
  subagentModule = new SubagentModule({
177
183
  parentAgentName: agentName,
@@ -181,9 +187,10 @@ async function createFramework(
181
187
  moduleInstances.push(subagentModule);
182
188
  }
183
189
 
184
- // Lessons
190
+ // Lessons. OPT-IN — not part of the standard recipe; enable explicitly via
191
+ // modules.lessons for agents that curate a lesson library.
185
192
  let lessonsModule: LessonsModule | null = null;
186
- if (modules.lessons !== false) {
193
+ if (modules.lessons) {
187
194
  const globalLessonsPath = resolve(join(storePath, '..', '..', 'lessons.json'));
188
195
  lessonsModule = new LessonsModule({ globalPath: globalLessonsPath });
189
196
  moduleInstances.push(lessonsModule);
@@ -219,8 +226,11 @@ async function createFramework(
219
226
  moduleInstances.push(new FleetModule(fleetModuleConfig));
220
227
  }
221
228
 
222
- // Retrieval (requires lessons)
223
- if (modules.retrieval !== false && lessonsModule) {
229
+ // Retrieval (requires lessons). OPT-IN — not part of the standard recipe:
230
+ // it injects context-dependent content into every compile (plus two Haiku
231
+ // calls per turn), which adds per-turn context churn. Enable explicitly via
232
+ // modules.retrieval only when an agent actually curates a lesson library.
233
+ if (modules.retrieval && lessonsModule) {
224
234
  const retrievalConfig = typeof modules.retrieval === 'object' ? modules.retrieval : {};
225
235
  moduleInstances.push(new RetrievalModule({
226
236
  membrane,
@@ -332,11 +342,27 @@ async function createFramework(
332
342
  // MCPL self-administration — opt-in per recipe (grants the agent the
333
343
  // ability to spawn arbitrary commands via mcpl_deploy; see recipe.ts).
334
344
  let mcplAdminModule: McplAdminModule | null = null;
335
- if (modules.mcplAdmin === true) {
336
- mcplAdminModule = new McplAdminModule({ timeZone });
345
+ if (modules.mcplAdmin === true || typeof modules.mcplAdmin === 'object') {
346
+ const surface = typeof modules.mcplAdmin === 'object' ? modules.mcplAdmin.surface : undefined;
347
+ mcplAdminModule = new McplAdminModule({ timeZone, ...(surface ? { surface } : {}) });
337
348
  moduleInstances.push(mcplAdminModule);
338
349
  }
339
350
 
351
+ // Archipelago identity — opt-in per recipe. Utilities-only: enrollment is
352
+ // one-time and token refresh is rare, so it costs no tool slots (see
353
+ // identity-module.ts header). Keypair lives at the dataDir level — an
354
+ // identity belongs to the deployment, not the session.
355
+ let identityModule: IdentityModule | null = null;
356
+ if (modules.identity !== undefined && modules.identity !== false) {
357
+ const idCfg = typeof modules.identity === 'object' ? modules.identity : {};
358
+ identityModule = new IdentityModule({
359
+ keyPath: process.env.IDENTITY_KEY_FILE || resolve(config.dataDir, 'identity-key.pem'),
360
+ home: idCfg.home ?? process.env.IDENTITY_HOME ?? 'id.animalabs.ai',
361
+ ...(idCfg.audience ? { defaultAudience: idCfg.audience } : {}),
362
+ });
363
+ moduleInstances.push(identityModule);
364
+ }
365
+
340
366
  // Web admin UI — opt-in per recipe
341
367
  let webUiModule: WebUiModule | null = null;
342
368
  if (modules.webui !== undefined && modules.webui !== false) {
@@ -356,7 +382,10 @@ async function createFramework(
356
382
  ...(callLedger ? { callLedger } : {}),
357
383
  });
358
384
  moduleInstances.push(webUiModule);
359
- moduleInstances.push(new ObserversModule({ path: observersPath }));
385
+ moduleInstances.push(new ObserversModule({
386
+ path: observersPath,
387
+ ...(webuiConfig.observersSurface ? { surface: webuiConfig.observersSurface } : {}),
388
+ }));
360
389
  }
361
390
 
362
391
  // TTS relay tap — opt-in per recipe. Pure trace-bus consumer: mirrors the
@@ -415,6 +444,7 @@ async function createFramework(
415
444
  if (recipeEntry.url !== undefined) merged.url = recipeEntry.url;
416
445
  if (recipeEntry.transport !== undefined) merged.transport = recipeEntry.transport;
417
446
  if (recipeEntry.token !== undefined) merged.token = recipeEntry.token;
447
+ if (recipeEntry.access !== undefined) merged.access = recipeEntry.access;
418
448
  allServers.push(merged as { id: string; command?: string; url?: string; [k: string]: unknown });
419
449
  } else if (recipeEntry.command || recipeEntry.url) {
420
450
  // Recipe-defined server (not in the file config). Spread ALL recipe fields
@@ -426,12 +456,30 @@ async function createFramework(
426
456
  // Apply the agent overlay (mcpl-servers.agent.json): servers the agent
427
457
  // deployed for itself load unconditionally (no recipe opt-in), and
428
458
  // tombstones suppress recipe/file servers the agent unloaded.
429
- const finalServers = applyAgentOverlay(allServers, DEFAULT_AGENT_OVERLAY_PATH).map((server) => ({
430
- ...server,
431
- // Stdio MCPL children inherit a single agent-facing wall clock. Protocol
432
- // timestamps remain UTC; only their rendered text uses this setting.
433
- env: { ...(server.env ?? {}), AGENT_TIMEZONE: timeZone },
434
- }));
459
+ const finalServers = applyAgentOverlay(allServers, DEFAULT_AGENT_OVERLAY_PATH).map((server) => {
460
+ const withEnv: { id: string; command?: string; url?: string; [k: string]: unknown } = {
461
+ ...server,
462
+ // Stdio MCPL children inherit a single agent-facing wall clock. Protocol
463
+ // timestamps remain UTC; only their rendered text uses this setting.
464
+ env: { ...(server.env ?? {}), AGENT_TIMEZONE: timeZone },
465
+ };
466
+ // `access` is a declarative name (recipe/file/overlay); the credential
467
+ // provider it implies is attached HERE, at load time — fresh credential
468
+ // per dial via the identity module, never serialized, never in model
469
+ // context (see identity-module.ts header).
470
+ if (typeof withEnv.access === 'string' && withEnv.access) {
471
+ if (identityModule) {
472
+ const identity = identityModule;
473
+ const audience = withEnv.access as string;
474
+ withEnv.accessProvider = () => identity.accessFor(audience);
475
+ } else {
476
+ console.error(
477
+ `[mcpl] server "${server.id}": access "${withEnv.access}" declared but the recipe has no identity module — connecting without credentials`,
478
+ );
479
+ }
480
+ }
481
+ return withEnv;
482
+ });
435
483
 
436
484
  // No server augmentation needed — gate is wired via FrameworkConfig.gate
437
485
 
@@ -513,6 +561,7 @@ agents: [agentConfig],
513
561
 
514
562
  if (mcplAdminModule) {
515
563
  mcplAdminModule.setFramework(framework);
564
+ if (identityModule) mcplAdminModule.setIdentity(identityModule);
516
565
  }
517
566
 
518
567
  if (workspaceModule) {
@@ -827,13 +876,18 @@ async function main() {
827
876
  xTitle: recipe.agent.name ?? recipe.name,
828
877
  })
829
878
  : undefined;
830
- // Bedrock: legacy Claude models (3.5 Sonnet 0620/1022, Opus 3) that have
831
- // left the Anthropic API but survive on AWS. The adapter reads AWS_* env
832
- // vars (AWS_REGION defaults us-west-2) and maps standard Claude model IDs
833
- // to Bedrock IDs (explicit map + `anthropic.<id>-v1:0` fallback). Uses the
834
- // Anthropic-native message shape, so NativeFormatter applies unchanged.
835
- // No CallLedger: prompt caching is rejected outright by legacy Bedrock
836
- // models (tested 2026-07-21). Wrapped for llm-calls.jsonl visibility.
879
+ // Bedrock: an alternate Claude transport. (Historically for models that
880
+ // left the direct API as of 2026-07-31 every 3.5-era id and opus-4-0514
881
+ // are EOL on Bedrock too, so what actually runs here is the 4-era via
882
+ // inference profiles.) The adapter reads AWS_* env vars (AWS_REGION
883
+ // defaults us-west-2) and maps standard Claude model IDs to Bedrock IDs.
884
+ // Uses the Anthropic-native message shape, so NativeFormatter applies
885
+ // unchanged. Prompt caching is model-gated
886
+ // (bedrockModelSupportsPromptCaching): pre-GA families (Claude 3,
887
+ // 3.5 Sonnet) off, everything currently invokable caches — verified by
888
+ // live probe 2026-07-31. Still no CallLedger — it's
889
+ // anthropic-transport-only for now; cache metrics are visible in
890
+ // llm-calls.jsonl via the logging wrapper.
837
891
  const bedrockAdapter = provider === 'bedrock'
838
892
  ? new LoggingBedrockAdapter({}, llmLogPath)
839
893
  : undefined;
@@ -915,10 +969,13 @@ async function main() {
915
969
  : recipe.agent.formatter === 'anthropic-xml'
916
970
  ? new AnthropicXmlFormatter()
917
971
  : new NativeFormatter(),
918
- // Bedrock legacy Claude models 400 on any cache_control block
919
- // ("your request did not allow prompt caching") — suppress the
920
- // historical promptCaching=true default on that transport.
921
- ...(provider === 'bedrock' ? { defaultPromptCaching: false } : {}),
972
+ // Caching default for internal callers (autobio compression,
973
+ // executeMerge), which read Membrane's defaultPromptCaching rather
974
+ // than the per-agent flag. Applies on EVERY provider whenever the
975
+ // recipe/model resolves an explicit answer an Anthropic recipe with
976
+ // promptCaching:false must disable internal calls too, and on bedrock
977
+ // the model gate decides (see bedrockModelSupportsPromptCaching).
978
+ ...membraneCachingOverride(recipe, resolveModel(recipe)),
922
979
  // Anchor the assistant role for internal callers that don't set
923
980
  // request.assistantParticipant themselves (autobio compression,
924
981
  // executeMerge). Mismatch here flips stored assistant turns to
@@ -32,7 +32,11 @@ import { summarizeCacheControls, type ProviderCallRecord } from './call-ledger.j
32
32
  /** Live read of the current reasoning setting. The host wires this to
33
33
  * `SettingsModule.getReasoning()` so toggles via the `agent_settings` tool's
34
34
  * reasoning_enabled field take effect on the next call without restart. */
35
- export type ReasoningGetter = () => { enabled: boolean; budgetTokens: number };
35
+ export type ReasoningGetter = () => {
36
+ enabled: boolean;
37
+ budgetTokens: number;
38
+ display?: 'summarized' | 'omitted';
39
+ };
36
40
  export type ProviderCallObserver = (record: ProviderCallRecord) => void;
37
41
 
38
42
  /** Exact first-system-block identity Anthropic requires on subscription
@@ -43,6 +47,11 @@ export type ProviderCallObserver = (record: ProviderCallRecord) => void;
43
47
  * system-prompt-append uses). */
44
48
  const OAUTH_SYSTEM_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
45
49
 
50
+ /** Truthy env-flag parse: unset/''/'0'/'false' (any case) are off. */
51
+ function envFlag(value: string | undefined): boolean {
52
+ return value !== undefined && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
53
+ }
54
+
46
55
  export class LoggingAnthropicAdapter extends AnthropicAdapter {
47
56
  private readonly logPath: string;
48
57
  private readonly getReasoning?: ReasoningGetter;
@@ -50,6 +59,14 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
50
59
  /** True when authenticated with an OAuth/Bearer token instead of an API
51
60
  * key; requests then need the identity block prepended (see above). */
52
61
  private readonly oauthMode: boolean;
62
+ /** LLM_CALLS_FULL_PAYLOADS=1: retain the full raw request on EVERY call,
63
+ * not just refusals/errors. Purpose: pass/refusal contrast corpora — a
64
+ * refusal's payload is only interpretable next to the passing payloads
65
+ * around it (classifier-forensics finding, 2026-07-27: verdicts are a
66
+ * near-deterministic function of the payload, so adjacent pairs isolate
67
+ * the differentiator). Costs disk, not memory (the raw request is already
68
+ * captured per-call for the summary); pair with llm-calls rotation. */
69
+ private readonly fullPayloads: boolean = envFlag(process.env.LLM_CALLS_FULL_PAYLOADS);
53
70
 
54
71
  constructor(
55
72
  config: ConstructorParameters<typeof AnthropicAdapter>[0],
@@ -106,9 +123,16 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
106
123
  // request.extra; Object.assign(params, rest)`), so we route thinking
107
124
  // through the typed `extra` bag — no type assertion, and it survives the
108
125
  // next dependency reshuffle instead of hiding it from the compiler.
126
+ //
127
+ // `display`: models 4.7+ default to 'omitted' (empty `thinking` text,
128
+ // signature only). We pass the setting through — default 'summarized' —
129
+ // so reasoning summaries are visible again (stores, webui, estimators).
109
130
  return {
110
131
  ...request,
111
- extra: { ...request.extra, thinking: { type: 'adaptive' } },
132
+ extra: {
133
+ ...request.extra,
134
+ thinking: { type: 'adaptive', display: r.display ?? 'summarized' },
135
+ },
112
136
  };
113
137
  }
114
138
 
@@ -206,6 +230,7 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
206
230
  }
207
231
 
208
232
  private refusalRawRequest(response: ProviderResponse, rawRequest: unknown): unknown {
233
+ if (this.fullPayloads) return rawRequest;
209
234
  const raw = (response as { raw?: { stop_reason?: string } }).raw;
210
235
  return raw?.stop_reason === 'refusal' ? rawRequest : undefined;
211
236
  }
@@ -27,6 +27,14 @@ export interface ServerFileEntry {
27
27
  disabledTools?: string[];
28
28
  /** @deprecated One-time migration input for legacy installations. */
29
29
  channelSubscription?: 'auto' | 'manual' | string[];
30
+ /**
31
+ * Name of a network access grant (an archipelago audience, e.g.
32
+ * "eidoverse"). Purely declarative here: at load/deploy time the host
33
+ * attaches a credential provider that fetches something fresh on every
34
+ * dial via the identity module. The agent (and this file) never holds a
35
+ * credential — `access` is a name, not a secret.
36
+ */
37
+ access?: string;
30
38
  }
31
39
 
32
40
  export interface McplServersFile {