@animalabs/connectome-host 0.7.3 → 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 +156 -10
- package/HEADLESS-FLEET-PLAN.md +22 -0
- package/README.md +12 -1
- package/docs/AGENT-ONBOARDING.md +1 -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 +2 -2
- package/scripts/audit-module-optins.ts +288 -0
- package/src/framework-strategy.ts +13 -4
- package/src/headless.ts +14 -0
- package/src/index.ts +12 -9
- package/src/modules/fleet-module.ts +60 -1
- package/src/modules/fleet-types.ts +30 -1
- package/src/modules/mcpl-admin-module.ts +33 -4
- package/src/modules/retrieval-module.ts +249 -51
- package/src/modules/retrieval-trace-page.ts +254 -0
- package/src/modules/retrieval-trace.ts +904 -0
- package/src/modules/tts-relay-module.ts +33 -18
- package/src/modules/web-ui-module.ts +445 -894
- package/src/recipe.ts +55 -4
- 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/fleet-panel-request.test.ts +90 -0
- package/test/framework-strategy-defaults.test.ts +22 -0
- package/test/frontdesk-strategy.test.ts +25 -37
- package/test/headless-panel-request.test.ts +201 -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/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/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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report-only audit of subagents/lessons/retrieval opt-ins across recipes.
|
|
3
|
+
*
|
|
4
|
+
* Background: published v0.7.2 and earlier treated these three modules as
|
|
5
|
+
* opt-OUT (an omitted `modules` key meant enabled), and DEFAULT_RECIPE
|
|
6
|
+
* enabled all three explicitly. Current main treats them as opt-IN
|
|
7
|
+
* (a4bd9fd). That flip is the right default, but it leaves existing
|
|
8
|
+
* deployments with two things only a human can settle:
|
|
9
|
+
*
|
|
10
|
+
* 1. A source recipe that explicitly says `lessons: true` stays enabled
|
|
11
|
+
* after upgrade — intentionally. Whether that `true` was a real choice
|
|
12
|
+
* or boilerplate copied from the old onboarding guide is not something
|
|
13
|
+
* a defaults change (or this script) can infer. It gets reported;
|
|
14
|
+
* the operator decides.
|
|
15
|
+
* 2. A persisted `data/.recipe.json` is a resolved snapshot of whatever
|
|
16
|
+
* was in effect at launch — under the old defaults that includes
|
|
17
|
+
* `subagents/lessons/retrieval: true` the operator never wrote. It is
|
|
18
|
+
* not necessarily the authoritative source recipe, so it's reported
|
|
19
|
+
* separately, as a pointer back to the source, never as a finding in
|
|
20
|
+
* itself.
|
|
21
|
+
*
|
|
22
|
+
* This script reads and reports. It never modifies a file, and it has no
|
|
23
|
+
* flag that would make it modify a file.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* bun scripts/audit-module-optins.ts <recipe.json | directory> [...more]
|
|
27
|
+
*
|
|
28
|
+
* Directories are scanned recursively for *.json (including .recipe.json
|
|
29
|
+
* snapshots; node_modules/.git skipped). Fleet children referenced by
|
|
30
|
+
* local path are followed automatically.
|
|
31
|
+
*
|
|
32
|
+
* Exit codes: 0 = nothing needs an operator decision; 2 = explicit enables
|
|
33
|
+
* (or inert-retrieval combinations) found — review the report; 1 = a path
|
|
34
|
+
* argument could not be read.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
38
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
39
|
+
|
|
40
|
+
export const AUDITED_MODULES = ['subagents', 'lessons', 'retrieval'] as const;
|
|
41
|
+
export type AuditedModule = (typeof AUDITED_MODULES)[number];
|
|
42
|
+
|
|
43
|
+
export type ModuleState = 'explicit-enable' | 'explicit-disable' | 'omitted';
|
|
44
|
+
|
|
45
|
+
export interface RecipeAudit {
|
|
46
|
+
/** Path as reported (relative to cwd where possible). */
|
|
47
|
+
path: string;
|
|
48
|
+
recipeName: string;
|
|
49
|
+
/** basename === '.recipe.json': a resolved launch snapshot, not a source. */
|
|
50
|
+
isSnapshot: boolean;
|
|
51
|
+
states: Record<AuditedModule, ModuleState>;
|
|
52
|
+
/** Snapshot has all three explicitly true — the old DEFAULT_RECIPE shape.
|
|
53
|
+
* Almost certainly captured pre-flip defaults, not an operator choice. */
|
|
54
|
+
matchesOldDefaultBoilerplate: boolean;
|
|
55
|
+
/** retrieval explicitly enabled while lessons is omitted: worked under the
|
|
56
|
+
* old defaults (omitted lessons = on), silently inert after upgrade. */
|
|
57
|
+
inertRetrieval: boolean;
|
|
58
|
+
/** Local fleet-children recipe paths referenced by this recipe. */
|
|
59
|
+
childRecipePaths: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface RawRecipeShape {
|
|
63
|
+
name?: unknown;
|
|
64
|
+
agent?: unknown;
|
|
65
|
+
modules?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A JSON document we treat as a recipe: object with a name and an agent
|
|
69
|
+
* block. Anything else in a scanned directory is silently skipped. */
|
|
70
|
+
export function looksLikeRecipe(raw: unknown): raw is RawRecipeShape {
|
|
71
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return false;
|
|
72
|
+
const o = raw as RawRecipeShape;
|
|
73
|
+
return typeof o.name === 'string' && !!o.agent && typeof o.agent === 'object';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function classifyModule(value: unknown): ModuleState {
|
|
77
|
+
if (value === undefined || value === null) return 'omitted';
|
|
78
|
+
if (value === false) return 'explicit-disable';
|
|
79
|
+
// true or a config object both enable (matches createFramework wiring).
|
|
80
|
+
return 'explicit-enable';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function auditRecipe(raw: RawRecipeShape, path: string): RecipeAudit {
|
|
84
|
+
const modules = (raw.modules && typeof raw.modules === 'object' ? raw.modules : {}) as Record<string, unknown>;
|
|
85
|
+
const states = Object.fromEntries(
|
|
86
|
+
AUDITED_MODULES.map((m) => [m, classifyModule(modules[m])]),
|
|
87
|
+
) as Record<AuditedModule, ModuleState>;
|
|
88
|
+
|
|
89
|
+
const isSnapshot = basename(path) === '.recipe.json';
|
|
90
|
+
|
|
91
|
+
const childRecipePaths: string[] = [];
|
|
92
|
+
const fleet = modules.fleet;
|
|
93
|
+
if (fleet && typeof fleet === 'object') {
|
|
94
|
+
const children = (fleet as { children?: unknown }).children;
|
|
95
|
+
if (Array.isArray(children)) {
|
|
96
|
+
for (const child of children) {
|
|
97
|
+
const ref = (child as { recipe?: unknown })?.recipe;
|
|
98
|
+
if (typeof ref !== 'string' || !ref) continue;
|
|
99
|
+
if (ref.startsWith('http://') || ref.startsWith('https://')) continue;
|
|
100
|
+
childRecipePaths.push(isAbsolute(ref) ? ref : resolve(dirname(resolve(path)), ref));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
path,
|
|
107
|
+
recipeName: String(raw.name),
|
|
108
|
+
isSnapshot,
|
|
109
|
+
states,
|
|
110
|
+
matchesOldDefaultBoilerplate:
|
|
111
|
+
isSnapshot &&
|
|
112
|
+
modules.subagents === true && modules.lessons === true && modules.retrieval === true,
|
|
113
|
+
inertRetrieval: states.retrieval === 'explicit-enable' && states.lessons === 'omitted',
|
|
114
|
+
childRecipePaths,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Filesystem walk (main-path only; the logic above is what the tests pin)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'web']);
|
|
123
|
+
|
|
124
|
+
function collectJsonFiles(root: string, out: string[]): void {
|
|
125
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
126
|
+
const full = join(root, entry.name);
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
if (!SKIP_DIRS.has(entry.name)) collectJsonFiles(full, out);
|
|
129
|
+
} else if (entry.name.endsWith('.json')) {
|
|
130
|
+
out.push(full);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function auditPaths(paths: string[]): { audits: RecipeAudit[]; unreadable: string[] } {
|
|
136
|
+
const files: string[] = [];
|
|
137
|
+
const unreadable: string[] = [];
|
|
138
|
+
for (const p of paths) {
|
|
139
|
+
if (!existsSync(p)) {
|
|
140
|
+
unreadable.push(p);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (statSync(p).isDirectory()) collectJsonFiles(p, files);
|
|
144
|
+
else files.push(p);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const audits: RecipeAudit[] = [];
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
const queue = [...files];
|
|
150
|
+
while (queue.length > 0) {
|
|
151
|
+
const file = queue.shift()!;
|
|
152
|
+
const key = resolve(file);
|
|
153
|
+
if (seen.has(key)) continue;
|
|
154
|
+
seen.add(key);
|
|
155
|
+
let raw: unknown;
|
|
156
|
+
try {
|
|
157
|
+
raw = JSON.parse(readFileSync(file, 'utf-8'));
|
|
158
|
+
} catch {
|
|
159
|
+
continue; // not JSON we can read — not our business to complain about
|
|
160
|
+
}
|
|
161
|
+
if (!looksLikeRecipe(raw)) continue;
|
|
162
|
+
const audit = auditRecipe(raw, file);
|
|
163
|
+
audits.push(audit);
|
|
164
|
+
// Follow local fleet children so a parent path argument covers the tree.
|
|
165
|
+
for (const child of audit.childRecipePaths) {
|
|
166
|
+
if (existsSync(child)) queue.push(child);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { audits, unreadable };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Report
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
const STATE_LINES: Record<AuditedModule, Record<ModuleState, string>> = {
|
|
177
|
+
subagents: {
|
|
178
|
+
'explicit-enable':
|
|
179
|
+
'explicitly enabled — stays on after upgrade. Keep if this agent really forks workers.',
|
|
180
|
+
'explicit-disable': 'explicitly disabled — no change on upgrade (belt-and-braces for old checkouts).',
|
|
181
|
+
omitted:
|
|
182
|
+
'omitted — ON under published ≤0.7.2, OFF after upgrade. Declare `true` only if this agent relied on forking.',
|
|
183
|
+
},
|
|
184
|
+
lessons: {
|
|
185
|
+
'explicit-enable':
|
|
186
|
+
'explicitly enabled — stays on after upgrade. Keep if this agent curates a lesson library.',
|
|
187
|
+
'explicit-disable': 'explicitly disabled — no change on upgrade (belt-and-braces for old checkouts).',
|
|
188
|
+
omitted:
|
|
189
|
+
'omitted — ON under published ≤0.7.2, OFF after upgrade. Declare `true` only if this agent relied on lessons.',
|
|
190
|
+
},
|
|
191
|
+
retrieval: {
|
|
192
|
+
'explicit-enable':
|
|
193
|
+
'explicitly enabled — stays on after upgrade: per-compile injection plus two Haiku calls per turn. Keep only as a real choice.',
|
|
194
|
+
'explicit-disable': 'explicitly disabled — no change on upgrade (belt-and-braces for old checkouts).',
|
|
195
|
+
omitted: 'omitted — ON under published ≤0.7.2 (when lessons ran), OFF after upgrade.',
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export function renderReport(audits: RecipeAudit[]): { text: string; needsDecision: number } {
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
let needsDecision = 0;
|
|
202
|
+
|
|
203
|
+
const sources = audits.filter((a) => !a.isSnapshot);
|
|
204
|
+
const snapshots = audits.filter((a) => a.isSnapshot);
|
|
205
|
+
|
|
206
|
+
for (const a of sources) {
|
|
207
|
+
lines.push(`${a.path} (recipe "${a.recipeName}")`);
|
|
208
|
+
for (const m of AUDITED_MODULES) {
|
|
209
|
+
const state = a.states[m];
|
|
210
|
+
if (state === 'explicit-enable') needsDecision++;
|
|
211
|
+
lines.push(` ${m}: ${STATE_LINES[m][state]}`);
|
|
212
|
+
}
|
|
213
|
+
if (a.inertRetrieval) {
|
|
214
|
+
needsDecision++;
|
|
215
|
+
lines.push(
|
|
216
|
+
' ⚠ retrieval is enabled but lessons is omitted. Under the old defaults omitted lessons still ran,',
|
|
217
|
+
' so retrieval worked; after upgrade lessons is off and retrieval is silently inert.',
|
|
218
|
+
' Either add `lessons: true` (if retrieval is a real choice) or drop `retrieval`.',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (snapshots.length > 0) {
|
|
225
|
+
lines.push('Resolved snapshots (data/.recipe.json) — informational only:');
|
|
226
|
+
lines.push(
|
|
227
|
+
' These are launch-time captures, not authoritative sources. Under the old defaults they',
|
|
228
|
+
' include enables the operator never wrote. Audit the source recipe each run was launched',
|
|
229
|
+
' from; the snapshot refreshes on the next launch from source.',
|
|
230
|
+
);
|
|
231
|
+
for (const a of snapshots) {
|
|
232
|
+
const enabled = AUDITED_MODULES.filter((m) => a.states[m] === 'explicit-enable');
|
|
233
|
+
const note = a.matchesOldDefaultBoilerplate
|
|
234
|
+
? 'all three enabled — matches the old DEFAULT_RECIPE shape, almost certainly pre-flip defaults, not a choice'
|
|
235
|
+
: enabled.length > 0
|
|
236
|
+
? `explicitly enabled here: ${enabled.join(', ')}`
|
|
237
|
+
: 'no audited modules enabled';
|
|
238
|
+
lines.push(` ${a.path} (recipe "${a.recipeName}"): ${note}`);
|
|
239
|
+
}
|
|
240
|
+
lines.push('');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { text: lines.join('\n'), needsDecision };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Main
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
function main(): void {
|
|
251
|
+
const args = process.argv.slice(2).filter((a) => !a.startsWith('--'));
|
|
252
|
+
if (args.length === 0) {
|
|
253
|
+
console.error('Usage: bun scripts/audit-module-optins.ts <recipe.json | directory> [...more]');
|
|
254
|
+
console.error('Report-only: reads recipes, changes nothing.');
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { audits, unreadable } = auditPaths(args);
|
|
259
|
+
for (const p of unreadable) console.error(`cannot read: ${p}`);
|
|
260
|
+
|
|
261
|
+
console.log('connectome-host module opt-in audit — report only, nothing is modified\n');
|
|
262
|
+
console.log(
|
|
263
|
+
'Published ≤0.7.2 treated subagents/lessons/retrieval as opt-OUT (omitted = enabled).\n' +
|
|
264
|
+
'Current main treats them as opt-IN (omitted = disabled). Explicit enables survive the\n' +
|
|
265
|
+
'upgrade by design — this report shows where each recipe stands so you can decide which\n' +
|
|
266
|
+
'of those are real choices and which are old boilerplate.\n',
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
let needsDecision = 0;
|
|
270
|
+
if (audits.length === 0) {
|
|
271
|
+
console.log('No recipe-shaped JSON found under the given paths.');
|
|
272
|
+
} else {
|
|
273
|
+
const report = renderReport(audits);
|
|
274
|
+
needsDecision = report.needsDecision;
|
|
275
|
+
console.log(report.text);
|
|
276
|
+
const sources = audits.filter((a) => !a.isSnapshot).length;
|
|
277
|
+
console.log(
|
|
278
|
+
`Summary: ${audits.length} recipe(s) audited (${sources} source, ${audits.length - sources} snapshot), ` +
|
|
279
|
+
`${needsDecision} item(s) need an operator decision.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Unreadable path arguments outrank findings in the exit code.
|
|
284
|
+
if (unreadable.length > 0) process.exit(1);
|
|
285
|
+
if (needsDecision > 0) process.exit(2);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (import.meta.main) main();
|
|
@@ -36,6 +36,7 @@ const PASSTHROUGH_KEYS: ReadonlyArray<keyof RecipeStrategy> = [
|
|
|
36
36
|
'summaryContextLabel',
|
|
37
37
|
'witnessedBeforeSequence',
|
|
38
38
|
'witnessedInstruction',
|
|
39
|
+
'identityReminder',
|
|
39
40
|
];
|
|
40
41
|
|
|
41
42
|
export function buildFrameworkStrategy(
|
|
@@ -82,9 +83,17 @@ export function buildFrameworkStrategy(
|
|
|
82
83
|
if (value !== undefined) autobiographicalOpts[key] = value;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// Autobiographical agents default to adaptive resolution
|
|
86
|
-
// opts out
|
|
87
|
-
|
|
86
|
+
// Autobiographical AND frontdesk agents default to adaptive resolution
|
|
87
|
+
// unless a recipe opts out. Frontdesk historically kept the hierarchical
|
|
88
|
+
// renderer; that geometry has no tail reservation and no way to shed
|
|
89
|
+
// summary mass, so a long-lived agent saturates a fixed budget into a
|
|
90
|
+
// terminal context refusal (2026-08-03 boter clerk outage). The adaptive
|
|
91
|
+
// picker solves a frontier to fit — recipes can still pin
|
|
92
|
+
// `adaptiveResolution: false` as a rollback lever.
|
|
93
|
+
if (
|
|
94
|
+
(strategyType === 'autobiographical' || strategyType === 'frontdesk') &&
|
|
95
|
+
autobiographicalOpts.adaptiveResolution === undefined
|
|
96
|
+
) {
|
|
88
97
|
autobiographicalOpts.adaptiveResolution = true;
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -99,7 +108,7 @@ export function buildFrameworkStrategy(
|
|
|
99
108
|
// 'Claude', which voices self-recollections as a stranger for any agent
|
|
100
109
|
// not named Claude. Summaries should speak as the agent itself.
|
|
101
110
|
if (
|
|
102
|
-
strategyType === 'autobiographical' &&
|
|
111
|
+
(strategyType === 'autobiographical' || strategyType === 'frontdesk') &&
|
|
103
112
|
autobiographicalOpts.adaptiveResolution !== false &&
|
|
104
113
|
autobiographicalOpts.foldingStrategy === undefined
|
|
105
114
|
) {
|
package/src/headless.ts
CHANGED
|
@@ -136,6 +136,8 @@ export async function runHeadless(app: AppContext, argv: string[] = []): Promise
|
|
|
136
136
|
'workspace-mounts-snapshot',
|
|
137
137
|
'workspace-tree-snapshot',
|
|
138
138
|
'workspace-file-snapshot',
|
|
139
|
+
'cancel-subagent-result',
|
|
140
|
+
'panel-response',
|
|
139
141
|
]);
|
|
140
142
|
|
|
141
143
|
function emit(event: Record<string, unknown>): void {
|
|
@@ -329,6 +331,18 @@ export async function runHeadless(app: AppContext, argv: string[] = []): Promise
|
|
|
329
331
|
}
|
|
330
332
|
return;
|
|
331
333
|
}
|
|
334
|
+
case 'panel-request': {
|
|
335
|
+
// Operator-panel query/mutation, shared verb. The SAME handler the
|
|
336
|
+
// WebUI host runs locally answers here, so parent and child views of
|
|
337
|
+
// any panel op can never drift. runPanelOp never throws — failures
|
|
338
|
+
// come back as {ok:false} and still produce a panel-response, which
|
|
339
|
+
// the parent's promise API requires to settle.
|
|
340
|
+
const op = typeof cmd.op === 'string' ? cmd.op : '';
|
|
341
|
+
const { runPanelOp } = await import('./web/panel-data.js');
|
|
342
|
+
const result = await runPanelOp(app, op, cmd.params ?? {});
|
|
343
|
+
emit({ type: 'panel-response', corrId: cmd.corrId, op, ...result });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
332
346
|
case 'cancel-subagent': {
|
|
333
347
|
const mod = app.framework.getAllModules().find((m) => m.name === 'subagent') as
|
|
334
348
|
| { cancelSubagent(name: string): boolean }
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
37
37
|
import { SubagentModule } from './modules/subagent-module.js';
|
|
38
38
|
import { LessonsModule } from './modules/lessons-module.js';
|
|
39
39
|
import { RetrievalModule } from './modules/retrieval-module.js';
|
|
40
|
+
import { buildRetrievalModuleConfig } from './retrieval-config.js';
|
|
40
41
|
import type { RecipeWorkspaceMount } from './recipe.js';
|
|
41
42
|
import { TuiModule } from './modules/tui-module.js';
|
|
42
43
|
import { TimeModule } from './modules/time-module.js';
|
|
@@ -103,6 +104,10 @@ interface AppContext {
|
|
|
103
104
|
branchState: BranchState;
|
|
104
105
|
userMessageCount: number;
|
|
105
106
|
codexAdapter?: CodexSubscriptionAdapter;
|
|
107
|
+
/** Content-free recent provider-call ledger. Consumed by the panel-data
|
|
108
|
+
* layer (health snapshots) in BOTH runtimes — WebUI host and headless
|
|
109
|
+
* fleet child. Null when the provider adapter exposes no ledger. */
|
|
110
|
+
callLedger: CallLedger | null;
|
|
106
111
|
|
|
107
112
|
/** Stop current framework, switch to a different session, start new framework. */
|
|
108
113
|
switchSession(id: string): Promise<void>;
|
|
@@ -227,16 +232,13 @@ async function createFramework(
|
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
// Retrieval (requires lessons). OPT-IN — not part of the standard recipe:
|
|
230
|
-
// it injects context-dependent content into every compile (plus two
|
|
231
|
-
//
|
|
232
|
-
//
|
|
235
|
+
// it injects context-dependent content into every compile (plus up to two
|
|
236
|
+
// configured retrieval-model calls), which adds per-turn context churn.
|
|
237
|
+
// Enable explicitly only when an agent actually curates a lesson library.
|
|
233
238
|
if (modules.retrieval && lessonsModule) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
retrievalModel: retrievalConfig.model,
|
|
238
|
-
maxInjectedLessons: retrievalConfig.maxInjected,
|
|
239
|
-
}));
|
|
239
|
+
moduleInstances.push(new RetrievalModule(
|
|
240
|
+
buildRetrievalModuleConfig(membrane, modules.retrieval, recipe.agent.provider),
|
|
241
|
+
));
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
// Gate config — core AF EventGate feature.
|
|
@@ -996,6 +998,7 @@ async function main() {
|
|
|
996
998
|
branchState: createBranchState(),
|
|
997
999
|
userMessageCount: 0,
|
|
998
1000
|
codexAdapter,
|
|
1001
|
+
callLedger,
|
|
999
1002
|
|
|
1000
1003
|
async switchSession(id: string) {
|
|
1001
1004
|
handleExport(this);
|
|
@@ -34,7 +34,7 @@ import { spawn as spawnProcess, type ChildProcess } from 'node:child_process';
|
|
|
34
34
|
import { connect as netConnect, type Socket } from 'node:net';
|
|
35
35
|
import { existsSync, mkdirSync, unlinkSync, openSync, closeSync, appendFileSync, realpathSync } from 'node:fs';
|
|
36
36
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
37
|
-
import { type IncomingCommand, type WireEvent, matchesSubscription } from './fleet-types.js';
|
|
37
|
+
import { type IncomingCommand, type WireEvent, type PanelResponseEvent, matchesSubscription } from './fleet-types.js';
|
|
38
38
|
import { loadRecipe } from '../recipe.js';
|
|
39
39
|
import { REDUCER_REQUIRED_EVENTS } from '../state/agent-tree-reducer.js';
|
|
40
40
|
|
|
@@ -1621,6 +1621,65 @@ export class FleetModule implements Module {
|
|
|
1621
1621
|
catch { return false; }
|
|
1622
1622
|
}
|
|
1623
1623
|
|
|
1624
|
+
/** Monotonic corrId source for requestPanel. */
|
|
1625
|
+
private panelSeq = 0;
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Run one operator-panel op (see src/web/panel-data.ts) in a fleet child
|
|
1629
|
+
* and await its `panel-response`. Promise-based counterpart to the
|
|
1630
|
+
* fire-and-forget request* verbs above: HTTP proxy routes need to await a
|
|
1631
|
+
* body, and the WS handlers are simpler for it too.
|
|
1632
|
+
*
|
|
1633
|
+
* Never rejects — a dead child, send failure, or timeout resolves as
|
|
1634
|
+
* `{ok:false, error, status}` (502 unreachable, 504 timeout), so callers
|
|
1635
|
+
* translate straight into a response without try/catch.
|
|
1636
|
+
*/
|
|
1637
|
+
requestPanel(
|
|
1638
|
+
childName: string,
|
|
1639
|
+
op: string,
|
|
1640
|
+
params?: Record<string, unknown>,
|
|
1641
|
+
timeoutMs = 30_000,
|
|
1642
|
+
): Promise<{ ok: boolean; data?: unknown; error?: string; status?: number }> {
|
|
1643
|
+
const child = this.children.get(childName);
|
|
1644
|
+
if (!child || !child.socket) {
|
|
1645
|
+
return Promise.resolve({
|
|
1646
|
+
ok: false,
|
|
1647
|
+
error: child ? `child '${childName}' is ${child.status}, not running` : `unknown child: ${childName}`,
|
|
1648
|
+
status: child ? 502 : 404,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
const corrId = `panel-${op}-${++this.panelSeq}-${Date.now().toString(36)}`;
|
|
1652
|
+
return new Promise((resolvePanel) => {
|
|
1653
|
+
let settled = false;
|
|
1654
|
+
const finish = (result: { ok: boolean; data?: unknown; error?: string; status?: number }): void => {
|
|
1655
|
+
if (settled) return;
|
|
1656
|
+
settled = true;
|
|
1657
|
+
unsub();
|
|
1658
|
+
clearTimeout(timer);
|
|
1659
|
+
resolvePanel(result);
|
|
1660
|
+
};
|
|
1661
|
+
const unsub = this.onChildEvent(childName, (_name, evt) => {
|
|
1662
|
+
if (evt.type !== 'panel-response') return;
|
|
1663
|
+
const e = evt as unknown as PanelResponseEvent;
|
|
1664
|
+
if (e.corrId !== corrId) return;
|
|
1665
|
+
finish({
|
|
1666
|
+
ok: e.ok === true,
|
|
1667
|
+
...(e.data !== undefined ? { data: e.data } : {}),
|
|
1668
|
+
...(typeof e.error === 'string' ? { error: e.error } : {}),
|
|
1669
|
+
...(typeof e.status === 'number' ? { status: e.status } : {}),
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
const timer = setTimeout(() => {
|
|
1673
|
+
finish({ ok: false, error: `panel op '${op}' timed out after ${timeoutMs}ms (child '${childName}' unresponsive)`, status: 504 });
|
|
1674
|
+
}, timeoutMs);
|
|
1675
|
+
try {
|
|
1676
|
+
this.sendToChild(child, { type: 'panel-request', op, ...(params ? { params } : {}), corrId });
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
finish({ ok: false, error: `send to child failed: ${err instanceof Error ? err.message : String(err)}`, status: 502 });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1624
1683
|
private async killChild(child: FleetChild): Promise<void> {
|
|
1625
1684
|
if (child.status === 'exited' || child.status === 'crashed') return;
|
|
1626
1685
|
const proc = child.process;
|
|
@@ -38,7 +38,18 @@ export type IncomingCommand =
|
|
|
38
38
|
* `cancel-subagent-result`. The child looks the agent up in its own
|
|
39
39
|
* SubagentModule, so this is the only way to stop a subagent that lives
|
|
40
40
|
* in a fleet child rather than the conductor. */
|
|
41
|
-
| { type: 'cancel-subagent'; name: string; corrId?: string }
|
|
41
|
+
| { type: 'cancel-subagent'; name: string; corrId?: string }
|
|
42
|
+
/**
|
|
43
|
+
* Run one operator-panel operation in the child (see PANEL_OPS in
|
|
44
|
+
* src/web/panel-data.ts: mcpl / settings(-update|-reset|-cancel-transition)
|
|
45
|
+
* / pins / pin-add / pin-remove / health / context-makeup /
|
|
46
|
+
* context-coverage / context-curve / context-preview / debug-context).
|
|
47
|
+
* Response is a single `panel-response` with the same corrId. One generic
|
|
48
|
+
* verb rather than a verb per panel: both ends dispatch through the SAME
|
|
49
|
+
* shared handler (`runPanelOp`), so a new panel surface needs no protocol
|
|
50
|
+
* change to work across the fleet.
|
|
51
|
+
*/
|
|
52
|
+
| { type: 'panel-request'; op: string; params?: Record<string, unknown>; corrId?: string };
|
|
42
53
|
|
|
43
54
|
// ---------------------------------------------------------------------------
|
|
44
55
|
// Child → Parent: events
|
|
@@ -134,6 +145,23 @@ export interface CancelSubagentResultEvent {
|
|
|
134
145
|
ts?: number;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
/** Response to a {type:'panel-request'} request. `data` is the same
|
|
149
|
+
* wire-shaped JSON the WebUI host serves locally for the given op;
|
|
150
|
+
* `ok:false` carries the error plus an HTTP-ish `status` so the parent's
|
|
151
|
+
* proxy routes can answer faithfully (404 unknown agent, 429 preview
|
|
152
|
+
* cooldown, 501 unsupported build). */
|
|
153
|
+
export interface PanelResponseEvent {
|
|
154
|
+
type: 'panel-response';
|
|
155
|
+
corrId?: string;
|
|
156
|
+
/** Echo of the requested op. */
|
|
157
|
+
op: string;
|
|
158
|
+
ok: boolean;
|
|
159
|
+
data?: unknown;
|
|
160
|
+
error?: string;
|
|
161
|
+
status?: number;
|
|
162
|
+
ts?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
/** Response to a {type:'request-workspace-file'} request. */
|
|
138
166
|
export interface WorkspaceFileSnapshotEvent {
|
|
139
167
|
type: 'workspace-file-snapshot';
|
|
@@ -163,6 +191,7 @@ export type WireEvent =
|
|
|
163
191
|
| WorkspaceTreeSnapshotEvent
|
|
164
192
|
| WorkspaceFileSnapshotEvent
|
|
165
193
|
| CancelSubagentResultEvent
|
|
194
|
+
| PanelResponseEvent
|
|
166
195
|
// Arbitrary framework TraceEvent passthrough. The child stamps every emitted
|
|
167
196
|
// event with `ts: Date.now()` in `emit()` (see headless.ts), so ts is always
|
|
168
197
|
// present on the wire even when the underlying TraceEvent doesn't declare it.
|
|
@@ -118,8 +118,9 @@ export class McplAdminModule implements Module {
|
|
|
118
118
|
{
|
|
119
119
|
name: 'mcpl_list',
|
|
120
120
|
description:
|
|
121
|
-
'List all MCPL servers:
|
|
122
|
-
'
|
|
121
|
+
'List all MCPL servers: connection/retry state, whether policy was established, ' +
|
|
122
|
+
'the effective grant, masked/denied capability paths, host-command authority, ' +
|
|
123
|
+
'tool count, target, and config source.',
|
|
123
124
|
inputSchema: { type: 'object', properties: {} },
|
|
124
125
|
},
|
|
125
126
|
{
|
|
@@ -227,7 +228,19 @@ export class McplAdminModule implements Module {
|
|
|
227
228
|
|
|
228
229
|
private handleList(): ToolResult {
|
|
229
230
|
const framework = this.requireFramework();
|
|
230
|
-
|
|
231
|
+
// These fields land in agent-framework 0.8's MCPL grant work. Keep them
|
|
232
|
+
// optional here so connectome-host remains truthful ("unknown") if it is
|
|
233
|
+
// temporarily run against an older framework package during rollout.
|
|
234
|
+
const live = framework.listMcplServers() as Array<
|
|
235
|
+
ReturnType<AgentFramework['listMcplServers']>[number] & {
|
|
236
|
+
retrying?: boolean;
|
|
237
|
+
policyEstablished?: boolean;
|
|
238
|
+
effectiveGrant?: string[];
|
|
239
|
+
maskedCapabilities?: string[];
|
|
240
|
+
deniedCapabilities?: string[];
|
|
241
|
+
allowHostCommands?: boolean;
|
|
242
|
+
}
|
|
243
|
+
>;
|
|
231
244
|
const overlay = readAgentOverlay(this.overlayPath);
|
|
232
245
|
const fileServers = readMcplServersFile(this.configPath);
|
|
233
246
|
|
|
@@ -237,8 +250,19 @@ export class McplAdminModule implements Module {
|
|
|
237
250
|
? 'agent-overlay'
|
|
238
251
|
: s.id in fileServers ? 'file/recipe' : 'recipe';
|
|
239
252
|
const target = s.command ?? s.url ?? '?';
|
|
253
|
+
const connectionState = s.connected ? 'CONNECTED' : s.retrying ? 'RETRYING' : 'DISCONNECTED';
|
|
254
|
+
const policyState = s.policyEstablished === undefined
|
|
255
|
+
? 'unknown'
|
|
256
|
+
: s.policyEstablished ? 'established' : 'not-established';
|
|
257
|
+
const hostCommands = s.allowHostCommands === undefined
|
|
258
|
+
? 'unknown'
|
|
259
|
+
: s.allowHostCommands ? 'allow' : 'deny';
|
|
240
260
|
lines.push(
|
|
241
|
-
`${s.id}: ${
|
|
261
|
+
`${s.id}: ${connectionState} — policy=${policyState}, ` +
|
|
262
|
+
`grant=${formatCapabilityList(s.effectiveGrant)}, ` +
|
|
263
|
+
`masked=${formatCapabilityList(s.maskedCapabilities)}, ` +
|
|
264
|
+
`denied=${formatCapabilityList(s.deniedCapabilities)}, ` +
|
|
265
|
+
`hostCommands=${hostCommands}; ${s.toolCount} tools, ` +
|
|
242
266
|
`prefix=${s.toolPrefix}, source=${source}, ${target}`,
|
|
243
267
|
);
|
|
244
268
|
}
|
|
@@ -378,3 +402,8 @@ export class McplAdminModule implements Module {
|
|
|
378
402
|
return ok(`Unloaded server "${id}" — its tools are gone from your toolset. ${persistNote}`);
|
|
379
403
|
}
|
|
380
404
|
}
|
|
405
|
+
|
|
406
|
+
function formatCapabilityList(paths: string[] | undefined): string {
|
|
407
|
+
if (paths === undefined) return 'unknown';
|
|
408
|
+
return `[${paths.join(',')}]`;
|
|
409
|
+
}
|