@alexkroman1/aai-cli 6.11.0 → 7.0.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/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +40 -19
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.test.ts +746 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/tools/answer_question.ts +17 -4
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.test.ts +64 -33
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -312,34 +312,3 @@ export function planSegments(format: WavFormat): Segment[] {
|
|
|
312
312
|
}
|
|
313
313
|
return segments;
|
|
314
314
|
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Put a canonical 44-byte WAV header in front of a slice of samples.
|
|
318
|
-
*
|
|
319
|
-
* A byte range of a WAV is raw PCM with no header, and the sync endpoint reads
|
|
320
|
-
* the rate and channel count out of one. Sending `audio/pcm` with a `config`
|
|
321
|
-
* part instead would work equally well; a header keeps the two parts of the
|
|
322
|
-
* request from being able to disagree.
|
|
323
|
-
*/
|
|
324
|
-
export function wavWithHeader(format: WavFormat, samples: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
325
|
-
const out = new Uint8Array(44 + samples.length);
|
|
326
|
-
const view = new DataView(out.buffer);
|
|
327
|
-
const write = (at: number, text: string) => {
|
|
328
|
-
for (let i = 0; i < text.length; i++) view.setUint8(at + i, text.charCodeAt(i));
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
write(0, "RIFF");
|
|
332
|
-
view.setUint32(4, 36 + samples.length, true);
|
|
333
|
-
write(8, "WAVEfmt ");
|
|
334
|
-
view.setUint32(16, 16, true); // `fmt ` payload length
|
|
335
|
-
view.setUint16(20, 1, true); // linear PCM
|
|
336
|
-
view.setUint16(22, format.channels, true);
|
|
337
|
-
view.setUint32(24, format.sampleRate, true);
|
|
338
|
-
view.setUint32(28, bytesPerSecond(format), true);
|
|
339
|
-
view.setUint16(32, blockAlign(format), true);
|
|
340
|
-
view.setUint16(34, format.bitsPerSample, true);
|
|
341
|
-
write(36, "data");
|
|
342
|
-
view.setUint32(40, samples.length, true);
|
|
343
|
-
out.set(samples, 44);
|
|
344
|
-
return out;
|
|
345
|
-
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
3
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createToolContext,
|
|
6
|
+
ok,
|
|
7
|
+
okPosition,
|
|
8
|
+
toolRunner,
|
|
9
|
+
withDiscoveredTools,
|
|
10
|
+
} from "@alexkroman1/aai/testing";
|
|
5
11
|
import { describe, expect, test } from "vitest";
|
|
6
12
|
import authoredAgent from "./agent.ts";
|
|
7
13
|
|
|
@@ -18,7 +24,14 @@ const agentDef = withDiscoveredTools(
|
|
|
18
24
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
19
25
|
);
|
|
20
26
|
|
|
21
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
activeAssistant,
|
|
29
|
+
FLIGHTS,
|
|
30
|
+
gateFlow,
|
|
31
|
+
tripProjection,
|
|
32
|
+
tripSlot,
|
|
33
|
+
tripView,
|
|
34
|
+
} from "./shared.ts";
|
|
22
35
|
|
|
23
36
|
// ─── Harness ─────────────────────────────────────────────────────────────────
|
|
24
37
|
|
|
@@ -26,11 +39,11 @@ import { activeAssistant, FLIGHTS, tripProjection, tripSlot, tripView } from "./
|
|
|
26
39
|
* below rests on. */
|
|
27
40
|
const makeCtx = (): ToolContext => createToolContext();
|
|
28
41
|
|
|
29
|
-
/** A tool by the name the model calls it by, bound to this agent. The lookup
|
|
30
|
-
*
|
|
31
|
-
* what is local is only which
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup,
|
|
43
|
+
* its "no such tool" message and the args-or-context shape are all
|
|
44
|
+
* `toolRunner`'s (`@alexkroman1/aai/testing`); what is local is only which
|
|
45
|
+
* agent it runs against. */
|
|
46
|
+
const run = toolRunner(agentDef);
|
|
34
47
|
|
|
35
48
|
/** The state a tool just mutated, read back the way `syncState` reads it. */
|
|
36
49
|
function stateOf(ctx: ToolContext) {
|
|
@@ -116,36 +129,47 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
116
129
|
const ctx = makeCtx();
|
|
117
130
|
await run("book_hotel", { hotelId: "H1", nights: 3 }, ctx);
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
// Staging moved the gate, in the same window it wrote `pending`.
|
|
133
|
+
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
134
|
+
|
|
135
|
+
// A gated tool answers the flow's POSITION wrapped around the body's own
|
|
136
|
+
// return value, so the applied sentence is under `result`. `okPosition`
|
|
137
|
+
// keeps the position and THROWS on a refusal, quoting it — where the cast
|
|
138
|
+
// it replaces read `undefined` off the failure and died three assertions
|
|
139
|
+
// later on a property of undefined.
|
|
140
|
+
const confirmed = okPosition<{ applied: string; reference: string }>(
|
|
141
|
+
await run("confirm_action", ctx),
|
|
142
|
+
);
|
|
143
|
+
expect(confirmed.result.applied).toContain("Harborview Suites");
|
|
124
144
|
// 3 nights at $265.
|
|
125
|
-
expect(
|
|
145
|
+
expect(confirmed.result.applied).toContain("$795");
|
|
146
|
+
// SETTLED: the gate re-armed as part of the same call.
|
|
147
|
+
expect(confirmed.state).toBe("browsing");
|
|
126
148
|
|
|
127
149
|
const state = stateOf(ctx);
|
|
128
150
|
expect(state.pending).toBeNull();
|
|
129
151
|
expect(state.bookings).toHaveLength(1);
|
|
130
|
-
expect(state.bookings[0]?.reference).toBe(
|
|
152
|
+
expect(state.bookings[0]?.reference).toBe(confirmed.result.reference);
|
|
131
153
|
|
|
132
|
-
// The gate re-arms
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
// The gate re-arms, and now REFUSES rather than reporting an empty apply —
|
|
155
|
+
// `when: "awaitingConfirmation"` is what a second confirm meets.
|
|
156
|
+
expect(await run("confirm_action", ctx)).toMatchObject({
|
|
157
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
135
158
|
});
|
|
136
159
|
});
|
|
137
160
|
|
|
138
161
|
test("cancel_action drops the staged change and leaves the booking alone", async () => {
|
|
139
162
|
const ctx = makeCtx();
|
|
140
163
|
await run("update_ticket", { flightId: "LX54" }, ctx);
|
|
141
|
-
const dropped = (await run("cancel_action",
|
|
142
|
-
expect(dropped.discarded).toContain("LX54");
|
|
164
|
+
const dropped = okPosition<{ discarded: string }>(await run("cancel_action", ctx));
|
|
165
|
+
expect(dropped.result.discarded).toContain("LX54");
|
|
166
|
+
expect(dropped.state).toBe("browsing");
|
|
143
167
|
|
|
144
168
|
const state = stateOf(ctx);
|
|
145
169
|
expect(state.pending).toBeNull();
|
|
146
170
|
expect(state.ticket?.flightId).toBe("LX40");
|
|
147
|
-
expect(await run("cancel_action",
|
|
148
|
-
|
|
171
|
+
expect(await run("cancel_action", ctx)).toMatchObject({
|
|
172
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
149
173
|
});
|
|
150
174
|
});
|
|
151
175
|
|
|
@@ -161,14 +185,14 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
161
185
|
|
|
162
186
|
test("cancelling the ticket makes a later ticket change impossible", async () => {
|
|
163
187
|
const ctx = makeCtx();
|
|
164
|
-
await run("cancel_ticket",
|
|
165
|
-
await run("confirm_action",
|
|
188
|
+
await run("cancel_ticket", ctx);
|
|
189
|
+
await run("confirm_action", ctx);
|
|
166
190
|
expect(stateOf(ctx).ticket).toBeNull();
|
|
167
191
|
|
|
168
192
|
expect(await run("update_ticket", { flightId: "LX52" }, ctx)).toEqual({
|
|
169
193
|
error: "This caller has no ticket to move — it was cancelled on this call.",
|
|
170
194
|
});
|
|
171
|
-
expect(await run("cancel_ticket",
|
|
195
|
+
expect(await run("cancel_ticket", ctx)).toEqual({ error: "There is no ticket to cancel." });
|
|
172
196
|
});
|
|
173
197
|
|
|
174
198
|
test("a second staging is REFUSED rather than overwriting the first", async () => {
|
|
@@ -193,7 +217,12 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
193
217
|
|
|
194
218
|
// The first staging is untouched, and it is what a yes applies.
|
|
195
219
|
expect(stateOf(ctx).pending).toEqual({ kind: "update_ticket", flightId: "LX52" });
|
|
196
|
-
|
|
220
|
+
// A refused SECOND staging must not have moved the gate either — it was
|
|
221
|
+
// already `awaitingConfirmation` and the refusal changed nothing.
|
|
222
|
+
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
223
|
+
// `ok` is `okPosition` with `.result` taken off: this assertion is about
|
|
224
|
+
// what the apply DID, not about where the gate landed.
|
|
225
|
+
const applied = ok<{ applied: string }>(await run("confirm_action", ctx));
|
|
197
226
|
expect(applied.applied).toContain("LX52");
|
|
198
227
|
expect(stateOf(ctx).bookings).toEqual([]);
|
|
199
228
|
|
|
@@ -207,7 +236,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
207
236
|
test("cancel_action clears the block, so a declined change does not wedge the desk", async () => {
|
|
208
237
|
const ctx = makeCtx();
|
|
209
238
|
await run("book_car_rental", { carId: "C2", days: 3 }, ctx);
|
|
210
|
-
await run("cancel_action",
|
|
239
|
+
await run("cancel_action", ctx);
|
|
211
240
|
const staged = (await run("book_excursion", { excursionId: "E2" }, ctx)) as {
|
|
212
241
|
awaitingConfirmation: boolean;
|
|
213
242
|
};
|
|
@@ -225,12 +254,14 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
225
254
|
|
|
226
255
|
await run("to_hotel_assistant", { request: "a room" }, first);
|
|
227
256
|
await run("book_hotel", { hotelId: "H3", nights: 1 }, first);
|
|
228
|
-
await run("confirm_action",
|
|
257
|
+
await run("confirm_action", first);
|
|
229
258
|
|
|
230
259
|
expect(activeAssistant(stateOf(second))).toBe("primary");
|
|
231
260
|
expect(stateOf(second).bookings).toEqual([]);
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
// The GATE is per-session too, not just the trip.
|
|
262
|
+
expect(gateFlow.position(second).state).toBe("browsing");
|
|
263
|
+
expect(await run("confirm_action", second)).toMatchObject({
|
|
264
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
234
265
|
});
|
|
235
266
|
expect(stateOf(first).bookings).toHaveLength(1);
|
|
236
267
|
});
|
|
@@ -283,8 +314,8 @@ describe("search tools", () => {
|
|
|
283
314
|
test("lookup_booking reports the ticket the caller is actually holding", async () => {
|
|
284
315
|
const ctx = makeCtx();
|
|
285
316
|
await run("update_ticket", { flightId: "LX52" }, ctx);
|
|
286
|
-
await run("confirm_action",
|
|
287
|
-
const booking = (await run("lookup_booking",
|
|
317
|
+
await run("confirm_action", ctx);
|
|
318
|
+
const booking = (await run("lookup_booking", ctx)) as {
|
|
288
319
|
passenger: string;
|
|
289
320
|
ticket: { flight: string; departs: string } | null;
|
|
290
321
|
};
|
|
@@ -327,9 +358,9 @@ describe("tripView projection", () => {
|
|
|
327
358
|
test("totals every confirmed booking", async () => {
|
|
328
359
|
const ctx = makeCtx();
|
|
329
360
|
await run("book_hotel", { hotelId: "H3", nights: 2 }, ctx); // 2 × 180
|
|
330
|
-
await run("confirm_action",
|
|
361
|
+
await run("confirm_action", ctx);
|
|
331
362
|
await run("book_excursion", { excursionId: "E1" }, ctx); // 35
|
|
332
|
-
await run("confirm_action",
|
|
363
|
+
await run("confirm_action", ctx);
|
|
333
364
|
|
|
334
365
|
const view = tripView(stateOf(ctx));
|
|
335
366
|
expect(view.total).toBe(395);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll, client, useAgentState
|
|
2
|
+
import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
3
|
import type { TripView } from "./shared.ts";
|
|
4
4
|
import { SPECIALIST_IDS, SPECIALISTS, tripProjection } from "./shared.ts";
|
|
5
5
|
|
|
@@ -15,7 +15,6 @@ function deskLabel(id: (typeof DESKS)[number]): string {
|
|
|
15
15
|
* the transcript — the whole delegation mechanism is otherwise invisible.
|
|
16
16
|
*/
|
|
17
17
|
function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
18
|
-
const theme = useTheme();
|
|
19
18
|
return (
|
|
20
19
|
<div className="flex flex-wrap gap-1.5">
|
|
21
20
|
{DESKS.map((id) => {
|
|
@@ -23,12 +22,9 @@ function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
|
23
22
|
return (
|
|
24
23
|
<span
|
|
25
24
|
key={id}
|
|
26
|
-
className=
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
color: on ? theme.bg : theme.text,
|
|
30
|
-
opacity: on ? 1 : 0.55,
|
|
31
|
-
}}
|
|
25
|
+
className={`rounded-full px-2.5 py-1 text-[11px] font-medium capitalize ${
|
|
26
|
+
on ? "bg-aai-primary text-aai-bg" : "bg-aai-surface text-aai-text opacity-55"
|
|
27
|
+
}`}
|
|
32
28
|
>
|
|
33
29
|
{deskLabel(id)}
|
|
34
30
|
</span>
|
|
@@ -39,11 +35,10 @@ function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
function ItinerarySidebar() {
|
|
42
|
-
const theme = useTheme();
|
|
43
38
|
const trip = useAgentState(tripProjection);
|
|
44
39
|
|
|
45
40
|
return (
|
|
46
|
-
<div className="flex flex-col gap-4 p-4 h-full min-h-0
|
|
41
|
+
<div className="flex flex-col gap-4 p-4 h-full min-h-0 text-aai-text">
|
|
47
42
|
<div className="flex flex-col gap-2">
|
|
48
43
|
<h3 className="text-sm font-bold opacity-60 uppercase tracking-wide">{trip.passenger}</h3>
|
|
49
44
|
<DeskStrip active={trip.assistant} />
|
|
@@ -52,10 +47,7 @@ function ItinerarySidebar() {
|
|
|
52
47
|
{/* The staged action — the browser half of the confirmation gate. What is
|
|
53
48
|
on screen is exactly what the concierge just asked out loud. */}
|
|
54
49
|
{trip.pending && (
|
|
55
|
-
<div
|
|
56
|
-
className="rounded-lg p-3 text-sm"
|
|
57
|
-
style={{ background: theme.surface, border: `1px solid ${theme.primary}` }}
|
|
58
|
-
>
|
|
50
|
+
<div className="rounded-lg p-3 text-sm bg-aai-surface border border-aai-primary">
|
|
59
51
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
|
|
60
52
|
Waiting on your yes
|
|
61
53
|
</p>
|
|
@@ -66,7 +58,7 @@ function ItinerarySidebar() {
|
|
|
66
58
|
<div className="flex flex-col gap-2">
|
|
67
59
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Ticket</p>
|
|
68
60
|
{trip.ticket ? (
|
|
69
|
-
<div className="rounded-lg p-3
|
|
61
|
+
<div className="rounded-lg p-3 bg-aai-surface">
|
|
70
62
|
<p className="text-sm font-medium">
|
|
71
63
|
{trip.ticket.flightId} · {trip.ticket.route}
|
|
72
64
|
</p>
|
|
@@ -85,24 +77,20 @@ function ItinerarySidebar() {
|
|
|
85
77
|
{trip.bookings.map((booking) => (
|
|
86
78
|
<div
|
|
87
79
|
key={booking.reference}
|
|
88
|
-
className="flex items-center justify-between gap-3 rounded-lg p-3"
|
|
89
|
-
style={{ background: theme.surface }}
|
|
80
|
+
className="flex items-center justify-between gap-3 rounded-lg p-3 bg-aai-surface"
|
|
90
81
|
>
|
|
91
82
|
<div className="min-w-0">
|
|
92
83
|
<p className="truncate text-sm">{booking.summary}</p>
|
|
93
84
|
<p className="text-xs opacity-60">{booking.reference}</p>
|
|
94
85
|
</div>
|
|
95
|
-
<span className="text-sm font-bold
|
|
86
|
+
<span className="text-sm font-bold text-aai-primary">
|
|
96
87
|
${booking.price.toLocaleString("en-US")}
|
|
97
88
|
</span>
|
|
98
89
|
</div>
|
|
99
90
|
))}
|
|
100
|
-
<div
|
|
101
|
-
className="flex justify-between border-t pt-2 text-sm font-bold"
|
|
102
|
-
style={{ borderColor: theme.border }}
|
|
103
|
-
>
|
|
91
|
+
<div className="flex justify-between border-t border-aai-border pt-2 text-sm font-bold">
|
|
104
92
|
<span>Total</span>
|
|
105
|
-
<span
|
|
93
|
+
<span className="text-aai-primary">${trip.total.toLocaleString("en-US")}</span>
|
|
106
94
|
</div>
|
|
107
95
|
</div>
|
|
108
96
|
)}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
activeAssistant,
|
|
25
25
|
applyPending,
|
|
26
26
|
describeAction,
|
|
27
|
+
gateFlow,
|
|
27
28
|
note,
|
|
28
29
|
SPECIALISTS,
|
|
29
30
|
type SpecialistId,
|
|
@@ -111,28 +112,46 @@ export const completeOrEscalate = tripSlot.updateTool({
|
|
|
111
112
|
* halts before a sensitive tool and resumes on approval; the halt here is that
|
|
112
113
|
* every sensitive tool stages instead of acting, so the approval has somewhere
|
|
113
114
|
* to arrive.
|
|
115
|
+
*
|
|
116
|
+
* **`when: "awaitingConfirmation"` is the other side of that halt.** It used to
|
|
117
|
+
* be a null check inside {@link applyPending} — "Nothing is waiting for
|
|
118
|
+
* confirmation. Use the booking tool first." — which is the question "where is
|
|
119
|
+
* this conversation", asked of the payload. The gate asks the machine instead,
|
|
120
|
+
* and its refusal carries the state's own instruction. `SETTLED` fires only when
|
|
121
|
+
* the body did NOT answer with a {@link ToolFailure}, so an application that
|
|
122
|
+
* failed leaves the change staged and the caller still being asked.
|
|
114
123
|
*/
|
|
115
|
-
export const confirmAction =
|
|
124
|
+
export const confirmAction = gateFlow.tool({
|
|
116
125
|
description:
|
|
117
126
|
"Apply the change the caller has just confirmed out loud. Only call this " +
|
|
118
127
|
"after you have read the change back and heard a clear yes.",
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
when: "awaitingConfirmation",
|
|
129
|
+
send: { type: "SETTLED" },
|
|
130
|
+
execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
|
|
122
131
|
});
|
|
123
132
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `cancel_action` — the caller said no. Drops the staged action, changes nothing.
|
|
135
|
+
*
|
|
136
|
+
* Gated for the same reason as `confirm_action`, and its own "nothing was
|
|
137
|
+
* waiting" arm is gone with the same argument.
|
|
138
|
+
*/
|
|
139
|
+
export const cancelAction = gateFlow.tool({
|
|
126
140
|
description:
|
|
127
141
|
"Discard the change the caller just declined. Call this when they say no, " +
|
|
128
142
|
"or when they want to change the details before confirming.",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
when: "awaitingConfirmation",
|
|
144
|
+
send: { type: "SETTLED" },
|
|
145
|
+
execute: (_args, ctx) =>
|
|
146
|
+
tripSlot.update(ctx, (trip) => {
|
|
147
|
+
const action = trip.pending;
|
|
148
|
+
// Reachable only if the position and the payload disagree; see
|
|
149
|
+
// `applyPending`. Reported rather than thrown, mid-call.
|
|
150
|
+
if (!action) return { discarded: null, message: "Nothing was staged after all." };
|
|
151
|
+
trip.pending = null;
|
|
152
|
+
const described = describeAction(action);
|
|
153
|
+
const summary = typeof described === "string" ? described : action.kind;
|
|
154
|
+
note(trip, `Declined: ${summary}`);
|
|
155
|
+
return { discarded: summary, message: "Nothing was changed." };
|
|
156
|
+
}),
|
|
138
157
|
});
|
|
@@ -35,7 +35,15 @@
|
|
|
35
35
|
* agent that can rebook a flight and one that can rebook a flight *by mistake*.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
type DeepReadonly,
|
|
40
|
+
type DialogSpec,
|
|
41
|
+
dialog,
|
|
42
|
+
pushCapped,
|
|
43
|
+
sessionSlot,
|
|
44
|
+
type ToolContext,
|
|
45
|
+
type ToolFailure,
|
|
46
|
+
} from "@alexkroman1/aai";
|
|
39
47
|
|
|
40
48
|
// ─── The booking world ───────────────────────────────────────────────────────
|
|
41
49
|
// Their notebook downloads a sqlite database of a real airline's schedule and
|
|
@@ -341,6 +349,51 @@ export function note(state: TripState, entry: string): void {
|
|
|
341
349
|
|
|
342
350
|
// ─── The confirmation gate ───────────────────────────────────────────────────
|
|
343
351
|
|
|
352
|
+
/**
|
|
353
|
+
* The gate as a STATE MAP — their `interrupt_before`, which is a graph construct.
|
|
354
|
+
*
|
|
355
|
+
* This port had to hand-roll it: a voice session had no graph, so "halt before a
|
|
356
|
+
* sensitive tool and resume on approval" became "every sensitive tool STAGES and
|
|
357
|
+
* `confirm_action` is the only thing that applies one". That half is unchanged
|
|
358
|
+
* and is still the mechanism — see {@link stageAction}. What the machine adds is
|
|
359
|
+
* the PRECONDITION on the other side of the halt: `confirm_action` and
|
|
360
|
+
* `cancel_action` are legal only while something is actually waiting, which was
|
|
361
|
+
* a null check inside each of them and is now `when`.
|
|
362
|
+
*
|
|
363
|
+
* **The staging tools are deliberately NOT gated on `browsing`.**
|
|
364
|
+
* {@link stageAction}'s refusal NAMES the sentence already waiting, which is what
|
|
365
|
+
* lets the model settle that one first and restage the rest; a state gate can
|
|
366
|
+
* only say "you are in awaitingConfirmation", since a state's instruction is
|
|
367
|
+
* static. So the specific refusal stays where it is and the flow follows it.
|
|
368
|
+
*/
|
|
369
|
+
const gateSpec = {
|
|
370
|
+
initial: "browsing",
|
|
371
|
+
states: {
|
|
372
|
+
browsing: {
|
|
373
|
+
instruction:
|
|
374
|
+
"Nothing is waiting for the caller's yes. Stage a change with a booking tool first.",
|
|
375
|
+
on: { STAGED: "awaitingConfirmation" },
|
|
376
|
+
},
|
|
377
|
+
awaitingConfirmation: {
|
|
378
|
+
instruction:
|
|
379
|
+
"Read the staged change back and hear a clear yes or no, then use confirm_action or cancel_action.",
|
|
380
|
+
on: { SETTLED: "browsing" },
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
} as const satisfies DialogSpec;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Whether a change is waiting on the caller's word.
|
|
387
|
+
*
|
|
388
|
+
* Its own slot, beside {@link tripSlot}: the flow holds the POSITION and the
|
|
389
|
+
* trip holds the staged action itself, because an inspectable
|
|
390
|
+
* {@link PendingAction} is what `confirm_action` re-derives the effect from. One
|
|
391
|
+
* tool call always moves both — {@link stageAction} sends `STAGED` in the same
|
|
392
|
+
* synchronous window it writes `pending` in, and the two settling tools send
|
|
393
|
+
* `SETTLED` only on success.
|
|
394
|
+
*/
|
|
395
|
+
export const gateFlow = dialog("gate", gateSpec);
|
|
396
|
+
|
|
344
397
|
/**
|
|
345
398
|
* Describe a staged action in one sentence, in the second person — this is
|
|
346
399
|
* read aloud, so it is a question's worth of text and not a receipt.
|
|
@@ -396,6 +449,7 @@ export function describeAction(action: DeepReadonly<PendingAction>): string | To
|
|
|
396
449
|
* model ask about that one first and restage the rest afterwards.
|
|
397
450
|
*/
|
|
398
451
|
export function stageAction(
|
|
452
|
+
ctx: ToolContext,
|
|
399
453
|
state: TripState,
|
|
400
454
|
action: PendingAction,
|
|
401
455
|
):
|
|
@@ -413,6 +467,11 @@ export function stageAction(
|
|
|
413
467
|
};
|
|
414
468
|
}
|
|
415
469
|
state.pending = action;
|
|
470
|
+
// The flow moves in the SAME synchronous window `pending` is written in, so
|
|
471
|
+
// the position and the payload cannot be observed disagreeing. `gateFlow` is
|
|
472
|
+
// a different slot from `tripSlot`, so this is not a nested write to the draft
|
|
473
|
+
// being held — the open-draft guard is per slot.
|
|
474
|
+
gateFlow.send(ctx, { type: "STAGED" });
|
|
416
475
|
note(state, `Awaiting confirmation: ${described}`);
|
|
417
476
|
return {
|
|
418
477
|
awaitingConfirmation: true,
|
|
@@ -421,13 +480,21 @@ export function stageAction(
|
|
|
421
480
|
};
|
|
422
481
|
}
|
|
423
482
|
|
|
424
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Apply the staged action. The ONE place any of them takes effect.
|
|
485
|
+
*
|
|
486
|
+
* The "nothing is waiting" guard is gone: `confirm_action` is gated on
|
|
487
|
+
* `awaitingConfirmation`, so reaching here means something IS staged. The
|
|
488
|
+
* remaining `!action` arm is the flow and the payload disagreeing, which one
|
|
489
|
+
* synchronous window per transition is what rules out — it reports rather than
|
|
490
|
+
* throwing, because a live call is the wrong place to find out.
|
|
491
|
+
*/
|
|
425
492
|
export function applyPending(
|
|
426
493
|
state: TripState,
|
|
427
494
|
): { applied: string; reference?: string } | ToolFailure {
|
|
428
495
|
const action = state.pending;
|
|
429
496
|
if (!action) {
|
|
430
|
-
return { error: "Nothing is
|
|
497
|
+
return { error: "Nothing is staged after all — ask the caller again what they want." };
|
|
431
498
|
}
|
|
432
499
|
const described = describeAction(action);
|
|
433
500
|
if (typeof described !== "string") return described;
|
|
@@ -10,8 +10,8 @@ export default tripSlot.updateTool({
|
|
|
10
10
|
carId: z.string().max(20).describe("The car id from search_car_rentals, e.g. 'C2'"),
|
|
11
11
|
days: z.number().int().min(1).max(60).describe("How many days"),
|
|
12
12
|
}),
|
|
13
|
-
execute(args, trip) {
|
|
14
|
-
return stageAction(trip, {
|
|
13
|
+
execute(args, trip, ctx) {
|
|
14
|
+
return stageAction(ctx, trip, {
|
|
15
15
|
kind: "book_car",
|
|
16
16
|
carId: args.carId.toUpperCase(),
|
|
17
17
|
days: args.days,
|
|
@@ -9,8 +9,8 @@ export default tripSlot.updateTool({
|
|
|
9
9
|
inputSchema: z.object({
|
|
10
10
|
excursionId: z.string().max(20).describe("The excursion id from search_excursions, e.g. 'E2'"),
|
|
11
11
|
}),
|
|
12
|
-
execute(args, trip) {
|
|
13
|
-
return stageAction(trip, {
|
|
12
|
+
execute(args, trip, ctx) {
|
|
13
|
+
return stageAction(ctx, trip, {
|
|
14
14
|
kind: "book_excursion",
|
|
15
15
|
excursionId: args.excursionId.toUpperCase(),
|
|
16
16
|
});
|
|
@@ -10,8 +10,8 @@ export default tripSlot.updateTool({
|
|
|
10
10
|
hotelId: z.string().max(20).describe("The hotel id from search_hotels, e.g. 'H1'"),
|
|
11
11
|
nights: z.number().int().min(1).max(30).describe("How many nights"),
|
|
12
12
|
}),
|
|
13
|
-
execute(args, trip) {
|
|
14
|
-
return stageAction(trip, {
|
|
13
|
+
execute(args, trip, ctx) {
|
|
14
|
+
return stageAction(ctx, trip, {
|
|
15
15
|
kind: "book_hotel",
|
|
16
16
|
hotelId: args.hotelId.toUpperCase(),
|
|
17
17
|
nights: args.nights,
|
|
@@ -9,8 +9,8 @@ export default tripSlot.updateTool({
|
|
|
9
9
|
description:
|
|
10
10
|
"Cancel the caller's ticket outright. This does NOT cancel anything yet — " +
|
|
11
11
|
"it stages the cancellation so you can read it back and hear a yes.",
|
|
12
|
-
execute(_args, trip) {
|
|
12
|
+
execute(_args, trip, ctx) {
|
|
13
13
|
if (!trip.ticket) return { error: "There is no ticket to cancel." };
|
|
14
|
-
return stageAction(trip, { kind: "cancel_ticket" });
|
|
14
|
+
return stageAction(ctx, trip, { kind: "cancel_ticket" });
|
|
15
15
|
},
|
|
16
16
|
});
|
|
@@ -13,10 +13,10 @@ export default tripSlot.updateTool({
|
|
|
13
13
|
inputSchema: z.object({
|
|
14
14
|
flightId: z.string().max(20).describe("The flight to move to, e.g. 'LX52'"),
|
|
15
15
|
}),
|
|
16
|
-
execute(args, trip) {
|
|
16
|
+
execute(args, trip, ctx) {
|
|
17
17
|
if (!trip.ticket) {
|
|
18
18
|
return { error: "This caller has no ticket to move — it was cancelled on this call." };
|
|
19
19
|
}
|
|
20
|
-
return stageAction(trip, { kind: "update_ticket", flightId: args.flightId.toUpperCase() });
|
|
20
|
+
return stageAction(ctx, trip, { kind: "update_ticket", flightId: args.flightId.toUpperCase() });
|
|
21
21
|
},
|
|
22
22
|
});
|