@genesislcap/ai-assistant 14.432.2 → 14.433.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.
- package/api-extractor.json +8 -1
- package/dist/ai-assistant.api.json +1216 -141
- package/dist/ai-assistant.d.ts +216 -15
- package/dist/dts/components/agent-picker/agent-picker.d.ts +69 -0
- package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts +2 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts +5 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/index.d.ts +2 -0
- package/dist/dts/components/agent-picker/index.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +21 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +22 -12
- package/dist/dts/config/config.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 +72 -4
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +1 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +39 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +6 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/animated-panel-toggle.d.ts +26 -0
- package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -0
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/esm/components/agent-picker/agent-picker.js +157 -0
- package/dist/esm/components/agent-picker/agent-picker.styles.js +73 -0
- package/dist/esm/components/agent-picker/agent-picker.template.js +72 -0
- package/dist/esm/components/agent-picker/index.js +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +48 -6
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +43 -6
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +215 -21
- package/dist/esm/main/main.styles.js +59 -0
- package/dist/esm/main/main.template.js +66 -12
- package/dist/esm/state/ai-assistant-slice.js +15 -0
- package/dist/esm/utils/animated-panel-toggle.js +62 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/gemini-empty-response.md +110 -0
- package/package.json +16 -16
- package/src/components/agent-picker/agent-picker.styles.ts +74 -0
- package/src/components/agent-picker/agent-picker.template.ts +88 -0
- package/src/components/agent-picker/agent-picker.ts +148 -0
- package/src/components/agent-picker/index.ts +1 -0
- package/src/components/chat-driver/chat-driver.ts +65 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +45 -6
- package/src/config/config.ts +28 -11
- package/src/index.ts +1 -0
- package/src/main/main.styles.ts +59 -0
- package/src/main/main.template.ts +79 -13
- package/src/main/main.ts +220 -19
- package/src/main/main.types.ts +2 -0
- package/src/state/ai-assistant-slice.ts +51 -1
- package/src/utils/animated-panel-toggle.ts +62 -0
- package/src/utils/index.ts +1 -0
|
@@ -189,6 +189,68 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
189
189
|
</div>
|
|
190
190
|
</div>
|
|
191
191
|
`)}
|
|
192
|
+
`;
|
|
193
|
+
// ── Agent picker ────────────────────────────────────────────────────────────
|
|
194
|
+
const agentToggleButtonTemplate = html `
|
|
195
|
+
<${buttonTag}
|
|
196
|
+
class="agent-toggle-button"
|
|
197
|
+
part="agent-toggle-button"
|
|
198
|
+
appearance="stealth"
|
|
199
|
+
title=${(x) => {
|
|
200
|
+
var _a;
|
|
201
|
+
return x.agentPickerOpen
|
|
202
|
+
? 'Close agent picker'
|
|
203
|
+
: x.pinnedAgentName !== null
|
|
204
|
+
? x.pinnedAgentHint
|
|
205
|
+
? `${x.pinnedAgentName} — ${x.pinnedAgentHint}`
|
|
206
|
+
: ((_a = x.pinnedAgentName) !== null && _a !== void 0 ? _a : '')
|
|
207
|
+
: 'Attempts to route messages to the correct available agent. Click to manually pin an agent.';
|
|
208
|
+
}}
|
|
209
|
+
?disabled=${(x) => x.state === 'loading'}
|
|
210
|
+
@click=${(x) => x.toggleAgentPicker()}
|
|
211
|
+
>
|
|
212
|
+
${when((x) => x.agentPickerOpen, html `<${iconTag} name="chevron-down"></${iconTag}>`)}
|
|
213
|
+
${when((x) => !x.agentPickerOpen && x.pinnedAgentName !== null, html `
|
|
214
|
+
<${iconTag}
|
|
215
|
+
name="thumbtack"
|
|
216
|
+
style="${(x) => (x.pinnedAgentColour ? `color: ${x.pinnedAgentColour}` : '')}"
|
|
217
|
+
></${iconTag}>
|
|
218
|
+
`)}
|
|
219
|
+
${when((x) => !x.agentPickerOpen && x.pinnedAgentName === null, html `
|
|
220
|
+
<span class="agent-toggle-label">Auto</span>
|
|
221
|
+
`)}
|
|
222
|
+
</${buttonTag}>
|
|
223
|
+
`;
|
|
224
|
+
const attachButtonTemplate = html `
|
|
225
|
+
<${buttonTag}
|
|
226
|
+
class="attach-button"
|
|
227
|
+
part="attach-button"
|
|
228
|
+
appearance="stealth"
|
|
229
|
+
title=${(x) => { var _a; return `Attach file (${(_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles})`; }}
|
|
230
|
+
?disabled=${(x) => x.state === 'loading'}
|
|
231
|
+
@click=${(x) => x.triggerFileInput()}
|
|
232
|
+
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
233
|
+
`;
|
|
234
|
+
const inputLeftControlsTemplate = html `
|
|
235
|
+
<div class="input-left-controls" part="input-left-controls">
|
|
236
|
+
${when((x) => x.agentPickerEnabled, agentToggleButtonTemplate)}
|
|
237
|
+
${when((x) => { var _a; return !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, attachButtonTemplate)}
|
|
238
|
+
</div>
|
|
239
|
+
`;
|
|
240
|
+
const agentPickerPanelTemplate = html `
|
|
241
|
+
<div class="agent-picker-panel" part="agent-picker-panel">
|
|
242
|
+
<foundation-ai-agent-picker
|
|
243
|
+
part="agent-picker"
|
|
244
|
+
:designSystemPrefix="${(x) => x.designSystemPrefix}"
|
|
245
|
+
:mode="${(x) => x.agentPicker}"
|
|
246
|
+
:agents="${(x) => { var _a; return (_a = x.agents) !== null && _a !== void 0 ? _a : []; }}"
|
|
247
|
+
:pinnedAgentName="${(x) => x.pinnedAgentName}"
|
|
248
|
+
@agent-pinned="${(x, c) => {
|
|
249
|
+
x.pinnedAgentName = c.event.detail;
|
|
250
|
+
x.toggleAgentPicker();
|
|
251
|
+
}}"
|
|
252
|
+
></foundation-ai-agent-picker>
|
|
253
|
+
</div>
|
|
192
254
|
`;
|
|
193
255
|
// ── Root template ───────────────────────────────────────────────────────────
|
|
194
256
|
return html `
|
|
@@ -382,23 +444,15 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
382
444
|
@suggestion-clicked="${(x, c) => x.handleSuggestionClick(c.event.detail)}"
|
|
383
445
|
></chat-suggestions>
|
|
384
446
|
`)}
|
|
385
|
-
${when((x) =>
|
|
447
|
+
${when((x) => x.agentPickerEnabled && x.agentPickerOpen, agentPickerPanelTemplate)}
|
|
448
|
+
${when((x) => !(x.state === 'loading' && x.effectiveChatInputDuringExecution === 'hidden'), html `
|
|
386
449
|
<div class="input-row" part="input-row">
|
|
387
|
-
${when((x) => { var _a; return !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); },
|
|
388
|
-
<${buttonTag}
|
|
389
|
-
class="attach-button"
|
|
390
|
-
part="attach-button"
|
|
391
|
-
appearance="stealth"
|
|
392
|
-
title=${(x) => { var _a; return `Attach file (${(_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles})`; }}
|
|
393
|
-
?disabled=${(x) => x.state === 'loading'}
|
|
394
|
-
@click=${(x) => x.triggerFileInput()}
|
|
395
|
-
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
396
|
-
`)}
|
|
450
|
+
${when((x) => { var _a; return x.agentPickerEnabled || !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, inputLeftControlsTemplate)}
|
|
397
451
|
<${textareaTag}
|
|
398
452
|
${ref('chatInputEl')}
|
|
399
453
|
class="chat-input"
|
|
400
454
|
part="input"
|
|
401
|
-
placeholder=${(x) => x.
|
|
455
|
+
placeholder=${(x) => x.effectivePlaceholder}
|
|
402
456
|
:value=${(x) => x.inputValue}
|
|
403
457
|
?disabled=${(x) => x.state === 'loading'}
|
|
404
458
|
@input=${(x, c) => (x.inputValue = c.event.target.value)}
|
|
@@ -10,9 +10,12 @@ export const defaultSessionState = {
|
|
|
10
10
|
contextTokens: undefined,
|
|
11
11
|
contextLimit: undefined,
|
|
12
12
|
activeAgent: undefined,
|
|
13
|
+
pinnedAgentName: null,
|
|
14
|
+
agentPickerOpen: false,
|
|
13
15
|
inputValue: '',
|
|
14
16
|
liveSubAgentTrace: [],
|
|
15
17
|
liveSubAgentName: null,
|
|
18
|
+
subAgentInputOverrides: [],
|
|
16
19
|
};
|
|
17
20
|
export const aiAssistantSlice = createSlice({
|
|
18
21
|
name: 'aiAssistant',
|
|
@@ -48,6 +51,12 @@ export const aiAssistantSlice = createSlice({
|
|
|
48
51
|
setActiveAgent(state, action) {
|
|
49
52
|
state.activeAgent = action.payload;
|
|
50
53
|
},
|
|
54
|
+
setPinnedAgentName(state, action) {
|
|
55
|
+
state.pinnedAgentName = action.payload;
|
|
56
|
+
},
|
|
57
|
+
setAgentPickerOpen(state, action) {
|
|
58
|
+
state.agentPickerOpen = action.payload;
|
|
59
|
+
},
|
|
51
60
|
setInputValue(state, action) {
|
|
52
61
|
state.inputValue = action.payload;
|
|
53
62
|
},
|
|
@@ -57,6 +66,12 @@ export const aiAssistantSlice = createSlice({
|
|
|
57
66
|
setLiveSubAgentName(state, action) {
|
|
58
67
|
state.liveSubAgentName = action.payload;
|
|
59
68
|
},
|
|
69
|
+
addSubAgentInputOverride(state, action) {
|
|
70
|
+
state.subAgentInputOverrides.push(action.payload);
|
|
71
|
+
},
|
|
72
|
+
removeSubAgentInputOverride(state, action) {
|
|
73
|
+
state.subAgentInputOverrides = state.subAgentInputOverrides.filter((o) => o.id !== action.payload.id);
|
|
74
|
+
},
|
|
60
75
|
},
|
|
61
76
|
selectors: {},
|
|
62
77
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the open/close lifecycle of a slide-out panel inside a component's
|
|
3
|
+
* shadow root, with a tracked close timer instead of an `animationend`
|
|
4
|
+
* listener so the panel cannot wedge open if it's unmounted mid-animation
|
|
5
|
+
* (pop-in/pop-out, dock change, host disconnect).
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export class AnimatedPanelToggle {
|
|
10
|
+
constructor(host, selector, closeDurationMs, getOpen, setOpen) {
|
|
11
|
+
this.host = host;
|
|
12
|
+
this.selector = selector;
|
|
13
|
+
this.closeDurationMs = closeDurationMs;
|
|
14
|
+
this.getOpen = getOpen;
|
|
15
|
+
this.setOpen = setOpen;
|
|
16
|
+
}
|
|
17
|
+
queryPanel() {
|
|
18
|
+
var _a;
|
|
19
|
+
return (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(this.selector);
|
|
20
|
+
}
|
|
21
|
+
toggle() {
|
|
22
|
+
var _a;
|
|
23
|
+
if (this.getOpen()) {
|
|
24
|
+
// Capture the panel reference now so the timer callback strips
|
|
25
|
+
// `closing` from the same node, regardless of any later remounts.
|
|
26
|
+
// Leftover `closing` is what breaks the next open: with
|
|
27
|
+
// `animation-fill-mode: forwards` the slide-out keyframe pins the panel
|
|
28
|
+
// to opacity 0, so a remount looks like a no-op.
|
|
29
|
+
const panel = this.queryPanel();
|
|
30
|
+
panel === null || panel === void 0 ? void 0 : panel.classList.add('closing');
|
|
31
|
+
if (this.timer != null)
|
|
32
|
+
clearTimeout(this.timer);
|
|
33
|
+
this.timer = setTimeout(() => {
|
|
34
|
+
this.timer = undefined;
|
|
35
|
+
panel === null || panel === void 0 ? void 0 : panel.classList.remove('closing');
|
|
36
|
+
this.setOpen(false);
|
|
37
|
+
}, this.closeDurationMs);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Re-opened mid-close: cancel the pending flip and strip the closing
|
|
41
|
+
// class so the panel snaps back to the open state.
|
|
42
|
+
if (this.timer != null) {
|
|
43
|
+
clearTimeout(this.timer);
|
|
44
|
+
this.timer = undefined;
|
|
45
|
+
(_a = this.queryPanel()) === null || _a === void 0 ? void 0 : _a.classList.remove('closing');
|
|
46
|
+
}
|
|
47
|
+
this.setOpen(true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Synchronously finalise an in-flight close — call from
|
|
52
|
+
* `disconnectedCallback` so the closed state lands in the store before any
|
|
53
|
+
* session ref is cleared.
|
|
54
|
+
*/
|
|
55
|
+
finalize() {
|
|
56
|
+
if (this.timer == null)
|
|
57
|
+
return;
|
|
58
|
+
clearTimeout(this.timer);
|
|
59
|
+
this.timer = undefined;
|
|
60
|
+
this.setOpen(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.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/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.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.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
|
|
1
|
+
{"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.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/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.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.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Gemini 2.5 empty-response quirk — follow-up
|
|
2
|
+
|
|
3
|
+
> **Status:** known issue, mitigated at the consumer level (showcase). Pending a
|
|
4
|
+
> proper transport-level fix in a follow-up PR.
|
|
5
|
+
|
|
6
|
+
## Symptom
|
|
7
|
+
|
|
8
|
+
On the first `generateContent` call against `gemini-2.5-flash` (and reportedly
|
|
9
|
+
also `gemini-2.5-pro`), the API occasionally returns a 200 response with **no
|
|
10
|
+
output** when:
|
|
11
|
+
|
|
12
|
+
- `contents` contains only user-role messages — i.e. no prior model turn, AND
|
|
13
|
+
- the request has a non-trivial `tools` array and/or a long `systemInstruction`.
|
|
14
|
+
|
|
15
|
+
The shape of the broken response:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"candidates": [{
|
|
20
|
+
"content": { "role": "model" },
|
|
21
|
+
"finishReason": "STOP",
|
|
22
|
+
"index": 0
|
|
23
|
+
}],
|
|
24
|
+
"usageMetadata": {
|
|
25
|
+
"promptTokenCount": 2373,
|
|
26
|
+
"totalTokenCount": 2373
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Note `totalTokenCount === promptTokenCount` — Gemini emitted **zero** output
|
|
32
|
+
tokens. `finishReason` is `STOP` (normal termination), not a safety block. The
|
|
33
|
+
`candidatesTokenCount` field is missing entirely.
|
|
34
|
+
|
|
35
|
+
In `ChatDriver` this hits the empty-response retry path. Three retries later we
|
|
36
|
+
give up and surface "Remote agent returned no response." The second user turn
|
|
37
|
+
always succeeds because there is now a prior model turn in history (even our
|
|
38
|
+
own "Remote agent returned no response" reply is enough of an anchor).
|
|
39
|
+
|
|
40
|
+
## Known mitigations
|
|
41
|
+
|
|
42
|
+
Only one workaround has been reported as effective: **inject a prior model turn
|
|
43
|
+
into the request**. Confirmed ineffective in community reports:
|
|
44
|
+
|
|
45
|
+
- Lowering `temperature`.
|
|
46
|
+
- Setting `thinkingConfig: { thinkingBudget: 0 }`.
|
|
47
|
+
|
|
48
|
+
References:
|
|
49
|
+
|
|
50
|
+
- <https://discuss.ai.google.dev/t/finishreason-stop-but-parts-is-missing-inside-candidate/99331>
|
|
51
|
+
- <https://github.com/livekit/agents/issues/4706>
|
|
52
|
+
- <https://github.com/BerriAI/litellm/issues/24442>
|
|
53
|
+
|
|
54
|
+
## Current mitigation (showcase only)
|
|
55
|
+
|
|
56
|
+
The showcase `genesis-assistant` configures a minimal `primerHistory` on the
|
|
57
|
+
trades agent — a benign `[user: "Hi", assistant: "Hi — how can I help…"]`
|
|
58
|
+
exchange. That single fake model turn is enough to satisfy Gemini.
|
|
59
|
+
|
|
60
|
+
We deliberately keep the primer **innocuous**: any text resembling a system
|
|
61
|
+
update or directive reinforcement (e.g. `[SYSTEM UPDATE]: Your core behavioral
|
|
62
|
+
directives are…`) is caught by Gemini's prompt-injection safety training and
|
|
63
|
+
rejected. Plain greeting language is fine.
|
|
64
|
+
|
|
65
|
+
## Proposed follow-up: fix in `gemini-transport`
|
|
66
|
+
|
|
67
|
+
The right place to absorb this is `packages/foundation/foundation-ai/src/transports/gemini-transport.ts`
|
|
68
|
+
— specifically `toGeminiContents`. If the assembled `contents` array contains
|
|
69
|
+
**only** user-role entries (no model turn at all) at send time, prepend a
|
|
70
|
+
synthetic anchor pair:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
contents.unshift(
|
|
74
|
+
{ role: 'user', parts: [{ text: 'Hi' }] },
|
|
75
|
+
{ role: 'model', parts: [{ text: 'Hi! How can I help today?' }] },
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Properties of this approach:
|
|
80
|
+
|
|
81
|
+
- **Transport-scoped** — only applies when sending to Gemini; other transports
|
|
82
|
+
(Claude, OpenAI, etc.) are unaffected.
|
|
83
|
+
- **Transparent to the framework** — `ChatDriver` and the rest of the UI never
|
|
84
|
+
see the synthetic turns. They are not stored in `this.history`, not rendered,
|
|
85
|
+
not surfaced to consumers.
|
|
86
|
+
- **Self-cleaning** — once the conversation has any real model turn, the
|
|
87
|
+
injection no-ops.
|
|
88
|
+
|
|
89
|
+
### Why we did **not** ship this now
|
|
90
|
+
|
|
91
|
+
A consumer-level fix is more visible and easier to reason about. Hiding the
|
|
92
|
+
workaround inside the transport risks masking related issues — e.g. if the
|
|
93
|
+
shape of system prompts or tool sets ever evolves such that Gemini fails
|
|
94
|
+
elsewhere, an always-on anchor would silently hide the regression. Surfacing
|
|
95
|
+
the workaround as an explicit `primerHistory` on each agent gives consumers a
|
|
96
|
+
chance to notice and remove it when it's no longer needed.
|
|
97
|
+
|
|
98
|
+
The transport-level fix is worth doing **once** the quirk is confirmed against
|
|
99
|
+
Google's official guidance or a forum/issue closeout — at which point we can
|
|
100
|
+
also remove the per-agent primers.
|
|
101
|
+
|
|
102
|
+
## Acceptance criteria for the follow-up PR
|
|
103
|
+
|
|
104
|
+
- [ ] Add the anchor-injection logic in `gemini-transport.ts:toGeminiContents`.
|
|
105
|
+
- [ ] Unit test: contents containing only user messages get the anchor prepended.
|
|
106
|
+
- [ ] Unit test: contents containing any model message do **not** get the anchor.
|
|
107
|
+
- [ ] Remove the showcase `tradesPrimerHistory` and confirm "Book a trade" works
|
|
108
|
+
on the first turn.
|
|
109
|
+
- [ ] Add an integration note / TODO so the workaround can be ripped out when
|
|
110
|
+
Google ships a fix.
|
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": "14.
|
|
4
|
+
"version": "14.433.0",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -64,24 +64,24 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@genesislcap/foundation-testing": "14.
|
|
68
|
-
"@genesislcap/genx": "14.
|
|
69
|
-
"@genesislcap/rollup-builder": "14.
|
|
70
|
-
"@genesislcap/ts-builder": "14.
|
|
71
|
-
"@genesislcap/uvu-playwright-builder": "14.
|
|
72
|
-
"@genesislcap/vite-builder": "14.
|
|
73
|
-
"@genesislcap/webpack-builder": "14.
|
|
67
|
+
"@genesislcap/foundation-testing": "14.433.0",
|
|
68
|
+
"@genesislcap/genx": "14.433.0",
|
|
69
|
+
"@genesislcap/rollup-builder": "14.433.0",
|
|
70
|
+
"@genesislcap/ts-builder": "14.433.0",
|
|
71
|
+
"@genesislcap/uvu-playwright-builder": "14.433.0",
|
|
72
|
+
"@genesislcap/vite-builder": "14.433.0",
|
|
73
|
+
"@genesislcap/webpack-builder": "14.433.0",
|
|
74
74
|
"@types/dompurify": "^3.0.5",
|
|
75
75
|
"@types/marked": "^5.0.2"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@genesislcap/foundation-ai": "14.
|
|
79
|
-
"@genesislcap/foundation-logger": "14.
|
|
80
|
-
"@genesislcap/foundation-redux": "14.
|
|
81
|
-
"@genesislcap/foundation-ui": "14.
|
|
82
|
-
"@genesislcap/foundation-utils": "14.
|
|
83
|
-
"@genesislcap/rapid-design-system": "14.
|
|
84
|
-
"@genesislcap/web-core": "14.
|
|
78
|
+
"@genesislcap/foundation-ai": "14.433.0",
|
|
79
|
+
"@genesislcap/foundation-logger": "14.433.0",
|
|
80
|
+
"@genesislcap/foundation-redux": "14.433.0",
|
|
81
|
+
"@genesislcap/foundation-ui": "14.433.0",
|
|
82
|
+
"@genesislcap/foundation-utils": "14.433.0",
|
|
83
|
+
"@genesislcap/rapid-design-system": "14.433.0",
|
|
84
|
+
"@genesislcap/web-core": "14.433.0",
|
|
85
85
|
"dompurify": "^3.3.1",
|
|
86
86
|
"marked": "^17.0.3"
|
|
87
87
|
},
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"publishConfig": {
|
|
94
94
|
"access": "public"
|
|
95
95
|
},
|
|
96
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "0ddedcdfa75d7b64b9c7b1397dcfa3008e23cd04"
|
|
97
97
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { css } from '@genesislcap/web-core';
|
|
2
|
+
|
|
3
|
+
export const styles = css`
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
:host([hidden]) {
|
|
9
|
+
display: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.picker {
|
|
13
|
+
position: relative;
|
|
14
|
+
padding: 6px calc(var(--design-unit) * 3px);
|
|
15
|
+
background-color: var(--neutral-layer-2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Clipping wrapper. The inner segmented-control is naturally inline-flex
|
|
19
|
+
and auto-sizes to its segments, so it would overflow the panel without
|
|
20
|
+
ever reporting scrollWidth > clientWidth. By giving the wrapper a
|
|
21
|
+
constrained width (flex+min-width:0) and overflow:hidden, the wrapper's
|
|
22
|
+
scrollWidth reflects the segments' natural width while clientWidth is
|
|
23
|
+
the available width — that's what we measure to decide on the fallback. */
|
|
24
|
+
.segmented-row {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex: 1 1 auto;
|
|
27
|
+
min-width: 0;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* When the fallback select is showing, take the segmented row out of layout
|
|
32
|
+
but keep it in the DOM so the ResizeObserver can re-measure when the
|
|
33
|
+
container grows back and we can swap back to segmented-control. */
|
|
34
|
+
.picker[data-effective='select'] .segmented-row {
|
|
35
|
+
position: absolute;
|
|
36
|
+
inset: 6px calc(var(--design-unit) * 3px);
|
|
37
|
+
visibility: hidden;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.segmented-control {
|
|
42
|
+
flex: 0 0 auto;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.select-fallback {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: 6px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.select-label {
|
|
52
|
+
font-family: var(--body-font);
|
|
53
|
+
font-size: 0.8em;
|
|
54
|
+
color: var(--neutral-foreground-hint, var(--neutral-foreground-rest));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.select-control {
|
|
58
|
+
flex: 1 1 auto;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
padding: 4px 8px;
|
|
61
|
+
background: var(--neutral-layer-3);
|
|
62
|
+
color: var(--neutral-foreground-rest);
|
|
63
|
+
border: 1px solid var(--neutral-stroke-rest);
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
font-family: var(--body-font);
|
|
66
|
+
font-size: 0.85em;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.select-control:focus-visible {
|
|
71
|
+
outline: 2px solid var(--accent-fill-rest);
|
|
72
|
+
outline-offset: 1px;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { html, ref, repeat, ViewTemplate, when } from '@genesislcap/web-core';
|
|
2
|
+
import type { AgentConfig } from '../../config/config';
|
|
3
|
+
import { AGENT_PICKER_AUTO_VALUE, type AgentPicker } from './agent-picker';
|
|
4
|
+
|
|
5
|
+
const AUTO_LABEL = 'Auto';
|
|
6
|
+
const AUTO_HINT = 'Let the orchestrator pick the right agent for each message.';
|
|
7
|
+
|
|
8
|
+
/** @internal */
|
|
9
|
+
export const AgentPickerTemplate = (designSystemPrefix: string): ViewTemplate<AgentPicker> => {
|
|
10
|
+
const segmentedControlTag = `${designSystemPrefix}-segmented-control`;
|
|
11
|
+
const segmentedItemTag = `${designSystemPrefix}-segmented-item`;
|
|
12
|
+
|
|
13
|
+
return html<AgentPicker>`
|
|
14
|
+
<div class="picker" data-effective="${(x) => x.effectiveMode}" part="picker">
|
|
15
|
+
${when(
|
|
16
|
+
(x) => x.mode === 'segmented-control',
|
|
17
|
+
html<AgentPicker>`
|
|
18
|
+
<div class="segmented-row" ${ref('segmentedRowEl')}>
|
|
19
|
+
<${segmentedControlTag}
|
|
20
|
+
class="segmented-control"
|
|
21
|
+
appearance="primary"
|
|
22
|
+
aria-label="Active agent"
|
|
23
|
+
:value="${(x) => x.currentValue}"
|
|
24
|
+
@change="${(x, c) => {
|
|
25
|
+
const value = (c.event.currentTarget as HTMLElement & { value?: string }).value;
|
|
26
|
+
// The segmented-control re-emits 'change' on its initial value
|
|
27
|
+
// sync; ignore it so we don't close the panel on open.
|
|
28
|
+
if (typeof value !== 'string' || value === x.currentValue) return;
|
|
29
|
+
x.selectByValue(value);
|
|
30
|
+
}}"
|
|
31
|
+
>
|
|
32
|
+
<${segmentedItemTag}
|
|
33
|
+
value="${AGENT_PICKER_AUTO_VALUE}"
|
|
34
|
+
title="${AUTO_HINT}"
|
|
35
|
+
>${AUTO_LABEL}</${segmentedItemTag}>
|
|
36
|
+
${repeat(
|
|
37
|
+
(x) => x.selectableAgents,
|
|
38
|
+
html<AgentConfig, AgentPicker>`
|
|
39
|
+
<${segmentedItemTag}
|
|
40
|
+
value="${(a) => a.name}"
|
|
41
|
+
title="${(a) => a.manualSelection?.hint ?? ''}"
|
|
42
|
+
>${(a) => a.name}</${segmentedItemTag}>
|
|
43
|
+
`,
|
|
44
|
+
)}
|
|
45
|
+
</${segmentedControlTag}>
|
|
46
|
+
</div>
|
|
47
|
+
`,
|
|
48
|
+
)}
|
|
49
|
+
${when(
|
|
50
|
+
(x) => x.effectiveMode === 'select',
|
|
51
|
+
html<AgentPicker>`
|
|
52
|
+
<div class="select-fallback">
|
|
53
|
+
<span class="select-label">Agent:</span>
|
|
54
|
+
<select
|
|
55
|
+
class="select-control"
|
|
56
|
+
aria-label="Active agent"
|
|
57
|
+
@change="${(x, c) => {
|
|
58
|
+
const value = (c.event.target as HTMLSelectElement).value;
|
|
59
|
+
x.selectByValue(value);
|
|
60
|
+
}}"
|
|
61
|
+
>
|
|
62
|
+
<option
|
|
63
|
+
value="${AGENT_PICKER_AUTO_VALUE}"
|
|
64
|
+
title="${AUTO_HINT}"
|
|
65
|
+
?selected="${(x) => x.pinnedAgentName === null}"
|
|
66
|
+
>
|
|
67
|
+
${AUTO_LABEL}
|
|
68
|
+
</option>
|
|
69
|
+
${repeat(
|
|
70
|
+
(x) => x.selectableAgents,
|
|
71
|
+
html<AgentConfig, AgentPicker>`
|
|
72
|
+
<option
|
|
73
|
+
value="${(a) => a.name}"
|
|
74
|
+
title="${(a) => a.manualSelection?.hint ?? ''}"
|
|
75
|
+
?selected="${(a, c) => c.parent.pinnedAgentName === a.name}"
|
|
76
|
+
>
|
|
77
|
+
${(a) =>
|
|
78
|
+
a.manualSelection?.hint ? `${a.name} — ${a.manualSelection.hint}` : a.name}
|
|
79
|
+
</option>
|
|
80
|
+
`,
|
|
81
|
+
)}
|
|
82
|
+
</select>
|
|
83
|
+
</div>
|
|
84
|
+
`,
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
`;
|
|
88
|
+
};
|