@genesislcap/ai-assistant 15.9.0 → 15.10.0

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.
@@ -0,0 +1,2 @@
1
+ import './align-event-globals';
2
+ //# sourceMappingURL=chat-driver.invocation-scope.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-driver.invocation-scope.test.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/chat-driver.invocation-scope.test.ts"],"names":[],"mappings":"AAWA,OAAO,uBAAuB,CAAC"}
@@ -0,0 +1,238 @@
1
+ import { __awaiter } from "tslib";
2
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
+ import { defineStatefulAgent } from '../../config/define-stateful-agent';
4
+ // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
5
+ // jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
6
+ import './align-event-globals';
7
+ import { ChatDriver } from './chat-driver';
8
+ // ---------------------------------------------------------------------------
9
+ // Sub-agent state scoping.
10
+ //
11
+ // A sub-agent config lives in the parent's `subAgentsMap`, keyed by NAME — one
12
+ // config object, reused for every invocation. Anything a handler closes over is
13
+ // therefore scoped to the *config*, not to the invocation.
14
+ //
15
+ // These tests PIN that behaviour; they do not wish it away. Per-conversation
16
+ // registration is deliberate, so a closure in a sub-agent config will always be
17
+ // config-scoped — no planned platform change (F8 included) alters it. F8 adds a
18
+ // separate invocation-scoped store to write to instead; it does not re-scope
19
+ // closures. Consumers that accumulate across a child's turns must use that store
20
+ // (today: a WeakMap keyed on `promptCtx.signal`), never a closure.
21
+ //
22
+ // ⚠ TO WHOEVER IMPLEMENTS THE PER-ACTIVATION-STATE TICKET
23
+ // (`defineStatefulAgent`: per-activation state and headless execution)
24
+ //
25
+ // Its acceptance criterion 2 — "two concurrent activations of the same config
26
+ // hold independent state, with a test that fails against today's shared closure
27
+ // variable" — reads almost word-for-word like the first two tests below. It is
28
+ // not the same subject, and they must be treated differently:
29
+ //
30
+ // • Criterion 2 concerns FRAMEWORK-owned state: `defineStatefulAgent`'s single
31
+ // `let state` (define-stateful-agent.ts:374). That is the bug it fixes, and
32
+ // it needs a NEW test of its own.
33
+ // • The first two tests below concern CONSUMER-owned closures in a plain
34
+ // `AgentConfig`. The platform cannot re-scope a closure someone wrote in
35
+ // their own factory, so these stay true afterwards. Do not delete or invert
36
+ // them to make criterion 2 look satisfied.
37
+ // • The third test — "a defineStatefulAgent config cannot be used as a
38
+ // sub-agent" — IS invalidated by that ticket (its criteria 1 and 3), and
39
+ // must be rewritten when it lands, not before.
40
+ //
41
+ // The provider here answers by rule rather than by scripted queue: sub-agent
42
+ // invocations dispatched from one assistant turn run concurrently under
43
+ // `Promise.all`, so a shared FIFO queue would make ordering — and the test —
44
+ // nondeterministic.
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * Answers by rule, not by scripted queue: sub-agent invocations dispatched from
48
+ * one assistant turn run concurrently under `Promise.all`, so a shared FIFO
49
+ * would make ordering — and the test — nondeterministic.
50
+ *
51
+ * `fanOut` controls how many `delegate` calls the parent emits in its single
52
+ * delegating turn. Parent turns after the first return plain text so the loop
53
+ * terminates.
54
+ */
55
+ const ruleProvider = (fanOut) => {
56
+ let parentTurns = 0;
57
+ return {
58
+ chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
59
+ var _a;
60
+ const tools = ((_a = options === null || options === void 0 ? void 0 : options.tools) !== null && _a !== void 0 ? _a : []).map((t) => t.name);
61
+ // Child turn: it can only finish via its completion tool.
62
+ if (tools.includes('work')) {
63
+ return {
64
+ role: 'assistant',
65
+ content: '',
66
+ toolCalls: [{ id: 'w1', name: 'work', args: {} }],
67
+ };
68
+ }
69
+ // Parent turn — delegate once, then wrap up.
70
+ if (tools.includes('delegate') && parentTurns === 0) {
71
+ parentTurns += 1;
72
+ return {
73
+ role: 'assistant',
74
+ content: '',
75
+ toolCalls: Array.from({ length: fanOut }, (_, i) => ({
76
+ id: `d${i}`,
77
+ name: 'delegate',
78
+ args: {},
79
+ })),
80
+ };
81
+ }
82
+ return { role: 'assistant', content: 'done' };
83
+ }),
84
+ };
85
+ };
86
+ const makeRegistry = (provider) => ({
87
+ get: () => provider,
88
+ default: () => provider,
89
+ defaultName: () => 'test',
90
+ names: () => ['test'],
91
+ getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
92
+ listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
93
+ });
94
+ const agent = (overrides) => (Object.assign({ description: 'test agent' }, overrides));
95
+ const makeDriver = (config, provider) => {
96
+ const driver = new ChatDriver(makeRegistry(provider), {
97
+ maxToolIterations: 20,
98
+ maxFoldOperations: 5,
99
+ sessionKey: '',
100
+ });
101
+ driver.applyAgent(config);
102
+ return driver;
103
+ };
104
+ const scope = createLogicSuite('ChatDriver sub-agent invocation scope');
105
+ // ---------------------------------------------------------------------------
106
+ scope('closure state is config-scoped, so concurrent invocations contaminate each other', () => __awaiter(void 0, void 0, void 0, function* () {
107
+ // The natural-looking way to accumulate across a sub-agent's turns — a closure
108
+ // in the config factory — is created once, with the config, and shared by every
109
+ // invocation. This is the trap; the assertion records it rather than wanting it.
110
+ const collected = [];
111
+ const worker = agent({
112
+ name: 'worker',
113
+ toolDefinitions: [
114
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
115
+ ],
116
+ toolHandlers: {
117
+ work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
118
+ var _a;
119
+ collected.push('item');
120
+ // Report what THIS invocation believes it has accumulated.
121
+ (_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { seen: collected.length });
122
+ return 'worked';
123
+ }),
124
+ },
125
+ });
126
+ const outcomes = [];
127
+ const parent = agent({
128
+ name: 'boss',
129
+ subAgents: [worker],
130
+ toolDefinitions: [
131
+ {
132
+ name: 'delegate',
133
+ description: 'delegate',
134
+ parameters: { type: 'object', properties: {} },
135
+ },
136
+ ],
137
+ toolHandlers: {
138
+ delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
139
+ const outcome = yield ctx.requestSubAgent('worker', { task: 'go' });
140
+ outcomes.push(outcome);
141
+ return 'delegated';
142
+ }),
143
+ },
144
+ });
145
+ yield makeDriver(parent, ruleProvider(2)).sendMessage('go');
146
+ assert.is(outcomes.length, 2, 'both delegations should have resolved');
147
+ // Each invocation did exactly one unit of work, so an invocation-scoped
148
+ // accumulator would report 1 from both. A config-scoped one reports 1 and 2.
149
+ // Deterministic despite the concurrency: the handler pushes and reads with no
150
+ // await between, so two invocations cannot interleave mid-body.
151
+ const seen = outcomes.map((o) => { var _a; return (_a = o === null || o === void 0 ? void 0 : o.result) === null || _a === void 0 ? void 0 : _a.seen; }).sort();
152
+ assert.equal(seen, [1, 2], "the second invocation sees the first's work — if this ever reports [1,1], closures " +
153
+ 'have become invocation-scoped and this test should be rewritten, not deleted');
154
+ }));
155
+ // ---------------------------------------------------------------------------
156
+ scope('closure state is config-scoped, so a later invocation resumes the earlier one', () => __awaiter(void 0, void 0, void 0, function* () {
157
+ // The sequential form of the same trap: nothing resets the closure between
158
+ // invocations, so a second delegation resumes where the first left off. This is
159
+ // the failure CG8 describes — a retried child whose cursor is already exhausted
160
+ // emits nothing and completes silently.
161
+ const collected = [];
162
+ const worker = agent({
163
+ name: 'worker',
164
+ toolDefinitions: [
165
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
166
+ ],
167
+ toolHandlers: {
168
+ work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
169
+ var _a;
170
+ collected.push('item');
171
+ (_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { seen: collected.length });
172
+ return 'worked';
173
+ }),
174
+ },
175
+ });
176
+ const outcomes = [];
177
+ const parent = agent({
178
+ name: 'boss',
179
+ subAgents: [worker],
180
+ toolDefinitions: [
181
+ { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
182
+ ],
183
+ toolHandlers: {
184
+ // Await them one after the other — no concurrency involved.
185
+ delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
186
+ outcomes.push(yield ctx.requestSubAgent('worker', { task: 'first' }));
187
+ outcomes.push(yield ctx.requestSubAgent('worker', { task: 'second' }));
188
+ return 'delegated';
189
+ }),
190
+ },
191
+ });
192
+ yield makeDriver(parent, ruleProvider(1)).sendMessage('go');
193
+ const seen = outcomes.map((o) => { var _a; return (_a = o === null || o === void 0 ? void 0 : o.result) === null || _a === void 0 ? void 0 : _a.seen; });
194
+ assert.equal(seen, [1, 2], "a second invocation does NOT start from zero — it resumes the first's accumulator");
195
+ }));
196
+ // ---------------------------------------------------------------------------
197
+ scope('a defineStatefulAgent config cannot be used as a sub-agent', () => __awaiter(void 0, void 0, void 0, function* () {
198
+ // `init` runs from `onActivate`, which only OrchestratingDriver fires on an
199
+ // agent switch. `invokeSubAgent` builds a raw ChatDriver and calls
200
+ // `applyAgent` directly, so a stateful sub-agent never initialises. This test
201
+ // pins that boundary — it is a documented-by-behaviour constraint, not a bug,
202
+ // and the failure is loud rather than silent.
203
+ const stateful = defineStatefulAgent({
204
+ name: 'worker',
205
+ description: 'stateful worker',
206
+ init: () => ({ count: 0 }),
207
+ systemPrompt: ({ state }) => `count=${state.count}`,
208
+ toolDefinitions: [
209
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
210
+ ],
211
+ toolHandlers: () => ({
212
+ work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
213
+ var _a;
214
+ (_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { ok: true });
215
+ return 'worked';
216
+ }),
217
+ }),
218
+ });
219
+ let outcome;
220
+ const parent = agent({
221
+ name: 'boss',
222
+ subAgents: [stateful],
223
+ toolDefinitions: [
224
+ { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
225
+ ],
226
+ toolHandlers: {
227
+ delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
228
+ outcome = yield ctx.requestSubAgent('worker', { task: 'go' });
229
+ return 'delegated';
230
+ }),
231
+ },
232
+ });
233
+ yield makeDriver(parent, ruleProvider(1)).sendMessage('go');
234
+ assert.ok(outcome, 'the delegation should have resolved rather than hanging');
235
+ assert.is(outcome.ok, false, `a stateful sub-agent never runs init (onActivate is orchestrator-only), so the ` +
236
+ `invocation must fail rather than silently run with undefined state — got ${JSON.stringify(outcome)}`);
237
+ }));
238
+ scope.run();
@@ -1 +1 @@
1
- {"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
1
+ {"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "15.9.0",
4
+ "version": "15.10.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -73,26 +73,26 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@genesislcap/foundation-testing": "15.9.0",
77
- "@genesislcap/genx": "15.9.0",
78
- "@genesislcap/rollup-builder": "15.9.0",
79
- "@genesislcap/ts-builder": "15.9.0",
80
- "@genesislcap/uvu-playwright-builder": "15.9.0",
81
- "@genesislcap/vite-builder": "15.9.0",
82
- "@genesislcap/webpack-builder": "15.9.0",
76
+ "@genesislcap/foundation-testing": "15.10.0",
77
+ "@genesislcap/genx": "15.10.0",
78
+ "@genesislcap/rollup-builder": "15.10.0",
79
+ "@genesislcap/ts-builder": "15.10.0",
80
+ "@genesislcap/uvu-playwright-builder": "15.10.0",
81
+ "@genesislcap/vite-builder": "15.10.0",
82
+ "@genesislcap/webpack-builder": "15.10.0",
83
83
  "@types/dompurify": "^3.0.5",
84
84
  "@types/marked": "^5.0.2",
85
85
  "esbuild": "0.25.12"
86
86
  },
87
87
  "dependencies": {
88
- "@genesislcap/foundation-ai": "15.9.0",
89
- "@genesislcap/foundation-logger": "15.9.0",
90
- "@genesislcap/foundation-notifications": "15.9.0",
91
- "@genesislcap/foundation-redux": "15.9.0",
92
- "@genesislcap/foundation-ui": "15.9.0",
93
- "@genesislcap/foundation-utils": "15.9.0",
94
- "@genesislcap/rapid-design-system": "15.9.0",
95
- "@genesislcap/web-core": "15.9.0",
88
+ "@genesislcap/foundation-ai": "15.10.0",
89
+ "@genesislcap/foundation-logger": "15.10.0",
90
+ "@genesislcap/foundation-notifications": "15.10.0",
91
+ "@genesislcap/foundation-redux": "15.10.0",
92
+ "@genesislcap/foundation-ui": "15.10.0",
93
+ "@genesislcap/foundation-utils": "15.10.0",
94
+ "@genesislcap/rapid-design-system": "15.10.0",
95
+ "@genesislcap/web-core": "15.10.0",
96
96
  "dompurify": "^3.3.1",
97
97
  "marked": "^17.0.3"
98
98
  },
@@ -105,5 +105,5 @@
105
105
  "access": "public"
106
106
  },
