@animalabs/connectome-host 0.4.0 → 0.5.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/CHANGELOG.md +63 -0
- package/bun.lock +6 -6
- package/package.json +1 -1
- package/src/index.ts +17 -5
- package/src/logging-provider-wrapper.ts +165 -0
- package/src/modules/web-ui-module.ts +280 -1
- package/src/web/protocol.ts +130 -0
- package/test/settings-protocol.test.ts +64 -0
- package/web/src/App.tsx +35 -2
- package/web/src/Settings.tsx +434 -0
package/src/web/protocol.ts
CHANGED
|
@@ -391,6 +391,41 @@ export interface McplListMessage {
|
|
|
391
391
|
}>;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Runtime context-settings snapshot.
|
|
396
|
+
*
|
|
397
|
+
* BROADCAST to every welcomed client on change — unlike `mcpl-list`, these are
|
|
398
|
+
* live process state, not a config file, so two operators must not see
|
|
399
|
+
* divergent values.
|
|
400
|
+
*/
|
|
401
|
+
export interface SettingsStateMessage {
|
|
402
|
+
type: 'settings-state';
|
|
403
|
+
agent: string;
|
|
404
|
+
/** Live values. `transition` is 'converging' while a paced descent runs. */
|
|
405
|
+
settings: {
|
|
406
|
+
contextBudgetTokens: number;
|
|
407
|
+
tailTokens?: number;
|
|
408
|
+
transitionPaceTokens?: number;
|
|
409
|
+
sameRoundThinkTextPolicy?: string;
|
|
410
|
+
sameRoundThinkTextPolicySource?: string;
|
|
411
|
+
transition: 'stable' | 'converging' | 'blocked';
|
|
412
|
+
/** Why a descent is stuck: pace below floor, or protected context too big. */
|
|
413
|
+
transitionReason?: string;
|
|
414
|
+
};
|
|
415
|
+
/** Keys currently overridden away from the recipe (i.e. persisted). */
|
|
416
|
+
overrides: string[];
|
|
417
|
+
/** Which keys this build can apply live. Everything else needs a restart —
|
|
418
|
+
* the UI must show that split rather than implying all knobs are hot. */
|
|
419
|
+
hotKeys: string[];
|
|
420
|
+
/** False when the strategy is not hot-configurable at all (e.g. passthrough):
|
|
421
|
+
* the panel should go read-only rather than offer controls that will throw. */
|
|
422
|
+
hotConfigurable: boolean;
|
|
423
|
+
/** True when this build's context-manager exposes dry-run preview. Older
|
|
424
|
+
* context-manager versions resolve without it, and the panel must say
|
|
425
|
+
* "preview unavailable" rather than silently showing nothing. */
|
|
426
|
+
previewAvailable: boolean;
|
|
427
|
+
}
|
|
428
|
+
|
|
394
429
|
/** A page of older history — response to `request-history`, routed only to
|
|
395
430
|
* the requesting client and correlated by `corrId`. */
|
|
396
431
|
export interface HistoryPageMessage {
|
|
@@ -474,6 +509,7 @@ export type WebUiServerMessage =
|
|
|
474
509
|
| QuitConfirmRequiredMessage
|
|
475
510
|
| LessonsListMessage
|
|
476
511
|
| McplListMessage
|
|
512
|
+
| SettingsStateMessage
|
|
477
513
|
| WorkspaceMountsMessage
|
|
478
514
|
| WorkspaceTreeMessage
|
|
479
515
|
| WorkspaceFileMessage
|
|
@@ -633,6 +669,58 @@ export interface McplSetEnvMessage {
|
|
|
633
669
|
env: Record<string, string>;
|
|
634
670
|
}
|
|
635
671
|
|
|
672
|
+
/** Pull the current runtime context settings for an agent, plus which knobs are
|
|
673
|
+
* live-appliable vs restart-only. Response is a `settings-state` envelope. */
|
|
674
|
+
export interface RequestSettingsMessage {
|
|
675
|
+
type: 'request-settings';
|
|
676
|
+
/** Agent name; defaults to the recipe's primary agent. */
|
|
677
|
+
agent?: string;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Apply runtime context settings live.
|
|
682
|
+
*
|
|
683
|
+
* Semantics that the UI must not misrepresent:
|
|
684
|
+
* - RAISING contextBudgetTokens applies immediately.
|
|
685
|
+
* - LOWERING it starts a PACED convergence (`transition: 'converging'`)
|
|
686
|
+
* rather than folding everything at once; it can be cancelled.
|
|
687
|
+
* - Only these keys are hot. Chunk size, head window, merge threshold and
|
|
688
|
+
* friends are recipe-and-restart only.
|
|
689
|
+
*
|
|
690
|
+
* `persist: false` makes the change ephemeral — live now, gone on restart —
|
|
691
|
+
* which is the mode for operator experiments. Default persists.
|
|
692
|
+
*
|
|
693
|
+
* `notify: true` pushes a notice into the agent's context. OFF by default and
|
|
694
|
+
* deliberately so: the notice is new text in the very context being tuned, so
|
|
695
|
+
* it invalidates the KV prefix and can itself trip a refusal classifier. The
|
|
696
|
+
* agent can always PULL current settings via its own `agent_settings` tool.
|
|
697
|
+
*/
|
|
698
|
+
export interface SettingsUpdateMessage {
|
|
699
|
+
type: 'settings-update';
|
|
700
|
+
agent?: string;
|
|
701
|
+
contextBudgetTokens?: number;
|
|
702
|
+
tailTokens?: number;
|
|
703
|
+
transitionPaceTokens?: number;
|
|
704
|
+
persist?: boolean;
|
|
705
|
+
notify?: boolean;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Revert named settings to their recipe values (all four when omitted). */
|
|
709
|
+
export interface SettingsResetMessage {
|
|
710
|
+
type: 'settings-reset';
|
|
711
|
+
agent?: string;
|
|
712
|
+
keys?: string[];
|
|
713
|
+
persist?: boolean;
|
|
714
|
+
notify?: boolean;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Abandon an in-flight paced descent, holding the current frontier. */
|
|
718
|
+
export interface SettingsCancelTransitionMessage {
|
|
719
|
+
type: 'settings-cancel-transition';
|
|
720
|
+
agent?: string;
|
|
721
|
+
persist?: boolean;
|
|
722
|
+
}
|
|
723
|
+
|
|
636
724
|
/** Pull the list of workspace mounts. Response is `workspace-mounts`.
|
|
637
725
|
* Optional `scope` selects a fleet child instead of the parent. */
|
|
638
726
|
export interface RequestWorkspaceMountsMessage {
|
|
@@ -706,6 +794,10 @@ export type WebUiClientMessage =
|
|
|
706
794
|
| McplAddMessage
|
|
707
795
|
| McplRemoveMessage
|
|
708
796
|
| McplSetEnvMessage
|
|
797
|
+
| RequestSettingsMessage
|
|
798
|
+
| SettingsUpdateMessage
|
|
799
|
+
| SettingsResetMessage
|
|
800
|
+
| SettingsCancelTransitionMessage
|
|
709
801
|
| RequestWorkspaceMountsMessage
|
|
710
802
|
| RequestWorkspaceTreeMessage
|
|
711
803
|
| RequestWorkspaceFileMessage
|
|
@@ -768,6 +860,30 @@ export function isClientMessage(value: unknown): value is WebUiClientMessage {
|
|
|
768
860
|
return isValidMcplId(v.id);
|
|
769
861
|
case 'mcpl-set-env':
|
|
770
862
|
return isValidMcplId(v.id) && isStringMap(v.env);
|
|
863
|
+
case 'request-settings':
|
|
864
|
+
return isOptionalNonEmptyString(v.agent);
|
|
865
|
+
case 'settings-update': {
|
|
866
|
+
if (!isOptionalNonEmptyString(v.agent)) return false;
|
|
867
|
+
if (!isOptionalBool(v.persist) || !isOptionalBool(v.notify)) return false;
|
|
868
|
+
// Positive-integer-or-absent for each knob. The semantic gate (budget must
|
|
869
|
+
// exceed max response tokens; tail/pace need a hot-configurable strategy)
|
|
870
|
+
// lives in Agent.validateRuntimeSettingsPatch — this only guarantees the
|
|
871
|
+
// shape so that gate gets numbers instead of strings.
|
|
872
|
+
for (const k of ['contextBudgetTokens', 'tailTokens', 'transitionPaceTokens']) {
|
|
873
|
+
if (!isOptionalPositiveInt(v[k])) return false;
|
|
874
|
+
}
|
|
875
|
+
// Require at least one actual knob: an empty patch would throw downstream.
|
|
876
|
+
return v.contextBudgetTokens !== undefined
|
|
877
|
+
|| v.tailTokens !== undefined
|
|
878
|
+
|| v.transitionPaceTokens !== undefined;
|
|
879
|
+
}
|
|
880
|
+
case 'settings-reset':
|
|
881
|
+
return isOptionalNonEmptyString(v.agent)
|
|
882
|
+
&& isOptionalBool(v.persist)
|
|
883
|
+
&& isOptionalBool(v.notify)
|
|
884
|
+
&& isOptionalStringArray(v.keys);
|
|
885
|
+
case 'settings-cancel-transition':
|
|
886
|
+
return isOptionalNonEmptyString(v.agent) && isOptionalBool(v.persist);
|
|
771
887
|
case 'request-workspace-mounts':
|
|
772
888
|
return v.scope === undefined || typeof v.scope === 'string';
|
|
773
889
|
case 'request-workspace-tree':
|
|
@@ -822,3 +938,17 @@ function isOptionalStringArray(v: unknown): v is string[] | undefined {
|
|
|
822
938
|
if (!Array.isArray(v)) return false;
|
|
823
939
|
return v.every((x) => typeof x === 'string');
|
|
824
940
|
}
|
|
941
|
+
|
|
942
|
+
function isOptionalNonEmptyString(v: unknown): v is string | undefined {
|
|
943
|
+
return v === undefined || isNonEmptyString(v);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function isOptionalBool(v: unknown): v is boolean | undefined {
|
|
947
|
+
return v === undefined || typeof v === 'boolean';
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Token counts: safe positive integers only. Rejects strings, NaN, Infinity,
|
|
951
|
+
* fractions and negatives before they reach the settings validator. */
|
|
952
|
+
function isOptionalPositiveInt(v: unknown): v is number | undefined {
|
|
953
|
+
return v === undefined || (typeof v === 'number' && Number.isSafeInteger(v) && v > 0);
|
|
954
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire validation for the context-settings messages.
|
|
3
|
+
*
|
|
4
|
+
* These messages mutate a LIVE agent's context budget, so malformed payloads
|
|
5
|
+
* must be rejected at the protocol edge rather than reaching
|
|
6
|
+
* Agent.validateRuntimeSettingsPatch as strings/NaN/fractions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'bun:test';
|
|
10
|
+
import { isClientMessage } from '../src/web/protocol.js';
|
|
11
|
+
|
|
12
|
+
describe('settings messages — wire validation', () => {
|
|
13
|
+
it('accepts a minimal budget update', () => {
|
|
14
|
+
expect(isClientMessage({ type: 'settings-update', contextBudgetTokens: 120_000 })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('accepts tail-only and pace-only updates', () => {
|
|
18
|
+
expect(isClientMessage({ type: 'settings-update', tailTokens: 30_000 })).toBe(true);
|
|
19
|
+
expect(isClientMessage({ type: 'settings-update', transitionPaceTokens: 8_000 })).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('accepts persist/notify flags and an agent name', () => {
|
|
23
|
+
expect(isClientMessage({
|
|
24
|
+
type: 'settings-update',
|
|
25
|
+
contextBudgetTokens: 100_000,
|
|
26
|
+
persist: false,
|
|
27
|
+
notify: true,
|
|
28
|
+
agent: 'mythos',
|
|
29
|
+
})).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('rejects an empty patch — it would throw downstream', () => {
|
|
33
|
+
expect(isClientMessage({ type: 'settings-update' })).toBe(false);
|
|
34
|
+
expect(isClientMessage({ type: 'settings-update', persist: true })).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rejects non-integer / non-positive / stringly token counts', () => {
|
|
38
|
+
for (const bad of ['100000', 100.5, -1, 0, NaN, Infinity, null, {}]) {
|
|
39
|
+
expect(isClientMessage({ type: 'settings-update', contextBudgetTokens: bad }))
|
|
40
|
+
.toBe(false);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects non-boolean flags and empty agent names', () => {
|
|
45
|
+
expect(isClientMessage({ type: 'settings-update', contextBudgetTokens: 1, persist: 'yes' })).toBe(false);
|
|
46
|
+
expect(isClientMessage({ type: 'settings-update', contextBudgetTokens: 1, notify: 1 })).toBe(false);
|
|
47
|
+
expect(isClientMessage({ type: 'settings-update', contextBudgetTokens: 1, agent: '' })).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('validates reset and cancel shapes', () => {
|
|
51
|
+
expect(isClientMessage({ type: 'settings-reset' })).toBe(true);
|
|
52
|
+
expect(isClientMessage({ type: 'settings-reset', keys: ['contextBudgetTokens'] })).toBe(true);
|
|
53
|
+
expect(isClientMessage({ type: 'settings-reset', keys: 'contextBudgetTokens' })).toBe(false);
|
|
54
|
+
expect(isClientMessage({ type: 'settings-cancel-transition' })).toBe(true);
|
|
55
|
+
expect(isClientMessage({ type: 'settings-cancel-transition', persist: false })).toBe(true);
|
|
56
|
+
expect(isClientMessage({ type: 'settings-cancel-transition', agent: 42 })).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('validates request-settings', () => {
|
|
60
|
+
expect(isClientMessage({ type: 'request-settings' })).toBe(true);
|
|
61
|
+
expect(isClientMessage({ type: 'request-settings', agent: 'mythos' })).toBe(true);
|
|
62
|
+
expect(isClientMessage({ type: 'request-settings', agent: 7 })).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
package/web/src/App.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { StreamPanel, formatStreamEvent, type StreamLine } from './Stream';
|
|
|
9
9
|
import { UsagePanel } from './Usage';
|
|
10
10
|
import { LessonsPanel, type LessonRow } from './Lessons';
|
|
11
11
|
import { McplPanel, type McplServerRow } from './Mcpl';
|
|
12
|
+
import { SettingsPanel, type SettingsState } from './Settings';
|
|
12
13
|
import { FilesPanel, FileViewerModal, type Mount, type FlatEntry, type FileViewer } from './Files';
|
|
13
14
|
import { ContextPanel } from './Context';
|
|
14
15
|
import { ContextDocument } from './ContextDocument';
|
|
@@ -272,7 +273,7 @@ export function App() {
|
|
|
272
273
|
|
|
273
274
|
/** Right-sidebar tab selection. The Tree is the most-used surface so it's
|
|
274
275
|
* the default; lessons / mcp / files are operator-driven panels. */
|
|
275
|
-
type SidebarTab = 'tree' | 'lessons' | 'mcp' | 'files' | 'context' | 'health';
|
|
276
|
+
type SidebarTab = 'tree' | 'lessons' | 'mcp' | 'files' | 'context' | 'settings' | 'health';
|
|
276
277
|
const [sidebarTab, setSidebarTab] = createSignal<SidebarTab>('tree');
|
|
277
278
|
const [mainView, setMainView] = createSignal<'chat' | 'context'>('chat');
|
|
278
279
|
|
|
@@ -311,6 +312,16 @@ export function App() {
|
|
|
311
312
|
wire.send({ type: 'request-mcpl' });
|
|
312
313
|
};
|
|
313
314
|
|
|
315
|
+
/** Context-settings panel state. The server BROADCASTS `settings-state` after
|
|
316
|
+
* every mutation (unlike mcpl-list, which is requester-only), because these
|
|
317
|
+
* are live process values — two operators must not see divergent budgets. */
|
|
318
|
+
const [settingsState, setSettingsState] = createSignal<SettingsState | null>(null);
|
|
319
|
+
const [settingsLoaded, setSettingsLoaded] = createSignal(false);
|
|
320
|
+
const refreshSettings = (): void => {
|
|
321
|
+
setSettingsLoaded(false);
|
|
322
|
+
wire.send({ type: 'request-settings' });
|
|
323
|
+
};
|
|
324
|
+
|
|
314
325
|
/** Workspace files panel state — mounts list + per-mount tree cache. */
|
|
315
326
|
const [mounts, setMounts] = createSignal<Mount[]>([]);
|
|
316
327
|
const [mountsLoaded, setMountsLoaded] = createSignal(false);
|
|
@@ -876,6 +887,10 @@ export function App() {
|
|
|
876
887
|
setMcplConfigPath(configPath);
|
|
877
888
|
setMcplServers(servers);
|
|
878
889
|
},
|
|
890
|
+
setSettings: (state) => {
|
|
891
|
+
setSettingsLoaded(true);
|
|
892
|
+
setSettingsState(state);
|
|
893
|
+
},
|
|
879
894
|
setMounts: (loaded, moduleLoaded, list) => {
|
|
880
895
|
setMountsLoaded(loaded);
|
|
881
896
|
setWorkspaceModuleLoaded(moduleLoaded);
|
|
@@ -1174,6 +1189,7 @@ export function App() {
|
|
|
1174
1189
|
if (tab === 'lessons' && !lessonsLoaded()) refreshLessons();
|
|
1175
1190
|
if (tab === 'mcp' && !mcplLoaded()) refreshMcpl();
|
|
1176
1191
|
if (tab === 'files' && !mountsLoaded()) refreshMounts();
|
|
1192
|
+
if (tab === 'settings' && !settingsLoaded()) refreshSettings();
|
|
1177
1193
|
}}
|
|
1178
1194
|
/>
|
|
1179
1195
|
<div class="flex-1 min-h-0">
|
|
@@ -1212,6 +1228,17 @@ export function App() {
|
|
|
1212
1228
|
onSetEnv={(id, env) => wire.send({ type: 'mcpl-set-env', id, env })}
|
|
1213
1229
|
/>
|
|
1214
1230
|
</Show>
|
|
1231
|
+
<Show when={sidebarTab() === 'settings'}>
|
|
1232
|
+
<SettingsPanel
|
|
1233
|
+
loaded={settingsLoaded()}
|
|
1234
|
+
state={settingsState()}
|
|
1235
|
+
onRefresh={refreshSettings}
|
|
1236
|
+
onApply={(patch) => wire.send({ type: 'settings-update', ...patch })}
|
|
1237
|
+
onReset={(keys, persist) =>
|
|
1238
|
+
wire.send({ type: 'settings-reset', ...(keys ? { keys } : {}), persist })}
|
|
1239
|
+
onCancelTransition={() => wire.send({ type: 'settings-cancel-transition' })}
|
|
1240
|
+
/>
|
|
1241
|
+
</Show>
|
|
1215
1242
|
<Show when={sidebarTab() === 'files'}>
|
|
1216
1243
|
<FilesPanel
|
|
1217
1244
|
loaded={mountsLoaded()}
|
|
@@ -1282,6 +1309,8 @@ interface HandlerHooks {
|
|
|
1282
1309
|
setLessons: (loaded: boolean, moduleLoaded: boolean, lessons: LessonRow[]) => void;
|
|
1283
1310
|
/** Apply an mcpl-list response from the server. */
|
|
1284
1311
|
setMcpl: (configPath: string, servers: McplServerRow[]) => void;
|
|
1312
|
+
/** Apply a settings-state broadcast. */
|
|
1313
|
+
setSettings: (state: SettingsState) => void;
|
|
1285
1314
|
/** Apply a workspace-mounts response. */
|
|
1286
1315
|
setMounts: (loaded: boolean, moduleLoaded: boolean, mounts: Mount[]) => void;
|
|
1287
1316
|
/** Apply a workspace-tree response for one mount. */
|
|
@@ -1412,6 +1441,9 @@ function handleServerMessage(
|
|
|
1412
1441
|
case 'mcpl-list':
|
|
1413
1442
|
hooks.setMcpl(msg.configPath, msg.servers);
|
|
1414
1443
|
return;
|
|
1444
|
+
case 'settings-state':
|
|
1445
|
+
hooks.setSettings(msg as unknown as SettingsState);
|
|
1446
|
+
return;
|
|
1415
1447
|
case 'workspace-mounts':
|
|
1416
1448
|
hooks.setMounts(true, msg.loaded, msg.mounts);
|
|
1417
1449
|
return;
|
|
@@ -1942,7 +1974,7 @@ function CommandSuggestions(props: { draft: string; onPick: (cmd: string) => voi
|
|
|
1942
1974
|
);
|
|
1943
1975
|
}
|
|
1944
1976
|
|
|
1945
|
-
type SidebarTabId = 'tree' | 'lessons' | 'mcp' | 'files' | 'context' | 'health';
|
|
1977
|
+
type SidebarTabId = 'tree' | 'lessons' | 'mcp' | 'files' | 'context' | 'settings' | 'health';
|
|
1946
1978
|
|
|
1947
1979
|
function SidebarTabs(props: {
|
|
1948
1980
|
current: SidebarTabId;
|
|
@@ -1954,6 +1986,7 @@ function SidebarTabs(props: {
|
|
|
1954
1986
|
{ id: 'mcp', label: 'MCP', title: 'MCPL servers' },
|
|
1955
1987
|
{ id: 'files', label: 'Files', title: 'Workspace mounts + files' },
|
|
1956
1988
|
{ id: 'context', label: 'Context', title: 'Compiled context makeup' },
|
|
1989
|
+
{ id: 'settings', label: 'Settings', title: 'Live context budget / tail, with preview' },
|
|
1957
1990
|
{ id: 'health', label: 'Health', title: 'Runtime settings, failures, quarantine' },
|
|
1958
1991
|
];
|
|
1959
1992
|
return (
|