@animalabs/connectome-host 0.3.5 → 0.3.8
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 +6 -0
- package/.github/workflows/ci.yml +52 -0
- package/.github/workflows/publish.yml +2 -2
- package/README.md +5 -0
- package/bun.lock +14 -6
- package/docs/AGENT-ONBOARDING.md +6 -1
- package/package.json +6 -4
- package/scripts/import-codex-rollout.ts +288 -0
- package/src/call-ledger.ts +371 -0
- package/src/call-pricing.ts +119 -0
- package/src/framework-agent-config.ts +51 -0
- package/src/framework-strategy.ts +70 -0
- package/src/index.ts +130 -105
- package/src/logging-adapter.ts +105 -12
- package/src/mcpl-config.ts +1 -0
- package/src/modules/channel-mode-module.ts +14 -16
- package/src/modules/fleet-module.ts +17 -7
- package/src/modules/mcpl-admin-module.ts +6 -15
- package/src/modules/settings-module.ts +83 -59
- package/src/modules/subscription-gc-module.ts +17 -20
- package/src/modules/time-module.ts +15 -9
- package/src/modules/web-ui-module.ts +88 -10
- package/src/modules/web-ui-observers.ts +35 -9
- package/src/recipe.ts +133 -6
- package/src/state/agent-tree-reducer.ts +17 -3
- package/src/strategies/frontdesk-strategy.ts +15 -9
- package/src/web/protocol.ts +89 -0
- package/test/agent-tree-reducer.test.ts +35 -3
- package/test/autobio-progress-snapshot.test.ts +27 -12
- package/test/call-ledger.test.ts +91 -0
- package/test/call-pricing.test.ts +69 -0
- package/test/framework-fkm-composition.test.ts +160 -0
- package/test/frontdesk-strategy.test.ts +10 -0
- package/test/import-codex-rollout.test.ts +36 -0
- package/test/logging-adapter-refusal.test.ts +57 -0
- package/test/logging-adapter.test.ts +58 -1
- package/test/recipe-cache-ttl.test.ts +7 -3
- package/test/recipe-compression-fallback.test.ts +36 -0
- package/test/recipe-primary-summary-fallback.test.ts +40 -0
- package/test/recipe-provider.test.ts +36 -0
- package/test/recipe-think-policy.test.ts +39 -0
- package/test/recipe-timezone.test.ts +20 -0
- package/test/subscription-gc-module.test.ts +7 -5
- package/test/time-module.test.ts +13 -0
- package/test/web-ui-observers.test.ts +70 -2
- package/web/package-lock.json +425 -302
- package/web/src/App.tsx +9 -0
- package/web/src/ObserverGate.tsx +21 -11
- package/web/src/Usage.tsx +116 -2
- package/web/src/wire.ts +5 -2
- package/web/bun.lock +0 -357
package/web/src/App.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type MessageBlock,
|
|
23
23
|
type TokenUsage,
|
|
24
24
|
type PerAgentCost,
|
|
25
|
+
type CallLedgerSnapshot,
|
|
25
26
|
} from '@conhost/web/protocol';
|
|
26
27
|
|
|
27
28
|
/** Client-side block: the wire MessageBlock plus live-stream bookkeeping. */
|
|
@@ -116,6 +117,7 @@ export function App() {
|
|
|
116
117
|
let pendingHistoryTimer: number | undefined;
|
|
117
118
|
const [usage, setUsage] = createSignal<TokenUsage>({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
118
119
|
const [perAgentCost, setPerAgentCost] = createSignal<PerAgentCost[]>([]);
|
|
120
|
+
const [callLedger, setCallLedger] = createSignal<CallLedgerSnapshot | null>(null);
|
|
119
121
|
const [draft, setDraft] = createSignal('');
|
|
120
122
|
/** Currently-focused tree node + the panel mode rendered on its behalf.
|
|
121
123
|
* `mode` decides whether the side panel shows live stream events or a
|
|
@@ -705,6 +707,7 @@ export function App() {
|
|
|
705
707
|
appendMessage: (m) => setMessages(produce(arr => arr.push(m))),
|
|
706
708
|
setUsage,
|
|
707
709
|
setPerAgentCost,
|
|
710
|
+
setCallLedger,
|
|
708
711
|
appendStreamToken,
|
|
709
712
|
appendToolUseBlocks,
|
|
710
713
|
updateToolStatus,
|
|
@@ -967,6 +970,7 @@ export function App() {
|
|
|
967
970
|
node={focusedNode()!}
|
|
968
971
|
sessionUsage={usage()}
|
|
969
972
|
perAgentCost={perAgentCost()}
|
|
973
|
+
callLedger={callLedger()}
|
|
970
974
|
onClose={closePanel}
|
|
971
975
|
/>
|
|
972
976
|
)}
|
|
@@ -1066,6 +1070,7 @@ interface HandlerHooks {
|
|
|
1066
1070
|
appendMessage: (msg: Message) => void;
|
|
1067
1071
|
setUsage: (u: TokenUsage) => void;
|
|
1068
1072
|
setPerAgentCost: (c: PerAgentCost[]) => void;
|
|
1073
|
+
setCallLedger: (ledger: CallLedgerSnapshot | null) => void;
|
|
1069
1074
|
appendStreamToken: (token: string, blockType?: string) => void;
|
|
1070
1075
|
/** Attach yielded tool calls to the streaming assistant message. */
|
|
1071
1076
|
appendToolUseBlocks: (calls: Array<{ id: string; name: string; input?: unknown }>) => void;
|
|
@@ -1099,6 +1104,7 @@ function handleServerMessage(
|
|
|
1099
1104
|
hooks.applyWelcome(msg);
|
|
1100
1105
|
hooks.setUsage(msg.usage);
|
|
1101
1106
|
hooks.setPerAgentCost(msg.perAgentCost ?? []);
|
|
1107
|
+
hooks.setCallLedger(msg.callLedger ?? null);
|
|
1102
1108
|
return;
|
|
1103
1109
|
}
|
|
1104
1110
|
case 'message-appended':
|
|
@@ -1111,6 +1117,9 @@ function handleServerMessage(
|
|
|
1111
1117
|
hooks.setUsage(msg.usage);
|
|
1112
1118
|
if (msg.perAgentCost) hooks.setPerAgentCost(msg.perAgentCost);
|
|
1113
1119
|
return;
|
|
1120
|
+
case 'call-ledger':
|
|
1121
|
+
hooks.setCallLedger(msg.ledger);
|
|
1122
|
+
return;
|
|
1114
1123
|
case 'trace': {
|
|
1115
1124
|
const e = msg.event;
|
|
1116
1125
|
switch (e.type) {
|
package/web/src/ObserverGate.tsx
CHANGED
|
@@ -37,17 +37,27 @@ export function ObserverGateScreen(props: { state: 'denied' | 'unavailable' }) {
|
|
|
37
37
|
This viewer shows an agent's interior — access is granted per device, by the agent or
|
|
38
38
|
its operator. Share this device's key fingerprint with them:
|
|
39
39
|
</p>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
<Show
|
|
41
|
+
when={!fingerprint.loading && fingerprint() === null}
|
|
42
|
+
fallback={
|
|
43
|
+
<div class="flex items-center gap-2">
|
|
44
|
+
<code class="font-mono text-xs bg-zinc-950 border border-zinc-800 rounded px-2 py-1.5 break-all flex-1">
|
|
45
|
+
{fingerprint.loading ? 'generating…' : fingerprint()}
|
|
46
|
+
</code>
|
|
47
|
+
<button
|
|
48
|
+
class="px-2 py-1.5 text-xs rounded border border-zinc-700 hover:bg-zinc-800"
|
|
49
|
+
onClick={copy}
|
|
50
|
+
>
|
|
51
|
+
copy
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<p class="text-amber-300/90">
|
|
57
|
+
This origin can't generate a device key (WebCrypto needs https or localhost) —
|
|
58
|
+
use password sign-in below, or open the agent's https URL.
|
|
59
|
+
</p>
|
|
60
|
+
</Show>
|
|
51
61
|
<p class="text-zinc-400">
|
|
52
62
|
Grant (agent tool or operator edit of <code class="font-mono">data/observers.json</code>):{' '}
|
|
53
63
|
<code class="font-mono text-xs">
|
package/web/src/Usage.tsx
CHANGED
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
import { For, Show } from 'solid-js';
|
|
15
15
|
import type { UiNode } from './tree';
|
|
16
16
|
import { aggregateTokens } from './tree';
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
TokenUsage,
|
|
19
|
+
PerAgentCost,
|
|
20
|
+
CallLedgerSnapshot,
|
|
21
|
+
CallLedgerRow,
|
|
22
|
+
CallLedgerVerdict,
|
|
23
|
+
} from '@conhost/web/protocol';
|
|
18
24
|
|
|
19
25
|
export function UsagePanel(props: {
|
|
20
26
|
node: UiNode;
|
|
@@ -24,6 +30,10 @@ export function UsagePanel(props: {
|
|
|
24
30
|
/** Per-agent cost slice (parent-process agents only). Used to label
|
|
25
31
|
* per-agent cost in process / per-agent breakdown views. */
|
|
26
32
|
perAgentCost: PerAgentCost[];
|
|
33
|
+
/** Recent provider calls. The ledger is process-local, so it is shown on
|
|
34
|
+
* the process usage view rather than pretending it can be split across
|
|
35
|
+
* fleet children. */
|
|
36
|
+
callLedger: CallLedgerSnapshot | null;
|
|
27
37
|
onClose(): void;
|
|
28
38
|
}) {
|
|
29
39
|
const costFor = (agentName: string): { total: number; currency: string } | undefined => {
|
|
@@ -70,7 +80,7 @@ export function UsagePanel(props: {
|
|
|
70
80
|
};
|
|
71
81
|
|
|
72
82
|
return (
|
|
73
|
-
<div class="border-l border-neutral-800 w-
|
|
83
|
+
<div class="border-l border-neutral-800 w-[52rem] max-w-[68vw] shrink-0 bg-neutral-950 flex flex-col h-full">
|
|
74
84
|
<div class="border-b border-neutral-800 px-3 py-2 flex items-center gap-2">
|
|
75
85
|
<span class="text-[10px] uppercase tracking-wider text-neutral-500 font-semibold">usage</span>
|
|
76
86
|
<span class="font-mono text-sm text-neutral-200 truncate">{props.node.label}</span>
|
|
@@ -99,6 +109,8 @@ export function UsagePanel(props: {
|
|
|
99
109
|
</Show>
|
|
100
110
|
|
|
101
111
|
<Show when={props.node.kind === 'process'}>
|
|
112
|
+
<CallLedgerSection ledger={props.callLedger} />
|
|
113
|
+
|
|
102
114
|
<section class="text-[10px] text-neutral-600 italic leading-snug">
|
|
103
115
|
Session total comes from the membrane's per-call usage stream.
|
|
104
116
|
"By node" is the per-agent ledger from the tree reducer; the two
|
|
@@ -110,6 +122,108 @@ export function UsagePanel(props: {
|
|
|
110
122
|
);
|
|
111
123
|
}
|
|
112
124
|
|
|
125
|
+
function CallLedgerSection(props: { ledger: CallLedgerSnapshot | null }) {
|
|
126
|
+
const rows = (): CallLedgerRow[] => [...(props.ledger?.rows ?? [])].slice(-30).reverse();
|
|
127
|
+
return (
|
|
128
|
+
<section>
|
|
129
|
+
<div class="flex items-baseline gap-2 mb-1">
|
|
130
|
+
<div class="text-neutral-500 uppercase tracking-wider text-[10px]">recent calls · cache ledger</div>
|
|
131
|
+
<Show when={props.ledger}>
|
|
132
|
+
{(ledger) => (
|
|
133
|
+
<span class="ml-auto text-[10px] text-neutral-500">
|
|
134
|
+
${fmtCost(ledger().summary.cost?.total ?? 0)} retained · {Math.round(ledger().summary.cacheHitRatio * 100)}% cached
|
|
135
|
+
<Show when={(ledger().summary.cost?.unpricedCalls ?? 0) > 0}>
|
|
136
|
+
{' '}· <span class="text-amber-400">{ledger().summary.cost!.unpricedCalls} unpriced</span>
|
|
137
|
+
</Show>
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
</Show>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<Show
|
|
144
|
+
when={rows().length > 0}
|
|
145
|
+
fallback={<div class="border border-neutral-800 rounded px-2 py-2 text-neutral-600">No provider calls recorded yet.</div>}
|
|
146
|
+
>
|
|
147
|
+
<div class="border border-neutral-800 rounded overflow-hidden">
|
|
148
|
+
<div class="grid grid-cols-[4.7rem_3.5rem_3rem_4rem_4rem_4rem_4rem_4.7rem_minmax(8rem,1fr)] gap-x-2 px-2 py-1 bg-neutral-900 text-[9px] uppercase tracking-wider text-neutral-600">
|
|
149
|
+
<span>time</span><span>origin</span><span>msgs</span><span class="text-right">in</span>
|
|
150
|
+
<span class="text-right">read</span><span class="text-right">write</span><span class="text-right">out</span>
|
|
151
|
+
<span class="text-right">cost</span><span>verdict / cause</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="max-h-80 overflow-y-auto divide-y divide-neutral-900">
|
|
154
|
+
<For each={rows()}>{(row) => <CallLedgerRowView row={row} />}</For>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="mt-1 text-[9px] text-neutral-600">
|
|
158
|
+
Costs marked in USD use provider-reported token buckets and the versioned public rate card. Calls missing an exact cache-write split or known rate stay explicitly unpriced. origin~ remains an estimated call class.
|
|
159
|
+
</div>
|
|
160
|
+
</Show>
|
|
161
|
+
</section>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function CallLedgerRowView(props: { row: CallLedgerRow }) {
|
|
166
|
+
const time = (): string => {
|
|
167
|
+
const d = new Date(props.row.timestamp);
|
|
168
|
+
return Number.isNaN(d.getTime()) ? '?' : d.toLocaleTimeString([], { hour12: false });
|
|
169
|
+
};
|
|
170
|
+
const flags = (): string => {
|
|
171
|
+
const bp = props.row.cache.breakpoints;
|
|
172
|
+
if (bp === undefined) return 'flags unknown';
|
|
173
|
+
if (bp === 0) return 'no cache flags';
|
|
174
|
+
return `${bp}bp:${props.row.cache.effectiveTtl}`;
|
|
175
|
+
};
|
|
176
|
+
const costTitle = (): string => {
|
|
177
|
+
const c = props.row.cost;
|
|
178
|
+
if (!c) return 'Unpriced: an authoritative usage bucket or public rate was unavailable';
|
|
179
|
+
return [
|
|
180
|
+
`billing-grade ${c.currency} · ${c.pricingVersion}`,
|
|
181
|
+
`input $${c.input.toFixed(6)}`,
|
|
182
|
+
`read $${c.cacheRead.toFixed(6)}`,
|
|
183
|
+
`write 5m $${c.cacheWrite5m.toFixed(6)}`,
|
|
184
|
+
`write 1h $${c.cacheWrite1h.toFixed(6)}`,
|
|
185
|
+
`output $${c.output.toFixed(6)}`,
|
|
186
|
+
].join(' · ');
|
|
187
|
+
};
|
|
188
|
+
return (
|
|
189
|
+
<div class="grid grid-cols-[4.7rem_3.5rem_3rem_4rem_4rem_4rem_4rem_4.7rem_minmax(8rem,1fr)] gap-x-2 px-2 py-1.5 items-start hover:bg-neutral-900/60">
|
|
190
|
+
<span class="text-neutral-400" title={`${props.row.timestamp} · ${Math.round(props.row.durationMs / 1000)}s`}>{time()}</span>
|
|
191
|
+
<span class="text-neutral-500">{props.row.originEstimate}</span>
|
|
192
|
+
<span class="text-neutral-400">{props.row.messages}</span>
|
|
193
|
+
<span class="text-right text-neutral-400">{fmt(props.row.tokens.input)}</span>
|
|
194
|
+
<span class="text-right text-sky-300">{fmt(props.row.tokens.cacheRead)}</span>
|
|
195
|
+
<span class="text-right text-amber-300">{fmt(props.row.tokens.cacheWrite)}</span>
|
|
196
|
+
<span class="text-right text-neutral-400">{fmt(props.row.tokens.output)}</span>
|
|
197
|
+
<span
|
|
198
|
+
class={`text-right ${props.row.cost ? 'text-emerald-300' : 'text-amber-500'}`}
|
|
199
|
+
title={costTitle()}
|
|
200
|
+
>
|
|
201
|
+
{props.row.cost ? `$${fmtCost(props.row.cost.total)}` : 'unpriced'}
|
|
202
|
+
</span>
|
|
203
|
+
<span class="min-w-0">
|
|
204
|
+
<span class={`font-semibold ${verdictColor(props.row.verdict)}`}>{props.row.verdict}</span>
|
|
205
|
+
<span class="ml-1 text-[9px] text-neutral-600">{flags()}</span>
|
|
206
|
+
<span class="block text-[9px] leading-snug text-neutral-500 break-words">{props.row.cause}</span>
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function verdictColor(verdict: CallLedgerVerdict): string {
|
|
213
|
+
switch (verdict) {
|
|
214
|
+
case 'HIT': return 'text-emerald-300';
|
|
215
|
+
case 'hit+extend': return 'text-lime-300';
|
|
216
|
+
case 'rewrite:expired': return 'text-orange-300';
|
|
217
|
+
case 'rewrite:prefix-mutated':
|
|
218
|
+
case 'rewrite:prefix-truncated': return 'text-red-300';
|
|
219
|
+
case 'rewrite:unexplained': return 'text-violet-300';
|
|
220
|
+
case 'ERROR': return 'text-red-400';
|
|
221
|
+
case 'first-write': return 'text-slate-300';
|
|
222
|
+
case 'uncached': return 'text-stone-400';
|
|
223
|
+
default: return 'text-neutral-400';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
113
227
|
function TotalsBlock(props: { totals: { input: number; output: number; cacheRead: number; cacheWrite: number; cost?: { total: number; currency: string } } }) {
|
|
114
228
|
const t = props.totals;
|
|
115
229
|
return (
|
package/web/src/wire.ts
CHANGED
|
@@ -128,8 +128,11 @@ export function createWireClient(opts: WireOptions = {}): WireClient {
|
|
|
128
128
|
}
|
|
129
129
|
if (ev.code === 4401) {
|
|
130
130
|
// Observer auth rejected — reconnect loops would spam the server;
|
|
131
|
-
// hold
|
|
132
|
-
|
|
131
|
+
// hold until the user acts (grant lands / password). The server also
|
|
132
|
+
// closes never-authenticated connections with 4401 on timeout, so
|
|
133
|
+
// don't overwrite 'unavailable' (no WebCrypto — a hello was never
|
|
134
|
+
// possible) with 'denied' (a key exists but holds no grant).
|
|
135
|
+
if (observerState() !== 'unavailable') setObserverState('denied');
|
|
133
136
|
setStatus('closed');
|
|
134
137
|
return;
|
|
135
138
|
}
|