@alexkroman1/aai-cli 13.3.0 → 15.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-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/_artifacts-BJOYGQPp.mjs +21 -0
- package/dist/_artifacts.d.ts +16 -0
- package/dist/_build-target.d.ts +172 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +1 -1
- package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/_server-common-De0haHr9.mjs +70 -0
- package/dist/_server-common.d.ts +20 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
- package/dist/_templates.d.ts +1 -1
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/_vercel-output.d.ts +63 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-BhEaxBPu.mjs +481 -0
- package/dist/build.d.ts +23 -10
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +155 -138
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
- package/dist/client-bundler.d.ts +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
- package/dist/init.d.ts +14 -0
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +155 -76
- package/dist/scaffold/package.json +6 -6
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/start.d.ts +112 -0
- package/dist/start.mjs +156 -0
- package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
- package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/code-interpreter/agent.test.ts +103 -0
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +81 -19
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/math-buddy/agent.test.ts +126 -0
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/personal-finance/agent.test.ts +127 -0
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/agent.ts +8 -0
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/routing.ts +64 -55
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
- package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/templates/web-researcher/agent.test.ts +130 -0
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.d.ts +1 -1
- package/dist/worker-bundler.mjs +8 -8
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +9 -4
- package/dist/_server-common-BV4_CrP3.mjs +0 -43
- package/dist/build-CWyFGe-W.mjs +0 -69
- package/dist/scaffold/server.mjs +0 -204
|
@@ -59,9 +59,11 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
59
59
|
import { plural } from "@alexkroman1/aai/utils";
|
|
60
60
|
import type { WorkflowInputOf, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
61
61
|
import {
|
|
62
|
+
BulletList,
|
|
63
|
+
Facts,
|
|
62
64
|
Form,
|
|
63
65
|
type FormValues,
|
|
64
|
-
|
|
66
|
+
mountPage,
|
|
65
67
|
SubmitButton,
|
|
66
68
|
TextAreaField,
|
|
67
69
|
useWorkflowSubmit,
|
|
@@ -70,7 +72,6 @@ import {
|
|
|
70
72
|
WorkflowProgress,
|
|
71
73
|
type WorkflowRun,
|
|
72
74
|
} from "@alexkroman1/aai-ui";
|
|
73
|
-
import { useState } from "react";
|
|
74
75
|
import type { redline } from "./agent.ts";
|
|
75
76
|
|
|
76
77
|
/**
|
|
@@ -124,13 +125,10 @@ export function toInput(values: FormValues): WorkflowInputOf<typeof redline> {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function RedlineDesk() {
|
|
127
|
-
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
128
|
-
// the page can tell "writing what you just briefed" from "picking up where
|
|
129
|
-
// you left off" — the hook reports the run, not who asked for it.
|
|
130
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
131
128
|
// The reload is covered by the hook's own key — see the module doc for why
|
|
132
129
|
// this desk wants the tab-scoped one it mints rather than a key of its own.
|
|
133
|
-
const { submit, run, pending, error, reset } =
|
|
130
|
+
const { submit, run, pending, error, reset, startedHere } =
|
|
131
|
+
useWorkflowSubmit<typeof redline>(WORKFLOW);
|
|
134
132
|
|
|
135
133
|
return (
|
|
136
134
|
<main className="mx-auto flex max-w-2xl flex-col gap-8 p-8">
|
|
@@ -142,13 +140,7 @@ function RedlineDesk() {
|
|
|
142
140
|
</p>
|
|
143
141
|
</header>
|
|
144
142
|
|
|
145
|
-
<Form
|
|
146
|
-
onSubmit={(values) => {
|
|
147
|
-
setStartedHere(true);
|
|
148
|
-
return submit(toInput(values));
|
|
149
|
-
}}
|
|
150
|
-
error={error}
|
|
151
|
-
>
|
|
143
|
+
<Form onSubmit={(values) => submit(toInput(values))} error={error}>
|
|
152
144
|
{/* The scalars: brief, audience, rounds. Declared, not written. */}
|
|
153
145
|
<WorkflowFields workflow={WORKFLOW} />
|
|
154
146
|
{/* The array the schema declares and no generic control can render. */}
|
|
@@ -177,7 +169,6 @@ function RedlineDesk() {
|
|
|
177
169
|
// The recovered run is dismissed as deliberately as one this load
|
|
178
170
|
// started: `reset()` is not undone by a second lookup (the lookup
|
|
179
171
|
// is a mount-time act), so Clear really does clear.
|
|
180
|
-
setStartedHere(false);
|
|
181
172
|
reset();
|
|
182
173
|
}}
|
|
183
174
|
/>
|
|
@@ -193,15 +184,16 @@ function Rounds({ rounds }: { rounds: Redline["rounds"] }) {
|
|
|
193
184
|
<ol className="flex flex-col gap-3">
|
|
194
185
|
{rounds.map((entry) => (
|
|
195
186
|
<li key={entry.round} className="flex flex-col gap-1 border-l pl-4">
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
<Facts
|
|
188
|
+
size="xs"
|
|
189
|
+
className="uppercase tracking-[1.2px]"
|
|
190
|
+
items={[
|
|
191
|
+
`Round ${entry.round}`,
|
|
192
|
+
`${entry.critique.score}/10`,
|
|
193
|
+
entry.critique.verdict === "ship" ? "ship it" : "revise",
|
|
194
|
+
]}
|
|
195
|
+
/>
|
|
196
|
+
<BulletList items={entry.critique.notes} size="sm" />
|
|
205
197
|
</li>
|
|
206
198
|
))}
|
|
207
199
|
</ol>
|
|
@@ -224,7 +216,7 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
224
216
|
{/* The run's own narration — the complement of the status line, which is
|
|
225
217
|
`running` for a run's whole life, so a one-round redline and a
|
|
226
218
|
three-round one look identical while they happen. These lines come from
|
|
227
|
-
the run itself (`
|
|
219
|
+
the run itself (`stepReport()` in `workflows/redline.ts`), and they REPLAY,
|
|
228
220
|
so a reload mid-run catches up rather than starting from whatever
|
|
229
221
|
arrives next. */}
|
|
230
222
|
<WorkflowProgress runId={run.runId} />
|
|
@@ -233,20 +225,30 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
233
225
|
without a cast. */}
|
|
234
226
|
{run.status === "completed" && (
|
|
235
227
|
<>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{
|
|
242
|
-
|
|
228
|
+
{/* Which of the two stop conditions ended the loop is the one thing a
|
|
229
|
+
reader cannot infer from the round count alone, so it is a fact of
|
|
230
|
+
its own rather than something left to the round count. */}
|
|
231
|
+
<Facts
|
|
232
|
+
size="xs"
|
|
233
|
+
items={[
|
|
234
|
+
`${run.output.words} words`,
|
|
235
|
+
`${run.output.roundsRun} ${plural(run.output.roundsRun, "round")}`,
|
|
236
|
+
run.output.shipped ? "the critic stopped it" : "the round budget stopped it",
|
|
237
|
+
]}
|
|
238
|
+
/>
|
|
243
239
|
<Rounds rounds={run.output.rounds} />
|
|
244
240
|
<article className="whitespace-pre-wrap text-sm leading-relaxed">
|
|
245
241
|
{run.output.draft}
|
|
246
242
|
</article>
|
|
247
243
|
</>
|
|
248
244
|
)}
|
|
249
|
-
{
|
|
245
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error: this
|
|
246
|
+
is the outcome the reader waited minutes for. */}
|
|
247
|
+
{run.status === "failed" && (
|
|
248
|
+
<p role="alert" className="text-red-600">
|
|
249
|
+
{run.error}
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
250
252
|
</section>
|
|
251
253
|
);
|
|
252
254
|
}
|
|
@@ -263,4 +265,4 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
263
265
|
*/
|
|
264
266
|
const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
|
|
265
267
|
|
|
266
|
-
|
|
268
|
+
mountPage({ name: "Redline", component: RedlineDesk });
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
* three functions read more tidily than one.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import type {
|
|
36
|
-
import {
|
|
35
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
36
|
+
import { stepReport } from "@alexkroman1/aai/step";
|
|
37
37
|
import {
|
|
38
38
|
FatalError,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
stepGenerateJsonOrFail,
|
|
40
|
+
stepGenerateOrFail,
|
|
41
41
|
} from "@alexkroman1/aai/step-errors";
|
|
42
42
|
import { countWords } from "@alexkroman1/aai/utils";
|
|
43
43
|
import { z } from "zod";
|
|
@@ -98,7 +98,7 @@ export interface Round {
|
|
|
98
98
|
* the page's render model — and `WorkflowOutputOf<typeof redline>` in
|
|
99
99
|
* `client.tsx` is that type, derived rather than restated.
|
|
100
100
|
*/
|
|
101
|
-
export async function redlineFlow(input: RedlineInput, ctx:
|
|
101
|
+
export async function redlineFlow(input: RedlineInput, ctx: WorkflowContext) {
|
|
102
102
|
// The three `maxAttempts` below were `maxRetries` properties on the functions
|
|
103
103
|
// (3, 5, 3 — retries AFTER the first attempt, so 4, 6, 4 in all). The policy
|
|
104
104
|
// is an argument to the CALL now, which is where it belongs: the same function
|
|
@@ -153,12 +153,12 @@ export async function writeDraft(input: RedlineInput): Promise<string> {
|
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
await
|
|
156
|
+
await stepReport(`Writing the first draft for ${input.audience}.`);
|
|
157
157
|
// No empty-reply guard here or in `reviseDraft`, and that is not an omission:
|
|
158
158
|
// `stepGenerate` already refuses an empty completion, as a RETRYABLE
|
|
159
159
|
// `StepGenerateError` — which is the right answer, and one a hand-written
|
|
160
160
|
// check would have to re-derive.
|
|
161
|
-
const draft = await
|
|
161
|
+
const draft = await stepGenerateOrFail(briefBlock(input), { system: WRITER_SYSTEM });
|
|
162
162
|
return draft.trim();
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -175,14 +175,14 @@ export async function critiqueDraft(
|
|
|
175
175
|
input: RedlineInput,
|
|
176
176
|
round: number,
|
|
177
177
|
): Promise<Critique> {
|
|
178
|
-
await
|
|
178
|
+
await stepReport(`Round ${round}: reading it back critically.`);
|
|
179
179
|
// `stepGenerateJson` owns the fence, the parse, the non-object case and the
|
|
180
180
|
// shape — and throws PLAINLY when any of them misses, unlike the fatal one
|
|
181
181
|
// above: a model that answered with prose may well obey on the next attempt.
|
|
182
|
-
const parsed = await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
182
|
+
const parsed = await stepGenerateJsonOrFail(`${briefBlock(input)}\n\nThe submission:\n${draft}`, {
|
|
183
|
+
schema: CritiqueReply,
|
|
184
|
+
system: CRITIC_SYSTEM,
|
|
185
|
+
});
|
|
186
186
|
|
|
187
187
|
const critique: Critique = {
|
|
188
188
|
verdict: parsed.verdict,
|
|
@@ -192,7 +192,7 @@ export async function critiqueDraft(
|
|
|
192
192
|
score: clampScore(parsed.score),
|
|
193
193
|
notes: parsed.notes.slice(0, MAX_NOTES),
|
|
194
194
|
};
|
|
195
|
-
await
|
|
195
|
+
await stepReport(
|
|
196
196
|
critique.verdict === "ship"
|
|
197
197
|
? `Round ${round}: the critic would ship it (${critique.score}/10).`
|
|
198
198
|
: `Round ${round}: ${critique.notes.length} note(s) to address (${critique.score}/10).`,
|
|
@@ -207,8 +207,8 @@ export async function reviseDraft(
|
|
|
207
207
|
input: RedlineInput,
|
|
208
208
|
round: number,
|
|
209
209
|
): Promise<string> {
|
|
210
|
-
await
|
|
211
|
-
const revised = await
|
|
210
|
+
await stepReport(`Round ${round}: revising.`);
|
|
211
|
+
const revised = await stepGenerateOrFail(
|
|
212
212
|
[
|
|
213
213
|
briefBlock(input),
|
|
214
214
|
`Your current draft:\n${draft}`,
|
|
@@ -241,8 +241,8 @@ export function clampScore(score: number): number {
|
|
|
241
241
|
// There is no local `ask()` any more, and its absence is the point. The SDK
|
|
242
242
|
// classifies the gateway's failure (`StepGenerateError.retryable`) and stops
|
|
243
243
|
// there — whether a terminal failure should burn the step's remaining attempts
|
|
244
|
-
// is the caller's call — so `
|
|
245
|
-
// `
|
|
244
|
+
// is the caller's call — so `stepGenerateOrFail` and
|
|
245
|
+
// `stepGenerateJsonOrFail` (`@alexkroman1/aai/step-errors`) are that call
|
|
246
246
|
// made one way: terminal stays terminal, and a rate limit becomes a
|
|
247
247
|
// `RetryableError` carrying the delay the gateway itself named, which beats
|
|
248
248
|
// `RetryableError`'s own one-second default. Three templates each wrapped the
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// endpointing, barge-in, whether two sentences merged into one turn.
|
|
30
30
|
|
|
31
31
|
import agentDef from "virtual:aai/agent";
|
|
32
|
-
import { stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
32
|
+
import { routeStepFetch, stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
33
33
|
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
34
34
|
import { type EvalToolCall, type EvalWorkflows, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
35
35
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
@@ -132,11 +132,11 @@ type ScriptedSteps = {
|
|
|
132
132
|
function scriptSteps(options: { hold?: boolean } = {}): ScriptedSteps {
|
|
133
133
|
const gate = Promise.withResolvers<void>();
|
|
134
134
|
const model = stubGatewayRoute(MODEL_SCRIPT);
|
|
135
|
+
// Throwing on an unrecognised request is `routeStepFetch`'s default and is
|
|
136
|
+
// what this file wants: every step here is a model call.
|
|
137
|
+
const route = routeStepFetch([model.route]);
|
|
135
138
|
const stub = installStubStepFetch(async (request) => {
|
|
136
|
-
const answered =
|
|
137
|
-
if (answered === undefined) {
|
|
138
|
-
throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
|
|
139
|
-
}
|
|
139
|
+
const answered = route(request);
|
|
140
140
|
// `model.calls` has already recorded this one, so a length of 1 IS the first
|
|
141
141
|
// answer — and holding after the route rather than before it keeps the reply
|
|
142
142
|
// this returns the one the script owed that call.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* exported async function, so its prompt handling, its parsing and its
|
|
17
17
|
* `FatalError` guards are all testable without an engine.
|
|
18
18
|
*
|
|
19
|
-
* The BODY is driven here only through `
|
|
19
|
+
* The BODY is driven here only through `createWorkflowContext`, which records what
|
|
20
20
|
* it asked for and replays nothing. That is a choice rather than a limit now:
|
|
21
21
|
* `runWorkflow` from `@alexkroman1/aai-runtime/testing` will run this body on
|
|
22
22
|
* the real engine, and `link-digest` is the template that shows it — three
|
|
@@ -31,13 +31,16 @@ import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
|
31
31
|
import {
|
|
32
32
|
createRunSnapshot,
|
|
33
33
|
createToolContext,
|
|
34
|
-
|
|
34
|
+
createWorkflowContext,
|
|
35
35
|
parseSchemaInput,
|
|
36
36
|
type StubGatewayCall,
|
|
37
37
|
schemaInputIssues,
|
|
38
38
|
toolRunner,
|
|
39
39
|
} from "@alexkroman1/aai/testing";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
installStubWorkflows,
|
|
42
|
+
installStubGateway as stubGateway,
|
|
43
|
+
} from "@alexkroman1/aai/testing/vitest";
|
|
41
44
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
42
45
|
import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
|
|
43
46
|
import { runWorkflow } from "@alexkroman1/aai-runtime/testing";
|
|
@@ -89,7 +92,7 @@ const run = toolRunner(agentDef);
|
|
|
89
92
|
*
|
|
90
93
|
* Returned WITHOUT a cast, which is the property worth keeping: a cast would
|
|
91
94
|
* also stop reporting the day `WorkflowClient` grows a method, and this stub is
|
|
92
|
-
* how the template's tools reach the client at all. `
|
|
95
|
+
* how the template's tools reach the client at all. `installStubWorkflows`
|
|
93
96
|
* (`@alexkroman1/aai/testing/vitest`) is what keeps that affordable — a `vi.fn`
|
|
94
97
|
* per method over one `runs` list, filling in what this desk does not drive, so
|
|
95
98
|
* the day the client does grow a method only the tests using it change.
|
|
@@ -101,7 +104,7 @@ function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
|
|
|
101
104
|
// Name only: `WorkflowSummary.description` is optional, so passing this
|
|
102
105
|
// desk's through would mean handing `description: undefined` to a field that
|
|
103
106
|
// does not accept it. Nothing here reads the description anyway.
|
|
104
|
-
return
|
|
107
|
+
return installStubWorkflows({ runs, names: ["research"] });
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
describe("the agent declares its workflow", () => {
|
|
@@ -445,7 +448,7 @@ describe("the steps that research", () => {
|
|
|
445
448
|
// could not have said differently.
|
|
446
449
|
// `planAngles`' result is what the fan-out iterates, so it is supplied
|
|
447
450
|
// rather than run — the rest of the body needs no page and no model.
|
|
448
|
-
const ctx =
|
|
451
|
+
const ctx = createWorkflowContext({
|
|
449
452
|
runSteps: false,
|
|
450
453
|
// Every step the body READS needs a value: with `runSteps: false` nothing
|
|
451
454
|
// runs, so this is the skeleton of a run rather than a run. That is the
|
|
@@ -556,7 +559,7 @@ describe("the steps that research", () => {
|
|
|
556
559
|
/**
|
|
557
560
|
* `researchFlow` itself, on the real replay engine.
|
|
558
561
|
*
|
|
559
|
-
* The block above drives this body through `
|
|
562
|
+
* The block above drives this body through `createWorkflowContext`, which records
|
|
560
563
|
* what it ASKED for and replays nothing — right for the retry policy and the
|
|
561
564
|
* step order, and silent about the desk's actual promise: **answer the caller
|
|
562
565
|
* now, finish the work later**. `runWorkflow`
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*
|
|
36
36
|
* ## Every stage REPORTS, and the report goes two places
|
|
37
37
|
*
|
|
38
|
-
* `
|
|
38
|
+
* `stepReport()` (`@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,9 +49,9 @@
|
|
|
49
49
|
* researcher CONCLUDED, which is exactly what the step returns.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import type {
|
|
53
|
-
import { mapConcurrent,
|
|
54
|
-
import {
|
|
52
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
53
|
+
import { mapConcurrent, stepReport } from "@alexkroman1/aai/step";
|
|
54
|
+
import { stepGenerateJsonOrFail, stepGenerateOrFail } from "@alexkroman1/aai/step-errors";
|
|
55
55
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
56
56
|
import { errorMessage, isToolFailure, plural } from "@alexkroman1/aai/utils";
|
|
57
57
|
import { z } from "zod";
|
|
@@ -107,7 +107,7 @@ export type Source = { title: string; url: string };
|
|
|
107
107
|
|
|
108
108
|
// ---- What each stage's model call has to come back as ------------------------
|
|
109
109
|
//
|
|
110
|
-
// `
|
|
110
|
+
// `stepGenerateJsonOrFail` validates against these, so a reply that missed
|
|
111
111
|
// is a plain throw and therefore a retry — where the hand-rolled `askJson<T>()`
|
|
112
112
|
// this replaces returned a value the compiler believed and nothing checked. They are
|
|
113
113
|
// deliberately LENIENT wherever the old hand-written coercion was: a model that
|
|
@@ -212,7 +212,7 @@ export type Findings = {
|
|
|
212
212
|
*/
|
|
213
213
|
export async function researchFlow(
|
|
214
214
|
input: { topic: string; requestedBy: string },
|
|
215
|
-
ctx:
|
|
215
|
+
ctx: WorkflowContext,
|
|
216
216
|
) {
|
|
217
217
|
const brief = await ctx.step("writeBrief", () => writeBrief(input.topic));
|
|
218
218
|
const angles = await ctx.step("planAngles", () => planAngles(brief));
|
|
@@ -270,11 +270,11 @@ export async function researchFlow(
|
|
|
270
270
|
* later model call.
|
|
271
271
|
*/
|
|
272
272
|
export async function writeBrief(topic: string): Promise<Brief> {
|
|
273
|
-
await
|
|
274
|
-
const parsed = await
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
273
|
+
await stepReport(`Working out what "${topic}" is really asking.`);
|
|
274
|
+
const parsed = await stepGenerateJsonOrFail(`Research request, as the caller said it: ${topic}`, {
|
|
275
|
+
system: BRIEF_SYSTEM,
|
|
276
|
+
schema: BriefReply,
|
|
277
|
+
});
|
|
278
278
|
return { brief: parsed.brief || topic, criteria: parsed.criteria.slice(0, MAX_ANGLES) };
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -286,7 +286,7 @@ export async function writeBrief(topic: string): Promise<Brief> {
|
|
|
286
286
|
* asking the model again and getting a different one.
|
|
287
287
|
*/
|
|
288
288
|
export async function planAngles(brief: Brief): Promise<string[]> {
|
|
289
|
-
const parsed = await
|
|
289
|
+
const parsed = await stepGenerateJsonOrFail(briefText(brief), {
|
|
290
290
|
system: PLAN_SYSTEM,
|
|
291
291
|
schema: AnglesReply,
|
|
292
292
|
});
|
|
@@ -294,10 +294,10 @@ export async function planAngles(brief: Brief): Promise<string[]> {
|
|
|
294
294
|
if (angles.length === 0) {
|
|
295
295
|
// Nothing to fan out over is a plan failure, not an empty result: the brief
|
|
296
296
|
// itself is the one angle that is always available.
|
|
297
|
-
await
|
|
297
|
+
await stepReport("No angles came back; researching the brief itself.");
|
|
298
298
|
return [brief.brief];
|
|
299
299
|
}
|
|
300
|
-
await
|
|
300
|
+
await stepReport(`Researching ${angles.length} ${plural(angles.length, "angle")}.`);
|
|
301
301
|
return angles;
|
|
302
302
|
}
|
|
303
303
|
|
|
@@ -309,7 +309,7 @@ export async function planAngles(brief: Brief): Promise<string[]> {
|
|
|
309
309
|
* at the end, which is where it becomes small enough to journal.
|
|
310
310
|
*/
|
|
311
311
|
export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
312
|
-
await
|
|
312
|
+
await stepReport(`Looking into: ${angle}`);
|
|
313
313
|
const seen: string[] = [];
|
|
314
314
|
const sources: Source[] = [];
|
|
315
315
|
|
|
@@ -323,7 +323,7 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
|
323
323
|
continue;
|
|
324
324
|
}
|
|
325
325
|
if (action.action === "read" && action.url) {
|
|
326
|
-
await
|
|
326
|
+
await stepReport(`Reading ${hostname(action.url)}`);
|
|
327
327
|
seen.push(`PAGE ${action.url}\n${await readPage(action.url)}`);
|
|
328
328
|
continue;
|
|
329
329
|
}
|
|
@@ -335,8 +335,6 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
|
335
335
|
return await compress(angle, seen, sources);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
/** UNUSED-ORPHAN-MARKER Retries beyond the default: the far side is a search engine and a model. */
|
|
339
|
-
|
|
340
338
|
/**
|
|
341
339
|
* The supervisor's second look.
|
|
342
340
|
*
|
|
@@ -346,12 +344,12 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
|
346
344
|
*/
|
|
347
345
|
export async function findGaps(brief: Brief, notes: readonly Note[]): Promise<string[]> {
|
|
348
346
|
if (notes.length === 0) return [];
|
|
349
|
-
const parsed = await
|
|
347
|
+
const parsed = await stepGenerateJsonOrFail(
|
|
350
348
|
`${briefText(brief)}\n\nWhat came back:\n${notes.map(noteText).join("\n\n")}`,
|
|
351
349
|
{ system: GAPS_SYSTEM, schema: AnglesReply },
|
|
352
350
|
);
|
|
353
351
|
const gaps = parsed.angles.slice(0, MAX_ANGLES - 1);
|
|
354
|
-
await
|
|
352
|
+
await stepReport(
|
|
355
353
|
gaps.length === 0
|
|
356
354
|
? "The brief is covered; writing it up."
|
|
357
355
|
: `Following up ${gaps.length} ${plural(gaps.length, "gap")}.`,
|
|
@@ -371,12 +369,12 @@ export async function writeReport(
|
|
|
371
369
|
brief: Brief,
|
|
372
370
|
notes: readonly Note[],
|
|
373
371
|
): Promise<{ report: string; summary: string }> {
|
|
374
|
-
await
|
|
375
|
-
const written = await
|
|
372
|
+
await stepReport(`Writing up ${notes.length} ${plural(notes.length, "angle")}.`);
|
|
373
|
+
const written = await stepGenerateOrFail(
|
|
376
374
|
`${briefText(brief)}\n\nFindings:\n${notes.map(noteText).join("\n\n")}`,
|
|
377
375
|
{ system: REPORT_SYSTEM },
|
|
378
376
|
);
|
|
379
|
-
const summary = await
|
|
377
|
+
const summary = await stepGenerateOrFail(`Topic: ${topic}\n\nReport:\n${written}`, {
|
|
380
378
|
system: BRIEF_SUMMARY_SYSTEM,
|
|
381
379
|
});
|
|
382
380
|
return { report: written, summary };
|
|
@@ -390,7 +388,7 @@ export async function writeReport(
|
|
|
390
388
|
* parameters carry `_` for the same reason.
|
|
391
389
|
*/
|
|
392
390
|
export async function file(_requestedBy: string, _topic: string): Promise<string> {
|
|
393
|
-
await
|
|
391
|
+
await stepReport("Filing the findings.");
|
|
394
392
|
return "filed";
|
|
395
393
|
}
|
|
396
394
|
|
|
@@ -406,7 +404,7 @@ async function nextAction(
|
|
|
406
404
|
seen: readonly string[],
|
|
407
405
|
left: number,
|
|
408
406
|
): Promise<Action> {
|
|
409
|
-
return await
|
|
407
|
+
return await stepGenerateJsonOrFail(
|
|
410
408
|
`${briefText(brief)}\n\nYour angle: ${angle}\n` +
|
|
411
409
|
`Actions left: ${left}\n\n` +
|
|
412
410
|
(seen.length === 0 ? "You have not looked at anything yet." : seen.join("\n\n")),
|
|
@@ -423,7 +421,7 @@ async function nextAction(
|
|
|
423
421
|
* pages exist" and gets run again, differently worded, until the budget is gone.
|
|
424
422
|
*/
|
|
425
423
|
async function search(query: string): Promise<{ summary: string; sources: Source[] }> {
|
|
426
|
-
await
|
|
424
|
+
await stepReport(`Searching: ${query}`);
|
|
427
425
|
try {
|
|
428
426
|
const results = await webSearch<{ results?: { title?: string; url?: string }[] }>({
|
|
429
427
|
query,
|
|
@@ -445,7 +443,7 @@ async function search(query: string): Promise<{ summary: string; sources: Source
|
|
|
445
443
|
};
|
|
446
444
|
} catch (err: unknown) {
|
|
447
445
|
const summary = `That search failed: ${errorMessage(err)}`;
|
|
448
|
-
await
|
|
446
|
+
await stepReport(summary);
|
|
449
447
|
return { summary, sources: [] };
|
|
450
448
|
}
|
|
451
449
|
}
|
|
@@ -475,7 +473,7 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
|
|
|
475
473
|
if (seen.length === 0) {
|
|
476
474
|
return { angle, findings: "Nothing was found on this angle.", sources: [] };
|
|
477
475
|
}
|
|
478
|
-
const parsed = await
|
|
476
|
+
const parsed = await stepGenerateJsonOrFail(`Angle: ${angle}\n\n${seen.join("\n\n")}`, {
|
|
479
477
|
system: COMPRESS_SYSTEM,
|
|
480
478
|
schema: CompressReply,
|
|
481
479
|
});
|
|
@@ -492,11 +490,11 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
|
|
|
492
490
|
//
|
|
493
491
|
// There is none left, and its absence is the point. This desk carried an `ask()`
|
|
494
492
|
// and an `askJson()` whose whole body was `.catch(throwStepError)`; the SDK's
|
|
495
|
-
// `
|
|
493
|
+
// `stepGenerateOrFail` and `stepGenerateJsonOrFail`
|
|
496
494
|
// (`@alexkroman1/aai/step-errors`) ARE that call — the `/step` one with the
|
|
497
495
|
// gateway's verdict classified, so a terminal failure stays terminal and a rate
|
|
498
496
|
// limit becomes a `RetryableError` carrying the delay the gateway itself named.
|
|
499
|
-
// `
|
|
497
|
+
// `stepGenerateJsonOrFail` also owns the four things every JSON stage used
|
|
500
498
|
// to re-derive — unwrap the fence, parse, reject a non-object, check the shape —
|
|
501
499
|
// and throws PLAINLY when any of them misses, which is what makes a malformed
|
|
502
500
|
// reply a retry rather than a failure.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
3
|
-
import { createToolContext,
|
|
3
|
+
import { createToolContext, expectToolOk } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
import type { AuthResult } from "./authenticate.ts";
|
|
6
6
|
import type { StagedResult } from "./pending.ts";
|
|
@@ -32,7 +32,7 @@ function makeCtx(): ToolContext {
|
|
|
32
32
|
/** A context already authenticated as `userId`, via the real tool. */
|
|
33
33
|
async function authedCtx(email: string): Promise<ToolContext> {
|
|
34
34
|
const ctx = makeCtx();
|
|
35
|
-
|
|
35
|
+
expectToolOk<AuthResult>(await findUserIdByEmail.execute({ email }, ctx));
|
|
36
36
|
return ctx;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -47,19 +47,19 @@ async function authedCtx(email: string): Promise<ToolContext> {
|
|
|
47
47
|
* would let this gate pass while carrying nothing to confirm.
|
|
48
48
|
*/
|
|
49
49
|
async function confirmed<R = unknown>(staged: unknown, ctx: ToolContext): Promise<R> {
|
|
50
|
-
const stage =
|
|
50
|
+
const stage = expectToolOk<StagedResult>(staged);
|
|
51
51
|
// A throw rather than an `expect`, which biome's `noMisplacedAssertion`
|
|
52
52
|
// rightly refuses outside a test body. A staged change with no sentence to
|
|
53
53
|
// read back is the one shape that would let the gate pass while carrying
|
|
54
54
|
// nothing to confirm, so it stops the spec here rather than downstream.
|
|
55
55
|
if (stage.read_back.length === 0) throw new Error(`${stage.staged} staged with no readback`);
|
|
56
|
-
return
|
|
56
|
+
return expectToolOk<R>(await confirmChange.execute({}, ctx));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
describe("authentication", () => {
|
|
60
60
|
test("find_user_id_by_email is case-insensitive and authenticates the session", async () => {
|
|
61
61
|
const ctx = makeCtx();
|
|
62
|
-
const result =
|
|
62
|
+
const result = expectToolOk<AuthResult>(
|
|
63
63
|
await findUserIdByEmail.execute({ email: "OLIVIA.ITO5204@EXAMPLE.COM" }, ctx),
|
|
64
64
|
);
|
|
65
65
|
expect(result.user_id).toBe("olivia_ito_3591");
|
|
@@ -81,7 +81,7 @@ describe("authentication", () => {
|
|
|
81
81
|
|
|
82
82
|
test("find_user_id_by_name_zip is case-insensitive on names and exact on zip", async () => {
|
|
83
83
|
const ctx = makeCtx();
|
|
84
|
-
const found =
|
|
84
|
+
const found = expectToolOk<AuthResult>(
|
|
85
85
|
await findUserIdByNameZip.execute(
|
|
86
86
|
{ first_name: "aarav", last_name: "ANDERSON", zip: "19031" },
|
|
87
87
|
ctx,
|
|
@@ -97,13 +97,13 @@ describe("authentication", () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
test("two customers share a first name — the zip is what separates them", async () => {
|
|
100
|
-
const a =
|
|
100
|
+
const a = expectToolOk<AuthResult>(
|
|
101
101
|
await findUserIdByNameZip.execute(
|
|
102
102
|
{ first_name: "Aarav", last_name: "Anderson", zip: "19031" },
|
|
103
103
|
makeCtx(),
|
|
104
104
|
),
|
|
105
105
|
);
|
|
106
|
-
const b =
|
|
106
|
+
const b = expectToolOk<AuthResult>(
|
|
107
107
|
await findUserIdByNameZip.execute(
|
|
108
108
|
{ first_name: "Aarav", last_name: "Gonzalez", zip: "78268" },
|
|
109
109
|
makeCtx(),
|
|
@@ -172,7 +172,7 @@ interface ProductTypesResult {
|
|
|
172
172
|
describe("read tools", () => {
|
|
173
173
|
test("get_user_details returns the caller's profile with gift-card balances", async () => {
|
|
174
174
|
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
175
|
-
const result =
|
|
175
|
+
const result = expectToolOk<UserDetailsResult>(
|
|
176
176
|
await getUserDetails.execute({ user_id: "olivia_ito_3591" }, ctx),
|
|
177
177
|
);
|
|
178
178
|
expect(result.email).toBe("olivia.ito5204@example.com");
|
|
@@ -189,7 +189,7 @@ describe("read tools", () => {
|
|
|
189
189
|
|
|
190
190
|
test("get_order_details resolves shorthand and sets focus", async () => {
|
|
191
191
|
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
192
|
-
const result =
|
|
192
|
+
const result = expectToolOk<OrderDetailsResult>(
|
|
193
193
|
await getOrderDetails.execute({ order_id: "the delivered one" }, ctx),
|
|
194
194
|
);
|
|
195
195
|
expect(result.order_id).toBe("#W5866402");
|
|
@@ -203,7 +203,7 @@ describe("read tools", () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
test("get_product_details lists variants and needs no authentication", async () => {
|
|
206
|
-
const result =
|
|
206
|
+
const result = expectToolOk<ProductDetailsResult>(
|
|
207
207
|
await getProductDetails.execute({ product_id: "9832717871" }, makeCtx()),
|
|
208
208
|
);
|
|
209
209
|
expect(result.name).toBe("Tea Kettle");
|
|
@@ -217,7 +217,7 @@ describe("read tools", () => {
|
|
|
217
217
|
});
|
|
218
218
|
|
|
219
219
|
test("get_item_details resolves an item without knowing its product", async () => {
|
|
220
|
-
const result =
|
|
220
|
+
const result = expectToolOk<ItemDetailsResult>(
|
|
221
221
|
await getItemDetails.execute({ item_id: "3909406921" }, makeCtx()),
|
|
222
222
|
);
|
|
223
223
|
expect(result.price).toBe(98.25);
|
|
@@ -225,7 +225,9 @@ describe("read tools", () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
test("list_all_product_types returns all 50, sorted by name", async () => {
|
|
228
|
-
const result =
|
|
228
|
+
const result = expectToolOk<ProductTypesResult>(
|
|
229
|
+
await listAllProductTypes.execute({}, makeCtx()),
|
|
230
|
+
);
|
|
229
231
|
expect(Object.keys(result.products)).toHaveLength(50);
|
|
230
232
|
const names = Object.keys(result.products);
|
|
231
233
|
// localeCompare, not the default lexicographic sort — the tool sorts for a
|
|
@@ -1148,7 +1150,7 @@ interface TransferResult {
|
|
|
1148
1150
|
|
|
1149
1151
|
describe("transfer_to_human_agents", () => {
|
|
1150
1152
|
test("works without authentication — the escape hatch cannot be gated", async () => {
|
|
1151
|
-
const result =
|
|
1153
|
+
const result = expectToolOk<TransferResult>(
|
|
1152
1154
|
await transferToHumanAgents.execute(
|
|
1153
1155
|
{ summary: "Caller wants to dispute a charge from 2019." },
|
|
1154
1156
|
makeCtx(),
|
|
@@ -1160,7 +1162,7 @@ describe("transfer_to_human_agents", () => {
|
|
|
1160
1162
|
|
|
1161
1163
|
test("the handoff is terminal, so the call cannot be worked afterwards", async () => {
|
|
1162
1164
|
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
1163
|
-
|
|
1165
|
+
expectToolOk(await transferToHumanAgents.execute({ summary: "wants a human" }, ctx));
|
|
1164
1166
|
|
|
1165
1167
|
const at = callFlow.position(ctx);
|
|
1166
1168
|
expect(at.state).toBe("transferred");
|