@animalabs/connectome-host 0.7.2 → 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 +47 -0
- package/README.md +11 -11
- package/docs/AGENT-ONBOARDING.md +20 -1
- package/package.json +2 -2
- package/scripts/warmup-session.ts +17 -3
- package/src/codex-subscription-adapter.ts +13 -1
- package/src/framework-agent-config.ts +59 -4
- package/src/framework-strategy.ts +21 -0
- package/src/index.ts +86 -29
- package/src/logging-adapter.ts +13 -2
- package/src/mcpl-config.ts +8 -0
- package/src/modules/identity-module.ts +274 -0
- package/src/modules/mcpl-admin-module.ts +45 -1
- package/src/modules/observers-module.ts +12 -0
- package/src/modules/retrieval-module.ts +5 -1
- package/src/modules/settings-module.ts +28 -2
- package/src/modules/subscription-gc-module.ts +54 -1
- package/src/recipe.ts +85 -11
- package/test/bedrock-prompt-caching.test.ts +170 -0
- package/test/framework-strategy-defaults.test.ts +88 -0
- package/test/identity-and-surfaces.test.ts +157 -0
- package/test/subscription-gc-module.test.ts +152 -0
- package/web/bun.lock +345 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
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
|
+
|
|
5
52
|
## 0.7.2 — 2026-07-27
|
|
6
53
|
|
|
7
54
|
### Added
|
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
|
|
126
|
-
- **Persistent lessons
|
|
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
|
package/docs/AGENT-ONBOARDING.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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:
|
|
831
|
-
// left the
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
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
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
|
|
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
|
package/src/logging-adapter.ts
CHANGED
|
@@ -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 = () => {
|
|
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
|
|
@@ -119,9 +123,16 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
|
|
|
119
123
|
// request.extra; Object.assign(params, rest)`), so we route thinking
|
|
120
124
|
// through the typed `extra` bag — no type assertion, and it survives the
|
|
121
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).
|
|
122
130
|
return {
|
|
123
131
|
...request,
|
|
124
|
-
extra: {
|
|
132
|
+
extra: {
|
|
133
|
+
...request.extra,
|
|
134
|
+
thinking: { type: 'adaptive', display: r.display ?? 'summarized' },
|
|
135
|
+
},
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
138
|
|
package/src/mcpl-config.ts
CHANGED
|
@@ -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 {
|