@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
|
@@ -33,6 +33,14 @@ import type {
|
|
|
33
33
|
export interface ReasoningSettings {
|
|
34
34
|
enabled: boolean;
|
|
35
35
|
budgetTokens: number;
|
|
36
|
+
/**
|
|
37
|
+
* How thinking content comes back from the API: 'summarized' returns a
|
|
38
|
+
* readable reasoning summary in the `thinking` field; 'omitted' returns an
|
|
39
|
+
* empty `thinking` field with only the encrypted signature. Models 4.7+
|
|
40
|
+
* default to 'omitted' server-side — we default to 'summarized' to restore
|
|
41
|
+
* the pre-4.7 behavior (visible reasoning in stores, webui, estimators).
|
|
42
|
+
*/
|
|
43
|
+
display: 'summarized' | 'omitted';
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export interface SettingsState {
|
|
@@ -40,7 +48,7 @@ export interface SettingsState {
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
const DEFAULTS: SettingsState = {
|
|
43
|
-
reasoning: { enabled: false, budgetTokens: 8192 },
|
|
51
|
+
reasoning: { enabled: false, budgetTokens: 8192, display: 'summarized' },
|
|
44
52
|
};
|
|
45
53
|
|
|
46
54
|
export class SettingsModule implements Module {
|
|
@@ -112,8 +120,16 @@ export class SettingsModule implements Module {
|
|
|
112
120
|
type: 'number',
|
|
113
121
|
description: 'Token budget for thinking blocks (min 1024).',
|
|
114
122
|
},
|
|
123
|
+
reasoning_display: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
enum: ['summarized', 'omitted'],
|
|
126
|
+
description:
|
|
127
|
+
"How your thinking is returned: 'summarized' (a readable summary of your reasoning " +
|
|
128
|
+
"is recorded alongside the signature) or 'omitted' (signature only, slightly faster " +
|
|
129
|
+
'first token; your reasoning is not visible to anyone, including you on replay).',
|
|
130
|
+
},
|
|
115
131
|
},
|
|
116
|
-
keys: ['reasoning_enabled', 'reasoning_budget_tokens'],
|
|
132
|
+
keys: ['reasoning_enabled', 'reasoning_budget_tokens', 'reasoning_display'],
|
|
117
133
|
get: () => this.reasoningSettingsView(),
|
|
118
134
|
update: (_agentName, patch) => {
|
|
119
135
|
const next = { ...this.state.reasoning };
|
|
@@ -130,6 +146,12 @@ export class SettingsModule implements Module {
|
|
|
130
146
|
}
|
|
131
147
|
next.budgetTokens = Math.max(1024, Math.round(budget));
|
|
132
148
|
}
|
|
149
|
+
if (patch.reasoning_display !== undefined) {
|
|
150
|
+
if (patch.reasoning_display !== 'summarized' && patch.reasoning_display !== 'omitted') {
|
|
151
|
+
throw new Error("reasoning_display must be 'summarized' or 'omitted'");
|
|
152
|
+
}
|
|
153
|
+
next.display = patch.reasoning_display;
|
|
154
|
+
}
|
|
133
155
|
this.state.reasoning = next;
|
|
134
156
|
this.ctx?.setState(this.state);
|
|
135
157
|
return this.reasoningSettingsView();
|
|
@@ -142,6 +164,9 @@ export class SettingsModule implements Module {
|
|
|
142
164
|
if (all || keys?.includes('reasoning_budget_tokens')) {
|
|
143
165
|
this.state.reasoning.budgetTokens = DEFAULTS.reasoning.budgetTokens;
|
|
144
166
|
}
|
|
167
|
+
if (all || keys?.includes('reasoning_display')) {
|
|
168
|
+
this.state.reasoning.display = DEFAULTS.reasoning.display;
|
|
169
|
+
}
|
|
145
170
|
this.ctx?.setState(this.state);
|
|
146
171
|
return this.reasoningSettingsView();
|
|
147
172
|
},
|
|
@@ -153,6 +178,7 @@ export class SettingsModule implements Module {
|
|
|
153
178
|
return {
|
|
154
179
|
reasoning_enabled: this.state.reasoning.enabled,
|
|
155
180
|
reasoning_budget_tokens: this.state.reasoning.budgetTokens,
|
|
181
|
+
reasoning_display: this.state.reasoning.display,
|
|
156
182
|
};
|
|
157
183
|
}
|
|
158
184
|
|
|
@@ -336,11 +336,25 @@ export class SubscriptionGcModule implements Module {
|
|
|
336
336
|
// Cross the threshold → close and clear the counter.
|
|
337
337
|
delete this.state.counters[channelId];
|
|
338
338
|
this.persistNow();
|
|
339
|
+
// A CONFIGURED numeric budget for this channel is an explicit idle
|
|
340
|
+
// lease: someone chose a close-at-N budget for this specific channel,
|
|
341
|
+
// so the registry may close even an explicitly-opened one. The state
|
|
342
|
+
// does not record WHO configured it (agent via agent_settings,
|
|
343
|
+
// operator, or imported before these semantics existed), so nothing
|
|
344
|
+
// downstream may claim "agent-set" — receipts say 'configured-budget',
|
|
345
|
+
// actor unknown. The global default is not consent of any kind; the
|
|
346
|
+
// registry refuses machine closes of explicit opens under it (#5).
|
|
347
|
+
const hasConfiguredLease = typeof this.state.overrides[channelId] === 'number';
|
|
339
348
|
const result = await this.ctx
|
|
340
349
|
?.callTool({
|
|
341
350
|
id: `gc-unsub-${this.callSeq++}`,
|
|
342
351
|
name: 'channel_close',
|
|
343
|
-
input: {
|
|
352
|
+
input: {
|
|
353
|
+
channelId,
|
|
354
|
+
serverId: this.serverId,
|
|
355
|
+
source: 'subscription-gc',
|
|
356
|
+
overrideExplicitOpen: hasConfiguredLease,
|
|
357
|
+
},
|
|
344
358
|
})
|
|
345
359
|
.catch((err: unknown) => ({
|
|
346
360
|
success: false,
|
|
@@ -349,6 +363,34 @@ export class SubscriptionGcModule implements Module {
|
|
|
349
363
|
}));
|
|
350
364
|
|
|
351
365
|
if (result && result.success) {
|
|
366
|
+
// Operator-side receipt (privacy-minimal: ids and thresholds, no
|
|
367
|
+
// content) — a GC close changes durable listening state and must
|
|
368
|
+
// not look spontaneous from outside the transcript. Duck-typed
|
|
369
|
+
// against a framework that may not have ModuleContext.notifyOps yet
|
|
370
|
+
// (skipped there), and invoked THROUGH the context object: the real
|
|
371
|
+
// ModuleContextImpl.notifyOps reads `this`, so a detached
|
|
372
|
+
// `const f = ctx.notifyOps; f(...)` throws in production while
|
|
373
|
+
// passing against arrow-function mocks.
|
|
374
|
+
const opsCtx = this.ctx as unknown as {
|
|
375
|
+
notifyOps?: (kind: string, agent: string, message: string, data?: Record<string, unknown>) => void;
|
|
376
|
+
} | null;
|
|
377
|
+
opsCtx?.notifyOps?.(
|
|
378
|
+
'subscription-gc-close',
|
|
379
|
+
this.ctx?.getAgents()[0]?.name ?? 'unknown',
|
|
380
|
+
`subscription-gc auto-closed channel ${channelId} (over ${limit} ambient chars ` +
|
|
381
|
+
`since last activation${hasConfiguredLease ? ', configured per-channel budget' : ', default budget'}). ` +
|
|
382
|
+
`Restore: channel_open ${channelId}, or agent_settings channel_idle_limits.`,
|
|
383
|
+
{
|
|
384
|
+
channelId,
|
|
385
|
+
limitChars: limit,
|
|
386
|
+
decisionSource: 'subscription-gc',
|
|
387
|
+
// 'configured-budget' deliberately does NOT claim an actor: the
|
|
388
|
+
// override state records no provenance (agent, operator, or
|
|
389
|
+
// imported are all possible).
|
|
390
|
+
lease: hasConfiguredLease ? 'configured-budget' : 'default',
|
|
391
|
+
restore: `channel_open ${channelId}`,
|
|
392
|
+
},
|
|
393
|
+
);
|
|
352
394
|
return {
|
|
353
395
|
addMessages: [
|
|
354
396
|
{
|
|
@@ -367,6 +409,17 @@ export class SubscriptionGcModule implements Module {
|
|
|
367
409
|
],
|
|
368
410
|
};
|
|
369
411
|
}
|
|
412
|
+
|
|
413
|
+
// The registry refused because the channel was explicitly opened and
|
|
414
|
+
// we hold no lease: stand down quietly — the janitor doesn't argue
|
|
415
|
+
// with stated intent. The counter stays cleared, so the next refusal
|
|
416
|
+
// is at least a full budget away; if the channel's provenance later
|
|
417
|
+
// changes (reopened by policy), GC self-heals.
|
|
418
|
+
const refusal = (result as { data?: { refusal?: string } } | undefined)?.data?.refusal;
|
|
419
|
+
if (refusal === 'explicit-open') {
|
|
420
|
+
return {};
|
|
421
|
+
}
|
|
422
|
+
|
|
370
423
|
// Close failed — keep the channel counted so we retry on the next
|
|
371
424
|
// ambient message rather than silently giving up.
|
|
372
425
|
this.state.counters[channelId] = next;
|
|
@@ -232,6 +232,13 @@ function rawChannelId(mcplId: string): string {
|
|
|
232
232
|
return ix === -1 ? mcplId : mcplId.slice(ix + 1);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/** Channel-bound inference traces carry `channelId` at runtime, but the
|
|
236
|
+
* framework's TraceEvent union doesn't declare it — read it through a cast. */
|
|
237
|
+
function traceChannelId(event: unknown): string | undefined {
|
|
238
|
+
const v = (event as { channelId?: unknown }).channelId;
|
|
239
|
+
return typeof v === 'string' ? v : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
235
242
|
const collapseWs = (s: string): string => s.replace(/\s+/g, ' ').trim();
|
|
236
243
|
|
|
237
244
|
export class TtsRelayModule implements Module {
|
|
@@ -338,18 +345,41 @@ export class TtsRelayModule implements Module {
|
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
private onTraceEvent(event: TraceEvent): void {
|
|
348
|
+
// `mcpl:speech-routed` is a custom trace type emitted by the speech
|
|
349
|
+
// router, not part of the framework's TraceEvent union — the typed
|
|
350
|
+
// switch below could never narrow to it. Handle it up front via the
|
|
351
|
+
// standard custom-trace cast.
|
|
352
|
+
if ((event as { type: string }).type === 'mcpl:speech-routed') {
|
|
353
|
+
// Remember what landed where, so an interruption can find its edit
|
|
354
|
+
// target: (channelId, messageId, text) per posted segment.
|
|
355
|
+
const e = event as unknown as {
|
|
356
|
+
channelId: string; messageId?: string; text: string; timestamp: number;
|
|
357
|
+
};
|
|
358
|
+
this.routed.push({
|
|
359
|
+
rawChannelId: rawChannelId(e.channelId),
|
|
360
|
+
mcplChannelId: e.channelId,
|
|
361
|
+
messageId: e.messageId,
|
|
362
|
+
text: e.text,
|
|
363
|
+
at: e.timestamp,
|
|
364
|
+
});
|
|
365
|
+
if (this.routed.length > ROUTED_RING_MAX) {
|
|
366
|
+
this.routed.splice(0, this.routed.length - ROUTED_RING_MAX);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
341
371
|
switch (event.type) {
|
|
342
372
|
case 'inference:started': {
|
|
343
373
|
// A context-budget restart re-emits inference:started mid-turn; the
|
|
344
374
|
// existing activation (announced or not) simply carries on.
|
|
345
375
|
const st = this.activation(event.agentName);
|
|
346
|
-
this.adoptChannel(st, event.agentName, event
|
|
376
|
+
this.adoptChannel(st, event.agentName, traceChannelId(event));
|
|
347
377
|
break;
|
|
348
378
|
}
|
|
349
379
|
|
|
350
380
|
case 'inference:tokens': {
|
|
351
381
|
const st = this.activation(event.agentName);
|
|
352
|
-
this.adoptChannel(st, event.agentName, event
|
|
382
|
+
this.adoptChannel(st, event.agentName, traceChannelId(event));
|
|
353
383
|
if (!st.rawChannelId) break; // no locus — nothing to voice into
|
|
354
384
|
st.blocks.set(event.blockIndex, (st.blocks.get(event.blockIndex) ?? '') + event.content);
|
|
355
385
|
this.client?.emit('chunk', {
|
|
@@ -367,7 +397,7 @@ export class TtsRelayModule implements Module {
|
|
|
367
397
|
|
|
368
398
|
case 'inference:content_block': {
|
|
369
399
|
const st = this.activation(event.agentName);
|
|
370
|
-
this.adoptChannel(st, event.agentName, event
|
|
400
|
+
this.adoptChannel(st, event.agentName, traceChannelId(event));
|
|
371
401
|
if (!st.rawChannelId) break;
|
|
372
402
|
const base = {
|
|
373
403
|
channelId: st.rawChannelId,
|
|
@@ -398,21 +428,6 @@ export class TtsRelayModule implements Module {
|
|
|
398
428
|
this.endActivation(event.agentName, 'error');
|
|
399
429
|
break;
|
|
400
430
|
|
|
401
|
-
case 'mcpl:speech-routed': {
|
|
402
|
-
// Remember what landed where, so an interruption can find its edit
|
|
403
|
-
// target: (channelId, messageId, text) per posted segment.
|
|
404
|
-
this.routed.push({
|
|
405
|
-
rawChannelId: rawChannelId(event.channelId),
|
|
406
|
-
mcplChannelId: event.channelId,
|
|
407
|
-
messageId: event.messageId,
|
|
408
|
-
text: event.text,
|
|
409
|
-
at: event.timestamp,
|
|
410
|
-
});
|
|
411
|
-
if (this.routed.length > ROUTED_RING_MAX) {
|
|
412
|
-
this.routed.splice(0, this.routed.length - ROUTED_RING_MAX);
|
|
413
|
-
}
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
431
|
|
|
417
432
|
default:
|
|
418
433
|
break;
|