@animalabs/connectome-host 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +26 -0
- package/.github/workflows/changelog.yml +32 -0
- package/.github/workflows/publish.yml +46 -0
- package/CHANGELOG.md +140 -0
- package/CONTRIBUTING.md +126 -0
- package/HEADLESS-FLEET-PLAN.md +1 -1
- package/README.md +52 -5
- package/UNIFIED-TREE-PLAN.md +2 -0
- package/docs/AGENT-ONBOARDING.md +9 -8
- package/docs/DEPLOYMENTS.md +2 -2
- package/docs/DEV-ENVIRONMENT.md +28 -26
- package/docs/LOCUS-ROUTING-DESIGN.md +8 -2
- package/package.json +5 -4
- package/scripts/release-changelog.ts +32 -0
- package/src/codex-subscription-adapter.ts +938 -0
- package/src/commands.ts +95 -4
- package/src/extensions.ts +201 -0
- package/src/framework-agent-config.ts +20 -9
- package/src/framework-strategy.ts +24 -0
- package/src/index.ts +111 -19
- package/src/logging-bedrock-adapter.ts +145 -0
- package/src/modules/channel-mode-module.ts +9 -6
- package/src/modules/subscription-gc-module.ts +163 -34
- package/src/modules/tts-relay-module.ts +481 -0
- package/src/modules/web-ui-module.ts +45 -1
- package/src/recipe.ts +196 -10
- package/src/tui.ts +527 -137
- package/src/web/protocol.ts +35 -0
- package/test/codex-subscription-adapter.test.ts +226 -0
- package/test/commands-checkpoint-restore.test.ts +118 -0
- package/test/fast-command.test.ts +39 -0
- package/test/recipe-extensions.test.ts +295 -0
- package/test/recipe-provider.test.ts +14 -1
- package/test/subscription-gc-module.test.ts +156 -1
- package/test/tui-quit-confirm.test.ts +42 -0
- package/test/tui-short-agent-name.test.ts +36 -0
- package/test/web-ui-observers.test.ts +14 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/web/src/App.tsx +235 -20
- package/web/src/Branches.tsx +150 -0
- package/web/src/Context.tsx +21 -13
- package/web/src/Health.tsx +274 -0
- package/web/src/Lessons.tsx +2 -1
package/src/commands.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* /clear — Clear conversation display
|
|
15
15
|
* /mcp list|add|remove|env — Manage MCPL server config
|
|
16
16
|
* /budget [N] — Show/set stream token budget (e.g. /budget 1m)
|
|
17
|
+
* /fast [on|off|status] — Toggle Codex subscription Fast mode
|
|
17
18
|
* /session — Session management (list, new, switch, rename, delete)
|
|
18
19
|
* /recipe — Show current recipe info
|
|
19
20
|
* /newtopic — Reset head window (auto-summarize or with user context)
|
|
@@ -37,6 +38,10 @@ interface AppContext {
|
|
|
37
38
|
sessionManager: import('./session-manager.js').SessionManager;
|
|
38
39
|
recipe: Recipe;
|
|
39
40
|
branchState: BranchState;
|
|
41
|
+
codexAdapter?: {
|
|
42
|
+
isFastMode(): boolean;
|
|
43
|
+
setFastMode(enabled: boolean): void;
|
|
44
|
+
};
|
|
40
45
|
switchSession(id: string): Promise<void>;
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -146,12 +151,15 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
146
151
|
{ text: ' /restore <name> Restore to checkpoint', style: 'system' },
|
|
147
152
|
{ text: ' /branches List Chronicle branches', style: 'system' },
|
|
148
153
|
{ text: ' /checkout <name> Switch to branch', style: 'system' },
|
|
149
|
-
{ text: ' /history
|
|
154
|
+
{ text: ' /history [n] Show state transitions (last n)', style: 'system' },
|
|
155
|
+
{ text: ' /find <text> Search messages for text', style: 'system' },
|
|
156
|
+
{ text: ' /branchto <msgId> Branch from a specific message', style: 'system' },
|
|
150
157
|
{ text: ' /mcp list List MCPL servers', style: 'system' },
|
|
151
158
|
{ text: ' /mcp add <id> <cmd> Add/overwrite server', style: 'system' },
|
|
152
159
|
{ text: ' /mcp remove <id> Remove a server', style: 'system' },
|
|
153
160
|
{ text: ' /mcp env <id> K=V ... Set env vars on server', style: 'system' },
|
|
154
161
|
{ text: ' /budget [tokens] Show/set stream token budget', style: 'system' },
|
|
162
|
+
{ text: ' /fast [on|off|status] Toggle Codex subscription Fast mode', style: 'system' },
|
|
155
163
|
{ text: ' /session Show current session', style: 'system' },
|
|
156
164
|
{ text: ' /session list List all sessions', style: 'system' },
|
|
157
165
|
{ text: ' /session new [name] Create new session', style: 'system' },
|
|
@@ -211,6 +219,9 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
211
219
|
case 'budget':
|
|
212
220
|
return handleBudget(framework, args[0]);
|
|
213
221
|
|
|
222
|
+
case 'fast':
|
|
223
|
+
return handleFast(app, args[0]);
|
|
224
|
+
|
|
214
225
|
case 'session':
|
|
215
226
|
return handleSession(app, args);
|
|
216
227
|
|
|
@@ -233,6 +244,42 @@ export function handleCommand(command: string, app: AppContext): CommandResult {
|
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
246
|
|
|
247
|
+
function handleFast(app: AppContext, mode?: string): CommandResult {
|
|
248
|
+
const adapter = app.codexAdapter;
|
|
249
|
+
if (!adapter) {
|
|
250
|
+
return {
|
|
251
|
+
lines: [{
|
|
252
|
+
text: '/fast is available only when agent.provider is "openai-codex".',
|
|
253
|
+
style: 'system',
|
|
254
|
+
}],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const normalized = mode?.toLowerCase();
|
|
259
|
+
if (normalized === undefined || normalized === 'status') {
|
|
260
|
+
return {
|
|
261
|
+
lines: [{
|
|
262
|
+
text: `Codex Fast mode request is ${adapter.isFastMode() ? 'ON' : 'OFF'}.`,
|
|
263
|
+
style: 'system',
|
|
264
|
+
}],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (normalized !== 'on' && normalized !== 'off') {
|
|
268
|
+
return { lines: [{ text: 'Usage: /fast [on|off|status]', style: 'system' }] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const enabled = normalized === 'on';
|
|
272
|
+
adapter.setFastMode(enabled);
|
|
273
|
+
return {
|
|
274
|
+
lines: [{
|
|
275
|
+
text: enabled
|
|
276
|
+
? 'Codex Fast mode requested (higher subscription credit consumption when applied).'
|
|
277
|
+
: 'Codex Fast mode request disabled.',
|
|
278
|
+
style: 'system',
|
|
279
|
+
}],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
236
283
|
// ---------------------------------------------------------------------------
|
|
237
284
|
// /session subcommands
|
|
238
285
|
// ---------------------------------------------------------------------------
|
|
@@ -702,7 +749,13 @@ function handleCheckpoint(app: AppContext, name?: string): CommandResult {
|
|
|
702
749
|
if (!cm) return { lines: [{ text: 'No agent context manager.', style: 'system' }] };
|
|
703
750
|
|
|
704
751
|
const branch = cm.currentBranch();
|
|
705
|
-
|
|
752
|
+
// A checkpoint is a *position*, not just a branch: record the id of the
|
|
753
|
+
// last message so /restore can branch back to this exact point even after
|
|
754
|
+
// the branch head has moved on. branchName alone would restore to the
|
|
755
|
+
// branch HEAD — i.e. not roll anything back.
|
|
756
|
+
const { messages } = cm.queryMessages({});
|
|
757
|
+
const messageId = messages.length > 0 ? messages[messages.length - 1]!.id : undefined;
|
|
758
|
+
app.branchState.checkpoints.set(name, { branchName: branch.name, messageId });
|
|
706
759
|
|
|
707
760
|
return { lines: [{ text: `Checkpoint "${name}" saved at branch ${branch.name} (head: ${branch.head}).`, style: 'system' }] };
|
|
708
761
|
}
|
|
@@ -729,12 +782,50 @@ function handleRestore(app: AppContext, name?: string): CommandResult {
|
|
|
729
782
|
const cm = getAgentCM(app.framework);
|
|
730
783
|
if (!cm) return { lines: [{ text: 'No agent context manager.', style: 'system' }] };
|
|
731
784
|
|
|
732
|
-
|
|
785
|
+
// Restore to the recorded *position*. If the checkpoint captured a message
|
|
786
|
+
// id, branch at that message (same machinery as /undo) — switching to the
|
|
787
|
+
// stored branch name alone would land on its head, which by now may
|
|
788
|
+
// include everything the user wanted to roll back. Checkpoints from before
|
|
789
|
+
// the messageId field (or taken on an empty branch) fall back to the head.
|
|
790
|
+
let target = point.branchName;
|
|
791
|
+
let exact = false;
|
|
792
|
+
if (point.messageId) {
|
|
793
|
+
// A checkpoint is a POSITION, so position equality is the whole
|
|
794
|
+
// "already there" test. Comparing branch names too would kill the guard
|
|
795
|
+
// after the first restore (you'd be on restore-{name}-{ts}, never the
|
|
796
|
+
// original branch again) and every repeat /restore would mint another
|
|
797
|
+
// branch pointing at the same message.
|
|
798
|
+
const { messages } = cm.queryMessages({});
|
|
799
|
+
const atCheckpoint = messages.length > 0
|
|
800
|
+
&& messages[messages.length - 1]!.id === point.messageId;
|
|
801
|
+
if (atCheckpoint) {
|
|
802
|
+
return { lines: [{ text: `Already at checkpoint "${name}".`, style: 'system' }] };
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
// branchAt returns the new branch's NAME (the only thing switchBranch accepts).
|
|
806
|
+
target = cm.branchAt(point.messageId, `restore-${name}-${Date.now()}`);
|
|
807
|
+
exact = true;
|
|
808
|
+
// The record follows the position: keep the newest branch that holds
|
|
809
|
+
// the checkpoint message as the fallback for future restores.
|
|
810
|
+
app.branchState.checkpoints.set(name, { branchName: target, messageId: point.messageId });
|
|
811
|
+
} catch {
|
|
812
|
+
// branchAt resolves ids against the CURRENT branch's view of the log;
|
|
813
|
+
// after e.g. /undo the checkpoint message may not be reachable from
|
|
814
|
+
// here. Degrade to the checkpoint's branch head (the pre-messageId
|
|
815
|
+
// behavior) rather than failing the restore outright.
|
|
816
|
+
target = point.branchName;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
733
819
|
|
|
734
820
|
// switchBranch is async — strategy reinit needs to complete before next op.
|
|
735
821
|
const asyncWork = Promise.resolve(cm.switchBranch(target))
|
|
736
822
|
.then(() => ({
|
|
737
|
-
lines: [
|
|
823
|
+
lines: [
|
|
824
|
+
{ text: `Restored to checkpoint "${name}" (branch: ${target}).`, style: 'system' as const },
|
|
825
|
+
...(point.messageId && !exact
|
|
826
|
+
? [{ text: ' (note: exact checkpoint position unreachable from the current branch — restored to the branch head instead)', style: 'system' as const }]
|
|
827
|
+
: []),
|
|
828
|
+
],
|
|
738
829
|
branchChanged: true,
|
|
739
830
|
}))
|
|
740
831
|
.catch(err => ({
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe extensions — the loading seam for deployment-specific code.
|
|
3
|
+
*
|
|
4
|
+
* A recipe may declare an `extensions` block mapping a name to a local
|
|
5
|
+
* TypeScript/JavaScript module that registers custom context strategies
|
|
6
|
+
* and/or framework modules:
|
|
7
|
+
*
|
|
8
|
+
* "extensions": {
|
|
9
|
+
* "zk-strategy": {
|
|
10
|
+
* "kind": "strategy",
|
|
11
|
+
* "path": "./extensions/zk-strategy/index.ts",
|
|
12
|
+
* "config": { "floodWindowMs": 250 }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* The entry module must export a `register` function (named or default):
|
|
17
|
+
*
|
|
18
|
+
* export function register(api: ExtensionApi, config: Record<string, unknown>) {
|
|
19
|
+
* api.registerStrategy('zk', (ctx) => new ZkStrategy({ ...ctx.config }));
|
|
20
|
+
* api.registerModule((ctx) => new ZkFeederModule(config));
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Design constraint: this loader is deliberately dumb. It resolves nothing,
|
|
24
|
+
* fetches nothing, and negotiates no versions — it imports a local path and
|
|
25
|
+
* calls `register`. The invariant "this path exists and is compatible with
|
|
26
|
+
* the host's dependency tree" is established at install time by build
|
|
27
|
+
* tooling (connectome-cook), not here. Extensions run in-process and share
|
|
28
|
+
* the host's node_modules, so `extends AutobiographicalStrategy` resolves
|
|
29
|
+
* against the exact @animalabs/* versions the host ships.
|
|
30
|
+
*
|
|
31
|
+
* `kind` declares the extension's primary purpose. It gates recipe
|
|
32
|
+
* validation (a non-built-in `agent.strategy.type` is only accepted when at
|
|
33
|
+
* least one `kind: "strategy"` extension is declared) and informs build
|
|
34
|
+
* tooling; it does NOT restrict what `register` may call — a strategy
|
|
35
|
+
* extension may also register a companion module.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { isAbsolute } from 'node:path';
|
|
39
|
+
import { pathToFileURL } from 'node:url';
|
|
40
|
+
import type { ContextStrategy, Module } from '@animalabs/agent-framework';
|
|
41
|
+
import type { Recipe, RecipeExtension, RecipeStrategy } from './recipe.js';
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Factory contexts
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export interface StrategyFactoryContext {
|
|
48
|
+
/** The recipe's `agent.strategy` block verbatim — including keys the
|
|
49
|
+
* built-in schema doesn't know about, so custom strategies can define
|
|
50
|
+
* their own knobs without upstream schema changes. */
|
|
51
|
+
config: RecipeStrategy & Record<string, unknown>;
|
|
52
|
+
/** Resolved agent model id (recipe/env fallback chain already applied). */
|
|
53
|
+
model: string;
|
|
54
|
+
/** Resolved IANA time zone for the session. */
|
|
55
|
+
timeZone: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type StrategyFactory = (ctx: StrategyFactoryContext) => ContextStrategy;
|
|
59
|
+
|
|
60
|
+
export interface ModuleFactoryContext {
|
|
61
|
+
timeZone: string;
|
|
62
|
+
/** Session store path (Chronicle data dir) — same value createFramework uses. */
|
|
63
|
+
storePath: string;
|
|
64
|
+
/** Resolved agent model id. */
|
|
65
|
+
model: string;
|
|
66
|
+
/** The declaring extension entry's `config` blob, verbatim. */
|
|
67
|
+
config: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ModuleFactory = (ctx: ModuleFactoryContext) => Module;
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Registry
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export interface ExtensionApi {
|
|
77
|
+
/** Register a context-strategy factory under a `agent.strategy.type` name.
|
|
78
|
+
* Built-in names and duplicate registrations are rejected. */
|
|
79
|
+
registerStrategy(type: string, factory: StrategyFactory): void;
|
|
80
|
+
/** Register a framework-module factory. Instantiated after the built-in
|
|
81
|
+
* module list is assembled; if the instance has a `setFramework` method
|
|
82
|
+
* it is called after framework creation (same duck-typed contract the
|
|
83
|
+
* built-in modules use). */
|
|
84
|
+
registerModule(factory: ModuleFactory): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RegisteredModule {
|
|
88
|
+
/** Name of the extension entry that registered this factory. */
|
|
89
|
+
extensionName: string;
|
|
90
|
+
factory: ModuleFactory;
|
|
91
|
+
/** The extension entry's `config`, passed to the factory context. */
|
|
92
|
+
config: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ExtensionRegistry {
|
|
96
|
+
strategies: Map<string, StrategyFactory>;
|
|
97
|
+
modules: RegisteredModule[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const BUILTIN_STRATEGY_TYPES = ['autobiographical', 'passthrough', 'frontdesk'] as const;
|
|
101
|
+
|
|
102
|
+
export function isBuiltinStrategyType(type: string): boolean {
|
|
103
|
+
return (BUILTIN_STRATEGY_TYPES as readonly string[]).includes(type);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** An empty registry — used when a recipe declares no extensions. */
|
|
107
|
+
export function emptyExtensionRegistry(): ExtensionRegistry {
|
|
108
|
+
return { strategies: new Map(), modules: [] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Loader
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Import every extension declared by the recipe and collect registrations.
|
|
117
|
+
*
|
|
118
|
+
* Paths must be absolute by the time this runs — `loadRecipe` resolves
|
|
119
|
+
* relative extension paths against the recipe file's directory. Errors are
|
|
120
|
+
* wrapped with the extension name + path so a broken deployment names the
|
|
121
|
+
* component instead of surfacing a bare import stack.
|
|
122
|
+
*/
|
|
123
|
+
export async function loadExtensions(recipe: Recipe): Promise<ExtensionRegistry> {
|
|
124
|
+
const registry = emptyExtensionRegistry();
|
|
125
|
+
for (const [name, ext] of Object.entries(recipe.extensions ?? {})) {
|
|
126
|
+
await loadOneExtension(name, ext, registry);
|
|
127
|
+
}
|
|
128
|
+
return registry;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadOneExtension(
|
|
132
|
+
name: string,
|
|
133
|
+
ext: RecipeExtension,
|
|
134
|
+
registry: ExtensionRegistry,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
if (!isAbsolute(ext.path)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`extension "${name}": path "${ext.path}" is not absolute. ` +
|
|
139
|
+
`Relative paths are resolved by loadRecipe against the recipe file's directory; ` +
|
|
140
|
+
`URL-loaded recipes must use absolute extension paths.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let mod: Record<string, unknown>;
|
|
145
|
+
try {
|
|
146
|
+
mod = await import(pathToFileURL(ext.path).href) as Record<string, unknown>;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`extension "${name}": failed to import ${ext.path}: ` +
|
|
150
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
151
|
+
{ cause: err },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const register = (mod.register ?? mod.default) as unknown;
|
|
156
|
+
if (typeof register !== 'function') {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`extension "${name}" (${ext.path}) must export a "register" function ` +
|
|
159
|
+
`(named export or default export); got ${typeof register}.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const config = ext.config ?? {};
|
|
164
|
+
const api: ExtensionApi = {
|
|
165
|
+
registerStrategy(type, factory) {
|
|
166
|
+
if (typeof type !== 'string' || !type) {
|
|
167
|
+
throw new Error(`extension "${name}": registerStrategy requires a non-empty type name`);
|
|
168
|
+
}
|
|
169
|
+
if (isBuiltinStrategyType(type)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`extension "${name}": strategy type "${type}" collides with a built-in strategy`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (registry.strategies.has(type)) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`extension "${name}": strategy type "${type}" is already registered by another extension`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (typeof factory !== 'function') {
|
|
180
|
+
throw new Error(`extension "${name}": registerStrategy("${type}") requires a factory function`);
|
|
181
|
+
}
|
|
182
|
+
registry.strategies.set(type, factory);
|
|
183
|
+
},
|
|
184
|
+
registerModule(factory) {
|
|
185
|
+
if (typeof factory !== 'function') {
|
|
186
|
+
throw new Error(`extension "${name}": registerModule requires a factory function`);
|
|
187
|
+
}
|
|
188
|
+
registry.modules.push({ extensionName: name, factory, config });
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await Promise.resolve((register as (api: ExtensionApi, config: Record<string, unknown>) => unknown)(api, config));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`extension "${name}" (${ext.path}): register() threw: ` +
|
|
197
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
198
|
+
{ cause: err },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -24,20 +24,28 @@ export function buildFrameworkAgentConfig(
|
|
|
24
24
|
maxStreamTokens: recipe.agent.maxStreamTokens ?? 150000,
|
|
25
25
|
contextBudgetTokens: recipe.agent.contextBudgetTokens,
|
|
26
26
|
...(recipe.agent.cacheTtl && { cacheTtl: recipe.agent.cacheTtl }),
|
|
27
|
-
|
|
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 }),
|
|
30
|
+
// Prefill scaffold (anthropic-xml formatter), e.g. chapterx CLI-sim's
|
|
31
|
+
// '<cmd>cat untitled.txt</cmd>' — part of migrating prefill-era bots.
|
|
32
|
+
...(recipe.agent.prefillUserMessage && { prefillUserMessage: recipe.agent.prefillUserMessage }),
|
|
33
|
+
...((recipe.agent.provider === 'openai-responses' || recipe.agent.provider === 'openai-codex') && {
|
|
28
34
|
providerParams: {
|
|
29
35
|
reasoning: {
|
|
30
36
|
effort: recipe.agent.responses?.reasoningEffort ?? 'high',
|
|
31
37
|
context: recipe.agent.responses?.reasoningContext ?? 'all_turns',
|
|
32
38
|
},
|
|
33
|
-
...(recipe.agent.responses
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
...(recipe.agent.provider === 'openai-responses' ? {
|
|
40
|
+
...(recipe.agent.responses?.serviceTier ? {
|
|
41
|
+
service_tier: recipe.agent.responses.serviceTier,
|
|
42
|
+
} : {}),
|
|
43
|
+
...(recipe.agent.responses?.compactThreshold ? {
|
|
44
|
+
context_management: [{
|
|
45
|
+
type: 'compaction',
|
|
46
|
+
compact_threshold: recipe.agent.responses.compactThreshold,
|
|
47
|
+
}],
|
|
48
|
+
} : {}),
|
|
41
49
|
} : {}),
|
|
42
50
|
},
|
|
43
51
|
}),
|
|
@@ -47,5 +55,8 @@ export function buildFrameworkAgentConfig(
|
|
|
47
55
|
...(recipe.agent.sameRoundThinkTextPolicy !== undefined
|
|
48
56
|
? { sameRoundThinkTextPolicy: recipe.agent.sameRoundThinkTextPolicy }
|
|
49
57
|
: {}),
|
|
58
|
+
...(recipe.agent.proseRouting !== undefined
|
|
59
|
+
? { proseRouting: recipe.agent.proseRouting }
|
|
60
|
+
: {}),
|
|
50
61
|
};
|
|
51
62
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@animalabs/agent-framework';
|
|
6
6
|
import type { Recipe, RecipeStrategy } from './recipe.js';
|
|
7
7
|
import { FrontdeskStrategy } from './strategies/frontdesk-strategy.js';
|
|
8
|
+
import { isBuiltinStrategyType, type ExtensionRegistry } from './extensions.js';
|
|
8
9
|
|
|
9
10
|
const PASSTHROUGH_KEYS: ReadonlyArray<keyof RecipeStrategy> = [
|
|
10
11
|
'enforceBudget',
|
|
@@ -39,13 +40,36 @@ export function buildFrameworkStrategy(
|
|
|
39
40
|
recipe: Recipe,
|
|
40
41
|
model: string,
|
|
41
42
|
timeZone: string,
|
|
43
|
+
extensions?: ExtensionRegistry,
|
|
42
44
|
): ContextStrategy {
|
|
43
45
|
const strategyConfig = recipe.agent.strategy;
|
|
44
46
|
const strategyType = strategyConfig?.type ?? 'autobiographical';
|
|
47
|
+
|
|
48
|
+
// Non-built-in types resolve through the extension registry. Validation
|
|
49
|
+
// already required a strategy-kind extension to be declared; this catches
|
|
50
|
+
// the declared-but-didn't-register case with a precise error.
|
|
51
|
+
if (!isBuiltinStrategyType(strategyType)) {
|
|
52
|
+
const factory = extensions?.strategies.get(strategyType);
|
|
53
|
+
if (!factory) {
|
|
54
|
+
const known = extensions ? Array.from(extensions.strategies.keys()) : [];
|
|
55
|
+
throw new Error(
|
|
56
|
+
`strategy type "${strategyType}" is not built-in and no loaded extension registered it. ` +
|
|
57
|
+
`Registered custom types: ${known.length ? known.join(', ') : '(none)'}.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return factory({
|
|
61
|
+
config: (strategyConfig ?? {}) as RecipeStrategy & Record<string, unknown>,
|
|
62
|
+
model,
|
|
63
|
+
timeZone,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
45
66
|
const autobiographicalOpts: Record<string, unknown> = {
|
|
46
67
|
headWindowTokens: strategyConfig?.headWindowTokens ?? 4000,
|
|
47
68
|
recentWindowTokens: strategyConfig?.recentWindowTokens ?? 30000,
|
|
48
69
|
compressionModel: strategyConfig?.compressionModel ?? model,
|
|
70
|
+
...(strategyConfig?.compressionMaxTokens !== undefined
|
|
71
|
+
? { compressionMaxTokens: strategyConfig.compressionMaxTokens }
|
|
72
|
+
: {}),
|
|
49
73
|
autoTickOnNewMessage: true,
|
|
50
74
|
maxMessageTokens: strategyConfig?.maxMessageTokens ?? 10000,
|
|
51
75
|
...(strategyType === 'frontdesk' ? { timeZone } : {}),
|