@animalabs/connectome-host 0.3.1
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 +20 -0
- package/.github/workflows/publish.yml +65 -0
- package/ARCHITECTURE.md +355 -0
- package/CHANGELOG.md +30 -0
- package/HEADLESS-FLEET-PLAN.md +330 -0
- package/README.md +189 -0
- package/UNIFIED-TREE-PLAN.md +242 -0
- package/bun.lock +288 -0
- package/docs/AGENT-MEMORY-GUIDE.md +214 -0
- package/docs/AGENT-ONBOARDING.md +541 -0
- package/docs/ATTENTION-AND-GATING.md +120 -0
- package/docs/DEPLOYMENTS.md +130 -0
- package/docs/DEV-ENVIRONMENT.md +215 -0
- package/docs/LIBRARY-PIPELINE.md +286 -0
- package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
- package/docs/claudeai-evacuation.md +228 -0
- package/docs/debug-context-api.md +208 -0
- package/docs/webui-deployment.md +219 -0
- package/package.json +33 -0
- package/recipes/SETUP.md +308 -0
- package/recipes/TRIUMVIRATE-SETUP.md +467 -0
- package/recipes/claude-export-revive.json +19 -0
- package/recipes/clerk.json +157 -0
- package/recipes/knowledge-miner.json +174 -0
- package/recipes/knowledge-reviewer.json +76 -0
- package/recipes/mcpl-editor-test.json +38 -0
- package/recipes/prompts/transplant-addendum.md +17 -0
- package/recipes/triumvirate.json +63 -0
- package/recipes/webui-fleet-test.json +27 -0
- package/recipes/webui-test.json +16 -0
- package/recipes/zulip-miner.json +87 -0
- package/scripts/connectome-doctor +373 -0
- package/scripts/evacuator.ts +956 -0
- package/scripts/import-claudeai-export.ts +747 -0
- package/scripts/lib/line-reader.ts +53 -0
- package/scripts/test-historical-thinking.ts +148 -0
- package/scripts/warmup-session.ts +336 -0
- package/src/agent-name.ts +58 -0
- package/src/commands.ts +1074 -0
- package/src/headless.ts +528 -0
- package/src/index.ts +867 -0
- package/src/logging-adapter.ts +208 -0
- package/src/mcpl-config.ts +199 -0
- package/src/modules/activity-module.ts +270 -0
- package/src/modules/channel-mode-module.ts +260 -0
- package/src/modules/fleet-module.ts +1705 -0
- package/src/modules/fleet-types.ts +225 -0
- package/src/modules/lessons-module.ts +465 -0
- package/src/modules/mcpl-admin-module.ts +345 -0
- package/src/modules/retrieval-module.ts +327 -0
- package/src/modules/settings-module.ts +143 -0
- package/src/modules/subagent-module.ts +2074 -0
- package/src/modules/subscription-gc-module.ts +322 -0
- package/src/modules/time-module.ts +85 -0
- package/src/modules/tui-module.ts +76 -0
- package/src/modules/web-ui-curve-page.ts +249 -0
- package/src/modules/web-ui-module.ts +2595 -0
- package/src/recipe.ts +1003 -0
- package/src/session-manager.ts +278 -0
- package/src/state/agent-tree-reducer.ts +431 -0
- package/src/state/fleet-tree-aggregator.ts +195 -0
- package/src/strategies/frontdesk-strategy.ts +364 -0
- package/src/synesthete.ts +68 -0
- package/src/tui.ts +2200 -0
- package/src/types/bun-ffi.d.ts +6 -0
- package/src/web/protocol.ts +648 -0
- package/test/agent-name.test.ts +62 -0
- package/test/agent-tree-fleet-launch.test.ts +64 -0
- package/test/agent-tree-reducer-parity.test.ts +194 -0
- package/test/agent-tree-reducer.test.ts +443 -0
- package/test/agent-tree-rollup.test.ts +109 -0
- package/test/autobio-progress-snapshot.test.ts +40 -0
- package/test/claudeai-export-importer.test.ts +386 -0
- package/test/evacuator.test.ts +332 -0
- package/test/fleet-commands.test.ts +244 -0
- package/test/fleet-no-subfleets.test.ts +147 -0
- package/test/fleet-orchestration.test.ts +353 -0
- package/test/fleet-recipe.test.ts +124 -0
- package/test/fleet-route.test.ts +44 -0
- package/test/fleet-smoke.test.ts +479 -0
- package/test/fleet-subscribe-union-e2e.test.ts +123 -0
- package/test/fleet-subscribe-union.test.ts +85 -0
- package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
- package/test/fleet-tree-aggregator.test.ts +215 -0
- package/test/frontdesk-strategy.test.ts +326 -0
- package/test/headless-describe.test.ts +182 -0
- package/test/headless-smoke.test.ts +190 -0
- package/test/logging-adapter.test.ts +87 -0
- package/test/mcpl-admin-module.test.ts +213 -0
- package/test/mcpl-agent-overlay.test.ts +126 -0
- package/test/mock-headless-child.ts +133 -0
- package/test/recipe-cache-ttl.test.ts +37 -0
- package/test/recipe-env.test.ts +157 -0
- package/test/recipe-path-resolution.test.ts +170 -0
- package/test/subagent-async-timeout.test.ts +381 -0
- package/test/subagent-fork-materialise.test.ts +241 -0
- package/test/subagent-peek-scoping.test.ts +180 -0
- package/test/subagent-peek-zombie.test.ts +148 -0
- package/test/subagent-reaper-in-flight.test.ts +229 -0
- package/test/subscription-gc-module.test.ts +136 -0
- package/test/warmup-handoff.test.ts +150 -0
- package/test/web-ui-bind.test.ts +51 -0
- package/test/web-ui-module.test.ts +246 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/tsconfig.json +14 -0
- package/web/bun.lock +357 -0
- package/web/index.html +12 -0
- package/web/package.json +24 -0
- package/web/src/App.tsx +1931 -0
- package/web/src/Context.tsx +158 -0
- package/web/src/ContextDocument.tsx +150 -0
- package/web/src/Files.tsx +271 -0
- package/web/src/Lessons.tsx +164 -0
- package/web/src/Mcpl.tsx +310 -0
- package/web/src/Stream.tsx +119 -0
- package/web/src/TreeSidebar.tsx +283 -0
- package/web/src/Usage.tsx +182 -0
- package/web/src/main.tsx +7 -0
- package/web/src/styles.css +26 -0
- package/web/src/tree.ts +268 -0
- package/web/src/wire.ts +120 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +32 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { resolveAgentName } from '../src/agent-name.js';
|
|
3
|
+
|
|
4
|
+
// The whole point of this helper is to prevent the priority chain from
|
|
5
|
+
// silently drifting (e.g. someone refactors "sidecar wins over CLI"
|
|
6
|
+
// because the sidecar is "the source of truth"). Spell every case out so
|
|
7
|
+
// any reversal lights up here, not in a six-month-later autobiography hole.
|
|
8
|
+
|
|
9
|
+
describe('resolveAgentName', () => {
|
|
10
|
+
test('explicit wins over sidecar and default', () => {
|
|
11
|
+
const r = resolveAgentName({ explicit: 'CLI', sidecar: 'Sidecar', default: 'D' });
|
|
12
|
+
expect(r.name).toBe('CLI');
|
|
13
|
+
expect(r.source).toBe('explicit');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('sidecar wins over default when no explicit', () => {
|
|
17
|
+
const r = resolveAgentName({ sidecar: 'Sidecar', default: 'D' });
|
|
18
|
+
expect(r.name).toBe('Sidecar');
|
|
19
|
+
expect(r.source).toBe('sidecar');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('falls back to default when neither explicit nor sidecar', () => {
|
|
23
|
+
const r = resolveAgentName({ default: 'D' });
|
|
24
|
+
expect(r.name).toBe('D');
|
|
25
|
+
expect(r.source).toBe('default');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('empty-string explicit falls through to sidecar', () => {
|
|
29
|
+
const r = resolveAgentName({ explicit: '', sidecar: 'S', default: 'D' });
|
|
30
|
+
expect(r.name).toBe('S');
|
|
31
|
+
expect(r.source).toBe('sidecar');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('empty-string sidecar falls through to default', () => {
|
|
35
|
+
const r = resolveAgentName({ sidecar: '', default: 'D' });
|
|
36
|
+
expect(r.name).toBe('D');
|
|
37
|
+
expect(r.source).toBe('default');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('mismatch reported when explicit and sidecar disagree', () => {
|
|
41
|
+
// Operator override still wins — but the mismatch is surfaced so the
|
|
42
|
+
// caller can log a warning. A previous warmup writing summaries under
|
|
43
|
+
// the sidecar name leaves them orphaned when the live agent runs
|
|
44
|
+
// under the explicit override; the user should know.
|
|
45
|
+
const r = resolveAgentName({ explicit: 'CLI', sidecar: 'OldImport', default: 'D' });
|
|
46
|
+
expect(r.name).toBe('CLI');
|
|
47
|
+
expect(r.source).toBe('explicit');
|
|
48
|
+
expect(r.mismatch).toEqual({ explicit: 'CLI', sidecar: 'OldImport' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('no mismatch reported when explicit equals sidecar', () => {
|
|
52
|
+
const r = resolveAgentName({ explicit: 'Claude', sidecar: 'Claude', default: 'D' });
|
|
53
|
+
expect(r.name).toBe('Claude');
|
|
54
|
+
expect(r.source).toBe('explicit');
|
|
55
|
+
expect(r.mismatch).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('no mismatch when only one of explicit/sidecar is set', () => {
|
|
59
|
+
expect(resolveAgentName({ explicit: 'X', default: 'D' }).mismatch).toBeUndefined();
|
|
60
|
+
expect(resolveAgentName({ sidecar: 'X', default: 'D' }).mismatch).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the AgentTreeReducer infers parent edges from fleet--launch tool
|
|
3
|
+
* calls (the actual call name; was fleet--spawn in an earlier draft).
|
|
4
|
+
*
|
|
5
|
+
* This is the seam by which the local-process tree links its conductor agent
|
|
6
|
+
* to the fleet children it launched, so the unified TUI tree can render
|
|
7
|
+
* "conductor → miner-1 → miner's commander" as one visual hierarchy.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from 'bun:test';
|
|
10
|
+
import { AgentTreeReducer } from '../src/state/agent-tree-reducer.js';
|
|
11
|
+
|
|
12
|
+
describe('AgentTreeReducer fleet--launch edge inference', () => {
|
|
13
|
+
test('local conductor calling fleet--launch records parent edge', () => {
|
|
14
|
+
const r = new AgentTreeReducer();
|
|
15
|
+
r.seedFrameworkAgents(['conductor']);
|
|
16
|
+
r.applyEvent({
|
|
17
|
+
type: 'inference:tool_calls_yielded',
|
|
18
|
+
agentName: 'conductor',
|
|
19
|
+
calls: [{
|
|
20
|
+
id: 'c1',
|
|
21
|
+
name: 'fleet--launch',
|
|
22
|
+
input: { name: 'miner-1', recipe: 'recipes/knowledge-miner.json' },
|
|
23
|
+
}],
|
|
24
|
+
timestamp: 1_000,
|
|
25
|
+
});
|
|
26
|
+
const child = r.getNode('miner-1');
|
|
27
|
+
expect(child).toBeDefined();
|
|
28
|
+
expect(child!.parent).toBe('conductor');
|
|
29
|
+
expect(r.getChildren('conductor').map(n => n.name)).toContain('miner-1');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('multiple fleet--launch calls in one round all create edges', () => {
|
|
33
|
+
const r = new AgentTreeReducer();
|
|
34
|
+
r.applyEvent({
|
|
35
|
+
type: 'inference:tool_calls_yielded',
|
|
36
|
+
agentName: 'conductor',
|
|
37
|
+
calls: [
|
|
38
|
+
{ id: 'c1', name: 'fleet--launch', input: { name: 'miner', recipe: 'm.json' } },
|
|
39
|
+
{ id: 'c2', name: 'fleet--launch', input: { name: 'reviewer', recipe: 'r.json' } },
|
|
40
|
+
],
|
|
41
|
+
timestamp: 1_000,
|
|
42
|
+
});
|
|
43
|
+
expect(r.getChildren('conductor').map(n => n.name).sort()).toEqual(['miner', 'reviewer']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('fleet--launch and subagent--spawn coexist in one tree', () => {
|
|
47
|
+
const r = new AgentTreeReducer();
|
|
48
|
+
r.applyEvent({
|
|
49
|
+
type: 'inference:tool_calls_yielded',
|
|
50
|
+
agentName: 'conductor',
|
|
51
|
+
calls: [
|
|
52
|
+
{ id: 'c1', name: 'fleet--launch', input: { name: 'miner', recipe: 'm.json' } },
|
|
53
|
+
{ id: 'c2', name: 'subagent--spawn', input: { name: 'side-quest', task: 'audit something' } },
|
|
54
|
+
],
|
|
55
|
+
timestamp: 1_000,
|
|
56
|
+
});
|
|
57
|
+
const miner = r.getNode('miner')!;
|
|
58
|
+
const side = r.getNode('side-quest')!;
|
|
59
|
+
expect(miner.kind).toBe('framework');
|
|
60
|
+
expect(side.kind).toBe('subagent');
|
|
61
|
+
expect(miner.parent).toBe('conductor');
|
|
62
|
+
expect(side.parent).toBe('conductor');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parity test: AgentTreeReducer's output matches what tui.ts currently builds
|
|
3
|
+
* from the same event stream.
|
|
4
|
+
*
|
|
5
|
+
* Approach: replicate the TUI's per-event fold inline (mirror of the
|
|
6
|
+
* subagentPhase, agentContextTokens, agentParent maps in tui.ts) and feed both
|
|
7
|
+
* folds the same canonical event stream. Assert they produce equivalent state.
|
|
8
|
+
*
|
|
9
|
+
* This test exists to prevent silent drift during Phase 2's extraction. Once
|
|
10
|
+
* the TUI is migrated to the reducer (Phase 4), the inline fold here can stay
|
|
11
|
+
* as a frozen reference of the pre-extraction behavior.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, test, expect } from 'bun:test';
|
|
14
|
+
import { AgentTreeReducer } from '../src/state/agent-tree-reducer.js';
|
|
15
|
+
|
|
16
|
+
type Phase = 'sending' | 'streaming' | 'invoking' | 'executing' | 'done' | 'failed' | 'idle';
|
|
17
|
+
|
|
18
|
+
interface InlineFold {
|
|
19
|
+
phaseByAgent: Map<string, Phase>;
|
|
20
|
+
inputTokensByAgent: Map<string, number>;
|
|
21
|
+
parentByChild: Map<string, string>;
|
|
22
|
+
toolCountByAgent: Map<string, number>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function inlineFold(events: Array<Record<string, unknown>>): InlineFold {
|
|
26
|
+
const phaseByAgent = new Map<string, Phase>();
|
|
27
|
+
const inputTokensByAgent = new Map<string, number>();
|
|
28
|
+
const parentByChild = new Map<string, string>();
|
|
29
|
+
const toolCountByAgent = new Map<string, number>();
|
|
30
|
+
// Mirror SubagentModule's callIdIndex so tool events route to the correct agent.
|
|
31
|
+
const callIdIndex = new Map<string, string>();
|
|
32
|
+
|
|
33
|
+
for (const e of events) {
|
|
34
|
+
const agent = e.agentName as string | undefined;
|
|
35
|
+
switch (e.type) {
|
|
36
|
+
case 'inference:started':
|
|
37
|
+
if (agent) phaseByAgent.set(agent, 'sending');
|
|
38
|
+
break;
|
|
39
|
+
case 'inference:tokens':
|
|
40
|
+
if (agent) phaseByAgent.set(agent, 'streaming');
|
|
41
|
+
break;
|
|
42
|
+
case 'inference:tool_calls_yielded': {
|
|
43
|
+
if (agent) phaseByAgent.set(agent, 'invoking');
|
|
44
|
+
const calls = (e.calls as Array<{ id: string; name: string; input?: unknown }>) ?? [];
|
|
45
|
+
for (const call of calls) {
|
|
46
|
+
if (agent) callIdIndex.set(call.id, agent);
|
|
47
|
+
if (call.name === 'subagent--spawn' || call.name === 'subagent--fork') {
|
|
48
|
+
const child = (call.input as { name?: string } | undefined)?.name;
|
|
49
|
+
if (child && agent) parentByChild.set(child, agent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'inference:usage': {
|
|
55
|
+
if (agent) {
|
|
56
|
+
const usage = e.tokenUsage as { input?: number } | undefined;
|
|
57
|
+
if (usage?.input) inputTokensByAgent.set(agent, usage.input);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'inference:completed': {
|
|
62
|
+
if (agent) {
|
|
63
|
+
phaseByAgent.set(agent, 'done');
|
|
64
|
+
const usage = e.tokenUsage as { input?: number } | undefined;
|
|
65
|
+
if (usage?.input) inputTokensByAgent.set(agent, usage.input);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case 'inference:failed':
|
|
70
|
+
if (agent) phaseByAgent.set(agent, 'failed');
|
|
71
|
+
break;
|
|
72
|
+
case 'tool:started': {
|
|
73
|
+
const callId = e.callId as string | undefined;
|
|
74
|
+
if (callId) {
|
|
75
|
+
const owner = callIdIndex.get(callId);
|
|
76
|
+
if (owner) {
|
|
77
|
+
phaseByAgent.set(owner, 'executing');
|
|
78
|
+
toolCountByAgent.set(owner, (toolCountByAgent.get(owner) ?? 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { phaseByAgent, inputTokensByAgent, parentByChild, toolCountByAgent };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reducerFold(events: Array<Record<string, unknown>>): InlineFold {
|
|
90
|
+
const r = new AgentTreeReducer();
|
|
91
|
+
for (const e of events) r.applyEvent(e as never);
|
|
92
|
+
const phaseByAgent = new Map<string, Phase>();
|
|
93
|
+
const inputTokensByAgent = new Map<string, number>();
|
|
94
|
+
const parentByChild = new Map<string, string>();
|
|
95
|
+
const toolCountByAgent = new Map<string, number>();
|
|
96
|
+
for (const node of r.getNodes()) {
|
|
97
|
+
phaseByAgent.set(node.name, node.phase as Phase);
|
|
98
|
+
if (node.tokens.input > 0) inputTokensByAgent.set(node.name, node.tokens.input);
|
|
99
|
+
if (node.parent) parentByChild.set(node.name, node.parent);
|
|
100
|
+
if (node.toolCallsCount > 0) toolCountByAgent.set(node.name, node.toolCallsCount);
|
|
101
|
+
}
|
|
102
|
+
return { phaseByAgent, inputTokensByAgent, parentByChild, toolCountByAgent };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertParity(events: Array<Record<string, unknown>>): void {
|
|
106
|
+
const inline = inlineFold(events);
|
|
107
|
+
const reducer = reducerFold(events);
|
|
108
|
+
// The reducer creates nodes for agents the inline fold never explicitly
|
|
109
|
+
// tracked (e.g. it preserves them after seeing only spawn-call events without
|
|
110
|
+
// an inference:started). We only assert that the inline fold's keys are a
|
|
111
|
+
// subset and agree on values.
|
|
112
|
+
for (const [agent, phase] of inline.phaseByAgent) {
|
|
113
|
+
expect(reducer.phaseByAgent.get(agent)).toBe(phase);
|
|
114
|
+
}
|
|
115
|
+
for (const [agent, tokens] of inline.inputTokensByAgent) {
|
|
116
|
+
expect(reducer.inputTokensByAgent.get(agent)).toBe(tokens);
|
|
117
|
+
}
|
|
118
|
+
for (const [child, parent] of inline.parentByChild) {
|
|
119
|
+
expect(reducer.parentByChild.get(child)).toBe(parent);
|
|
120
|
+
}
|
|
121
|
+
for (const [agent, count] of inline.toolCountByAgent) {
|
|
122
|
+
expect(reducer.toolCountByAgent.get(agent)).toBe(count);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const t = (offset: number) => 1_700_000_000_000 + offset;
|
|
127
|
+
|
|
128
|
+
describe('AgentTreeReducer parity with inline fold', () => {
|
|
129
|
+
test('simple inference round', () => {
|
|
130
|
+
assertParity([
|
|
131
|
+
{ type: 'inference:started', agentName: 'commander', timestamp: t(0) },
|
|
132
|
+
{ type: 'inference:tokens', agentName: 'commander', content: 'hello', timestamp: t(1) },
|
|
133
|
+
{ type: 'inference:usage', agentName: 'commander', tokenUsage: { input: 1234, output: 50 }, timestamp: t(2) },
|
|
134
|
+
{ type: 'inference:completed', agentName: 'commander', durationMs: 100, tokenUsage: { input: 1234, output: 50 }, timestamp: t(3) },
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('tool round with two tool calls', () => {
|
|
139
|
+
assertParity([
|
|
140
|
+
{ type: 'inference:started', agentName: 'commander', timestamp: t(0) },
|
|
141
|
+
{
|
|
142
|
+
type: 'inference:tool_calls_yielded',
|
|
143
|
+
agentName: 'commander',
|
|
144
|
+
calls: [
|
|
145
|
+
{ id: 'c1', name: 'files--read', input: { path: '/x' } },
|
|
146
|
+
{ id: 'c2', name: 'files--read', input: { path: '/y' } },
|
|
147
|
+
],
|
|
148
|
+
timestamp: t(1),
|
|
149
|
+
},
|
|
150
|
+
{ type: 'tool:started', callId: 'c1', tool: 'files--read', module: 'files', timestamp: t(2) },
|
|
151
|
+
{ type: 'tool:started', callId: 'c2', tool: 'files--read', module: 'files', timestamp: t(3) },
|
|
152
|
+
{ type: 'tool:completed', callId: 'c1', tool: 'files--read', module: 'files', durationMs: 5, timestamp: t(4) },
|
|
153
|
+
{ type: 'tool:completed', callId: 'c2', tool: 'files--read', module: 'files', durationMs: 5, timestamp: t(5) },
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('subagent spawn with nested inference', () => {
|
|
158
|
+
assertParity([
|
|
159
|
+
{ type: 'inference:started', agentName: 'commander', timestamp: t(0) },
|
|
160
|
+
{
|
|
161
|
+
type: 'inference:tool_calls_yielded',
|
|
162
|
+
agentName: 'commander',
|
|
163
|
+
calls: [{
|
|
164
|
+
id: 'c1',
|
|
165
|
+
name: 'subagent--spawn',
|
|
166
|
+
input: { name: 'researcher', task: 'investigate X' },
|
|
167
|
+
}],
|
|
168
|
+
timestamp: t(1),
|
|
169
|
+
},
|
|
170
|
+
{ type: 'tool:started', callId: 'c1', tool: 'subagent--spawn', module: 'subagent', timestamp: t(2) },
|
|
171
|
+
{ type: 'inference:started', agentName: 'researcher', timestamp: t(3) },
|
|
172
|
+
{ type: 'inference:tokens', agentName: 'researcher', content: 'thinking', timestamp: t(4) },
|
|
173
|
+
{ type: 'inference:usage', agentName: 'researcher', tokenUsage: { input: 5000, output: 100 }, timestamp: t(5) },
|
|
174
|
+
{ type: 'inference:completed', agentName: 'researcher', durationMs: 200, tokenUsage: { input: 5000, output: 100 }, timestamp: t(6) },
|
|
175
|
+
{ type: 'tool:completed', callId: 'c1', tool: 'subagent--spawn', module: 'subagent', durationMs: 250, timestamp: t(7) },
|
|
176
|
+
]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('failed inference', () => {
|
|
180
|
+
assertParity([
|
|
181
|
+
{ type: 'inference:started', agentName: 'a', timestamp: t(0) },
|
|
182
|
+
{ type: 'inference:failed', agentName: 'a', error: 'rate limited', timestamp: t(1) },
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('multiple usage events accumulate context size correctly', () => {
|
|
187
|
+
// Last input tokens wins (= current context window).
|
|
188
|
+
assertParity([
|
|
189
|
+
{ type: 'inference:usage', agentName: 'a', tokenUsage: { input: 1000, output: 50 }, timestamp: t(0) },
|
|
190
|
+
{ type: 'inference:usage', agentName: 'a', tokenUsage: { input: 2000, output: 80 }, timestamp: t(1) },
|
|
191
|
+
{ type: 'inference:usage', agentName: 'a', tokenUsage: { input: 3500, output: 120 }, timestamp: t(2) },
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
});
|