@genesislcap/ai-assistant 14.467.1 → 14.467.2
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/dist/ai-assistant.api.json +39 -53
- package/dist/ai-assistant.d.ts +20 -25
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +1 -20
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +16 -0
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.test.d.ts +2 -0
- package/dist/dts/state/debug-event-log.test.d.ts.map +1 -0
- package/dist/dts/utils/flatten-sub-agent-messages.d.ts +51 -0
- package/dist/dts/utils/flatten-sub-agent-messages.d.ts.map +1 -0
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +2 -0
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +1 -0
- package/dist/dts/utils/strip-agent-handlers.d.ts +29 -0
- package/dist/dts/utils/strip-agent-handlers.d.ts.map +1 -0
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +2 -0
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +48 -12
- package/dist/esm/components/chat-driver/chat-driver.test.js +29 -0
- package/dist/esm/main/main.js +14 -38
- package/dist/esm/state/debug-event-log.js +47 -0
- package/dist/esm/state/debug-event-log.test.js +67 -0
- package/dist/esm/utils/flatten-sub-agent-messages.js +49 -0
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +139 -0
- package/dist/esm/utils/strip-agent-handlers.js +51 -0
- package/dist/esm/utils/strip-agent-handlers.test.js +81 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/chat-driver/chat-driver.test.ts +43 -0
- package/src/components/chat-driver/chat-driver.ts +64 -10
- package/src/index.ts +1 -0
- package/src/main/main.ts +16 -37
- package/src/state/debug-event-log.test.ts +89 -0
- package/src/state/debug-event-log.ts +48 -0
- package/src/utils/flatten-sub-agent-messages.test.ts +163 -0
- package/src/utils/flatten-sub-agent-messages.ts +88 -0
- package/src/utils/strip-agent-handlers.test.ts +99 -0
- package/src/utils/strip-agent-handlers.ts +52 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A chat message lifted into the debug-log timeline as a `kind: 'message'` entry.
|
|
5
|
+
*
|
|
6
|
+
* @beta
|
|
7
|
+
*/
|
|
8
|
+
export type TimelineMessage = ChatMessage & {
|
|
9
|
+
kind: 'message';
|
|
10
|
+
/**
|
|
11
|
+
* Set only on entries hoisted out of a `subAgentTrace`: the sub-agent's own
|
|
12
|
+
* (un-breadcrumbed) `agentName`, kept for filtering since the displayed
|
|
13
|
+
* `agentName` is rewritten to a `"<parent> › <sub-agent>"` breadcrumb.
|
|
14
|
+
*/
|
|
15
|
+
subAgentName?: string;
|
|
16
|
+
/** Delegation depth — absent/0 for top-level, 1 for a sub-agent, 2 for a sub-agent's sub-agent, … */
|
|
17
|
+
subAgentDepth?: number;
|
|
18
|
+
/** Id of the parent tool call that spawned this run — correlates a hoisted message back to its delegation. */
|
|
19
|
+
subAgentOf?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Flatten sub-agent conversations into a single top-level message stream for the
|
|
24
|
+
* debug-log timeline.
|
|
25
|
+
*
|
|
26
|
+
* A tool call that delegates to a sub-agent carries the sub-agent's entire
|
|
27
|
+
* conversation on `toolCall.subAgentTrace`. The live UI reads that nested shape,
|
|
28
|
+
* but a top-to-bottom timeline reads far better with those messages inline next to
|
|
29
|
+
* the turn that produced them. This walks the list and, for every tool call with a
|
|
30
|
+
* `subAgentTrace`, hoists the sub-agent's messages up as their own timeline
|
|
31
|
+
* entries while **removing** the nested copy from the emitted parent tool call — a
|
|
32
|
+
* move, not a copy. The data is relocated, never duplicated: the export stays the
|
|
33
|
+
* same size and a naive `sum(timeline[].cost)` does not double-count (the
|
|
34
|
+
* canonical `sumCosts` total still reads the un-flattened history).
|
|
35
|
+
*
|
|
36
|
+
* Hoisted entries are:
|
|
37
|
+
* - **breadcrumbed**: `agentName` becomes `"<parent> › <sub-agent>"`, composing for
|
|
38
|
+
* nested sub-agents (`"<parent> › <sub> › <subsub>"`); the raw sub-agent name is
|
|
39
|
+
* preserved on `subAgentName`.
|
|
40
|
+
* - **depth-tagged** (`subAgentDepth`) and **correlated** to the spawning tool call
|
|
41
|
+
* (`subAgentOf`), so the delegation tree is reconstructable even when two
|
|
42
|
+
* sub-agents run within the same parent turn.
|
|
43
|
+
*
|
|
44
|
+
* Top-level messages (depth 0) are emitted unchanged apart from the trace strip.
|
|
45
|
+
* Order is preserved within each list; the caller sorts the whole timeline by
|
|
46
|
+
* timestamp afterwards, and every sub-agent message carries its own timestamp, so
|
|
47
|
+
* hoisted entries interleave chronologically between the parent's tool-call and
|
|
48
|
+
* tool-result messages.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export function flattenSubAgentMessages(
|
|
53
|
+
messages: readonly ChatMessage[],
|
|
54
|
+
prefix = '',
|
|
55
|
+
depth = 0,
|
|
56
|
+
subAgentOf?: string,
|
|
57
|
+
): TimelineMessage[] {
|
|
58
|
+
return messages.flatMap((m) => {
|
|
59
|
+
const breadcrumb = prefix ? `${prefix} › ${m.agentName ?? '?'}` : m.agentName;
|
|
60
|
+
const traceCalls = m.toolCalls?.filter((tc) => tc.subAgentTrace?.length) ?? [];
|
|
61
|
+
|
|
62
|
+
const entry: TimelineMessage = {
|
|
63
|
+
kind: 'message',
|
|
64
|
+
...m,
|
|
65
|
+
// Move the (about-to-be-hoisted) child conversations out of the emitted tool
|
|
66
|
+
// calls. Setting it to `undefined` drops the key from the serialized export.
|
|
67
|
+
...(traceCalls.length > 0 && {
|
|
68
|
+
toolCalls: m.toolCalls!.map((tc) =>
|
|
69
|
+
tc.subAgentTrace ? { ...tc, subAgentTrace: undefined } : tc,
|
|
70
|
+
),
|
|
71
|
+
}),
|
|
72
|
+
// Mark + breadcrumb hoisted sub-agent messages; top-level (depth 0) stays as-is.
|
|
73
|
+
...(depth > 0 && {
|
|
74
|
+
agentName: breadcrumb,
|
|
75
|
+
subAgentName: m.agentName,
|
|
76
|
+
subAgentDepth: depth,
|
|
77
|
+
subAgentOf,
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (traceCalls.length === 0) return [entry];
|
|
82
|
+
|
|
83
|
+
const hoisted = traceCalls.flatMap((tc) =>
|
|
84
|
+
flattenSubAgentMessages(tc.subAgentTrace!, breadcrumb ?? '', depth + 1, tc.id),
|
|
85
|
+
);
|
|
86
|
+
return [entry, ...hoisted];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import type { AgentConfig } from '../config/config';
|
|
3
|
+
import { stripAgentHandlers } from './strip-agent-handlers';
|
|
4
|
+
|
|
5
|
+
const cfg = (overrides: Record<string, unknown>): AgentConfig =>
|
|
6
|
+
({ name: 'agent', ...overrides }) as unknown as AgentConfig;
|
|
7
|
+
|
|
8
|
+
/** Mirrors what redux's serializable-state check does: find any function anywhere. */
|
|
9
|
+
const hasFunctionDeep = (value: unknown): boolean => {
|
|
10
|
+
if (typeof value === 'function') return true;
|
|
11
|
+
if (Array.isArray(value)) return value.some(hasFunctionDeep);
|
|
12
|
+
if (value !== null && typeof value === 'object') {
|
|
13
|
+
return Object.values(value).some(hasFunctionDeep);
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const suite = createLogicSuite('stripAgentHandlers');
|
|
19
|
+
|
|
20
|
+
suite('drops object-form toolHandlers (the redux-leak regression)', () => {
|
|
21
|
+
const stripped = stripAgentHandlers(
|
|
22
|
+
cfg({ toolHandlers: { vfs_list: async () => 'x', vfs_read: async () => 'y' } }),
|
|
23
|
+
) as Record<string, unknown>;
|
|
24
|
+
// The whole handler bag is gone — not left behind with live functions inside.
|
|
25
|
+
assert.not.ok(stripped.toolHandlers);
|
|
26
|
+
assert.not.ok(hasFunctionDeep(stripped));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
suite('drops factory-function toolHandlers and the lifecycle/dispatch hooks', () => {
|
|
30
|
+
const stripped = stripAgentHandlers(
|
|
31
|
+
cfg({
|
|
32
|
+
toolHandlers: () => ({ t: async () => 'x' }),
|
|
33
|
+
onActivate: async () => {},
|
|
34
|
+
onDeactivate: async () => {},
|
|
35
|
+
getDebugSnapshot: () => ({}),
|
|
36
|
+
onUnresolvedTool: () => 'redirect',
|
|
37
|
+
}),
|
|
38
|
+
) as Record<string, unknown>;
|
|
39
|
+
assert.not.ok(stripped.toolHandlers);
|
|
40
|
+
assert.not.ok(stripped.onActivate);
|
|
41
|
+
assert.not.ok(stripped.onDeactivate);
|
|
42
|
+
assert.not.ok(stripped.getDebugSnapshot);
|
|
43
|
+
assert.not.ok(stripped.onUnresolvedTool);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
suite('drops function-form resolvers but keeps static forms and plain data', () => {
|
|
47
|
+
const stripped = stripAgentHandlers(
|
|
48
|
+
cfg({
|
|
49
|
+
systemPrompt: () => 'dynamic', // function → dropped
|
|
50
|
+
displayName: 'Static Name', // string → kept
|
|
51
|
+
temperature: 0.5, // number → kept
|
|
52
|
+
toolChoice: 'auto', // string → kept
|
|
53
|
+
toolDefinitions: [{ name: 'x', description: 'd', parameters: {} }], // data array → kept
|
|
54
|
+
manualSelection: { enabled: true, hint: 'pick me' }, // data object → kept
|
|
55
|
+
}),
|
|
56
|
+
) as Record<string, unknown>;
|
|
57
|
+
assert.not.ok(stripped.systemPrompt);
|
|
58
|
+
assert.is(stripped.displayName, 'Static Name');
|
|
59
|
+
assert.is(stripped.temperature, 0.5);
|
|
60
|
+
assert.is(stripped.toolChoice, 'auto');
|
|
61
|
+
assert.equal(stripped.toolDefinitions, [{ name: 'x', description: 'd', parameters: {} }]);
|
|
62
|
+
assert.equal(stripped.manualSelection, { enabled: true, hint: 'pick me' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
suite('recurses into subAgents, stripping their object-form toolHandlers', () => {
|
|
66
|
+
const stripped = stripAgentHandlers(
|
|
67
|
+
cfg({
|
|
68
|
+
name: 'boss',
|
|
69
|
+
toolHandlers: () => ({}),
|
|
70
|
+
subAgents: [
|
|
71
|
+
cfg({
|
|
72
|
+
name: 'worker',
|
|
73
|
+
toolHandlers: { vfs_list: async () => 'x', vfs_read: async () => 'y' },
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
}),
|
|
77
|
+
) as Record<string, unknown>;
|
|
78
|
+
const subs = stripped.subAgents as Array<Record<string, unknown>>;
|
|
79
|
+
assert.is(subs.length, 1);
|
|
80
|
+
assert.is(subs[0].name, 'worker');
|
|
81
|
+
assert.not.ok(subs[0].toolHandlers, 'sub-agent toolHandlers must be stripped (the reported bug)');
|
|
82
|
+
// Nothing function-valued survives anywhere in the projection.
|
|
83
|
+
assert.not.ok(hasFunctionDeep(stripped));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
suite('produces a projection with no function at any depth', () => {
|
|
87
|
+
const stripped = stripAgentHandlers(
|
|
88
|
+
cfg({
|
|
89
|
+
name: 'boss',
|
|
90
|
+
onActivate: async () => {},
|
|
91
|
+
subAgents: [cfg({ name: 'w', toolHandlers: { t: async () => {} } })],
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
assert.not.ok(hasFunctionDeep(stripped));
|
|
95
|
+
// Round-trips to the expected serializable shape.
|
|
96
|
+
assert.equal(JSON.parse(JSON.stringify(stripped)), { name: 'boss', subAgents: [{ name: 'w' }] });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
suite.run();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AgentConfig } from '../config/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Project an `AgentConfig` down to a JSON-serializable shape for the redux
|
|
5
|
+
* session store. Drops two kinds of field:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Directly function-valued** — the lifecycle/dispatch hooks (`onActivate`,
|
|
8
|
+
* `onDeactivate`, `getDebugSnapshot`, `onUnresolvedTool`) and the function
|
|
9
|
+
* form of the per-turn resolvers (`systemPrompt`, `toolDefinitions`,
|
|
10
|
+
* `displayName`, `provider`, `temperature`, `toolChoice`, `toolHandlers`).
|
|
11
|
+
* 2. **Object "handler bags" whose *values* are functions** — `toolHandlers` in
|
|
12
|
+
* its object form is `{ name: handler }`, so `typeof` is `'object'`, not
|
|
13
|
+
* `'function'`. A by-value check on the field alone misses it, leaking a live
|
|
14
|
+
* handler into store state and tripping redux's serializability check (the
|
|
15
|
+
* regression this guards against — top-level agents tend to use the factory
|
|
16
|
+
* function form, but sub-agents commonly declare an object literal).
|
|
17
|
+
*
|
|
18
|
+
* `subAgents` are projected recursively. Static forms (string / number / array /
|
|
19
|
+
* plain data object) pass through unchanged.
|
|
20
|
+
*
|
|
21
|
+
* Filtering by value rather than an explicit field list means a new
|
|
22
|
+
* function-valued field on `AgentConfig` is handled automatically — no denylist
|
|
23
|
+
* to maintain. The live config on the driver stays the source of truth; the
|
|
24
|
+
* slice only holds this serializable projection and functions are never read
|
|
25
|
+
* back from it.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export function stripAgentHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
30
|
+
const serializable: Record<string, unknown> = {};
|
|
31
|
+
for (const [key, value] of Object.entries(agent)) {
|
|
32
|
+
// Projected recursively below.
|
|
33
|
+
if (key === 'subAgents') continue;
|
|
34
|
+
// Directly function-valued field.
|
|
35
|
+
if (typeof value === 'function') continue;
|
|
36
|
+
// Object/array whose values include a function (e.g. `toolHandlers` in object
|
|
37
|
+
// form). Drop the whole field — the projection type omits it and a function
|
|
38
|
+
// must never reach the store.
|
|
39
|
+
if (
|
|
40
|
+
value !== null &&
|
|
41
|
+
typeof value === 'object' &&
|
|
42
|
+
Object.values(value).some((v) => typeof v === 'function')
|
|
43
|
+
) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
serializable[key] = value;
|
|
47
|
+
}
|
|
48
|
+
if (agent.subAgents?.length) {
|
|
49
|
+
serializable.subAgents = agent.subAgents.map(stripAgentHandlers);
|
|
50
|
+
}
|
|
51
|
+
return serializable as unknown as Omit<AgentConfig, 'toolHandlers'>;
|
|
52
|
+
}
|