107
107
  "customElements": "dist/custom-elements.json",
108
- "gitHead": "640fddb70d899b01c9337563ae874ddb5226cbcd"
108
+ "gitHead": "83fc356fb2a585ea66bc15458dbd176c8247c68f"
109
109
  }
@@ -0,0 +1,285 @@
1
+ import type {
2
+ AIProvider,
3
+ AIProviderRegistry,
4
+ ChatMessage,
5
+ ChatRequestOptions,
6
+ } from '@genesislcap/foundation-ai';
7
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
8
+ import type { AgentConfig } from '../../config/config';
9
+ import { defineStatefulAgent } from '../../config/define-stateful-agent';
10
+ // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
11
+ // jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
12
+ import './align-event-globals';
13
+ import { ChatDriver } from './chat-driver';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Sub-agent state scoping.
17
+ //
18
+ // A sub-agent config lives in the parent's `subAgentsMap`, keyed by NAME — one
19
+ // config object, reused for every invocation. Anything a handler closes over is
20
+ // therefore scoped to the *config*, not to the invocation.
21
+ //
22
+ // These tests PIN that behaviour; they do not wish it away. Per-conversation
23
+ // registration is deliberate, so a closure in a sub-agent config will always be
24
+ // config-scoped — no planned platform change (F8 included) alters it. F8 adds a
25
+ // separate invocation-scoped store to write to instead; it does not re-scope
26
+ // closures. Consumers that accumulate across a child's turns must use that store
27
+ // (today: a WeakMap keyed on `promptCtx.signal`), never a closure.
28
+ //
29
+ // ⚠ TO WHOEVER IMPLEMENTS THE PER-ACTIVATION-STATE TICKET
30
+ // (`defineStatefulAgent`: per-activation state and headless execution)
31
+ //
32
+ // Its acceptance criterion 2 — "two concurrent activations of the same config
33
+ // hold independent state, with a test that fails against today's shared closure
34
+ // variable" — reads almost word-for-word like the first two tests below. It is
35
+ // not the same subject, and they must be treated differently:
36
+ //
37
+ // • Criterion 2 concerns FRAMEWORK-owned state: `defineStatefulAgent`'s single
38
+ // `let state` (define-stateful-agent.ts:374). That is the bug it fixes, and
39
+ // it needs a NEW test of its own.
40
+ // • The first two tests below concern CONSUMER-owned closures in a plain
41
+ // `AgentConfig`. The platform cannot re-scope a closure someone wrote in
42
+ // their own factory, so these stay true afterwards. Do not delete or invert
43
+ // them to make criterion 2 look satisfied.
44
+ // • The third test — "a defineStatefulAgent config cannot be used as a
45
+ // sub-agent" — IS invalidated by that ticket (its criteria 1 and 3), and
46
+ // must be rewritten when it lands, not before.
47
+ //
48
+ // The provider here answers by rule rather than by scripted queue: sub-agent
49
+ // invocations dispatched from one assistant turn run concurrently under
50
+ // `Promise.all`, so a shared FIFO queue would make ordering — and the test —
51
+ // nondeterministic.
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Answers by rule, not by scripted queue: sub-agent invocations dispatched from
56
+ * one assistant turn run concurrently under `Promise.all`, so a shared FIFO
57
+ * would make ordering — and the test — nondeterministic.
58
+ *
59
+ * `fanOut` controls how many `delegate` calls the parent emits in its single
60
+ * delegating turn. Parent turns after the first return plain text so the loop
61
+ * terminates.
62
+ */
63
+ const ruleProvider = (fanOut: number): AIProvider => {
64
+ let parentTurns = 0;
65
+ return {
66
+ chat: async (
67
+ _history: ChatMessage[],
68
+ _userMessage: string,
69
+ options?: ChatRequestOptions,
70
+ ): Promise<ChatMessage> => {
71
+ const tools = (options?.tools ?? []).map((t) => t.name);
72
+ // Child turn: it can only finish via its completion tool.
73
+ if (tools.includes('work')) {
74
+ return {
75
+ role: 'assistant',
76
+ content: '',
77
+ toolCalls: [{ id: 'w1', name: 'work', args: {} }],
78
+ };
79
+ }
80
+ // Parent turn — delegate once, then wrap up.
81
+ if (tools.includes('delegate') && parentTurns === 0) {
82
+ parentTurns += 1;
83
+ return {
84
+ role: 'assistant',
85
+ content: '',
86
+ toolCalls: Array.from({ length: fanOut }, (_, i) => ({
87
+ id: `d${i}`,
88
+ name: 'delegate',
89
+ args: {},
90
+ })),
91
+ };
92
+ }
93
+ return { role: 'assistant', content: 'done' };
94
+ },
95
+ };
96
+ };
97
+
98
+ const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
99
+ get: () => provider,
100
+ default: () => provider,
101
+ defaultName: () => 'test',
102
+ names: () => ['test'],
103
+ getStatus: async () => null,
104
+ listStatuses: async () => [],
105
+ });
106
+
107
+ const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
108
+ ({ description: 'test agent', ...overrides }) as AgentConfig;
109
+
110
+ const makeDriver = (config: AgentConfig, provider: AIProvider): ChatDriver => {
111
+ const driver = new ChatDriver(makeRegistry(provider), {
112
+ maxToolIterations: 20,
113
+ maxFoldOperations: 5,
114
+ sessionKey: '',
115
+ });
116
+ driver.applyAgent(config);
117
+ return driver;
118
+ };
119
+
120
+ const scope = createLogicSuite('ChatDriver sub-agent invocation scope');
121
+
122
+ // ---------------------------------------------------------------------------
123
+
124
+ scope(
125
+ 'closure state is config-scoped, so concurrent invocations contaminate each other',
126
+ async () => {
127
+ // The natural-looking way to accumulate across a sub-agent's turns — a closure
128
+ // in the config factory — is created once, with the config, and shared by every
129
+ // invocation. This is the trap; the assertion records it rather than wanting it.
130
+ const collected: string[] = [];
131
+
132
+ const worker = agent({
133
+ name: 'worker',
134
+ toolDefinitions: [
135
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
136
+ ],
137
+ toolHandlers: {
138
+ work: async (_args, ctx) => {
139
+ collected.push('item');
140
+ // Report what THIS invocation believes it has accumulated.
141
+ ctx.completeSubAgent?.({ seen: collected.length });
142
+ return 'worked';
143
+ },
144
+ },
145
+ });
146
+
147
+ const outcomes: unknown[] = [];
148
+ const parent = agent({
149
+ name: 'boss',
150
+ subAgents: [worker],
151
+ toolDefinitions: [
152
+ {
153
+ name: 'delegate',
154
+ description: 'delegate',
155
+ parameters: { type: 'object', properties: {} },
156
+ },
157
+ ],
158
+ toolHandlers: {
159
+ delegate: async (_args, ctx) => {
160
+ const outcome = await ctx.requestSubAgent!('worker', { task: 'go' });
161
+ outcomes.push(outcome);
162
+ return 'delegated';
163
+ },
164
+ },
165
+ });
166
+
167
+ await makeDriver(parent, ruleProvider(2)).sendMessage('go');
168
+
169
+ assert.is(outcomes.length, 2, 'both delegations should have resolved');
170
+
171
+ // Each invocation did exactly one unit of work, so an invocation-scoped
172
+ // accumulator would report 1 from both. A config-scoped one reports 1 and 2.
173
+ // Deterministic despite the concurrency: the handler pushes and reads with no
174
+ // await between, so two invocations cannot interleave mid-body.
175
+ const seen = outcomes.map((o: any) => o?.result?.seen).sort();
176
+ assert.equal(
177
+ seen,
178
+ [1, 2],
179
+ "the second invocation sees the first's work — if this ever reports [1,1], closures " +
180
+ 'have become invocation-scoped and this test should be rewritten, not deleted',
181
+ );
182
+ },
183
+ );
184
+
185
+ // ---------------------------------------------------------------------------
186
+
187
+ scope('closure state is config-scoped, so a later invocation resumes the earlier one', async () => {
188
+ // The sequential form of the same trap: nothing resets the closure between
189
+ // invocations, so a second delegation resumes where the first left off. This is
190
+ // the failure CG8 describes — a retried child whose cursor is already exhausted
191
+ // emits nothing and completes silently.
192
+ const collected: string[] = [];
193
+
194
+ const worker = agent({
195
+ name: 'worker',
196
+ toolDefinitions: [
197
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
198
+ ],
199
+ toolHandlers: {
200
+ work: async (_args, ctx) => {
201
+ collected.push('item');
202
+ ctx.completeSubAgent?.({ seen: collected.length });
203
+ return 'worked';
204
+ },
205
+ },
206
+ });
207
+
208
+ const outcomes: unknown[] = [];
209
+ const parent = agent({
210
+ name: 'boss',
211
+ subAgents: [worker],
212
+ toolDefinitions: [
213
+ { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
214
+ ],
215
+ toolHandlers: {
216
+ // Await them one after the other — no concurrency involved.
217
+ delegate: async (_args, ctx) => {
218
+ outcomes.push(await ctx.requestSubAgent!('worker', { task: 'first' }));
219
+ outcomes.push(await ctx.requestSubAgent!('worker', { task: 'second' }));
220
+ return 'delegated';
221
+ },
222
+ },
223
+ });
224
+
225
+ await makeDriver(parent, ruleProvider(1)).sendMessage('go');
226
+
227
+ const seen = (outcomes as any[]).map((o) => o?.result?.seen);
228
+ assert.equal(
229
+ seen,
230
+ [1, 2],
231
+ "a second invocation does NOT start from zero — it resumes the first's accumulator",
232
+ );
233
+ });
234
+
235
+ // ---------------------------------------------------------------------------
236
+
237
+ scope('a defineStatefulAgent config cannot be used as a sub-agent', async () => {
238
+ // `init` runs from `onActivate`, which only OrchestratingDriver fires on an
239
+ // agent switch. `invokeSubAgent` builds a raw ChatDriver and calls
240
+ // `applyAgent` directly, so a stateful sub-agent never initialises. This test
241
+ // pins that boundary — it is a documented-by-behaviour constraint, not a bug,
242
+ // and the failure is loud rather than silent.
243
+ const stateful = defineStatefulAgent<{ count: number }>({
244
+ name: 'worker',
245
+ description: 'stateful worker',
246
+ init: () => ({ count: 0 }),
247
+ systemPrompt: ({ state }) => `count=${state.count}`,
248
+ toolDefinitions: [
249
+ { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
250
+ ],
251
+ toolHandlers: () => ({
252
+ work: async (_args, ctx) => {
253
+ ctx.completeSubAgent?.({ ok: true });
254
+ return 'worked';
255
+ },
256
+ }),
257
+ });
258
+
259
+ let outcome: any;
260
+ const parent = agent({
261
+ name: 'boss',
262
+ subAgents: [stateful],
263
+ toolDefinitions: [
264
+ { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
265
+ ],
266
+ toolHandlers: {
267
+ delegate: async (_args, ctx) => {
268
+ outcome = await ctx.requestSubAgent!('worker', { task: 'go' });
269
+ return 'delegated';
270
+ },
271
+ },
272
+ });
273
+
274
+ await makeDriver(parent, ruleProvider(1)).sendMessage('go');
275
+
276
+ assert.ok(outcome, 'the delegation should have resolved rather than hanging');
277
+ assert.is(
278
+ outcome.ok,
279
+ false,
280
+ `a stateful sub-agent never runs init (onActivate is orchestrator-only), so the ` +
281
+ `invocation must fail rather than silently run with undefined state — got ${JSON.stringify(outcome)}`,
282
+ );
283
+ });
284
+
285
+ scope.run();