@animalabs/connectome-host 0.7.2 → 0.7.4
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 +203 -10
- package/HEADLESS-FLEET-PLAN.md +22 -0
- package/README.md +22 -11
- package/docs/AGENT-ONBOARDING.md +20 -1
- package/docs/debug-context-api.md +2 -2
- package/docs/retrieval-traces.md +173 -0
- package/docs/webui-deployment.md +2 -1
- package/package.json +3 -3
- package/scripts/audit-module-optins.ts +288 -0
- 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 +33 -3
- package/src/headless.ts +14 -0
- package/src/index.ts +95 -35
- package/src/logging-adapter.ts +13 -2
- package/src/mcpl-config.ts +8 -0
- package/src/modules/fleet-module.ts +60 -1
- package/src/modules/fleet-types.ts +30 -1
- package/src/modules/identity-module.ts +274 -0
- package/src/modules/mcpl-admin-module.ts +78 -5
- package/src/modules/observers-module.ts +12 -0
- package/src/modules/retrieval-module.ts +254 -52
- package/src/modules/retrieval-trace-page.ts +254 -0
- package/src/modules/retrieval-trace.ts +904 -0
- package/src/modules/settings-module.ts +28 -2
- package/src/modules/subscription-gc-module.ts +54 -1
- package/src/modules/tts-relay-module.ts +33 -18
- package/src/modules/web-ui-module.ts +445 -894
- package/src/recipe.ts +137 -12
- package/src/retrieval-config.ts +39 -0
- package/src/strategies/frontdesk-strategy.ts +34 -125
- package/src/tui.ts +325 -54
- package/src/web/panel-data.ts +1187 -0
- package/src/web/protocol.ts +75 -10
- package/test/audit-module-optins.test.ts +167 -0
- package/test/bedrock-prompt-caching.test.ts +170 -0
- package/test/fleet-panel-request.test.ts +90 -0
- package/test/framework-strategy-defaults.test.ts +110 -0
- package/test/frontdesk-strategy.test.ts +25 -37
- package/test/headless-panel-request.test.ts +201 -0
- package/test/identity-and-surfaces.test.ts +157 -0
- package/test/mcpl-admin-module.test.ts +23 -0
- package/test/mock-headless-child.ts +14 -0
- package/test/retrieval-auth-loopback.test.ts +49 -0
- package/test/retrieval-config.test.ts +74 -0
- package/test/retrieval-module.test.ts +821 -0
- package/test/subscription-gc-module.test.ts +152 -0
- package/test/tui-format.test.ts +106 -0
- package/test/web-ui-context-coverage.test.ts +1 -1
- package/test/web-ui-module.test.ts +189 -3
- package/test/web-ui-observers.test.ts +8 -5
- package/test/web-ui-protocol.test.ts +0 -0
- package/web/bun.lock +345 -0
- package/web/src/App.tsx +159 -44
- package/web/src/Context.tsx +35 -8
- package/web/src/ContextDocument.tsx +20 -5
- package/web/src/Files.tsx +2 -8
- package/web/src/Lessons.tsx +2 -38
- package/web/src/Mcpl.tsx +80 -14
- package/web/src/Pins.tsx +5 -0
- package/web/src/Settings.tsx +5 -0
- package/web/vite.config.ts +8 -2
package/src/recipe.ts
CHANGED
|
@@ -71,16 +71,19 @@ export interface RecipeStrategy {
|
|
|
71
71
|
/** kv-stable: quality-gap override threshold (§13.4). Default 0.35. */
|
|
72
72
|
kvStableQualityGapRatio?: number;
|
|
73
73
|
compressionSlackRatio?: number;
|
|
74
|
+
/** Adaptive-resolution fold planner. The host defaults this to 'kv-stable'
|
|
75
|
+
* (cache-stable compile plans; see buildFrameworkStrategy) — set explicitly
|
|
76
|
+
* only to opt into the legacy planners. */
|
|
74
77
|
foldingStrategy?: 'flat-profile' | 'oldest-first' | 'kv-stable';
|
|
75
78
|
speculativeProduction?: boolean;
|
|
76
79
|
/** L1 production holdback: keep the newest N closed chunks out of the
|
|
77
80
|
* speculative compression queue (default 1); demand still overrides. */
|
|
78
81
|
l1HoldbackChunks?: number;
|
|
79
|
-
// Self-voice / compression framing.
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
82
|
+
// Self-voice / compression framing. The host defaults summaryParticipant to
|
|
83
|
+
// `agent.name` (buildFrameworkStrategy), so self-recollections speak as the
|
|
84
|
+
// agent itself. Set explicitly only to voice summaries as someone else —
|
|
85
|
+
// a mismatched participant surfaces in the compiled prompt as another voice
|
|
86
|
+
// speaking in first person about the agent.
|
|
84
87
|
summaryParticipant?: string;
|
|
85
88
|
summarySystemPrompt?: string;
|
|
86
89
|
summaryUserPrompt?: string;
|
|
@@ -92,6 +95,11 @@ export interface RecipeStrategy {
|
|
|
92
95
|
/** Override wording for the witnessed-record instruction ({targetTokens}
|
|
93
96
|
* substituted). */
|
|
94
97
|
witnessedInstruction?: string;
|
|
98
|
+
/** Identity reminder appended to every compression/merge instruction.
|
|
99
|
+
* For agents in multi-resident channels (and older models especially):
|
|
100
|
+
* names the agent and directs attribution so pure-witness chunks don't
|
|
101
|
+
* flip the summarizer into another speaker's identity. */
|
|
102
|
+
identityReminder?: string;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
export interface RecipeAgent {
|
|
@@ -121,8 +129,18 @@ export interface RecipeAgent {
|
|
|
121
129
|
* ContextManager default (100k) applies. Raise for large-context models. */
|
|
122
130
|
contextBudgetTokens?: number;
|
|
123
131
|
/** Prompt-cache TTL ('5m' | '1h') forwarded to the provider. Defaults to
|
|
124
|
-
* '1h'; set '5m' explicitly for high-frequency, sub-5-minute workloads.
|
|
132
|
+
* '1h'; set '5m' explicitly for high-frequency, sub-5-minute workloads.
|
|
133
|
+
* Not forwarded on bedrock — that transport only has the default 5m
|
|
134
|
+
* cache and rejects the ttl field. */
|
|
125
135
|
cacheTtl?: '5m' | '1h';
|
|
136
|
+
/**
|
|
137
|
+
* Explicit prompt-caching override. Unset means provider-appropriate
|
|
138
|
+
* default: on for everything except bedrock models that predate caching
|
|
139
|
+
* support there (see bedrockModelSupportsPromptCaching). Set false if a
|
|
140
|
+
* transport/account rejects cache_control markers — AWS's "your request
|
|
141
|
+
* did not allow prompt caching" can also be account/region-dependent.
|
|
142
|
+
*/
|
|
143
|
+
promptCaching?: boolean;
|
|
126
144
|
/**
|
|
127
145
|
* Same-round routing policy for ordinary text emitted beside think().
|
|
128
146
|
* Omitted preserves the compatibility carry-forward in Agent Framework.
|
|
@@ -150,6 +168,13 @@ export interface RecipeAgent {
|
|
|
150
168
|
thinking?: {
|
|
151
169
|
enabled: boolean;
|
|
152
170
|
budgetTokens?: number;
|
|
171
|
+
/** 'enabled' (explicit budget, legacy) or 'adaptive' (model-managed;
|
|
172
|
+
* required by opus-4-7+ / fable-5 era models). */
|
|
173
|
+
type?: 'enabled' | 'adaptive';
|
|
174
|
+
/** 'summarized' returns readable reasoning summaries in `thinking`;
|
|
175
|
+
* 'omitted' returns empty text + signature only. Models 4.7+ default
|
|
176
|
+
* to 'omitted' server-side. */
|
|
177
|
+
display?: 'summarized' | 'omitted';
|
|
153
178
|
};
|
|
154
179
|
/** OpenAI Responses settings. Reasoning applies to both OpenAI providers;
|
|
155
180
|
* compaction and serviceTier are API-key transport settings. */
|
|
@@ -188,6 +213,12 @@ export interface RecipeMcpServer {
|
|
|
188
213
|
transport?: 'stdio' | 'websocket';
|
|
189
214
|
/** Bearer token for WebSocket auth (appended as ?token= query param). */
|
|
190
215
|
token?: string;
|
|
216
|
+
/**
|
|
217
|
+
* Name of a host-managed access grant (archipelago audience, e.g.
|
|
218
|
+
* "eidoverse"). Requires the `identity` module. The host resolves fresh
|
|
219
|
+
* credentials per dial; neither the recipe nor the agent holds one.
|
|
220
|
+
*/
|
|
221
|
+
access?: string;
|
|
191
222
|
toolPrefix?: string;
|
|
192
223
|
enabledFeatureSets?: string[];
|
|
193
224
|
disabledFeatureSets?: string[];
|
|
@@ -382,9 +413,32 @@ export interface RecipeWorkspaceMount {
|
|
|
382
413
|
}
|
|
383
414
|
|
|
384
415
|
export interface RecipeModules {
|
|
416
|
+
/**
|
|
417
|
+
* Subagent forking (spawn/fork parallel agents). OPT-IN — defaults to off
|
|
418
|
+
* and is not part of the standard recipe.
|
|
419
|
+
*/
|
|
385
420
|
subagents?: boolean | { defaultModel?: string; defaultMaxTokens?: number };
|
|
421
|
+
/**
|
|
422
|
+
* Lesson library (persistent knowledge store + lesson tools). OPT-IN —
|
|
423
|
+
* defaults to off and is not part of the standard recipe.
|
|
424
|
+
*/
|
|
386
425
|
lessons?: boolean;
|
|
387
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Lesson retrieval-injection (requires `lessons`). OPT-IN — defaults to off
|
|
428
|
+
* and is deliberately not part of the standard recipe: it injects
|
|
429
|
+
* context-dependent content into every compile and spends up to two
|
|
430
|
+
* configured retrieval-model calls. Enable only for agents that actually
|
|
431
|
+
* curate a lesson library.
|
|
432
|
+
*/
|
|
433
|
+
retrieval?: boolean | {
|
|
434
|
+
model?: string;
|
|
435
|
+
maxInjected?: number;
|
|
436
|
+
/**
|
|
437
|
+
* Optional OpenAI Responses/Codex reasoning effort for both retrieval calls.
|
|
438
|
+
* Requires an explicit retrieval model.
|
|
439
|
+
*/
|
|
440
|
+
reasoningEffort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
441
|
+
};
|
|
388
442
|
wake?: boolean | import('@animalabs/agent-framework').GateConfig;
|
|
389
443
|
workspace?: boolean | { mounts: RecipeWorkspaceMount[]; configMount?: boolean };
|
|
390
444
|
/**
|
|
@@ -404,8 +458,23 @@ export interface RecipeModules {
|
|
|
404
458
|
*
|
|
405
459
|
* SECURITY: `mcpl_deploy` spawns arbitrary commands as the host user —
|
|
406
460
|
* enabling this module means trusting the agent with code execution.
|
|
461
|
+
*
|
|
462
|
+
* `{ surface: 'utilities' }` keeps the module but parks its four tools
|
|
463
|
+
* behind the framework's single `utils` meta-tool (mcpl management is
|
|
464
|
+
* rare; it needn't cost four schemas on every inference). `true` keeps
|
|
465
|
+
* the historical first-class surface.
|
|
407
466
|
*/
|
|
408
|
-
mcplAdmin?: boolean;
|
|
467
|
+
mcplAdmin?: boolean | { surface?: 'tools' | 'utilities' };
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The agent's own archipelago-home identity (connectome docs/home-node.md):
|
|
471
|
+
* an ed25519 keypair in the data dir, enrolled at the home node via an
|
|
472
|
+
* operator invite, exchanged for fresh aid1 audience tokens on demand.
|
|
473
|
+
* Utilities-only (`utils run identity--status/enroll/token`) — costs no
|
|
474
|
+
* tool slots. `home` defaults to id.animalabs.ai; `audience` is the
|
|
475
|
+
* default for `token` calls.
|
|
476
|
+
*/
|
|
477
|
+
identity?: boolean | { home?: string; audience?: string };
|
|
409
478
|
/**
|
|
410
479
|
* Cross-process child fleet. When true (shorthand), FleetModule is attached
|
|
411
480
|
* with no pre-configured children. When an object, declares children the
|
|
@@ -485,6 +554,13 @@ export interface RecipeModules {
|
|
|
485
554
|
export interface RecipeWebUi {
|
|
486
555
|
port?: number;
|
|
487
556
|
host?: string;
|
|
557
|
+
/**
|
|
558
|
+
* Where the observer grant tools (observers--get/grant/revoke) surface:
|
|
559
|
+
* 'tools' (default, historical) or 'utilities' (behind the `utils`
|
|
560
|
+
* meta-tool — grant edits are rare). Consent semantics unchanged: the
|
|
561
|
+
* agent holds the pen either way.
|
|
562
|
+
*/
|
|
563
|
+
observersSurface?: 'tools' | 'utilities';
|
|
488
564
|
/**
|
|
489
565
|
* Basic-Auth credentials. Required whenever the bind host is non-loopback
|
|
490
566
|
* (which is the default). `${VAR}`-substitutable from .env.
|
|
@@ -651,13 +727,13 @@ export const DEFAULT_RECIPE: Recipe = {
|
|
|
651
727
|
'You are a helpful assistant. You have access to tools provided by connected MCP servers.',
|
|
652
728
|
'Use them to help the user with their tasks.',
|
|
653
729
|
'',
|
|
654
|
-
'You can
|
|
730
|
+
'You can create persistent notes and write files to `products/` as outputs of your work.',
|
|
655
731
|
].join('\n'),
|
|
656
732
|
},
|
|
657
733
|
modules: {
|
|
658
|
-
subagents
|
|
659
|
-
|
|
660
|
-
|
|
734
|
+
// subagents + lessons + retrieval deliberately omitted — all opt-in only
|
|
735
|
+
// (retrieval additionally adds per-turn context churn + retrieval-model costs);
|
|
736
|
+
// see the RecipeModules field docs.
|
|
661
737
|
wake: true,
|
|
662
738
|
workspace: true,
|
|
663
739
|
},
|
|
@@ -914,6 +990,12 @@ export function validateRecipe(raw: unknown): Recipe {
|
|
|
914
990
|
}
|
|
915
991
|
agent.cacheTtl ??= '1h';
|
|
916
992
|
|
|
993
|
+
if (agent.promptCaching !== undefined && typeof agent.promptCaching !== 'boolean') {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`Recipe agent.promptCaching must be a boolean, got ${JSON.stringify(agent.promptCaching)}.`,
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
917
999
|
if (
|
|
918
1000
|
agent.sameRoundThinkTextPolicy !== undefined &&
|
|
919
1001
|
agent.sameRoundThinkTextPolicy !== 'public' &&
|
|
@@ -938,6 +1020,12 @@ export function validateRecipe(raw: unknown): Recipe {
|
|
|
938
1020
|
if (thinking.budgetTokens !== undefined && (typeof thinking.budgetTokens !== 'number' || thinking.budgetTokens <= 0)) {
|
|
939
1021
|
throw new Error('Recipe agent.thinking.budgetTokens must be a positive number.');
|
|
940
1022
|
}
|
|
1023
|
+
if (thinking.type !== undefined && thinking.type !== 'enabled' && thinking.type !== 'adaptive') {
|
|
1024
|
+
throw new Error('Recipe agent.thinking.type must be "enabled" or "adaptive".');
|
|
1025
|
+
}
|
|
1026
|
+
if (thinking.display !== undefined && thinking.display !== 'summarized' && thinking.display !== 'omitted') {
|
|
1027
|
+
throw new Error('Recipe agent.thinking.display must be "summarized" or "omitted".');
|
|
1028
|
+
}
|
|
941
1029
|
if (thinking.enabled === true && typeof thinking.budgetTokens === 'number' && typeof agent.maxTokens === 'number') {
|
|
942
1030
|
if (thinking.budgetTokens >= agent.maxTokens) {
|
|
943
1031
|
throw new Error(
|
|
@@ -1181,6 +1269,43 @@ export function validateRecipe(raw: unknown): Recipe {
|
|
|
1181
1269
|
}
|
|
1182
1270
|
}
|
|
1183
1271
|
|
|
1272
|
+
// Validate retrieval provider reasoning when configured.
|
|
1273
|
+
const retrieval = mods.retrieval;
|
|
1274
|
+
if (retrieval !== undefined && typeof retrieval !== 'boolean') {
|
|
1275
|
+
if (!retrieval || typeof retrieval !== 'object' || Array.isArray(retrieval)) {
|
|
1276
|
+
throw new Error('Recipe modules.retrieval must be a boolean or object.');
|
|
1277
|
+
}
|
|
1278
|
+
const retrievalConfig = retrieval as Record<string, unknown>;
|
|
1279
|
+
if (retrievalConfig.reasoningContext !== undefined) {
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
'modules.retrieval.reasoningContext is not supported: retrieval model calls ' +
|
|
1282
|
+
'are independent one-shot requests with no earlier reasoning items.',
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const efforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
1286
|
+
if (retrievalConfig.reasoningEffort !== undefined &&
|
|
1287
|
+
(typeof retrievalConfig.reasoningEffort !== 'string'
|
|
1288
|
+
|| !efforts.includes(retrievalConfig.reasoningEffort))) {
|
|
1289
|
+
throw new Error(`Invalid modules.retrieval.reasoningEffort ${JSON.stringify(retrievalConfig.reasoningEffort)}.`);
|
|
1290
|
+
}
|
|
1291
|
+
if (retrievalConfig.reasoningEffort !== undefined
|
|
1292
|
+
&& agent.provider !== 'openai-responses'
|
|
1293
|
+
&& agent.provider !== 'openai-codex') {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
'modules.retrieval.reasoningEffort requires agent.provider ' +
|
|
1296
|
+
'"openai-responses" or "openai-codex".',
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (retrievalConfig.reasoningEffort !== undefined
|
|
1300
|
+
&& (typeof retrievalConfig.model !== 'string'
|
|
1301
|
+
|| !retrievalConfig.model.trim())) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
'modules.retrieval.model must be a non-empty string when ' +
|
|
1304
|
+
'modules.retrieval.reasoningEffort is configured.',
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1184
1309
|
// Validate ttsRelay if present — url + token are load-bearing, and a
|
|
1185
1310
|
// recipe that names the module but can't reach a relay should fail at
|
|
1186
1311
|
// load, not silently stream nowhere.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Membrane } from '@animalabs/membrane';
|
|
2
|
+
import type { RecipeAgent, RecipeModules } from './recipe.js';
|
|
3
|
+
import type { RetrievalModuleConfig } from './modules/retrieval-module.js';
|
|
4
|
+
|
|
5
|
+
type RetrievalRecipeConfig = Exclude<RecipeModules['retrieval'], boolean | undefined>;
|
|
6
|
+
|
|
7
|
+
/** Translate the recipe's retrieval block into the module's runtime config. */
|
|
8
|
+
export function buildRetrievalModuleConfig(
|
|
9
|
+
membrane: Membrane,
|
|
10
|
+
retrieval: RecipeModules['retrieval'],
|
|
11
|
+
provider: RecipeAgent['provider'] = 'anthropic',
|
|
12
|
+
): RetrievalModuleConfig {
|
|
13
|
+
const config: RetrievalRecipeConfig = typeof retrieval === 'object' ? retrieval : {};
|
|
14
|
+
if (config.reasoningEffort
|
|
15
|
+
&& provider !== 'openai-responses'
|
|
16
|
+
&& provider !== 'openai-codex') {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'modules.retrieval.reasoningEffort requires agent.provider ' +
|
|
19
|
+
'"openai-responses" or "openai-codex".',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (config.reasoningEffort
|
|
23
|
+
&& (typeof config.model !== 'string' || !config.model.trim())) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'modules.retrieval.model must be a non-empty string when ' +
|
|
26
|
+
'modules.retrieval.reasoningEffort is configured.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const retrievalReasoning = config.reasoningEffort
|
|
30
|
+
? { effort: config.reasoningEffort }
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
membrane,
|
|
35
|
+
retrievalModel: config.model,
|
|
36
|
+
retrievalReasoning,
|
|
37
|
+
maxInjectedLessons: config.maxInjected,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
import { AutobiographicalStrategy } from '@animalabs/context-manager';
|
|
2
2
|
import type {
|
|
3
3
|
AutobiographicalConfig,
|
|
4
|
+
Chunk,
|
|
4
5
|
ContextEntry,
|
|
5
6
|
MessageStoreView,
|
|
6
7
|
ContextLogView,
|
|
7
8
|
TokenBudget,
|
|
8
9
|
StoredMessage,
|
|
9
|
-
SummaryEntry,
|
|
10
10
|
} from '@animalabs/context-manager';
|
|
11
11
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
12
12
|
import { formatZonedTime, resolveTimeZone } from '@animalabs/agent-framework';
|
|
13
13
|
|
|
14
|
-
// Structural mirror of AutobiographicalStrategy's internal Chunk.
|
|
15
|
-
// Kept inline because @animalabs/context-manager does not currently export it.
|
|
16
|
-
interface Chunk {
|
|
17
|
-
index: number;
|
|
18
|
-
startIndex: number;
|
|
19
|
-
endIndex: number;
|
|
20
|
-
messages: StoredMessage[];
|
|
21
|
-
tokens: number;
|
|
22
|
-
compressed: boolean;
|
|
23
|
-
diary?: string;
|
|
24
|
-
summaryId?: string;
|
|
25
|
-
phaseType?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
14
|
export type FrontdeskStrategyOptions = Partial<AutobiographicalConfig> & { timeZone?: string };
|
|
29
15
|
|
|
30
16
|
/**
|
|
@@ -35,10 +21,22 @@ export type FrontdeskStrategyOptions = Partial<AutobiographicalConfig> & { timeZ
|
|
|
35
21
|
* 1. Provenance wrapping — prepends a `[zulip · #channel · topic · @user · HH:MM · msg-id]`
|
|
36
22
|
* header to each MCPL-originated entry so the agent knows the message came from a
|
|
37
23
|
* channel and which reply path to use.
|
|
38
|
-
* 2. Topic-aware compression — chunk boundaries
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
24
|
+
* 2. Topic-aware compression — chunk boundaries close at Zulip-topic transitions
|
|
25
|
+
* (via the base chunker's `chunkBoundaryHint` seam) and the compression prompt
|
|
26
|
+
* instructs per-topic structure.
|
|
27
|
+
* 3. Question/mention salience — unanswered user questions and @mentions are named
|
|
28
|
+
* verbatim in the compression prompt so summaries preserve them.
|
|
29
|
+
*
|
|
30
|
+
* History note: through conhost 0.7.x this class forked the whole of
|
|
31
|
+
* `rebuildChunks` for feature 2 — written against a pre-chunk-persistence
|
|
32
|
+
* base, silently bypassing chunk records and the fail-closed orphan guard —
|
|
33
|
+
* and biased the hierarchical renderer's L1 selection for feature 3.
|
|
34
|
+
* Frontdesk agents now ride the adaptive path (see framework-strategy.ts
|
|
35
|
+
* defaults): chunking goes through the base implementation with a boundary
|
|
36
|
+
* hint, and salient content survives through the compression prompt rather
|
|
37
|
+
* than selection-order bias. Stores created by the fork carry no chunk
|
|
38
|
+
* records; context-manager's `migrateChunkRecords` backfills them from L1
|
|
39
|
+
* `sourceIds` on first load.
|
|
42
40
|
*/
|
|
43
41
|
export class FrontdeskStrategy extends AutobiographicalStrategy {
|
|
44
42
|
override readonly name: string = 'frontdesk';
|
|
@@ -162,77 +160,13 @@ export class FrontdeskStrategy extends AutobiographicalStrategy {
|
|
|
162
160
|
// ==========================================================================
|
|
163
161
|
|
|
164
162
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
163
|
+
* Close chunk boundaries at Zulip-topic transitions, so summaries of
|
|
164
|
+
* unrelated topics are not fused. Rides the base chunker (context-manager
|
|
165
|
+
* ≥0.6.3): record persistence, minimum-size and tool-pairing guards all
|
|
166
|
+
* apply to hinted closes.
|
|
167
167
|
*/
|
|
168
|
-
protected override
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const existingCompressed = new Map<string, Chunk>();
|
|
172
|
-
for (const chunk of this.chunks as unknown as Chunk[]) {
|
|
173
|
-
if (chunk.compressed) {
|
|
174
|
-
existingCompressed.set(this.chunkKey(chunk as never), chunk);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
this.chunks = [];
|
|
179
|
-
this.compressionQueue = [];
|
|
180
|
-
|
|
181
|
-
let currentChunk: StoredMessage[] = [];
|
|
182
|
-
let currentTokens = 0;
|
|
183
|
-
let chunkFilteredStart = 0;
|
|
184
|
-
const MIN_CHUNK = 4;
|
|
185
|
-
|
|
186
|
-
const push = (startIdx: number, endIdx: number, msgs: StoredMessage[], tokens: number) => {
|
|
187
|
-
const chunk = this.createChunk(
|
|
188
|
-
this.chunks.length,
|
|
189
|
-
startIdx,
|
|
190
|
-
endIdx,
|
|
191
|
-
msgs,
|
|
192
|
-
tokens,
|
|
193
|
-
existingCompressed as never,
|
|
194
|
-
);
|
|
195
|
-
this.chunks.push(chunk);
|
|
196
|
-
if (!chunk.compressed) this.compressionQueue.push(chunk.index);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
for (let i = 0; i < messagesToChunk.length; i++) {
|
|
200
|
-
const msg = messagesToChunk[i];
|
|
201
|
-
let msgTokens = store.estimateTokens(msg);
|
|
202
|
-
if (this.config.attachmentsIgnoreSize) {
|
|
203
|
-
msgTokens = this.estimateTextOnlyTokens(msg);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Topic boundary: close current chunk BEFORE adding msg when topic changes
|
|
207
|
-
// and the chunk has at least MIN_CHUNK messages. This keeps summaries of
|
|
208
|
-
// unrelated topics from being merged.
|
|
209
|
-
if (
|
|
210
|
-
currentChunk.length >= MIN_CHUNK &&
|
|
211
|
-
this.isTopicBoundary(currentChunk[currentChunk.length - 1], msg)
|
|
212
|
-
) {
|
|
213
|
-
push(chunkFilteredStart, i, currentChunk, currentTokens);
|
|
214
|
-
currentChunk = [];
|
|
215
|
-
currentTokens = 0;
|
|
216
|
-
chunkFilteredStart = i;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
currentChunk.push(msg);
|
|
220
|
-
currentTokens += msgTokens;
|
|
221
|
-
|
|
222
|
-
const shouldClose =
|
|
223
|
-
currentTokens >= this.config.targetChunkTokens && currentChunk.length >= MIN_CHUNK;
|
|
224
|
-
|
|
225
|
-
if (shouldClose) {
|
|
226
|
-
push(chunkFilteredStart, i + 1, currentChunk, currentTokens);
|
|
227
|
-
currentChunk = [];
|
|
228
|
-
currentTokens = 0;
|
|
229
|
-
chunkFilteredStart = i + 1;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (currentChunk.length >= MIN_CHUNK) {
|
|
234
|
-
push(chunkFilteredStart, messagesToChunk.length, currentChunk, currentTokens);
|
|
235
|
-
}
|
|
168
|
+
protected override chunkBoundaryHint(prev: StoredMessage, next: StoredMessage): boolean {
|
|
169
|
+
return this.isTopicBoundary(prev, next);
|
|
236
170
|
}
|
|
237
171
|
|
|
238
172
|
protected isTopicBoundary(prev: StoredMessage, curr: StoredMessage): boolean {
|
|
@@ -255,6 +189,11 @@ export class FrontdeskStrategy extends AutobiographicalStrategy {
|
|
|
255
189
|
// ==========================================================================
|
|
256
190
|
|
|
257
191
|
protected override getCompressionInstruction(chunk: Chunk, targetTokens: number): string {
|
|
192
|
+
// Witnessed chunks keep the base treatment (recipe-configurable
|
|
193
|
+
// witnessed prompt); the old fork predated it and steamrolled it.
|
|
194
|
+
if (this.chunkIsWitnessed(chunk)) {
|
|
195
|
+
return super.getCompressionInstruction(chunk, targetTokens);
|
|
196
|
+
}
|
|
258
197
|
const topics = new Set<string>();
|
|
259
198
|
for (const m of chunk.messages) {
|
|
260
199
|
const t = this.extractTopicKey(m);
|
|
@@ -289,43 +228,13 @@ export class FrontdeskStrategy extends AutobiographicalStrategy {
|
|
|
289
228
|
}
|
|
290
229
|
|
|
291
230
|
// ==========================================================================
|
|
292
|
-
//
|
|
293
|
-
// ==========================================================================
|
|
294
|
-
|
|
295
|
-
protected override selectL1Summaries(
|
|
296
|
-
shownL1: SummaryEntry[],
|
|
297
|
-
budget: number,
|
|
298
|
-
maxTokens: number,
|
|
299
|
-
): { selected: SummaryEntry[]; tokensUsed: number } {
|
|
300
|
-
if (shownL1.length === 0) return { selected: [], tokensUsed: 0 };
|
|
301
|
-
|
|
302
|
-
const isSalient = (s: SummaryEntry): boolean =>
|
|
303
|
-
s.sourceIds.some((id) => this.salientSourceIds.has(id));
|
|
304
|
-
|
|
305
|
-
const salient: SummaryEntry[] = [];
|
|
306
|
-
const routine: SummaryEntry[] = [];
|
|
307
|
-
for (const s of shownL1) {
|
|
308
|
-
(isSalient(s) ? salient : routine).push(s);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const selected: SummaryEntry[] = [];
|
|
312
|
-
let used = 0;
|
|
313
|
-
|
|
314
|
-
for (const group of [salient, routine]) {
|
|
315
|
-
for (const s of group) {
|
|
316
|
-
if (used + s.tokens > budget) break;
|
|
317
|
-
if (used + s.tokens > maxTokens) break;
|
|
318
|
-
selected.push(s);
|
|
319
|
-
used += s.tokens;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return { selected, tokensUsed: used };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ==========================================================================
|
|
327
|
-
// Salience tracking (shared by 3a and 3b)
|
|
231
|
+
// Salience tracking (feeds the compression instruction)
|
|
328
232
|
// ==========================================================================
|
|
233
|
+
// The pre-adaptive frontdesk also overrode the hierarchical renderer's
|
|
234
|
+
// selectL1Summaries to emit salient L1s first under budget pressure. Under
|
|
235
|
+
// adaptive resolution the picker selects a coverage frontier — emission
|
|
236
|
+
// order is not budget-competitive — so that bias is retired; salient
|
|
237
|
+
// content survives because getCompressionInstruction names it verbatim.
|
|
329
238
|
|
|
330
239
|
/**
|
|
331
240
|
* Recompute which user messages are "unanswered questions or mentions":
|