@genesislcap/ai-assistant 15.60.0 → 15.62.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/dist/ai-assistant.api.json +18231 -11171
- package/dist/ai-assistant.d.ts +1235 -9
- package/dist/chat-driver.cjs +658 -50
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +657 -50
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +4419 -377
- package/dist/dts/autopilot/autopilot-driver.d.ts +278 -0
- package/dist/dts/autopilot/autopilot-driver.d.ts.map +1 -0
- package/dist/dts/autopilot/autopilot-session.d.ts +163 -0
- package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -0
- package/dist/dts/autopilot/autopilot-tools.d.ts +15 -0
- package/dist/dts/autopilot/autopilot-tools.d.ts.map +1 -0
- package/dist/dts/autopilot/decision-message.d.ts +62 -0
- package/dist/dts/autopilot/decision-message.d.ts.map +1 -0
- package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts +69 -0
- package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts.map +1 -0
- package/dist/dts/autopilot/hand-back/hand-back.d.ts +22 -0
- package/dist/dts/autopilot/hand-back/hand-back.d.ts.map +1 -0
- package/dist/dts/autopilot/index.d.ts +4 -0
- package/dist/dts/autopilot/index.d.ts.map +1 -0
- package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
- package/dist/dts/channel/ai-activity-channel.d.ts +17 -0
- package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
- package/dist/dts/channel/autopilot-needs-user.d.ts +8 -0
- package/dist/dts/channel/autopilot-needs-user.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.constants.d.ts +10 -0
- package/dist/dts/components/agent-picker/agent-picker.constants.d.ts.map +1 -1
- package/dist/dts/components/agent-picker/agent-picker.d.ts +4 -0
- package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -1
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +34 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +3 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
- package/dist/dts/components/chat-bubble/chat-bubble.d.ts +21 -0
- package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
- package/dist/dts/components/chat-bubble/chat-bubble.styles.d.ts.map +1 -1
- package/dist/dts/components/chat-bubble/chat-bubble.template.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +5 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +27 -3
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +10 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +183 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +31 -1
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/index.d.ts +7 -3
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +128 -5
- 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 +2 -2
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/react.d.ts +10 -0
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/decision-surface.d.ts +46 -0
- package/dist/dts/state/decision-surface.d.ts.map +1 -0
- package/dist/dts/state/interaction-context.d.ts +21 -1
- package/dist/dts/state/interaction-context.d.ts.map +1 -1
- package/dist/dts/state/interaction-registry.d.ts +63 -0
- package/dist/dts/state/interaction-registry.d.ts.map +1 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts +21 -0
- package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
- package/dist/dts/state/run-steps.d.ts +66 -0
- package/dist/dts/state/run-steps.d.ts.map +1 -0
- package/dist/dts/types/ai-chat-widget.d.ts +7 -1
- package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
- package/dist/dts/types/interaction-context.d.ts +66 -1
- package/dist/dts/types/interaction-context.d.ts.map +1 -1
- package/dist/dts/types/interaction-interceptor.d.ts +60 -2
- package/dist/dts/types/interaction-interceptor.d.ts.map +1 -1
- package/dist/dts/types/interaction-surface.d.ts +195 -0
- package/dist/dts/types/interaction-surface.d.ts.map +1 -0
- package/dist/dts/utils/context-tokens.d.ts +6 -0
- package/dist/dts/utils/context-tokens.d.ts.map +1 -1
- package/dist/dts/utils/delegated-message.d.ts +13 -0
- package/dist/dts/utils/delegated-message.d.ts.map +1 -1
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/dts/utils/preserved-user-messages.d.ts +21 -0
- package/dist/dts/utils/preserved-user-messages.d.ts.map +1 -0
- package/dist/esm/autopilot/autopilot-driver.js +906 -0
- package/dist/esm/autopilot/autopilot-session.js +449 -0
- package/dist/esm/autopilot/autopilot-tools.js +68 -0
- package/dist/esm/autopilot/decision-message.js +206 -0
- package/dist/esm/autopilot/hand-back/hand-back.controller.js +47 -0
- package/dist/esm/autopilot/hand-back/hand-back.js +129 -0
- package/dist/esm/autopilot/index.js +4 -0
- package/dist/esm/channel/ai-activity-bus.js +7 -0
- package/dist/esm/channel/autopilot-needs-user.js +26 -0
- package/dist/esm/components/agent-picker/agent-picker.constants.js +10 -0
- package/dist/esm/components/agent-picker/agent-picker.js +11 -1
- package/dist/esm/components/agent-picker/agent-picker.template.js +2 -4
- package/dist/esm/components/ai-driver/ai-driver.js +2 -0
- package/dist/esm/components/ai-driver/base-driver-decorator.js +8 -0
- package/dist/esm/components/chat-bubble/chat-bubble.js +45 -0
- package/dist/esm/components/chat-bubble/chat-bubble.styles.js +26 -0
- package/dist/esm/components/chat-bubble/chat-bubble.template.js +13 -0
- package/dist/esm/components/chat-driver/chat-driver.js +53 -18
- package/dist/esm/components/chat-driver/interaction-controller.js +297 -12
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +16 -2
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +28 -11
- package/dist/esm/config/define-stateful-agent.js +34 -29
- package/dist/esm/index.js +3 -0
- package/dist/esm/main/main.js +334 -15
- package/dist/esm/main/main.styles.js +126 -0
- package/dist/esm/main/main.template.js +116 -11
- package/dist/esm/state/debug-event-log.js +28 -1
- package/dist/esm/state/decision-surface.js +209 -0
- package/dist/esm/state/interaction-context.js +89 -1
- package/dist/esm/state/interaction-registry.js +33 -0
- package/dist/esm/state/persistence/session-persister.js +23 -11
- package/dist/esm/state/persistence/session-snapshot.js +1 -1
- package/dist/esm/state/run-steps.js +77 -0
- package/dist/esm/types/interaction-surface.js +1 -0
- package/dist/esm/utils/context-tokens.js +23 -9
- package/dist/esm/utils/delegated-message.js +15 -0
- package/dist/esm/utils/history-transform.js +18 -7
- package/dist/esm/utils/preserved-user-messages.js +31 -0
- package/dist/react.cjs +7 -0
- package/dist/react.mjs +6 -0
- package/docs/styling.md +1 -1
- package/package.json +17 -17
- package/sandbox/README.md +38 -2
- package/sandbox/autopilot.ts +358 -0
- package/sandbox/controls.ts +24 -1
- package/sandbox/fixtures.ts +2 -0
- package/sandbox/sandbox.ts +188 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The autopilot UI in the sandbox (GENC-1660, Part 1 UI track). See `README.md`, "Checking the
|
|
3
|
+
* autopilot UI".
|
|
4
|
+
*
|
|
5
|
+
* Two halves, both driving the element through its ordinary inputs:
|
|
6
|
+
*
|
|
7
|
+
* - `SandboxAutopilotHost` stands in for `AutopilotDriver` behind the slim bar, the banner and the
|
|
8
|
+
* badge. Its buttons really move it: Pause pauses, Resume resumes, Keep paused keeps it paused.
|
|
9
|
+
* - The demo widget (`sandbox-confirm`) is a real registered widget with a DOM-free controller,
|
|
10
|
+
* opened through the driver's `presentInteraction` and claimed by a real interceptor, so `heldBy`
|
|
11
|
+
* moves through a real `InteractionHold` and the element decides what to show for itself.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
14
|
+
import { customElement, GenesisElement, html, observable, when } from '@genesislcap/web-core';
|
|
15
|
+
import type { AutopilotHost, AutopilotHostState } from '../src/autopilot/autopilot-driver';
|
|
16
|
+
import type { AiDriver } from '../src/components/ai-driver/ai-driver';
|
|
17
|
+
import type { AutopilotCopy } from '../src/config/config';
|
|
18
|
+
import { registerInteraction } from '../src/state/interaction-registry';
|
|
19
|
+
import type { AiChatInteraction } from '../src/types/ai-chat-widget';
|
|
20
|
+
import type { InteractionContext } from '../src/types/interaction-context';
|
|
21
|
+
import type { InteractionHold } from '../src/types/interaction-interceptor';
|
|
22
|
+
|
|
23
|
+
// ─── The stub host ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** What the panel sets. `off` is no autopilot at all: the element shows exactly what it does today. */
|
|
26
|
+
export type SandboxAutopilotMode = 'off' | 'running' | 'paused' | 'self-paused';
|
|
27
|
+
|
|
28
|
+
const COPY: AutopilotCopy = {
|
|
29
|
+
running: 'Building from your plan…',
|
|
30
|
+
paused: 'Paused',
|
|
31
|
+
selfPaused: "Waiting for your answer: I'll carry on after this",
|
|
32
|
+
handBackLead: 'I need your input on this one:',
|
|
33
|
+
toggleLabel: 'Autopilot',
|
|
34
|
+
description: "Answers the assistant's questions on your behalf, so the work keeps going.",
|
|
35
|
+
pauseLabel: 'Pause autopilot',
|
|
36
|
+
resumeLabel: 'Resume autopilot',
|
|
37
|
+
resumeDecideLabel: 'Resume autopilot and force it to decide this one',
|
|
38
|
+
keepPausedLabel: 'Keep paused',
|
|
39
|
+
markLabel: 'Done in autopilot mode',
|
|
40
|
+
checkInMessage: "I've done a lot in one go, so I've stopped to check in. Shall I carry on?",
|
|
41
|
+
carryOnLabel: 'Carry on',
|
|
42
|
+
tooLongMessage: 'This conversation has grown too long for me to carry on as it is.',
|
|
43
|
+
compactLabel: 'Compact and carry on',
|
|
44
|
+
failedMessage: 'Something went wrong on that last step.',
|
|
45
|
+
tryAgainLabel: 'Try again',
|
|
46
|
+
sendLabel: 'Send',
|
|
47
|
+
budgetReason: 'This one is taking more working out than I expected, so over to you.',
|
|
48
|
+
errorReason: 'Something went wrong while I was deciding this one.',
|
|
49
|
+
noDecisionReason: "I couldn't settle this one.",
|
|
50
|
+
watchdogReason: 'This is taking longer than expected, so I need you to look at it.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const HAND_BACK_REASON = "your brief doesn't say whether quantities can be decimals.";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An `AutopilotHost` whose mode the panel sets and whose buttons move it, as the real one's do.
|
|
57
|
+
* `onChange` tells the panel, so its controls follow a click on the slim bar.
|
|
58
|
+
*/
|
|
59
|
+
export class SandboxAutopilotHost implements AutopilotHost {
|
|
60
|
+
readonly copy = COPY;
|
|
61
|
+
private readonly listeners = new Set<() => void>();
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private mode: SandboxAutopilotMode,
|
|
65
|
+
private status: string,
|
|
66
|
+
private needsUser: boolean,
|
|
67
|
+
/** The pending demo widget's id, which a self-pause hands back. */
|
|
68
|
+
private interactionId: () => string | undefined,
|
|
69
|
+
private readonly onChange: (mode: SandboxAutopilotMode) => void,
|
|
70
|
+
/**
|
|
71
|
+
* Whether something is happening, as the real driver reports it: derived by the panel from the
|
|
72
|
+
* simulated state and the demo widget. Running and idle, the composer comes back.
|
|
73
|
+
*/
|
|
74
|
+
private readonly working: () => boolean,
|
|
75
|
+
) {}
|
|
76
|
+
|
|
77
|
+
get state(): AutopilotHostState {
|
|
78
|
+
const selfPaused = this.mode === 'self-paused';
|
|
79
|
+
// A self-pause is the autopilot's own, for one interaction: it is still running.
|
|
80
|
+
const mode = (
|
|
81
|
+
{
|
|
82
|
+
off: 'unavailable',
|
|
83
|
+
running: 'running',
|
|
84
|
+
paused: 'paused',
|
|
85
|
+
'self-paused': 'running',
|
|
86
|
+
} as const
|
|
87
|
+
)[this.mode];
|
|
88
|
+
const status =
|
|
89
|
+
this.status ||
|
|
90
|
+
(selfPaused ? COPY.selfPaused : this.mode === 'running' ? COPY.running : COPY.paused);
|
|
91
|
+
return {
|
|
92
|
+
mode,
|
|
93
|
+
...(selfPaused
|
|
94
|
+
? {
|
|
95
|
+
pause: {
|
|
96
|
+
scope: 'this-interaction' as const,
|
|
97
|
+
interactionId: this.interactionId(),
|
|
98
|
+
reason: HAND_BACK_REASON,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: this.mode === 'paused'
|
|
102
|
+
? { pause: { scope: 'until-resumed' as const } }
|
|
103
|
+
: {}),
|
|
104
|
+
status: this.mode === 'off' ? '' : status,
|
|
105
|
+
needsUser: this.needsUser || selfPaused,
|
|
106
|
+
working: this.mode !== 'off' && this.working(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The panel moved: take its values and tell the element. */
|
|
111
|
+
update(mode: SandboxAutopilotMode, status: string, needsUser: boolean): void {
|
|
112
|
+
this.mode = mode;
|
|
113
|
+
this.status = status;
|
|
114
|
+
this.needsUser = needsUser;
|
|
115
|
+
this.notify();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
subscribe(listener: () => void): () => void {
|
|
119
|
+
this.listeners.add(listener);
|
|
120
|
+
return () => {
|
|
121
|
+
this.listeners.delete(listener);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pause(): void {
|
|
126
|
+
this.move('paused');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
resume(): void {
|
|
130
|
+
this.move('running');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
stop(): void {
|
|
134
|
+
this.move('paused');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
keepPaused(): void {
|
|
138
|
+
this.move('paused');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private move(mode: SandboxAutopilotMode): void {
|
|
142
|
+
if (this.mode === 'off') return;
|
|
143
|
+
this.mode = mode;
|
|
144
|
+
this.onChange(mode);
|
|
145
|
+
this.notify();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
notify(): void {
|
|
149
|
+
for (const fn of this.listeners) fn();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── The demo widget: Appendix A's confirm button ────────────────────────────
|
|
154
|
+
|
|
155
|
+
export const DEMO_WIDGET = 'sandbox-confirm';
|
|
156
|
+
|
|
157
|
+
interface ConfirmData {
|
|
158
|
+
question: string;
|
|
159
|
+
label: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** DOM-free: the button and a claimant both reach `press()`. */
|
|
163
|
+
class ConfirmController {
|
|
164
|
+
constructor(
|
|
165
|
+
readonly data: ConfirmData,
|
|
166
|
+
private readonly ctx: InteractionContext,
|
|
167
|
+
) {
|
|
168
|
+
ctx.exposeDecision({ options: [data.label], choose: () => this.press() });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
press(): void {
|
|
172
|
+
this.ctx.resolve({ status: 'approved', payload: { pressed: true } });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Register the demo widget, with how it behaves while a claimant holds it. */
|
|
177
|
+
export const registerDemoWidget = (whileAutopilot: 'visible' | 'hidden'): void => {
|
|
178
|
+
registerInteraction(DEMO_WIDGET, {
|
|
179
|
+
controller: (data, ctx) => new ConfirmController(data as ConfirmData, ctx),
|
|
180
|
+
whileAutopilot,
|
|
181
|
+
asText: (data, result) => {
|
|
182
|
+
const d = data as ConfirmData;
|
|
183
|
+
return result ? `${d.question} → ${d.label}` : d.question;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const template = html<SandboxConfirm>`
|
|
189
|
+
<p class="question">${(x) => x.data?.question}</p>
|
|
190
|
+
${when(
|
|
191
|
+
(x) => x.interaction?.status === 'resolved',
|
|
192
|
+
html<SandboxConfirm>`
|
|
193
|
+
<span>✓ ${(x) => x.data?.label}</span>
|
|
194
|
+
`,
|
|
195
|
+
)}
|
|
196
|
+
${when(
|
|
197
|
+
(x) => x.interaction?.status === 'pending',
|
|
198
|
+
html<SandboxConfirm>`
|
|
199
|
+
<rapid-button
|
|
200
|
+
appearance="accent"
|
|
201
|
+
?hidden=${(x) => x.claimantOwns}
|
|
202
|
+
@click=${(x) => {
|
|
203
|
+
x.controller?.press();
|
|
204
|
+
return true;
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
${(x) => x.data?.label}
|
|
208
|
+
</rapid-button>
|
|
209
|
+
${when(
|
|
210
|
+
(x) => x.claimantOwns,
|
|
211
|
+
html<SandboxConfirm>`
|
|
212
|
+
<small class="held">Held by ${(x) => x.heldBy}: its button is the claimant's.</small>
|
|
213
|
+
`,
|
|
214
|
+
)}
|
|
215
|
+
`,
|
|
216
|
+
)}
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The view. It renders from `heldBy` (through `claimantOwns`, so a button the controller did not
|
|
221
|
+
* expose is never hidden) and calls the controller; it never resolves anything itself.
|
|
222
|
+
*/
|
|
223
|
+
@customElement({
|
|
224
|
+
name: DEMO_WIDGET,
|
|
225
|
+
template,
|
|
226
|
+
styles: `
|
|
227
|
+
:host { display: block; }
|
|
228
|
+
.question { margin: 0 0 8px; }
|
|
229
|
+
.held { display: block; opacity: 0.7; font-style: italic; }
|
|
230
|
+
`,
|
|
231
|
+
})
|
|
232
|
+
export class SandboxConfirm extends GenesisElement {
|
|
233
|
+
interactionId = '';
|
|
234
|
+
@observable interaction?: AiChatInteraction<ConfirmData>;
|
|
235
|
+
@observable heldBy = 'user';
|
|
236
|
+
@observable claimantOwns = false;
|
|
237
|
+
private unsubscribe?: () => void;
|
|
238
|
+
|
|
239
|
+
get data(): ConfirmData | undefined {
|
|
240
|
+
return this.interaction?.data;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get controller(): ConfirmController | undefined {
|
|
244
|
+
return this.interaction?.status === 'pending'
|
|
245
|
+
? (this.interaction.interactionContext.controller as ConfirmController)
|
|
246
|
+
: undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interactionChanged(): void {
|
|
250
|
+
this.unsubscribe?.();
|
|
251
|
+
this.unsubscribe = undefined;
|
|
252
|
+
if (this.interaction?.status !== 'pending') return;
|
|
253
|
+
const ctx = this.interaction.interactionContext;
|
|
254
|
+
const read = () => {
|
|
255
|
+
this.heldBy = ctx.heldBy;
|
|
256
|
+
this.claimantOwns = ctx.claimantOwns(this.data?.label ?? '');
|
|
257
|
+
};
|
|
258
|
+
read();
|
|
259
|
+
this.unsubscribe = ctx.subscribe(read);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
disconnectedCallback(): void {
|
|
263
|
+
this.unsubscribe?.();
|
|
264
|
+
super.disconnectedCallback();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Claiming it through a real interceptor ──────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/** The id the sandbox's interceptor claims under: the same one the real autopilot uses. */
|
|
271
|
+
const CLAIMANT = 'autopilot';
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Install an interceptor on `driver` that claims the demo widget when `claim()` says so, and keeps
|
|
275
|
+
* its hold so the panel can hand it to the person and back. Returns the remover and the hold.
|
|
276
|
+
*/
|
|
277
|
+
export const installDemoClaimant = (
|
|
278
|
+
driver: AiDriver,
|
|
279
|
+
claim: () => boolean,
|
|
280
|
+
/** Told when the demo widget opens or closes, as the real autopilot notifies its host. */
|
|
281
|
+
onChange: () => void,
|
|
282
|
+
): {
|
|
283
|
+
remove: () => void;
|
|
284
|
+
hold: () => InteractionHold | undefined;
|
|
285
|
+
pendingId: () => string | undefined;
|
|
286
|
+
} => {
|
|
287
|
+
let hold: InteractionHold | undefined;
|
|
288
|
+
let pendingId: string | undefined;
|
|
289
|
+
const remove =
|
|
290
|
+
driver.addInteractionInterceptor?.({
|
|
291
|
+
id: CLAIMANT,
|
|
292
|
+
onInteractionOpened: (opened) => {
|
|
293
|
+
if (opened.componentName !== DEMO_WIDGET) return undefined;
|
|
294
|
+
hold = opened.hold;
|
|
295
|
+
pendingId = opened.interactionId;
|
|
296
|
+
opened.ctx.onDispose(() => {
|
|
297
|
+
if (pendingId === opened.interactionId) {
|
|
298
|
+
hold = undefined;
|
|
299
|
+
pendingId = undefined;
|
|
300
|
+
onChange();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
// After the claim lands, so the host reads the widget's id and holder as they now are.
|
|
304
|
+
queueMicrotask(onChange);
|
|
305
|
+
return claim() ? 'claim' : undefined;
|
|
306
|
+
},
|
|
307
|
+
}) ?? (() => undefined);
|
|
308
|
+
return { remove, hold: () => hold, pendingId: () => pendingId };
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Open the demo widget, as a working agent's tool would ask. */
|
|
312
|
+
export const openDemoWidget = (driver: AiDriver): void => {
|
|
313
|
+
void driver
|
|
314
|
+
.presentInteraction?.(DEMO_WIDGET, { question: 'Deploy the app now?', label: 'Deploy' })
|
|
315
|
+
.catch(() => undefined);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ─── Delegated messages, for the Transcript seed ─────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Each kind of delegated message: narration before a widget the autopilot answered, the "I chose"
|
|
322
|
+
* echo, and a written answer given on the user's behalf. All render as the assistant, with the mark.
|
|
323
|
+
* Unstamped: the fixtures stamp it like every other seed.
|
|
324
|
+
*/
|
|
325
|
+
export const autopilotTranscript = (): ChatMessage[] => [
|
|
326
|
+
{ role: 'user', content: 'Build me a trade blotter from my plan.' },
|
|
327
|
+
{
|
|
328
|
+
role: 'assistant',
|
|
329
|
+
content: "Your brief has no external feeds, so I'll skip Data Feeds.",
|
|
330
|
+
delegate: 'autopilot',
|
|
331
|
+
excludeFromProvider: true,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
role: 'assistant',
|
|
335
|
+
content: '',
|
|
336
|
+
interaction: {
|
|
337
|
+
interactionId: 'sandbox-answered',
|
|
338
|
+
componentName: DEMO_WIDGET,
|
|
339
|
+
data: { question: 'Add data feeds?', label: 'No, skip this step' },
|
|
340
|
+
resolved: { status: 'approved', payload: { pressed: true } },
|
|
341
|
+
answeredBy: 'autopilot',
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
role: 'assistant',
|
|
346
|
+
content: 'I chose: Approve trade, Reject trade',
|
|
347
|
+
delegate: 'autopilot',
|
|
348
|
+
excludeFromProvider: true,
|
|
349
|
+
},
|
|
350
|
+
{ role: 'assistant', content: 'Which currency should notional amounts use?' },
|
|
351
|
+
{
|
|
352
|
+
role: 'user',
|
|
353
|
+
content: 'GBP — the brief prices everything in sterling.',
|
|
354
|
+
delegate: 'autopilot',
|
|
355
|
+
delegateFraming: "(Answered on the user's behalf, from their brief.)",
|
|
356
|
+
},
|
|
357
|
+
{ role: 'assistant', content: 'Using GBP for every notional field.' },
|
|
358
|
+
];
|
package/sandbox/controls.ts
CHANGED
|
@@ -59,6 +59,28 @@ const el = <K extends keyof HTMLElementTagNameMap>(
|
|
|
59
59
|
return node;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Which groups are open, by title, so a reload keeps the panel as it was left. Separate from the
|
|
64
|
+
* sandbox state: it is panel chrome, not something the assistant is driven by.
|
|
65
|
+
*/
|
|
66
|
+
export const GROUPS_OPEN_KEY = 'ai-assistant-sandbox:groups-open';
|
|
67
|
+
|
|
68
|
+
const readGroupsOpen = (): Record<string, boolean> => {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(localStorage.getItem(GROUPS_OPEN_KEY) ?? '{}') ?? {};
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const writeGroupOpen = (title: string, open: boolean): void => {
|
|
77
|
+
try {
|
|
78
|
+
localStorage.setItem(GROUPS_OPEN_KEY, JSON.stringify({ ...readGroupsOpen(), [title]: open }));
|
|
79
|
+
} catch {
|
|
80
|
+
// Storage unavailable: the panel still works, it just forgets.
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
62
84
|
const withHint = (row: HTMLElement, hint?: string): HTMLElement => {
|
|
63
85
|
if (hint) {
|
|
64
86
|
const small = el('small', 'hint');
|
|
@@ -98,7 +120,8 @@ export function renderControls<S extends Record<string, any>>(
|
|
|
98
120
|
|
|
99
121
|
for (const group of groups) {
|
|
100
122
|
const details = el('details', 'group');
|
|
101
|
-
details.open = group.open ?? true;
|
|
123
|
+
details.open = readGroupsOpen()[group.title] ?? group.open ?? true;
|
|
124
|
+
details.addEventListener('toggle', () => writeGroupOpen(group.title, details.open));
|
|
102
125
|
const summary = el('summary');
|
|
103
126
|
summary.textContent = group.title;
|
|
104
127
|
details.appendChild(summary);
|
package/sandbox/fixtures.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ChatRequestOptions,
|
|
12
12
|
} from '@genesislcap/foundation-ai';
|
|
13
13
|
import type { AgentConfig } from '../src/config/config';
|
|
14
|
+
import { autopilotTranscript } from './autopilot';
|
|
14
15
|
|
|
15
16
|
/** The stub ignores it; it exists only to keep the driver's warning out of the console. */
|
|
16
17
|
const STUB_PROMPT = 'Sandbox stub. Replies are canned.';
|
|
@@ -328,6 +329,7 @@ export const TRANSCRIPTS = {
|
|
|
328
329
|
huge: HUGE,
|
|
329
330
|
loop: LOOP,
|
|
330
331
|
wall: WALL,
|
|
332
|
+
autopilot: stamped(autopilotTranscript()),
|
|
331
333
|
} as const;
|
|
332
334
|
|
|
333
335
|
export type TranscriptName = keyof typeof TRANSCRIPTS;
|
package/sandbox/sandbox.ts
CHANGED
|
@@ -14,7 +14,14 @@ import type { FoundationAiAssistant } from '../src/main/main';
|
|
|
14
14
|
import type { PopoutMode } from '../src/main/main.types';
|
|
15
15
|
import { WebStorageSessionProvider } from '../src/state/persistence';
|
|
16
16
|
import { estimateContextTokens } from '../src/utils/context-tokens';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
installDemoClaimant,
|
|
19
|
+
openDemoWidget,
|
|
20
|
+
registerDemoWidget,
|
|
21
|
+
SandboxAutopilotHost,
|
|
22
|
+
type SandboxAutopilotMode,
|
|
23
|
+
} from './autopilot';
|
|
24
|
+
import { GROUPS_OPEN_KEY, renderControls, type ControlGroup } from './controls';
|
|
18
25
|
import {
|
|
19
26
|
buildAgents,
|
|
20
27
|
buildAttachmentChips,
|
|
@@ -151,6 +158,15 @@ interface SandboxState {
|
|
|
151
158
|
budgetMeter: 'off' | 'single' | 'segmented' | 'segmented-remainder';
|
|
152
159
|
// Simulated runtime
|
|
153
160
|
runtime: 'idle' | 'loading' | 'cancelling' | 'blocked' | 'compacting' | 'restoring';
|
|
161
|
+
// Autopilot (GENC-1660)
|
|
162
|
+
autopilotMode: SandboxAutopilotMode;
|
|
163
|
+
autopilotHidesComposer: boolean;
|
|
164
|
+
autopilotStatus: string;
|
|
165
|
+
autopilotNeedsUser: boolean;
|
|
166
|
+
demoHeldBy: 'user' | 'autopilot';
|
|
167
|
+
demoWhileAutopilot: 'visible' | 'hidden';
|
|
168
|
+
delegateMarker: 'subtle' | 'none';
|
|
169
|
+
showBubble: boolean;
|
|
154
170
|
transcript: TranscriptName;
|
|
155
171
|
transcriptAge: TranscriptAge;
|
|
156
172
|
// Stage
|
|
@@ -198,6 +214,14 @@ const DEFAULT_STATE: SandboxState = {
|
|
|
198
214
|
flowInProgress: false,
|
|
199
215
|
budgetMeter: 'off',
|
|
200
216
|
runtime: 'idle',
|
|
217
|
+
autopilotMode: 'off',
|
|
218
|
+
autopilotHidesComposer: true,
|
|
219
|
+
autopilotStatus: '',
|
|
220
|
+
autopilotNeedsUser: false,
|
|
221
|
+
demoHeldBy: 'autopilot',
|
|
222
|
+
demoWhileAutopilot: 'hidden',
|
|
223
|
+
delegateMarker: 'subtle',
|
|
224
|
+
showBubble: false,
|
|
201
225
|
transcript: 'rich',
|
|
202
226
|
transcriptAge: 'fresh',
|
|
203
227
|
width: 480,
|
|
@@ -353,6 +377,7 @@ const buildChatConfig = (s: SandboxState): ChatConfig => ({
|
|
|
353
377
|
// `undefined` disables the feature outright (no cog control, nothing enabled);
|
|
354
378
|
// `userConfigurable` turns the defaults on and adds the multiselect to the cog.
|
|
355
379
|
animations: s.animations ? { userConfigurable: true } : undefined,
|
|
380
|
+
delegatedMessages: { marker: s.delegateMarker },
|
|
356
381
|
messageFeedback:
|
|
357
382
|
s.messageFeedback === 'off'
|
|
358
383
|
? undefined
|
|
@@ -550,6 +575,78 @@ const applyRuntime = (s: SandboxState): void => {
|
|
|
550
575
|
assistant.showLoadingIndicator = s.runtime === 'loading' || s.runtime === 'cancelling';
|
|
551
576
|
};
|
|
552
577
|
|
|
578
|
+
// ─── Autopilot (GENC-1660) ───────────────────────────────────────────────────
|
|
579
|
+
//
|
|
580
|
+
// The stub host goes on as `autopilotHostOverride`, the element's ordinary input for one; the demo
|
|
581
|
+
// widget is claimed by a real interceptor on the element's own driver. See `autopilot.ts`.
|
|
582
|
+
|
|
583
|
+
let autopilotHost: SandboxAutopilotHost | undefined;
|
|
584
|
+
let demoClaimant: ReturnType<typeof installDemoClaimant> | undefined;
|
|
585
|
+
let demoDriver: unknown;
|
|
586
|
+
let demoRegisteredAs: string | undefined;
|
|
587
|
+
let bubble: HTMLElement | undefined;
|
|
588
|
+
|
|
589
|
+
/** The id a per-call override is filed under, as an agent's tool would file its own. */
|
|
590
|
+
const SANDBOX_OVERRIDE = 'sandbox-autopilot';
|
|
591
|
+
|
|
592
|
+
const applyAutopilot = (s: SandboxState): void => {
|
|
593
|
+
if (demoRegisteredAs !== s.demoWhileAutopilot) {
|
|
594
|
+
registerDemoWidget(s.demoWhileAutopilot);
|
|
595
|
+
demoRegisteredAs = s.demoWhileAutopilot;
|
|
596
|
+
}
|
|
597
|
+
autopilotHost ??= new SandboxAutopilotHost(
|
|
598
|
+
s.autopilotMode,
|
|
599
|
+
s.autopilotStatus,
|
|
600
|
+
s.autopilotNeedsUser,
|
|
601
|
+
() => demoClaimant?.pendingId(),
|
|
602
|
+
// A click on the slim bar or the banner moves the panel too.
|
|
603
|
+
(mode) => {
|
|
604
|
+
state.autopilotMode = mode;
|
|
605
|
+
saveState(state);
|
|
606
|
+
refreshReadouts();
|
|
607
|
+
},
|
|
608
|
+
// Working, as the real driver says it: the simulated state is Busy, or the demo widget is open
|
|
609
|
+
// and the autopilot holds it (deciding it).
|
|
610
|
+
() =>
|
|
611
|
+
state.runtime === 'loading' ||
|
|
612
|
+
state.runtime === 'cancelling' ||
|
|
613
|
+
(state.demoHeldBy === 'autopilot' && !!demoClaimant?.pendingId()),
|
|
614
|
+
);
|
|
615
|
+
autopilotHost.update(s.autopilotMode, s.autopilotStatus, s.autopilotNeedsUser);
|
|
616
|
+
const host = s.autopilotMode === 'off' ? undefined : autopilotHost;
|
|
617
|
+
if (assistant.autopilotHostOverride !== host) assistant.autopilotHostOverride = host;
|
|
618
|
+
|
|
619
|
+
// The agent's own rule hiding the composer while it works — bootstrap's
|
|
620
|
+
// `chatInputDuringExecution: 'hidden'` — as the per-call override a tool would file.
|
|
621
|
+
const actions = (assistant as any)._sessionRef?.actions.aiAssistant;
|
|
622
|
+
actions?.removeInputOverride({ id: SANDBOX_OVERRIDE });
|
|
623
|
+
if (s.autopilotHidesComposer) actions?.addInputOverride({ id: SANDBOX_OVERRIDE, mode: 'hidden' });
|
|
624
|
+
|
|
625
|
+
// Re-installed whenever the element's driver changes (a remount, or new agents).
|
|
626
|
+
const driver = (assistant as any).driver;
|
|
627
|
+
if (driver !== demoDriver) {
|
|
628
|
+
demoClaimant?.remove();
|
|
629
|
+
demoClaimant = driver
|
|
630
|
+
? installDemoClaimant(
|
|
631
|
+
driver,
|
|
632
|
+
() => state.demoHeldBy === 'autopilot',
|
|
633
|
+
() => autopilotHost?.notify(),
|
|
634
|
+
)
|
|
635
|
+
: undefined;
|
|
636
|
+
demoDriver = driver;
|
|
637
|
+
}
|
|
638
|
+
demoClaimant?.hold()?.set(s.demoHeldBy === 'autopilot' ? 'claimant' : 'user');
|
|
639
|
+
|
|
640
|
+
// The collapsed assistant, for the "needs you" badge.
|
|
641
|
+
if (s.showBubble && !bubble) {
|
|
642
|
+
bubble = document.createElement('foundation-ai-chat-bubble');
|
|
643
|
+
document.body.appendChild(bubble);
|
|
644
|
+
} else if (!s.showBubble && bubble) {
|
|
645
|
+
bubble.remove();
|
|
646
|
+
bubble = undefined;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
553
650
|
// FAST's `baseLayerLuminance` drives the whole neutral ramp. These are
|
|
554
651
|
// `StandardLuminance.LightMode` / `DarkMode` from `@microsoft/fast-components`,
|
|
555
652
|
// inlined so the sandbox doesn't reach past the design system for two numbers.
|
|
@@ -610,6 +707,7 @@ function apply(): void {
|
|
|
610
707
|
applyContext(state);
|
|
611
708
|
applyBudgetMeter(state);
|
|
612
709
|
applyRuntime(state);
|
|
710
|
+
applyAutopilot(state);
|
|
613
711
|
applyGradientSend(state.gradientSend);
|
|
614
712
|
applyDivider(state.showDivider);
|
|
615
713
|
assistant.attachments = buildAttachmentChips(state.chips);
|
|
@@ -910,6 +1008,93 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
910
1008
|
},
|
|
911
1009
|
],
|
|
912
1010
|
},
|
|
1011
|
+
{
|
|
1012
|
+
title: 'Autopilot',
|
|
1013
|
+
open: false,
|
|
1014
|
+
controls: [
|
|
1015
|
+
{
|
|
1016
|
+
kind: 'select',
|
|
1017
|
+
key: 'autopilotMode',
|
|
1018
|
+
label: 'Mode',
|
|
1019
|
+
featured: true,
|
|
1020
|
+
hint: 'Unavailable is the UI as it is today. Self-paused hands the demo widget back, with its banner.',
|
|
1021
|
+
options: [
|
|
1022
|
+
{ value: 'off', label: 'Unavailable (no autopilot)' },
|
|
1023
|
+
{ value: 'running', label: 'Running' },
|
|
1024
|
+
{ value: 'paused', label: 'Paused' },
|
|
1025
|
+
{ value: 'self-paused', label: 'Self-paused (handed one back)' },
|
|
1026
|
+
],
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
kind: 'toggle',
|
|
1030
|
+
key: 'autopilotHidesComposer',
|
|
1031
|
+
label: 'Agent hides the composer while working',
|
|
1032
|
+
hint: "Bootstrap's chatInputDuringExecution: 'hidden'. Only while busy: set Simulated state to Busy. Paused then shows the slim bar; off, it shows the composer with the Autopilot pill.",
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
kind: 'text',
|
|
1036
|
+
key: 'autopilotStatus',
|
|
1037
|
+
label: 'Status text',
|
|
1038
|
+
placeholder: 'From the copy',
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
kind: 'toggle',
|
|
1042
|
+
key: 'autopilotNeedsUser',
|
|
1043
|
+
label: 'Needs you',
|
|
1044
|
+
hint: 'Drives the badge on the collapsed bubble.',
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
kind: 'toggle',
|
|
1048
|
+
key: 'showBubble',
|
|
1049
|
+
label: 'Show the collapsed bubble',
|
|
1050
|
+
hint: 'The floating orb, bottom right, where the badge appears.',
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
kind: 'action',
|
|
1054
|
+
label: 'Open the demo widget',
|
|
1055
|
+
hint: 'A confirm button with a controller, as a tool would ask. One at a time.',
|
|
1056
|
+
run: () => openDemoWidget((assistant as any).driver),
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
kind: 'select',
|
|
1060
|
+
key: 'demoHeldBy',
|
|
1061
|
+
label: 'Demo widget held by',
|
|
1062
|
+
hint: 'Moved through the real hold, so the element decides what to show.',
|
|
1063
|
+
options: [
|
|
1064
|
+
{ value: 'autopilot', label: 'The autopilot' },
|
|
1065
|
+
{ value: 'user', label: 'The person' },
|
|
1066
|
+
],
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
kind: 'select',
|
|
1070
|
+
key: 'demoWhileAutopilot',
|
|
1071
|
+
label: 'Demo widget while held',
|
|
1072
|
+
hint: 'Its registration: hidden drops the whole bubble, visible hides only its button.',
|
|
1073
|
+
options: [
|
|
1074
|
+
{ value: 'hidden', label: 'Hidden' },
|
|
1075
|
+
{ value: 'visible', label: 'Visible' },
|
|
1076
|
+
],
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
kind: 'select',
|
|
1080
|
+
key: 'delegateMarker',
|
|
1081
|
+
label: 'Delegated mark',
|
|
1082
|
+
hint: 'Seed the Autopilot transcript to see each kind of delegated message.',
|
|
1083
|
+
options: [
|
|
1084
|
+
{ value: 'subtle', label: 'Subtle' },
|
|
1085
|
+
{ value: 'none', label: 'None' },
|
|
1086
|
+
],
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
kind: 'readout',
|
|
1090
|
+
label: 'Element',
|
|
1091
|
+
render: () =>
|
|
1092
|
+
assistant
|
|
1093
|
+
? `held by ${assistant.activeHeldBy} · halo ${assistant.showHalo} · slim bar ${assistant.slimBarVisible ? 'on' : 'off'}`
|
|
1094
|
+
: '—',
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
},
|
|
913
1098
|
{
|
|
914
1099
|
title: 'Transcript',
|
|
915
1100
|
controls: [
|
|
@@ -922,6 +1107,7 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
922
1107
|
{ value: 'short', label: 'Short' },
|
|
923
1108
|
{ value: 'rich', label: 'Tools, markdown, agent switch' },
|
|
924
1109
|
{ value: 'long', label: 'Long (28 turns)' },
|
|
1110
|
+
{ value: 'autopilot', label: 'Autopilot: delegated messages' },
|
|
925
1111
|
{ value: 'huge', label: 'Huge (~20k tokens)' },
|
|
926
1112
|
{ value: 'loop', label: 'Tool loop (~22k, no user turns inside)' },
|
|
927
1113
|
{ value: 'wall', label: 'Wall (~24k in one indivisible tool result)' },
|
|
@@ -1040,6 +1226,7 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
1040
1226
|
label: 'Reset sandbox',
|
|
1041
1227
|
run: () => {
|
|
1042
1228
|
localStorage.removeItem(STORAGE_KEY);
|
|
1229
|
+
localStorage.removeItem(GROUPS_OPEN_KEY);
|
|
1043
1230
|
sessionStorage.clear();
|
|
1044
1231
|
location.reload();
|
|
1045
1232
|
},
|