@alexkroman1/aai-cli 6.10.1 → 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 +93 -14
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +13 -4
- package/dist/scaffold/vite.config.ts +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 +818 -0
- package/dist/templates/call-audit/agent.ts +171 -0
- package/dist/templates/call-audit/client.tsx +198 -0
- package/dist/templates/call-audit/workflows/audit.ts +298 -0
- package/dist/templates/call-audit/workflows/ingest.ts +251 -0
- package/dist/templates/call-audit/workflows/media.ts +637 -0
- package/dist/templates/call-audit/workflows/summarize.ts +201 -0
- package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
- 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 +107 -96
- 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 +48 -41
- 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 +314 -0
- package/dist/templates/spoken-summary/agent.ts +155 -0
- package/dist/templates/spoken-summary/client.tsx +185 -0
- package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
- package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
- 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 +246 -123
- package/dist/templates/transcription-workflow/agent.ts +21 -7
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
- package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
- package/dist/templates/transcription-workflow/workflows/wav.ts +31 -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/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
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
* builds a project and runs one.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import type {
|
|
23
|
+
import type { WorkflowClient } from "@alexkroman1/aai";
|
|
24
24
|
import {
|
|
25
|
-
createProgressStream,
|
|
26
25
|
createRunSnapshot,
|
|
27
|
-
createStubWorkflows,
|
|
28
26
|
createToolContext,
|
|
29
|
-
|
|
27
|
+
parseSchemaInput,
|
|
30
28
|
type StubGatewayCall,
|
|
29
|
+
schemaInputIssues,
|
|
30
|
+
toolRunner,
|
|
31
31
|
withDiscoveredTools,
|
|
32
32
|
} from "@alexkroman1/aai/testing";
|
|
33
|
-
import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
33
|
+
import { mockWorkflows, installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
34
34
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
35
|
+
import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
|
|
35
36
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
36
37
|
import { FatalError, RetryableError } from "workflow";
|
|
37
38
|
import authoredAgent from "./agent.ts";
|
|
@@ -75,37 +76,34 @@ const agentDef = withDiscoveredTools(
|
|
|
75
76
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
76
77
|
);
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Every tool here is driven through the agent's own table, by the name the model
|
|
81
|
+
* calls.
|
|
82
|
+
*
|
|
83
|
+
* The second parameter is args-or-context, which is `runTool`'s own shape: three
|
|
84
|
+
* of this desk's four tools take no arguments, and the `{}` those calls were
|
|
85
|
+
* obliged to pass sat between the two values a reader cares about.
|
|
86
|
+
*/
|
|
87
|
+
const run = toolRunner(agentDef);
|
|
81
88
|
|
|
82
89
|
/**
|
|
83
90
|
* A `ctx.workflows` that records `start` and answers `find` from a fixture.
|
|
84
91
|
*
|
|
85
92
|
* Returned WITHOUT a cast, which is the property worth keeping: a cast would
|
|
86
93
|
* also stop reporting the day `WorkflowClient` grows a method, and this stub is
|
|
87
|
-
* how the template's tools reach the client at all. `
|
|
88
|
-
* what keeps that affordable —
|
|
89
|
-
*
|
|
94
|
+
* how the template's tools reach the client at all. `mockWorkflows`
|
|
95
|
+
* (`@alexkroman1/aai/testing/vitest`) is what keeps that affordable — a `vi.fn`
|
|
96
|
+
* per method over one `runs` list, filling in what this desk does not drive, so
|
|
97
|
+
* the day the client does grow a method only the tests using it change.
|
|
98
|
+
* `stream`/`streamTail` are left rejecting on purpose: `research_progress` reads
|
|
99
|
+
* progress through `lastLine`, and composing those two by hand is the hazard
|
|
100
|
+
* `lastLine` exists to remove.
|
|
90
101
|
*/
|
|
91
102
|
function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
recent: vi.fn(async () => runs),
|
|
97
|
-
cancel: vi.fn(async () => true),
|
|
98
|
-
wakeUp: vi.fn(async () => 0),
|
|
99
|
-
// A tail of 0 means "one line written", which is the case the tools read.
|
|
100
|
-
// The `-1` case is overridden per test, because it is the one that decides
|
|
101
|
-
// whether the stream is opened at all.
|
|
102
|
-
streamTail: vi.fn(async () => 0),
|
|
103
|
-
stream: vi.fn(async () => createProgressStream([])),
|
|
104
|
-
// Name only: `WorkflowDef.description` is optional, so passing it through
|
|
105
|
-
// would mean handing `description: undefined` to a field that does not
|
|
106
|
-
// accept it. Nothing here reads the description anyway.
|
|
107
|
-
listing: () => [{ name: "research" }],
|
|
108
|
-
});
|
|
103
|
+
// Name only: `WorkflowSummary.description` is optional, so passing this
|
|
104
|
+
// desk's through would mean handing `description: undefined` to a field that
|
|
105
|
+
// does not accept it. Nothing here reads the description anyway.
|
|
106
|
+
return mockWorkflows({ runs, names: ["research"] });
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
describe("the agent declares its workflow", () => {
|
|
@@ -115,13 +113,16 @@ describe("the agent declares its workflow", () => {
|
|
|
115
113
|
});
|
|
116
114
|
|
|
117
115
|
test("with an input schema, so a bad topic fails at the call site", async () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
expect(
|
|
116
|
+
// `parseSchemaInput` / `schemaInputIssues` rather than a reach through
|
|
117
|
+
// `["~standard"].validate`: that is the vendor WIRE contract, and whether it
|
|
118
|
+
// answers synchronously or with a promise is the vendor's business — a
|
|
119
|
+
// missing `await` there leaves `.issues` undefined and the refusing half
|
|
120
|
+
// passes for the wrong reason.
|
|
121
|
+
const parsed = await parseSchemaInput(research.input, { topic: "otters", requestedBy: "s" });
|
|
122
|
+
expect(parsed).toMatchObject({ topic: "otters" });
|
|
123
|
+
expect(
|
|
124
|
+
await schemaInputIssues(research.input, { topic: "no", requestedBy: "s" }),
|
|
125
|
+
).toBeDefined();
|
|
125
126
|
});
|
|
126
127
|
});
|
|
127
128
|
|
|
@@ -184,7 +185,7 @@ describe("request_research", () => {
|
|
|
184
185
|
describe("research_status", () => {
|
|
185
186
|
test("says nothing was started when the key has no runs", async () => {
|
|
186
187
|
const ctx = createToolContext({ workflows: stubWorkflows([]) });
|
|
187
|
-
const result = await run("research_status",
|
|
188
|
+
const result = await run("research_status", ctx);
|
|
188
189
|
expect(result).toMatchObject({ runs: [], note: "Nothing started yet." });
|
|
189
190
|
});
|
|
190
191
|
|
|
@@ -197,7 +198,7 @@ describe("research_status", () => {
|
|
|
197
198
|
}),
|
|
198
199
|
];
|
|
199
200
|
const ctx = createToolContext({ workflows: stubWorkflows(runs) });
|
|
200
|
-
const result = (await run("research_status",
|
|
201
|
+
const result = (await run("research_status", ctx)) as { runs: string[] };
|
|
201
202
|
expect(result.runs[0]).toContain("Otters use tools.");
|
|
202
203
|
expect(result.runs[0]).toContain("3 sources");
|
|
203
204
|
});
|
|
@@ -206,7 +207,7 @@ describe("research_status", () => {
|
|
|
206
207
|
const ctx = createToolContext({
|
|
207
208
|
workflows: stubWorkflows([createRunSnapshot({ workflow: "research", status: "running" })]),
|
|
208
209
|
});
|
|
209
|
-
const result = (await run("research_status",
|
|
210
|
+
const result = (await run("research_status", ctx)) as { runs: string[] };
|
|
210
211
|
expect(result.runs[0]).toContain("Still working on it.");
|
|
211
212
|
});
|
|
212
213
|
|
|
@@ -215,14 +216,14 @@ describe("research_status", () => {
|
|
|
215
216
|
createRunSnapshot({ workflow: "research", status: "failed", error: "model unavailable" }),
|
|
216
217
|
];
|
|
217
218
|
const ctx = createToolContext({ workflows: stubWorkflows(runs) });
|
|
218
|
-
const result = (await run("research_status",
|
|
219
|
+
const result = (await run("research_status", ctx)) as { runs: string[] };
|
|
219
220
|
expect(result.runs[0]).toContain("model unavailable");
|
|
220
221
|
});
|
|
221
222
|
|
|
222
223
|
test("bounds how many past runs it reads aloud", async () => {
|
|
223
224
|
const workflows = stubWorkflows([]);
|
|
224
225
|
const ctx = createToolContext({ workflows });
|
|
225
|
-
await run("research_status",
|
|
226
|
+
await run("research_status", ctx);
|
|
226
227
|
// A voice reply cannot be a list of twenty runs.
|
|
227
228
|
expect(workflows.find).toHaveBeenCalledWith(research, ctx.sessionId, { limit: 3 });
|
|
228
229
|
});
|
|
@@ -233,40 +234,40 @@ describe("research_progress", () => {
|
|
|
233
234
|
const workflows = stubWorkflows([
|
|
234
235
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
235
236
|
]);
|
|
236
|
-
vi.mocked(workflows.
|
|
237
|
-
const
|
|
238
|
-
const result = await run("research_progress", {}, ctx);
|
|
237
|
+
vi.mocked(workflows.lastLine).mockResolvedValue("Found 3 sources.");
|
|
238
|
+
const result = await run("research_progress", createToolContext({ workflows }));
|
|
239
239
|
expect(result).toMatchObject({ progress: "Found 3 sources." });
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
test("asks for the LAST line, not the whole log", async () => {
|
|
243
|
-
// A voice reply cannot recite every line the run has written.
|
|
243
|
+
// A voice reply cannot recite every line the run has written. `lastLine` is
|
|
244
|
+
// the whole request — the bound that keeps an empty channel from hanging
|
|
245
|
+
// belongs to the method, so nothing here composes `streamTail` and `stream`.
|
|
244
246
|
const workflows = stubWorkflows([
|
|
245
247
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
246
248
|
]);
|
|
247
|
-
vi.mocked(workflows.
|
|
248
|
-
await run("research_progress",
|
|
249
|
-
expect(workflows.
|
|
249
|
+
vi.mocked(workflows.lastLine).mockResolvedValue("a");
|
|
250
|
+
await run("research_progress", createToolContext({ workflows }));
|
|
251
|
+
expect(workflows.lastLine).toHaveBeenCalledWith("wrun_1");
|
|
250
252
|
});
|
|
251
253
|
|
|
252
|
-
test("a run that has written nothing yet says so
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
254
|
+
test("a run that has written nothing yet says so", async () => {
|
|
255
|
+
// `lastLine` resolves `undefined` for an empty channel, and this is the arm
|
|
256
|
+
// the tool branches on. That an empty channel does not HANG — it is never
|
|
257
|
+
// closed, so a stream opened on one waits for a line that may never come —
|
|
258
|
+
// is `lastLine`'s own guarantee now, and `aai`'s to test.
|
|
256
259
|
const workflows = stubWorkflows([
|
|
257
260
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
258
261
|
]);
|
|
259
|
-
|
|
260
|
-
const result = await run("research_progress", {}, createToolContext({ workflows }));
|
|
262
|
+
const result = await run("research_progress", createToolContext({ workflows }));
|
|
261
263
|
expect(result).toMatchObject({ note: "Started, nothing to report yet." });
|
|
262
|
-
expect(workflows.stream).not.toHaveBeenCalled();
|
|
263
264
|
});
|
|
264
265
|
|
|
265
266
|
test("says nothing was started when the key has no runs", async () => {
|
|
266
267
|
const workflows = stubWorkflows([]);
|
|
267
|
-
const result = await run("research_progress",
|
|
268
|
+
const result = await run("research_progress", createToolContext({ workflows }));
|
|
268
269
|
expect(result).toMatchObject({ note: "Nothing started yet." });
|
|
269
|
-
expect(workflows.
|
|
270
|
+
expect(workflows.lastLine).not.toHaveBeenCalled();
|
|
270
271
|
});
|
|
271
272
|
});
|
|
272
273
|
|
|
@@ -276,7 +277,7 @@ describe("file_it_now", () => {
|
|
|
276
277
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
277
278
|
]);
|
|
278
279
|
vi.mocked(workflows.wakeUp).mockResolvedValue(1);
|
|
279
|
-
const result = await run("file_it_now",
|
|
280
|
+
const result = await run("file_it_now", createToolContext({ workflows }));
|
|
280
281
|
expect(workflows.wakeUp).toHaveBeenCalledWith("wrun_1");
|
|
281
282
|
expect(result).toMatchObject({ filed: true });
|
|
282
283
|
});
|
|
@@ -288,13 +289,13 @@ describe("file_it_now", () => {
|
|
|
288
289
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
289
290
|
]);
|
|
290
291
|
vi.mocked(workflows.wakeUp).mockResolvedValue(0);
|
|
291
|
-
const result = await run("file_it_now",
|
|
292
|
+
const result = await run("file_it_now", createToolContext({ workflows }));
|
|
292
293
|
expect(result).toMatchObject({ filed: false });
|
|
293
294
|
});
|
|
294
295
|
|
|
295
296
|
test("says nothing was started when the key has no runs", async () => {
|
|
296
297
|
const workflows = stubWorkflows([]);
|
|
297
|
-
const result = await run("file_it_now",
|
|
298
|
+
const result = await run("file_it_now", createToolContext({ workflows }));
|
|
298
299
|
expect(result).toMatchObject({ note: "Nothing started yet." });
|
|
299
300
|
expect(workflows.wakeUp).not.toHaveBeenCalled();
|
|
300
301
|
});
|
|
@@ -10,17 +10,14 @@ export default tool({
|
|
|
10
10
|
// has WRITTEN (`getWritable()` in `workflows/research.ts`). Between "still
|
|
11
11
|
// working on it" and a finished summary there is otherwise nothing to say.
|
|
12
12
|
//
|
|
13
|
-
// `
|
|
14
|
-
// never closed — no step knows it is the last one — so
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const stream = await ctx.workflows.stream(latest.runId, { startIndex: -1 });
|
|
23
|
-
for await (const line of stream) return { progress: String(line) };
|
|
24
|
-
return { note: "Started, nothing to report yet." };
|
|
13
|
+
// `lastLine` rather than `streamTail` + `stream` composed here: a progress
|
|
14
|
+
// channel is never closed — no step knows it is the last one — so a stream
|
|
15
|
+
// opened on a run that has written nothing waits forever, which down a phone
|
|
16
|
+
// is a turn that stops with no error and nothing in a log. The bound that
|
|
17
|
+
// prevents it belongs to the method now, and `undefined` is "nothing yet".
|
|
18
|
+
const line = await ctx.workflows.lastLine(latest.runId);
|
|
19
|
+
return line === undefined
|
|
20
|
+
? { note: "Started, nothing to report yet." }
|
|
21
|
+
: { progress: String(line) };
|
|
25
22
|
},
|
|
26
23
|
});
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type WorkflowOutputOf,
|
|
5
|
-
type WorkflowRunSnapshot,
|
|
6
|
-
} from "@alexkroman1/aai";
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { plural } from "@alexkroman1/aai/utils";
|
|
3
|
+
import { isTerminal, type WorkflowRunOf } from "@alexkroman1/aai/workflow-api";
|
|
7
4
|
import { research } from "../shared.ts";
|
|
8
5
|
|
|
9
6
|
/** How many past runs the status tool will look at. Newest first. */
|
|
@@ -12,17 +9,18 @@ const RECENT_RUNS = 3;
|
|
|
12
9
|
/**
|
|
13
10
|
* One line a voice agent can read aloud about a run.
|
|
14
11
|
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* `WorkflowRunOf` is the snapshot with its output already typed — the
|
|
13
|
+
* `WorkflowRunSnapshot<WorkflowOutputOf<typeof research>>` this file used to
|
|
14
|
+
* compose by hand, which cost a three-name import for one type. Still the
|
|
15
|
+
* discriminated union, so `isTerminal` below narrows exactly as it did.
|
|
18
16
|
*/
|
|
19
|
-
function describeRun(run:
|
|
17
|
+
function describeRun(run: WorkflowRunOf<typeof research>): string {
|
|
20
18
|
// `isTerminal` narrows to the three finished statuses, which is what makes
|
|
21
19
|
// `run.output` and `run.error` reachable without a cast.
|
|
22
20
|
if (!isTerminal(run)) return "Still working on it.";
|
|
23
21
|
switch (run.status) {
|
|
24
22
|
case "completed":
|
|
25
|
-
return `Done: ${run.output.summary} (${run.output.sources} sources)`;
|
|
23
|
+
return `Done: ${run.output.summary} (${run.output.sources} ${plural(run.output.sources, "source")})`;
|
|
26
24
|
case "failed":
|
|
27
25
|
return `That one failed: ${run.error}`;
|
|
28
26
|
default:
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*
|
|
36
36
|
* ## Every stage REPORTS, and the report goes two places
|
|
37
37
|
*
|
|
38
|
-
* `report()` (`@alexkroman1/aai/
|
|
38
|
+
* `report()` (`@alexkroman1/aai/step`) writes to the run's own stream — which
|
|
39
39
|
* `research_progress` reads back down the phone and a page renders — and to the
|
|
40
40
|
* server log, with the attempt number appended past the first. A pass that is
|
|
41
41
|
* retrying and one that is working print the same sentences otherwise.
|
|
@@ -49,16 +49,10 @@
|
|
|
49
49
|
* researcher CONCLUDED, which is exactly what the step returns.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import {
|
|
52
|
+
import { mapConcurrent, report } from "@alexkroman1/aai/step";
|
|
53
|
+
import { stepGenerateClassified, stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
|
|
53
54
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
54
|
-
import {
|
|
55
|
-
errorMessage,
|
|
56
|
-
isToolFailure,
|
|
57
|
-
mapInBatches,
|
|
58
|
-
report,
|
|
59
|
-
stepGenerate,
|
|
60
|
-
stepGenerateJson,
|
|
61
|
-
} from "@alexkroman1/aai/utils";
|
|
55
|
+
import { errorMessage, isToolFailure, plural } from "@alexkroman1/aai/utils";
|
|
62
56
|
import { sleep } from "workflow";
|
|
63
57
|
import { z } from "zod";
|
|
64
58
|
import {
|
|
@@ -108,9 +102,9 @@ export type Source = { title: string; url: string };
|
|
|
108
102
|
|
|
109
103
|
// ---- What each stage's model call has to come back as ------------------------
|
|
110
104
|
//
|
|
111
|
-
// `
|
|
112
|
-
// throw and therefore a retry — where the hand-rolled `askJson<T>()`
|
|
113
|
-
// replaces returned a value the compiler believed and nothing checked. They are
|
|
105
|
+
// `stepGenerateJsonClassified` validates against these, so a reply that missed
|
|
106
|
+
// is a plain throw and therefore a retry — where the hand-rolled `askJson<T>()`
|
|
107
|
+
// this replaces returned a value the compiler believed and nothing checked. They are
|
|
114
108
|
// deliberately LENIENT wherever the old hand-written coercion was: a model that
|
|
115
109
|
// put one number in an array of strings should cost that element, not the whole
|
|
116
110
|
// pass.
|
|
@@ -221,12 +215,14 @@ export async function researchFlow(input: { topic: string; requestedBy: string }
|
|
|
221
215
|
// failed angle fails the RUN: its finished siblings are already journaled, so
|
|
222
216
|
// the resume replays them for free and re-issues only what is missing, where
|
|
223
217
|
// catching here would file a report with a silent hole in it.
|
|
224
|
-
const first = await
|
|
218
|
+
const first = await mapConcurrent(angles, ANGLE_CONCURRENCY, (angle) =>
|
|
219
|
+
investigate(brief, angle),
|
|
220
|
+
);
|
|
225
221
|
|
|
226
222
|
// The supervisor's second look. Usually empty — a second wave costs the caller
|
|
227
223
|
// minutes, and the prompt says so.
|
|
228
224
|
const gaps = await findGaps(brief, first);
|
|
229
|
-
const second = await
|
|
225
|
+
const second = await mapConcurrent(gaps, ANGLE_CONCURRENCY, (angle) => investigate(brief, angle));
|
|
230
226
|
|
|
231
227
|
const notes = [...first, ...second];
|
|
232
228
|
const written = await writeReport(input.topic, brief, notes);
|
|
@@ -261,10 +257,9 @@ export async function writeBrief(topic: string): Promise<Brief> {
|
|
|
261
257
|
"use step";
|
|
262
258
|
|
|
263
259
|
await report(`Working out what "${topic}" is really asking.`);
|
|
264
|
-
const parsed = await
|
|
260
|
+
const parsed = await stepGenerateJsonClassified(
|
|
265
261
|
`Research request, as the caller said it: ${topic}`,
|
|
266
|
-
BRIEF_SYSTEM,
|
|
267
|
-
BriefReply,
|
|
262
|
+
{ system: BRIEF_SYSTEM, schema: BriefReply },
|
|
268
263
|
);
|
|
269
264
|
return { brief: parsed.brief || topic, criteria: parsed.criteria.slice(0, MAX_ANGLES) };
|
|
270
265
|
}
|
|
@@ -279,7 +274,10 @@ export async function writeBrief(topic: string): Promise<Brief> {
|
|
|
279
274
|
export async function planAngles(brief: Brief): Promise<string[]> {
|
|
280
275
|
"use step";
|
|
281
276
|
|
|
282
|
-
const parsed = await
|
|
277
|
+
const parsed = await stepGenerateJsonClassified(briefText(brief), {
|
|
278
|
+
system: PLAN_SYSTEM,
|
|
279
|
+
schema: AnglesReply,
|
|
280
|
+
});
|
|
283
281
|
const angles = parsed.angles.slice(0, MAX_ANGLES);
|
|
284
282
|
if (angles.length === 0) {
|
|
285
283
|
// Nothing to fan out over is a plan failure, not an empty result: the brief
|
|
@@ -287,7 +285,7 @@ export async function planAngles(brief: Brief): Promise<string[]> {
|
|
|
287
285
|
await report("No angles came back; researching the brief itself.");
|
|
288
286
|
return [brief.brief];
|
|
289
287
|
}
|
|
290
|
-
await report(`Researching ${angles.length}
|
|
288
|
+
await report(`Researching ${angles.length} ${plural(angles.length, "angle")}.`);
|
|
291
289
|
return angles;
|
|
292
290
|
}
|
|
293
291
|
|
|
@@ -341,16 +339,15 @@ export async function findGaps(brief: Brief, notes: readonly Note[]): Promise<st
|
|
|
341
339
|
"use step";
|
|
342
340
|
|
|
343
341
|
if (notes.length === 0) return [];
|
|
344
|
-
const parsed = await
|
|
342
|
+
const parsed = await stepGenerateJsonClassified(
|
|
345
343
|
`${briefText(brief)}\n\nWhat came back:\n${notes.map(noteText).join("\n\n")}`,
|
|
346
|
-
GAPS_SYSTEM,
|
|
347
|
-
AnglesReply,
|
|
344
|
+
{ system: GAPS_SYSTEM, schema: AnglesReply },
|
|
348
345
|
);
|
|
349
346
|
const gaps = parsed.angles.slice(0, MAX_ANGLES - 1);
|
|
350
347
|
await report(
|
|
351
348
|
gaps.length === 0
|
|
352
349
|
? "The brief is covered; writing it up."
|
|
353
|
-
: `Following up ${gaps.length}
|
|
350
|
+
: `Following up ${gaps.length} ${plural(gaps.length, "gap")}.`,
|
|
354
351
|
);
|
|
355
352
|
return gaps;
|
|
356
353
|
}
|
|
@@ -369,12 +366,14 @@ export async function writeReport(
|
|
|
369
366
|
): Promise<{ report: string; summary: string }> {
|
|
370
367
|
"use step";
|
|
371
368
|
|
|
372
|
-
await report(`Writing up ${notes.length}
|
|
373
|
-
const written = await
|
|
369
|
+
await report(`Writing up ${notes.length} ${plural(notes.length, "angle")}.`);
|
|
370
|
+
const written = await stepGenerateClassified(
|
|
374
371
|
`${briefText(brief)}\n\nFindings:\n${notes.map(noteText).join("\n\n")}`,
|
|
375
|
-
REPORT_SYSTEM,
|
|
372
|
+
{ system: REPORT_SYSTEM },
|
|
376
373
|
);
|
|
377
|
-
const summary = await
|
|
374
|
+
const summary = await stepGenerateClassified(`Topic: ${topic}\n\nReport:\n${written}`, {
|
|
375
|
+
system: BRIEF_SUMMARY_SYSTEM,
|
|
376
|
+
});
|
|
378
377
|
return { report: written, summary };
|
|
379
378
|
}
|
|
380
379
|
|
|
@@ -404,12 +403,11 @@ async function nextAction(
|
|
|
404
403
|
seen: readonly string[],
|
|
405
404
|
left: number,
|
|
406
405
|
): Promise<Action> {
|
|
407
|
-
return await
|
|
406
|
+
return await stepGenerateJsonClassified(
|
|
408
407
|
`${briefText(brief)}\n\nYour angle: ${angle}\n` +
|
|
409
408
|
`Actions left: ${left}\n\n` +
|
|
410
409
|
(seen.length === 0 ? "You have not looked at anything yet." : seen.join("\n\n")),
|
|
411
|
-
RESEARCH_SYSTEM,
|
|
412
|
-
ActionReply,
|
|
410
|
+
{ system: RESEARCH_SYSTEM, schema: ActionReply },
|
|
413
411
|
);
|
|
414
412
|
}
|
|
415
413
|
|
|
@@ -474,11 +472,10 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
|
|
|
474
472
|
if (seen.length === 0) {
|
|
475
473
|
return { angle, findings: "Nothing was found on this angle.", sources: [] };
|
|
476
474
|
}
|
|
477
|
-
const parsed = await
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
);
|
|
475
|
+
const parsed = await stepGenerateJsonClassified(`Angle: ${angle}\n\n${seen.join("\n\n")}`, {
|
|
476
|
+
system: COMPRESS_SYSTEM,
|
|
477
|
+
schema: CompressReply,
|
|
478
|
+
});
|
|
482
479
|
return {
|
|
483
480
|
angle,
|
|
484
481
|
findings: parsed.findings ?? seen.join("\n\n"),
|
|
@@ -489,31 +486,17 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
|
|
|
489
486
|
}
|
|
490
487
|
|
|
491
488
|
// ---- Model plumbing ---------------------------------------------------------
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return await stepGenerate(prompt, { system }).catch(throwStepError);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* The same call, for a stage whose reply is JSON of a known shape.
|
|
508
|
-
*
|
|
509
|
-
* `stepGenerateJson` owns the four things every such stage used to re-derive —
|
|
510
|
-
* unwrap the fence, parse, reject a non-object, check the shape — and throws
|
|
511
|
-
* PLAINLY when any of them misses, which is what makes a malformed reply a
|
|
512
|
-
* retry rather than a failure.
|
|
513
|
-
*/
|
|
514
|
-
async function askJson<S extends z.ZodType>(prompt: string, system: string, schema: S) {
|
|
515
|
-
return await stepGenerateJson(prompt, { system, schema }).catch(throwStepError);
|
|
516
|
-
}
|
|
489
|
+
//
|
|
490
|
+
// There is none left, and its absence is the point. This desk carried an `ask()`
|
|
491
|
+
// and an `askJson()` whose whole body was `.catch(throwStepError)`; the SDK's
|
|
492
|
+
// `stepGenerateClassified` and `stepGenerateJsonClassified`
|
|
493
|
+
// (`@alexkroman1/aai/step-errors`) ARE that call — the `/step` one with the
|
|
494
|
+
// gateway's verdict classified, so a terminal failure stays terminal and a rate
|
|
495
|
+
// limit becomes a `RetryableError` carrying the delay the gateway itself named.
|
|
496
|
+
// `stepGenerateJsonClassified` also owns the four things every JSON stage used
|
|
497
|
+
// to re-derive — unwrap the fence, parse, reject a non-object, check the shape —
|
|
498
|
+
// and throws PLAINLY when any of them misses, which is what makes a malformed
|
|
499
|
+
// reply a retry rather than a failure.
|
|
517
500
|
|
|
518
501
|
// ---- Pure helpers -----------------------------------------------------------
|
|
519
502
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
3
|
-
import { createToolContext } from "@alexkroman1/aai/testing";
|
|
3
|
+
import { createToolContext, ok } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
import type { AuthResult } from "./authenticate.ts";
|
|
6
6
|
import type { Address } from "./shared.ts";
|
|
7
|
-
import { retailSlot } from "./store.ts";
|
|
7
|
+
import { callFlow, retailSlot } from "./store.ts";
|
|
8
8
|
import cancelPendingOrder from "./tools/cancel_pending_order.ts";
|
|
9
9
|
import exchangeDeliveredOrderItems from "./tools/exchange_delivered_order_items.ts";
|
|
10
10
|
import findUserIdByEmail from "./tools/find_user_id_by_email.ts";
|
|
@@ -27,27 +27,6 @@ function makeCtx(): ToolContext {
|
|
|
27
27
|
return createToolContext();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* The success half of a tool result, or a thrown failure carrying the tool's
|
|
32
|
-
* own message.
|
|
33
|
-
*
|
|
34
|
-
* `ToolDef["execute"]`'s public signature always returns `unknown` — the wire
|
|
35
|
-
* type is fixed so any tool is assignable to `ToolDef`, whatever its body
|
|
36
|
-
* really returns — so every test that reads a field back off a result needed a
|
|
37
|
-
* cast. There were EIGHTEEN of them here, each the same three lines, which is
|
|
38
|
-
* the shape the repo's guides call a missing typed seam: a concentration of
|
|
39
|
-
* identical casts is one narrowing that belongs in one helper, not a cast per
|
|
40
|
-
* assertion. This is that helper, and it is the file's only cast.
|
|
41
|
-
*
|
|
42
|
-
* It also removes the second half of the boilerplate: a `ToolFailure` fails the
|
|
43
|
-
* test HERE, naming what the tool refused, rather than surfacing three lines
|
|
44
|
-
* later as `undefined` on a field nobody assigned.
|
|
45
|
-
*/
|
|
46
|
-
function ok<T>(result: unknown): T {
|
|
47
|
-
if (isToolFailure(result)) throw new Error(`tool refused: ${result.error}`);
|
|
48
|
-
return result as T;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
30
|
/** A context already authenticated as `userId`, via the real tool. */
|
|
52
31
|
async function authedCtx(email: string): Promise<ToolContext> {
|
|
53
32
|
const ctx = makeCtx();
|
|
@@ -63,6 +42,8 @@ describe("authentication", () => {
|
|
|
63
42
|
);
|
|
64
43
|
expect(result.user_id).toBe("olivia_ito_3591");
|
|
65
44
|
expect(retailSlot.get(ctx).authenticatedUserId).toBe("olivia_ito_3591");
|
|
45
|
+
// Two facts, two homes: the store latches WHO, the flow holds WHETHER.
|
|
46
|
+
expect(callFlow.position(ctx).state).toBe("serving");
|
|
66
47
|
});
|
|
67
48
|
|
|
68
49
|
test("an unknown email is refused and leaves the session unauthenticated", async () => {
|
|
@@ -70,6 +51,9 @@ describe("authentication", () => {
|
|
|
70
51
|
const result = await findUserIdByEmail.execute({ email: "nobody@example.com" }, ctx);
|
|
71
52
|
expect(isToolFailure(result)).toBe(true);
|
|
72
53
|
expect(retailSlot.get(ctx).authenticatedUserId).toBeNull();
|
|
54
|
+
// `IDENTIFIED` is not sent when the body answers a `ToolFailure`, so a
|
|
55
|
+
// lookup that found nobody cannot leave the call one step ahead of itself.
|
|
56
|
+
expect(callFlow.position(ctx).state).toBe("identifying");
|
|
73
57
|
});
|
|
74
58
|
|
|
75
59
|
test("find_user_id_by_name_zip is case-insensitive on names and exact on zip", async () => {
|
|
@@ -121,6 +105,9 @@ describe("authentication", () => {
|
|
|
121
105
|
);
|
|
122
106
|
expect(isToolFailure(switched) && switched.error.toLowerCase()).toContain("one customer");
|
|
123
107
|
expect(retailSlot.get(ctx).authenticatedUserId).toBe("olivia_ito_3591");
|
|
108
|
+
// The tool is legal in `serving` — the refusal comes from `authenticateAs`,
|
|
109
|
+
// not from the gate — so the call stays exactly where it was.
|
|
110
|
+
expect(callFlow.position(ctx).state).toBe("serving");
|
|
124
111
|
});
|
|
125
112
|
|
|
126
113
|
test("switching via name + zip is refused too — both doors, one lock", async () => {
|
|
@@ -1090,4 +1077,20 @@ describe("transfer_to_human_agents", () => {
|
|
|
1090
1077
|
expect(isToolFailure(result)).toBe(false);
|
|
1091
1078
|
expect(result.transferred).toBe(true);
|
|
1092
1079
|
});
|
|
1080
|
+
|
|
1081
|
+
test("the handoff is terminal, so the call cannot be worked afterwards", async () => {
|
|
1082
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
1083
|
+
ok(await transferToHumanAgents.execute({ summary: "wants a human" }, ctx));
|
|
1084
|
+
|
|
1085
|
+
const at = callFlow.position(ctx);
|
|
1086
|
+
expect(at.state).toBe("transferred");
|
|
1087
|
+
expect(at.done).toBe(true);
|
|
1088
|
+
|
|
1089
|
+
// The policy's "say nothing else after that" used to be enforced by
|
|
1090
|
+
// nothing: every tool stayed callable, so a model that kept going kept
|
|
1091
|
+
// acting on a call it had given away.
|
|
1092
|
+
const refused = await getUserDetails.execute({ user_id: "olivia_ito_3591" }, ctx);
|
|
1093
|
+
expect(isToolFailure(refused)).toBe(true);
|
|
1094
|
+
expect(isToolFailure(refused) && refused.error).toContain('"transferred"');
|
|
1095
|
+
});
|
|
1093
1096
|
});
|