@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.
Files changed (40) hide show
  1. package/dist/ai-assistant.api.json +39 -53
  2. package/dist/ai-assistant.d.ts +20 -25
  3. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  4. package/dist/dts/index.d.ts +1 -0
  5. package/dist/dts/index.d.ts.map +1 -1
  6. package/dist/dts/main/main.d.ts +1 -20
  7. package/dist/dts/main/main.d.ts.map +1 -1
  8. package/dist/dts/state/debug-event-log.d.ts +16 -0
  9. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  10. package/dist/dts/state/debug-event-log.test.d.ts +2 -0
  11. package/dist/dts/state/debug-event-log.test.d.ts.map +1 -0
  12. package/dist/dts/utils/flatten-sub-agent-messages.d.ts +51 -0
  13. package/dist/dts/utils/flatten-sub-agent-messages.d.ts.map +1 -0
  14. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +2 -0
  15. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +1 -0
  16. package/dist/dts/utils/strip-agent-handlers.d.ts +29 -0
  17. package/dist/dts/utils/strip-agent-handlers.d.ts.map +1 -0
  18. package/dist/dts/utils/strip-agent-handlers.test.d.ts +2 -0
  19. package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +1 -0
  20. package/dist/esm/components/chat-driver/chat-driver.js +48 -12
  21. package/dist/esm/components/chat-driver/chat-driver.test.js +29 -0
  22. package/dist/esm/main/main.js +14 -38
  23. package/dist/esm/state/debug-event-log.js +47 -0
  24. package/dist/esm/state/debug-event-log.test.js +67 -0
  25. package/dist/esm/utils/flatten-sub-agent-messages.js +49 -0
  26. package/dist/esm/utils/flatten-sub-agent-messages.test.js +139 -0
  27. package/dist/esm/utils/strip-agent-handlers.js +51 -0
  28. package/dist/esm/utils/strip-agent-handlers.test.js +81 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +16 -16
  31. package/src/components/chat-driver/chat-driver.test.ts +43 -0
  32. package/src/components/chat-driver/chat-driver.ts +64 -10
  33. package/src/index.ts +1 -0
  34. package/src/main/main.ts +16 -37
  35. package/src/state/debug-event-log.test.ts +89 -0
  36. package/src/state/debug-event-log.ts +48 -0
  37. package/src/utils/flatten-sub-agent-messages.test.ts +163 -0
  38. package/src/utils/flatten-sub-agent-messages.ts +88 -0
  39. package/src/utils/strip-agent-handlers.test.ts +99 -0
  40. 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
+ }