@animalabs/connectome-host 0.8.0 → 0.8.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 +75 -0
- package/bun.lock +4 -6
- package/package.json +3 -3
- package/src/commands.ts +125 -32
- package/src/framework-agent-config.ts +3 -0
- package/src/framework-strategy.ts +5 -0
- package/src/gate-telemetry.ts +30 -2
- package/src/index.ts +35 -8
- package/src/modules/web-ui-module.ts +32 -4
- package/src/recipe.ts +89 -0
- package/src/web/panel-data.ts +25 -1
- package/test/commands-qa-family.test.ts +239 -0
- package/test/count-tokens-model.test.ts +31 -0
- package/test/framework-strategy-defaults.test.ts +41 -0
- package/test/gate-telemetry.test.ts +34 -1
- package/test/subconscious-recipe.test.ts +86 -0
- package/test/tool-wrapper-prose-guard-recipe.test.ts +37 -0
- package/test/web-ui-module.test.ts +41 -0
- package/web/src/App.tsx +9 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe surface for tune-out's subconscious resident (agent-framework#77):
|
|
3
|
+
* schema validation of the `subconscious` block. The block passes through to
|
|
4
|
+
* FrameworkConfig.subconscious verbatim, so validation is the host's whole
|
|
5
|
+
* contribution — a typo'd key or a missing mode block must fail at recipe
|
|
6
|
+
* load, not surface as a subconscious running on an empty system prompt.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, test, expect } from 'bun:test';
|
|
9
|
+
import { validateRecipe } from '../src/recipe.js';
|
|
10
|
+
|
|
11
|
+
function baseRecipe(extra: Record<string, unknown> = {}): Record<string, unknown> {
|
|
12
|
+
return {
|
|
13
|
+
name: 'Test',
|
|
14
|
+
agent: { name: 'sherlock', systemPrompt: 'test' },
|
|
15
|
+
...extra,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MODE = 'You are the Subconscious. Watch tuned-out channels and report in second person.';
|
|
20
|
+
|
|
21
|
+
describe('validateRecipe — subconscious schema', () => {
|
|
22
|
+
test('absent block is accepted', () => {
|
|
23
|
+
expect(() => validateRecipe(baseRecipe())).not.toThrow();
|
|
24
|
+
expect(validateRecipe(baseRecipe()).subconscious).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('full valid block is accepted and passes through verbatim', () => {
|
|
28
|
+
const block = {
|
|
29
|
+
enabled: true,
|
|
30
|
+
name: 'Subconscious',
|
|
31
|
+
model: 'claude-sonnet-4-5',
|
|
32
|
+
systemPrompt: MODE,
|
|
33
|
+
allowChannelSpeech: false,
|
|
34
|
+
reAnchorFraction: 0.5,
|
|
35
|
+
};
|
|
36
|
+
const recipe = validateRecipe(baseRecipe({ subconscious: block }));
|
|
37
|
+
expect(recipe.subconscious).toEqual(block);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('minimal block: enabled + systemPrompt', () => {
|
|
41
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE } }))).not.toThrow();
|
|
42
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: false, systemPrompt: MODE } }))).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('non-object block is refused', () => {
|
|
46
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: true }))).toThrow(/subconscious must be an object/);
|
|
47
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: [] }))).toThrow(/subconscious must be an object/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('enabled must be a boolean', () => {
|
|
51
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: 'yes', systemPrompt: MODE } })))
|
|
52
|
+
.toThrow(/subconscious\.enabled must be a boolean/);
|
|
53
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { systemPrompt: MODE } })))
|
|
54
|
+
.toThrow(/subconscious\.enabled must be a boolean/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('systemPrompt is required and non-empty — the mode block is the whole character', () => {
|
|
58
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true } })))
|
|
59
|
+
.toThrow(/subconscious\.systemPrompt must be a non-empty string/);
|
|
60
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: ' ' } })))
|
|
61
|
+
.toThrow(/subconscious\.systemPrompt must be a non-empty string/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('unknown fields are refused by name', () => {
|
|
65
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, cadence: 30 } })))
|
|
66
|
+
.toThrow(/unknown field "cadence"/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('name and model must be non-empty strings when present', () => {
|
|
70
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, name: '' } })))
|
|
71
|
+
.toThrow(/subconscious\.name must be a non-empty string/);
|
|
72
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, model: 7 } })))
|
|
73
|
+
.toThrow(/subconscious\.model must be a non-empty string/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('allowChannelSpeech must be a boolean; reAnchorFraction must be in (0, 1]', () => {
|
|
77
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, allowChannelSpeech: 'no' } })))
|
|
78
|
+
.toThrow(/allowChannelSpeech must be a boolean/);
|
|
79
|
+
for (const bad of [0, 1.5, -0.2, 'half']) {
|
|
80
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, reAnchorFraction: bad } })))
|
|
81
|
+
.toThrow(/reAnchorFraction must be a number in \(0, 1\]/);
|
|
82
|
+
}
|
|
83
|
+
expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, reAnchorFraction: 1 } })))
|
|
84
|
+
.not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { buildFrameworkAgentConfig } from '../src/framework-agent-config.js';
|
|
3
|
+
import { validateRecipe } from '../src/recipe.js';
|
|
4
|
+
|
|
5
|
+
const base = (value?: unknown) => ({
|
|
6
|
+
name: 'tool-wrapper-prose-guard',
|
|
7
|
+
agent: {
|
|
8
|
+
systemPrompt: 'sys',
|
|
9
|
+
...(value === undefined ? {} : { toolWrapperProseGuard: value }),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('toolWrapperProseGuard recipe wiring', () => {
|
|
14
|
+
test('is default-off by omission and composes true into AgentConfig', () => {
|
|
15
|
+
const absent = validateRecipe(base());
|
|
16
|
+
expect(absent.agent.toolWrapperProseGuard).toBeUndefined();
|
|
17
|
+
expect(buildFrameworkAgentConfig(absent, 'a', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBeUndefined();
|
|
18
|
+
|
|
19
|
+
const enabled = validateRecipe(base(true));
|
|
20
|
+
expect(enabled.agent.toolWrapperProseGuard).toBe(true);
|
|
21
|
+
expect(buildFrameworkAgentConfig(enabled, 'a', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(true);
|
|
22
|
+
|
|
23
|
+
const disabled = validateRecipe(base(false));
|
|
24
|
+
expect(disabled.agent.toolWrapperProseGuard).toBe(false);
|
|
25
|
+
expect(buildFrameworkAgentConfig(disabled, 'b', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(false);
|
|
26
|
+
|
|
27
|
+
const reloaded = validateRecipe(JSON.parse(JSON.stringify(enabled)));
|
|
28
|
+
expect(buildFrameworkAgentConfig(reloaded, 'c', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(true);
|
|
29
|
+
expect(buildFrameworkAgentConfig(absent, 'd', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('rejects non-boolean values instead of silently disabling the guard', () => {
|
|
33
|
+
for (const value of ['true', 1, null, {}]) {
|
|
34
|
+
expect(() => validateRecipe(base(value))).toThrow(/toolWrapperProseGuard/);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -330,6 +330,47 @@ describe('WebUiModule HTTP', () => {
|
|
|
330
330
|
const body = res.status < 500 ? await res.text() : '';
|
|
331
331
|
expect(body).not.toContain('pwned');
|
|
332
332
|
});
|
|
333
|
+
|
|
334
|
+
// Honest-error regression set: unknown API routes and missing bundle
|
|
335
|
+
// assets used to fall through to the SPA shell with a 200, and wrong
|
|
336
|
+
// methods executed the GET handlers.
|
|
337
|
+
|
|
338
|
+
test('unknown /debug/* route returns a JSON 404, not the SPA shell', async () => {
|
|
339
|
+
for (const path of ['/debug/context/', '/debug/CONTEXT', '/debug/nonexistent']) {
|
|
340
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}${path}`, {
|
|
341
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
342
|
+
});
|
|
343
|
+
expect(res.status).toBe(404);
|
|
344
|
+
expect(res.headers.get('content-type')).toContain('application/json');
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('missing /assets/* returns 404, not HTML', async () => {
|
|
349
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/assets/nonexistent.js`, {
|
|
350
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
351
|
+
});
|
|
352
|
+
expect(res.status).toBe(404);
|
|
353
|
+
expect(res.headers.get('content-type') ?? '').not.toContain('text/html');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('non-GET methods get 405 with Allow header', async () => {
|
|
357
|
+
for (const method of ['POST', 'DELETE', 'PUT']) {
|
|
358
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/debug/context`, {
|
|
359
|
+
method,
|
|
360
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
361
|
+
});
|
|
362
|
+
expect(res.status).toBe(405);
|
|
363
|
+
expect(res.headers.get('allow')).toContain('GET');
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('SPA client-side routes still fall back to the shell', async () => {
|
|
368
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/totally/fake/route`, {
|
|
369
|
+
headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
|
|
370
|
+
});
|
|
371
|
+
expect(res.status).toBe(200);
|
|
372
|
+
expect(await res.text()).toContain('<!doctype html>');
|
|
373
|
+
});
|
|
333
374
|
});
|
|
334
375
|
|
|
335
376
|
describe('WebUiModule WebSocket', () => {
|
package/web/src/App.tsx
CHANGED
|
@@ -1058,6 +1058,15 @@ export function App() {
|
|
|
1058
1058
|
if (!text) return;
|
|
1059
1059
|
setDraft('');
|
|
1060
1060
|
if (text.startsWith('/')) {
|
|
1061
|
+
// /clear is display-local (the TUI clears its scrollback the same
|
|
1062
|
+
// way): wipe this client's transcript view without touching the
|
|
1063
|
+
// Chronicle or other clients. Sending it to the host was a no-op
|
|
1064
|
+
// that APPENDED a "(cleared)" line — the opposite of clearing.
|
|
1065
|
+
if (text === '/clear' || text.startsWith('/clear ')) {
|
|
1066
|
+
setMessages(produce((arr) => { arr.length = 0; }));
|
|
1067
|
+
setStreamLines([]);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1061
1070
|
wire.send({ type: 'command', command: text });
|
|
1062
1071
|
return;
|
|
1063
1072
|
}
|