@alexkroman1/aai-cli 8.2.0 → 9.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/_dev-env.d.ts +15 -2
- package/dist/{_dev-server-Dt3TCjhj.mjs → _dev-server-BAR8Ya43.mjs} +182 -55
- package/dist/_dev-server.d.ts +5 -30
- package/dist/_dev-typecheck.d.ts +38 -0
- package/dist/_dev-vite-config.d.ts +74 -0
- package/dist/_e2e-test-utils.d.ts +33 -1
- package/dist/_init-BNotRgV1.mjs +163 -0
- package/dist/_resource-commands.d.ts +0 -1
- package/dist/{_slug-api-z-USxPtL.mjs → _slug-api--ZBmIGH-.mjs} +1 -1
- package/dist/{_studio-DR5yzZ1T.mjs → _studio-CKrsixd0.mjs} +2 -1
- package/dist/_studio.d.ts +3 -1
- package/dist/{build-BBxf8vom.mjs → build-DvDdSwAS.mjs} +1 -1
- package/dist/cli.mjs +26 -97
- package/dist/{delete-Dl_LSfvI.mjs → delete-8pjH8n8n.mjs} +3 -2
- package/dist/{deploy-DRC8jWr8.mjs → deploy-CjU3e-3P.mjs} +4 -4
- package/dist/{dev-QuuOOUtv.mjs → dev-dFzdvwa7.mjs} +3 -2
- package/dist/dev.d.ts +2 -0
- package/dist/{eval-DfQ8cm5p.mjs → eval-DwNwdqmS.mjs} +1 -1
- package/dist/{init-CUo87gcI.mjs → init-DK7GpIF9.mjs} +7 -6
- package/dist/{logs-CQ2T46dr.mjs → logs-D2Yhqe2w.mjs} +2 -2
- package/dist/scaffold/.env.example +7 -4
- package/dist/scaffold/CLAUDE.md +152 -97
- package/dist/scaffold/global.d.ts +12 -0
- package/dist/scaffold/package.json +4 -4
- package/dist/scaffold/server.mjs +49 -2
- package/dist/scaffold/tsconfig.json +1 -0
- package/dist/scaffold/vitest.config.ts +24 -0
- package/dist/{secret-Ct34kdgU.mjs → secret-CwgDLpSy.mjs} +2 -2
- package/dist/{studio-CzTzPqe1.mjs → studio-D3-HWIfk.mjs} +7 -5
- package/dist/templates/briefing-desk/agent.test.ts +3 -15
- package/dist/templates/briefing-desk/shared.ts +3 -3
- package/dist/templates/call-audit/agent.eval.test.ts +8 -1
- package/dist/templates/call-audit/agent.test.ts +0 -1
- package/dist/templates/call-audit/agent.ts +4 -3
- package/dist/templates/call-audit/client.tsx +4 -7
- package/dist/templates/call-audit/workflows/audit.ts +10 -1
- package/dist/templates/code-interpreter/agent.eval.test.ts +14 -43
- package/dist/templates/dispatch-center/agent.eval.test.ts +16 -26
- package/dist/templates/embedded-assets/agent.eval.test.ts +6 -16
- package/dist/templates/embedded-assets/agent.test.ts +3 -16
- package/dist/templates/health-assistant/agent.eval.test.ts +5 -15
- package/dist/templates/health-assistant/agent.test.ts +3 -14
- package/dist/templates/infocom-adventure/agent.eval.test.ts +6 -16
- package/dist/templates/infocom-adventure/agent.test.ts +3 -17
- package/dist/templates/link-digest/agent.test.ts +0 -14
- package/dist/templates/link-digest/agent.ts +9 -5
- package/dist/templates/link-digest/client.tsx +8 -6
- package/dist/templates/math-buddy/agent.eval.test.ts +11 -40
- package/dist/templates/night-owl/agent.eval.test.ts +15 -44
- package/dist/templates/night-owl/agent.test.ts +2 -15
- package/dist/templates/personal-finance/agent.eval.test.ts +10 -39
- package/dist/templates/pipeline-simple/agent.test.ts +73 -0
- package/dist/templates/pizza-ordering/agent.eval.test.ts +2 -17
- package/dist/templates/pizza-ordering/agent.test.ts +2 -17
- package/dist/templates/plan-and-execute/agent.eval.test.ts +2 -12
- package/dist/templates/plan-and-execute/agent.test.ts +3 -23
- package/dist/templates/plan-and-execute/shared.ts +1 -1
- package/dist/templates/podcast-digest/agent.test.ts +0 -1
- package/dist/templates/podcast-digest/client.tsx +3 -6
- package/dist/templates/podcast-digest/workflows/digest.ts +16 -8
- package/dist/templates/recap-workflow/agent.eval.test.ts +7 -14
- package/dist/templates/recap-workflow/agent.test.ts +3 -18
- package/dist/templates/recap-workflow/agent.ts +7 -5
- package/dist/templates/redline/agent.test.ts +0 -13
- package/dist/templates/redline/agent.ts +3 -2
- package/dist/templates/redline/client.tsx +9 -4
- package/dist/templates/research-workflow/agent.eval.test.ts +7 -14
- package/dist/templates/research-workflow/agent.test.ts +3 -15
- package/dist/templates/research-workflow/agent.ts +7 -5
- package/dist/templates/research-workflow/workflows/research.ts +8 -2
- package/dist/templates/retail/agent.eval.test.ts +10 -25
- package/dist/templates/retail/registry.test.ts +16 -13
- package/dist/templates/solo-rpg/agent.eval.test.ts +5 -15
- package/dist/templates/solo-rpg/agent.test.ts +17 -124
- package/dist/templates/solo-rpg/shared.ts +13 -61
- package/dist/templates/spoken-summary/agent.test.ts +0 -1
- package/dist/templates/spoken-summary/agent.ts +5 -4
- package/dist/templates/spoken-summary/client.tsx +4 -7
- package/dist/templates/support-line/agent.eval.test.ts +2 -12
- package/dist/templates/support-line/agent.test.ts +3 -22
- package/dist/templates/transcription-workflow/agent.ts +4 -4
- package/dist/templates/transcription-workflow/client.tsx +5 -5
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +10 -1
- package/dist/templates/travel-concierge/agent.eval.test.ts +5 -15
- package/dist/templates/travel-concierge/agent.test.ts +3 -23
- package/dist/templates/web-researcher/agent.eval.test.ts +2 -5
- package/dist/test-CvwgeVSQ.mjs +181 -0
- package/dist/test.d.ts +33 -2
- package/package.json +4 -4
- package/dist/_init-BhEfQ6Yi.mjs +0 -93
- package/dist/storage-i_79ZwAR.mjs +0 -66
- package/dist/storage.d.ts +0 -22
- package/dist/templates/solo-rpg/tools/load_game.ts +0 -37
- package/dist/templates/solo-rpg/tools/save_game.ts +0 -32
- package/dist/test-BvyT6sAc.mjs +0 -104
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type DeepReadonly, dialog, sessionSlot
|
|
2
|
-
import { z } from "zod";
|
|
1
|
+
import { type DeepReadonly, dialog, sessionSlot } from "@alexkroman1/aai";
|
|
3
2
|
|
|
4
3
|
// ── Tuning Constants ─────────────────────────────────────────────────────────
|
|
5
4
|
export const MAX_SESSION_LOG = 50;
|
|
@@ -390,7 +389,7 @@ export const gameProjection = gameSlot.projection((game) => game);
|
|
|
390
389
|
* `storyFlow.reset` — and it is what lets `gameOver` be a genuinely `final`
|
|
391
390
|
* state, which is the whole point of having one: `position().done` means the
|
|
392
391
|
* story ended, and XState delivers no events to a done actor, so an `on: {
|
|
393
|
-
* SETUP }` there would have been dead config that looked live.
|
|
392
|
+
* SETUP }` there would have been dead config that looked live. A resumed run
|
|
394
393
|
* resets for the same reason. `SETUP` therefore appears once, on the only state
|
|
395
394
|
* that can be transitioned out of.
|
|
396
395
|
*/
|
|
@@ -450,65 +449,18 @@ export const storyFlow = dialog("story", storySpec);
|
|
|
450
449
|
*/
|
|
451
450
|
export type FrozenGameState = DeepReadonly<GameState>;
|
|
452
451
|
|
|
453
|
-
// ──
|
|
454
|
-
// save_game / load_game
|
|
455
|
-
//
|
|
456
|
-
//
|
|
452
|
+
// ── No cross-session saves, and why ─────────────────────────────────────────
|
|
453
|
+
// `save_game` / `load_game` stood here, keyed by slot name and backed by an
|
|
454
|
+
// `app_state` table through `ctx.db`. Both are gone with `ctx.db` itself: the
|
|
455
|
+
// platform provides tool code no database, and a template cannot reach one — the
|
|
456
|
+
// scaffold ships no Postgres client, and shipped template code cannot import
|
|
457
|
+
// `@alexkroman1/aai-runtime` (templates type-check under the scaffold tsconfig,
|
|
458
|
+
// which that package's source is not clean under).
|
|
457
459
|
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
export function saveSlotKey(slot?: string): string {
|
|
463
|
-
return `save:${slot ?? "autosave"}`;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/** The slot-name grammar, shared by save_game and load_game so a name that
|
|
467
|
-
* can be saved can always be loaded. */
|
|
468
|
-
export const saveSlotParam = z
|
|
469
|
-
.string()
|
|
470
|
-
.regex(/^[A-Za-z0-9_-]{1,32}$/, "letters, digits, dashes, underscores; max 32 chars")
|
|
471
|
-
.describe("Save slot name, defaults to autosave")
|
|
472
|
-
.optional();
|
|
473
|
-
|
|
474
|
-
const ENSURE_APP_STATE = `create table if not exists app_state (
|
|
475
|
-
key text primary key,
|
|
476
|
-
value jsonb not null,
|
|
477
|
-
updated_at timestamptz not null default now()
|
|
478
|
-
)`;
|
|
479
|
-
|
|
480
|
-
// Memoized per process (each session's tools run in a fresh sandbox, so this
|
|
481
|
-
// is at most one round-trip per session); a failure clears the memo so the
|
|
482
|
-
// next call retries instead of caching the error forever.
|
|
483
|
-
let ensureP: Promise<unknown> | null = null;
|
|
484
|
-
function ensureTable(ctx: ToolContext): Promise<unknown> {
|
|
485
|
-
ensureP ??= ctx.db.query(ENSURE_APP_STATE).catch((err) => {
|
|
486
|
-
ensureP = null;
|
|
487
|
-
throw err;
|
|
488
|
-
});
|
|
489
|
-
return ensureP;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/** Read one saved value. jsonb columns come back from the postgres driver
|
|
493
|
-
* already parsed, so the value needs no JSON.parse here. */
|
|
494
|
-
export async function loadState<T>(ctx: ToolContext, key: string): Promise<T | null> {
|
|
495
|
-
await ensureTable(ctx);
|
|
496
|
-
const rows = await ctx.db.query<{ value: T }>("select value from app_state where key = $1", [
|
|
497
|
-
key,
|
|
498
|
-
]);
|
|
499
|
-
return rows[0]?.value ?? null;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/** Upsert one value. Serialized explicitly and cast with `::jsonb` so the
|
|
503
|
-
* write is driver-agnostic about object parameters. */
|
|
504
|
-
export async function saveState(ctx: ToolContext, key: string, value: unknown): Promise<void> {
|
|
505
|
-
await ensureTable(ctx);
|
|
506
|
-
await ctx.db.query(
|
|
507
|
-
"insert into app_state (key, value, updated_at) values ($1, $2::jsonb, now()) " +
|
|
508
|
-
"on conflict (key) do update set value = excluded.value, updated_at = now()",
|
|
509
|
-
[key, JSON.stringify(value)],
|
|
510
|
-
);
|
|
511
|
-
}
|
|
460
|
+
// So this adventure is SINGLE-SESSION: everything lives in `sessionSlot`s and
|
|
461
|
+
// ends when the call does. An author who wants saves adds a client of their own
|
|
462
|
+
// (`postgres`, `pg`, a provider SDK) and a `DATABASE_URL` secret — which is the
|
|
463
|
+
// supported pattern, just not one a shipped template can demonstrate.
|
|
512
464
|
|
|
513
465
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
514
466
|
export function d(sides: number): number {
|
|
@@ -54,7 +54,6 @@ describe("the declaration", () => {
|
|
|
54
54
|
// The page calls `api.start("spokenSummary", …)`, so a rename here is a
|
|
55
55
|
// runtime 400 rather than a compile error. This is what pins it.
|
|
56
56
|
expect(Object.keys(agentDef.workflows ?? {})).toEqual(["spokenSummary"]);
|
|
57
|
-
expect(agentDef.page).toBe("static");
|
|
58
57
|
});
|
|
59
58
|
|
|
60
59
|
test("declares no providers and exactly the one credential its steps read", () => {
|
|
@@ -41,10 +41,11 @@
|
|
|
41
41
|
* three services this uses: transcription, the LLM Gateway, and the voice.
|
|
42
42
|
* `requiredEnv` below is what makes a deploy check for it rather than letting
|
|
43
43
|
* the first run find out.
|
|
44
|
-
* - **
|
|
45
|
-
*
|
|
46
|
-
* workflow apps: an upload's record is a row, and this
|
|
47
|
-
* BOTH ends — the recording coming in and the summary
|
|
44
|
+
* - **A `DATABASE_URL`** — a secret when deployed, `.env` under `aai dev`, and a
|
|
45
|
+
* Postgres you bring since the platform provisions none. REQUIRED here, and
|
|
46
|
+
* more so than for most workflow apps: an upload's record is a row, and this
|
|
47
|
+
* app uses uploads at BOTH ends — the recording coming in and the summary
|
|
48
|
+
* going out.
|
|
48
49
|
*
|
|
49
50
|
* ## The recording is UPLOADED, and the run carries its id
|
|
50
51
|
*
|
|
@@ -37,7 +37,6 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
37
37
|
// nothing — and it is what stops this file restating a shape
|
|
38
38
|
// `workflows/summarize.ts` already declares.
|
|
39
39
|
import { formatDuration } from "@alexkroman1/aai/utils";
|
|
40
|
-
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
41
40
|
import {
|
|
42
41
|
createWorkflowApi,
|
|
43
42
|
Form,
|
|
@@ -51,9 +50,6 @@ import {
|
|
|
51
50
|
} from "@alexkroman1/aai-ui";
|
|
52
51
|
import type { spokenSummary } from "./agent.ts";
|
|
53
52
|
|
|
54
|
-
/** What a completed run reports, derived from the workflow rather than restated. */
|
|
55
|
-
type Summary = WorkflowOutputOf<typeof spokenSummary>;
|
|
56
|
-
|
|
57
53
|
/**
|
|
58
54
|
* The workflow's name, as a page starts a run by one.
|
|
59
55
|
*
|
|
@@ -88,8 +84,9 @@ function captionsUrl(text: string, durationMs: number): string {
|
|
|
88
84
|
export function App() {
|
|
89
85
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
90
86
|
// `run.output` instead of `unknown`.
|
|
91
|
-
const {
|
|
92
|
-
|
|
87
|
+
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
|
|
88
|
+
typeof spokenSummary
|
|
89
|
+
>(WORKFLOW, { api });
|
|
93
90
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
94
91
|
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
95
92
|
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
@@ -105,7 +102,7 @@ export function App() {
|
|
|
105
102
|
</p>
|
|
106
103
|
</header>
|
|
107
104
|
|
|
108
|
-
<Form onSubmit={
|
|
105
|
+
<Form onSubmit={submitForm} error={error} className="flex flex-col gap-4">
|
|
109
106
|
{/* Every control, from the workflow's own input schema. See the module doc. */}
|
|
110
107
|
<WorkflowFields workflow={WORKFLOW} />
|
|
111
108
|
<SubmitButton pending={pending} pendingLabel="Working…">
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
1
|
// An EVAL: does the line really refuse to say what it cannot ground? Run it
|
|
4
2
|
// with `aai eval`.
|
|
5
3
|
//
|
|
@@ -27,20 +25,12 @@
|
|
|
27
25
|
// product questions from its own knowledge and skips the tools the prompt exists
|
|
28
26
|
// to route it through, so a case run that way measures nothing it claims to.
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
import
|
|
28
|
+
/** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
|
|
29
|
+
import agentDef from "virtual:aai/agent";
|
|
32
30
|
import { type EvalSession, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
33
31
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
34
32
|
import { expect } from "vitest";
|
|
35
33
|
import { z } from "zod";
|
|
36
|
-
import authoredAgent from "./agent.ts";
|
|
37
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
38
|
-
|
|
39
|
-
/** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
|
|
40
|
-
const agentDef = withSystemPrompt(
|
|
41
|
-
withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
|
|
42
|
-
systemPrompt,
|
|
43
|
-
);
|
|
44
34
|
|
|
45
35
|
/**
|
|
46
36
|
* What the BROWSER is sent, as this eval reads it.
|
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createToolContext,
|
|
5
|
-
stubGenerate,
|
|
6
|
-
toolRunner,
|
|
7
|
-
withDiscoveredTools,
|
|
8
|
-
} from "@alexkroman1/aai/testing";
|
|
1
|
+
/** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
|
|
2
|
+
import agentDef from "virtual:aai/agent";
|
|
3
|
+
import { createToolContext, stubGenerate, toolRunner } from "@alexkroman1/aai/testing";
|
|
9
4
|
import { describe, expect, test } from "vitest";
|
|
10
|
-
import authoredAgent from "./agent.ts";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
14
|
-
*
|
|
15
|
-
* The glob is written HERE rather than reached for from a shared helper because
|
|
16
|
-
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
17
|
-
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
18
|
-
* the file containing it either way. This is the pattern a user writes.
|
|
19
|
-
*/
|
|
20
|
-
const agentDef = withDiscoveredTools(
|
|
21
|
-
authoredAgent,
|
|
22
|
-
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
23
|
-
);
|
|
24
5
|
|
|
25
6
|
import { MAX_ATTEMPTS, runCorrectiveRag } from "./procedure.ts";
|
|
26
7
|
import {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* `aai secret put ASSEMBLYAI_API_KEY` once deployed. `requiredEnv` below is
|
|
18
18
|
* what makes a deploy check for it rather than letting the first run find out.
|
|
19
19
|
* A step reads it with `requireStepEnv`; see `@alexkroman1/aai/step`.
|
|
20
|
-
* - **
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* below refuses by name
|
|
20
|
+
* - **A `DATABASE_URL`** — a secret when deployed, `.env` under `aai dev` — and
|
|
21
|
+
* the platform provisions none, so it is a Postgres you bring. REQUIRED here,
|
|
22
|
+
* unlike most workflow apps: runs are durable on the platform either way, but
|
|
23
|
+
* an UPLOAD's record is a row, so the form below refuses by name without one.
|
|
24
24
|
* - **ffmpeg, under `aai dev` only.** A deployed guest's image installs it; on a
|
|
25
25
|
* laptop it is whatever is on `PATH` (or `AAI_FFMPEG_PATH`). The `transcribe`
|
|
26
26
|
* flow needs it for anything that is not already a linear-PCM WAV, because the
|
|
@@ -359,14 +359,14 @@ function TranscriptionDesk() {
|
|
|
359
359
|
// ALL THREE hooks are called every render, because a hook may not be conditional —
|
|
360
360
|
// and that costs nothing here: none of them does anything until its `submit` is
|
|
361
361
|
// called, and `useWorkflowRun` underneath them holds no id until then either.
|
|
362
|
-
const streamed = useWorkflowStream<
|
|
363
|
-
const stored = useWorkflowSubmit<
|
|
364
|
-
const batched = useWorkflowSubmit<
|
|
362
|
+
const streamed = useWorkflowStream<typeof transcribe>(WORKFLOWS.streaming, { parallel });
|
|
363
|
+
const stored = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.classic, { parallel });
|
|
364
|
+
const batched = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.batch, { parallel });
|
|
365
365
|
// The batch flow uploads the same way the classic one does — the id comes from the
|
|
366
366
|
// store — so it is the SAME hook against a different workflow. Only the streaming
|
|
367
367
|
// mode needs the other one, because only it needs the id before the bytes.
|
|
368
368
|
const active = mode === "streaming" ? streamed : mode === "batch" ? batched : stored;
|
|
369
|
-
const {
|
|
369
|
+
const { submitForm, run, upload, pending, error, reset, pauseUpload, resumeUpload } = active;
|
|
370
370
|
// History is per WORKFLOW, so the list follows the mode: two flows that produce
|
|
371
371
|
// the same output are still two different things to have run, and merging them
|
|
372
372
|
// would put a run under a heading that cannot explain it.
|
|
@@ -422,7 +422,7 @@ function TranscriptionDesk() {
|
|
|
422
422
|
<Form
|
|
423
423
|
onSubmit={(values) => {
|
|
424
424
|
total.start();
|
|
425
|
-
return
|
|
425
|
+
return submitForm(values);
|
|
426
426
|
}}
|
|
427
427
|
error={error}
|
|
428
428
|
>
|
|
@@ -167,7 +167,16 @@ export const BYTES_IN_FLIGHT = 640 * 1024 * 1024;
|
|
|
167
167
|
*
|
|
168
168
|
* The table above was measured under the old per-round barrier. Re-measuring it is
|
|
169
169
|
* worth doing before this number moves again: the window makes a wide fan-out
|
|
170
|
-
* cheaper at the tail, which if anything argues for a HIGHER knee.
|
|
170
|
+
* cheaper at the tail, which if anything argues for a HIGHER knee. *
|
|
171
|
+
* **What EXECUTES at this width is the world's call, not this number's.**
|
|
172
|
+
* `mapConcurrent` bounds how many step calls the body has in flight; how many
|
|
173
|
+
* run at once is the workflow world's worker concurrency, which on the
|
|
174
|
+
* `DATABASE_URL` path defaults to three — so on a default deployment a width
|
|
175
|
+
* above three is inert while still costing a queued job per item. That makes
|
|
176
|
+
* this the FAR SIDE's knee and the width to use once an operator has raised
|
|
177
|
+
* the ceiling, not a promise about a stock deployment. See "The WINDOW is not
|
|
178
|
+
* the concurrency" in `@alexkroman1/aai/step`'s `mapConcurrent`; the numbers
|
|
179
|
+
* above were measured against the endpoint and say nothing about that layer.
|
|
171
180
|
*/
|
|
172
181
|
export const MAX_SEGMENT_CONCURRENCY = 32;
|
|
173
182
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
1
|
// An EVAL: does the confirmation gate actually gate? Run it with `aai eval`.
|
|
4
2
|
//
|
|
5
3
|
// `agent.test.ts` drives each tool directly and asserts about the state it
|
|
@@ -22,15 +20,6 @@
|
|
|
22
20
|
// product questions from its own knowledge and skips the tools the prompt exists
|
|
23
21
|
// to route it through, so a case run that way measures nothing it claims to.
|
|
24
22
|
|
|
25
|
-
import { withSystemPrompt } from "@alexkroman1/aai/manifest";
|
|
26
|
-
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
27
|
-
import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
|
|
28
|
-
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
29
|
-
import { expect } from "vitest";
|
|
30
|
-
import { z } from "zod";
|
|
31
|
-
import authoredAgent from "./agent.ts";
|
|
32
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
33
|
-
|
|
34
23
|
/**
|
|
35
24
|
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
36
25
|
*
|
|
@@ -39,10 +28,11 @@ import systemPrompt from "./system-prompt.md?raw";
|
|
|
39
28
|
* outside its own template. An eval that forgot it would run an agent with NO
|
|
40
29
|
* tools and read as a model that refuses to act.
|
|
41
30
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
import agentDef from "virtual:aai/agent";
|
|
32
|
+
import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
|
|
33
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
34
|
+
import { expect } from "vitest";
|
|
35
|
+
import { z } from "zod";
|
|
46
36
|
|
|
47
37
|
/**
|
|
48
38
|
* What the BROWSER is sent, as this eval reads it.
|
|
@@ -1,28 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
|
|
2
|
+
import agentDef from "virtual:aai/agent";
|
|
3
3
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
-
import {
|
|
5
|
-
createToolContext,
|
|
6
|
-
ok,
|
|
7
|
-
okPosition,
|
|
8
|
-
toolRunner,
|
|
9
|
-
withDiscoveredTools,
|
|
10
|
-
} from "@alexkroman1/aai/testing";
|
|
4
|
+
import { createToolContext, ok, okPosition, toolRunner } from "@alexkroman1/aai/testing";
|
|
11
5
|
import { describe, expect, test } from "vitest";
|
|
12
|
-
import authoredAgent from "./agent.ts";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
16
|
-
*
|
|
17
|
-
* The glob is written HERE rather than reached for from a shared helper because
|
|
18
|
-
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
19
|
-
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
20
|
-
* the file containing it either way. This is the pattern a user writes.
|
|
21
|
-
*/
|
|
22
|
-
const agentDef = withDiscoveredTools(
|
|
23
|
-
authoredAgent,
|
|
24
|
-
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
25
|
-
);
|
|
26
6
|
|
|
27
7
|
import {
|
|
28
8
|
activeAssistant,
|
|
@@ -12,13 +12,10 @@
|
|
|
12
12
|
// `agent.ts` alone would measure Scout with none of its own rules — and its
|
|
13
13
|
// rules are the entire subject of this file. The reasoning is spelled out in
|
|
14
14
|
// `../code-interpreter/agent.eval.test.ts`.
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
import agentDef from "virtual:aai/agent";
|
|
16
17
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
17
18
|
import { expect } from "vitest";
|
|
18
|
-
import authored from "./agent.ts";
|
|
19
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
20
|
-
|
|
21
|
-
const agentDef = withSystemPrompt(authored, systemPrompt);
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
21
|
* The registrable label of every host this turn's tool results mentioned —
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as ok, n as fail } from "./_output-CKkmWs7i.mjs";
|
|
3
|
+
import { n as log, r as notify } from "./_ui-u7T4YooX.mjs";
|
|
4
|
+
import { i as errorCode, o as errorMessage$1, r as binFromPackageJson } from "./_utils-B8QmtFhK.mjs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { omitUndefined } from "@alexkroman1/aai/utils";
|
|
9
|
+
import { execaSync } from "execa";
|
|
10
|
+
//#region test.ts
|
|
11
|
+
/**
|
|
12
|
+
* `aai test` — run agent tests via vitest.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the agent project's own vitest binary so tests run without the
|
|
16
|
+
* npx resolution overhead (and its potential network fetch of vitest).
|
|
17
|
+
*
|
|
18
|
+
* Resolves `vitest/package.json` from the agent directory, derives the bin
|
|
19
|
+
* script, and runs it with the current Node executable. Falls back to
|
|
20
|
+
* `npx vitest` only when no local install is resolvable.
|
|
21
|
+
*/
|
|
22
|
+
function resolveVitestCommand(cwd, resolve = createRequire(path.join(cwd, "package.json")).resolve) {
|
|
23
|
+
try {
|
|
24
|
+
const bin = binFromPackageJson(resolve("vitest/package.json"), "vitest");
|
|
25
|
+
if (bin) return {
|
|
26
|
+
cmd: process.execPath,
|
|
27
|
+
args: [bin]
|
|
28
|
+
};
|
|
29
|
+
} catch {}
|
|
30
|
+
return {
|
|
31
|
+
cmd: "npx",
|
|
32
|
+
args: ["vitest"]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The files `aai test` runs, in preference order. */
|
|
36
|
+
const TEST_FILES = ["agent.test.ts", "agent.test.js"];
|
|
37
|
+
/**
|
|
38
|
+
* Run vitest over one of `candidates` in the given project directory.
|
|
39
|
+
*
|
|
40
|
+
* Returns the FILE it ran, or `false` if none of the candidate files exists.
|
|
41
|
+
* Throws on failure.
|
|
42
|
+
*
|
|
43
|
+
* A vitest FILTER, not an include glob: the argument is matched as a substring
|
|
44
|
+
* against the paths vitest's own include globs already found, which is why the
|
|
45
|
+
* candidates are named `*.test.ts` — `agent.test.ts` cannot match
|
|
46
|
+
* `agent.eval.test.ts` and vice versa, so the two commands stay disjoint
|
|
47
|
+
* without either one having to exclude the other's file.
|
|
48
|
+
*/
|
|
49
|
+
function runVitest(cwd, opts = { candidates: TEST_FILES }) {
|
|
50
|
+
const testFile = opts.candidates.find((name) => existsSync(path.join(cwd, name)));
|
|
51
|
+
if (!testFile) return false;
|
|
52
|
+
const { cmd, args } = resolveVitestCommand(cwd);
|
|
53
|
+
execaSync(cmd, [
|
|
54
|
+
...args,
|
|
55
|
+
"run",
|
|
56
|
+
"--root",
|
|
57
|
+
".",
|
|
58
|
+
...opts.extraArgs ?? [],
|
|
59
|
+
testFile
|
|
60
|
+
], {
|
|
61
|
+
cwd,
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
...omitUndefined({ env: opts.env ? {
|
|
64
|
+
...process.env,
|
|
65
|
+
...opts.env
|
|
66
|
+
} : void 0 })
|
|
67
|
+
});
|
|
68
|
+
return testFile;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Classify a {@link runVitest} failure. execaSync throws an ENOENT-coded
|
|
72
|
+
* error when the binary itself couldn't be spawned (infrastructure problem)
|
|
73
|
+
* and an exit-code error when vitest ran and the tests failed.
|
|
74
|
+
*/
|
|
75
|
+
function classifyVitestError(err, label = "Tests") {
|
|
76
|
+
if (errorCode(err) === "ENOENT") return {
|
|
77
|
+
code: "spawn_failed",
|
|
78
|
+
message: `Could not launch the test runner: ${errorMessage$1(err)} — is the binary on your PATH?`
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
code: "test_failed",
|
|
82
|
+
message: `${label} failed: ${errorMessage$1(err)}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Directories a project's own specs never live in. */
|
|
86
|
+
const UNSCANNED_DIRS = /* @__PURE__ */ new Set([
|
|
87
|
+
"node_modules",
|
|
88
|
+
".aai",
|
|
89
|
+
".git",
|
|
90
|
+
"dist",
|
|
91
|
+
".workflow-data"
|
|
92
|
+
]);
|
|
93
|
+
/** What counts as a spec file. */
|
|
94
|
+
const SPEC_FILE_RE = /\.test\.(ts|js|tsx|mts|cts)$/;
|
|
95
|
+
/**
|
|
96
|
+
* Spec files in the project that `aai test` did NOT run.
|
|
97
|
+
*
|
|
98
|
+
* `runVitest` passes ONE filename as a vitest FILTER, which is what keeps `test`
|
|
99
|
+
* and `eval` disjoint without either excluding the other's file — see its doc.
|
|
100
|
+
* The cost is that every other `*.test.ts` in the project is skipped, and the
|
|
101
|
+
* skip was SILENT: the shipped `retail` template carries seven of them, so
|
|
102
|
+
* `aai test` there ran 1 file / 67 tests, printed "Tests passed", and left
|
|
103
|
+
* 211 of the project's 278 tests unrun with nothing saying so.
|
|
104
|
+
*
|
|
105
|
+
* A silent skip is the worst outcome available, so the skip is announced rather
|
|
106
|
+
* than the filter widened: which files `aai test` runs is a documented contract
|
|
107
|
+
* (the scaffold guide says "Run agent.test.ts via vitest"), and running a
|
|
108
|
+
* project's other specs by default could reach ones that are slow or want
|
|
109
|
+
* credentials. Naming them costs nothing and is what a reader needs.
|
|
110
|
+
*
|
|
111
|
+
* Eval files are excluded because they have their OWN command, named in the
|
|
112
|
+
* message.
|
|
113
|
+
*/
|
|
114
|
+
function unrunSpecFiles(cwd, ran) {
|
|
115
|
+
const found = [];
|
|
116
|
+
collectSpecs(cwd, "", ran, found);
|
|
117
|
+
return found.sort(compareCodeUnits);
|
|
118
|
+
}
|
|
119
|
+
/** Code-unit comparison — see {@link unrunSpecFiles} for why not `localeCompare`. */
|
|
120
|
+
function compareCodeUnits(a, b) {
|
|
121
|
+
if (a < b) return -1;
|
|
122
|
+
return a > b ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
/** One directory of {@link unrunSpecFiles}, recursing into the ones that count. */
|
|
125
|
+
function collectSpecs(dir, prefix, ran, out) {
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
129
|
+
} catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const e of entries) {
|
|
133
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
134
|
+
if (e.isDirectory()) {
|
|
135
|
+
if (!(UNSCANNED_DIRS.has(e.name) || e.name.startsWith("."))) collectSpecs(path.join(dir, e.name), rel, ran, out);
|
|
136
|
+
} else if (isUnrunSpec(e.name, rel, ran)) out.push(rel);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** A spec file this run did not cover. The `.eval.` INFIX is the tier convention. */
|
|
140
|
+
function isUnrunSpec(name, rel, ran) {
|
|
141
|
+
if (!SPEC_FILE_RE.test(name)) return false;
|
|
142
|
+
return rel !== ran && !name.includes(".eval.test.");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Warn, once, naming the spec files this run did not cover.
|
|
146
|
+
*
|
|
147
|
+
* `ran` is `false` when there was no `agent.test.ts` to run, and that case
|
|
148
|
+
* needs the warning MORE rather than less: `aai test` then prints "No test file
|
|
149
|
+
* found" while the project's spec files sit right there unrun, which reads as
|
|
150
|
+
* "this project has no tests". Measured on a project whose only spec was
|
|
151
|
+
* `tools/echo_back.test.ts` — `{"passed":true,"skipped":true}` and not a word
|
|
152
|
+
* about it. It stays silent when there is nothing to name, in both arms.
|
|
153
|
+
*/
|
|
154
|
+
function warnUnrunSpecs(cwd, ran) {
|
|
155
|
+
const skipped = unrunSpecFiles(cwd, ran === false ? "" : ran);
|
|
156
|
+
if (skipped.length === 0) return;
|
|
157
|
+
notify("warn", `${ran === false ? `\`aai test\` found no agent.test.ts, so it ran nothing. ${skipped.length} spec file(s) exist and were NOT run:` : `\`aai test\` ran ${ran} only. ${skipped.length} other spec file(s) were NOT run:`} ${skipped.join(", ")}. Run them with your own vitest (\`npx vitest run\`); behaviour evals have their own command (\`aai eval\`).`);
|
|
158
|
+
}
|
|
159
|
+
/** Execute agent tests and return structured result. */
|
|
160
|
+
async function executeTest(cwd) {
|
|
161
|
+
log.step("Running agent tests");
|
|
162
|
+
try {
|
|
163
|
+
const ran = runVitest(cwd);
|
|
164
|
+
if (!ran) {
|
|
165
|
+
log.info("No test file found. Create agent.test.ts to add tests.");
|
|
166
|
+
warnUnrunSpecs(cwd, ran);
|
|
167
|
+
return ok({
|
|
168
|
+
passed: true,
|
|
169
|
+
skipped: true
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
log.success("Tests passed");
|
|
173
|
+
warnUnrunSpecs(cwd, ran);
|
|
174
|
+
return ok({ passed: true });
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const { code, message } = classifyVitestError(err);
|
|
177
|
+
return fail(code, message);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
export { classifyVitestError, executeTest, runVitest };
|
package/dist/test.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type VitestRunOptions = {
|
|
|
37
37
|
/**
|
|
38
38
|
* Run vitest over one of `candidates` in the given project directory.
|
|
39
39
|
*
|
|
40
|
-
* Returns
|
|
40
|
+
* Returns the FILE it ran, or `false` if none of the candidate files exists.
|
|
41
41
|
* Throws on failure.
|
|
42
42
|
*
|
|
43
43
|
* A vitest FILTER, not an include glob: the argument is matched as a substring
|
|
@@ -46,7 +46,7 @@ export type VitestRunOptions = {
|
|
|
46
46
|
* `agent.eval.test.ts` and vice versa, so the two commands stay disjoint
|
|
47
47
|
* without either one having to exclude the other's file.
|
|
48
48
|
*/
|
|
49
|
-
export declare function runVitest(cwd: string, opts?: VitestRunOptions):
|
|
49
|
+
export declare function runVitest(cwd: string, opts?: VitestRunOptions): string | false;
|
|
50
50
|
/**
|
|
51
51
|
* Classify a {@link runVitest} failure. execaSync throws an ENOENT-coded
|
|
52
52
|
* error when the binary itself couldn't be spawned (infrastructure problem)
|
|
@@ -58,6 +58,37 @@ label?: string): {
|
|
|
58
58
|
code: "spawn_failed" | "test_failed";
|
|
59
59
|
message: string;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Spec files in the project that `aai test` did NOT run.
|
|
63
|
+
*
|
|
64
|
+
* `runVitest` passes ONE filename as a vitest FILTER, which is what keeps `test`
|
|
65
|
+
* and `eval` disjoint without either excluding the other's file — see its doc.
|
|
66
|
+
* The cost is that every other `*.test.ts` in the project is skipped, and the
|
|
67
|
+
* skip was SILENT: the shipped `retail` template carries seven of them, so
|
|
68
|
+
* `aai test` there ran 1 file / 67 tests, printed "Tests passed", and left
|
|
69
|
+
* 211 of the project's 278 tests unrun with nothing saying so.
|
|
70
|
+
*
|
|
71
|
+
* A silent skip is the worst outcome available, so the skip is announced rather
|
|
72
|
+
* than the filter widened: which files `aai test` runs is a documented contract
|
|
73
|
+
* (the scaffold guide says "Run agent.test.ts via vitest"), and running a
|
|
74
|
+
* project's other specs by default could reach ones that are slow or want
|
|
75
|
+
* credentials. Naming them costs nothing and is what a reader needs.
|
|
76
|
+
*
|
|
77
|
+
* Eval files are excluded because they have their OWN command, named in the
|
|
78
|
+
* message.
|
|
79
|
+
*/
|
|
80
|
+
export declare function unrunSpecFiles(cwd: string, ran: string): string[];
|
|
81
|
+
/**
|
|
82
|
+
* Warn, once, naming the spec files this run did not cover.
|
|
83
|
+
*
|
|
84
|
+
* `ran` is `false` when there was no `agent.test.ts` to run, and that case
|
|
85
|
+
* needs the warning MORE rather than less: `aai test` then prints "No test file
|
|
86
|
+
* found" while the project's spec files sit right there unrun, which reads as
|
|
87
|
+
* "this project has no tests". Measured on a project whose only spec was
|
|
88
|
+
* `tools/echo_back.test.ts` — `{"passed":true,"skipped":true}` and not a word
|
|
89
|
+
* about it. It stays silent when there is nothing to name, in both arms.
|
|
90
|
+
*/
|
|
91
|
+
export declare function warnUnrunSpecs(cwd: string, ran: string | false): void;
|
|
61
92
|
/** Execute agent tests and return structured result. */
|
|
62
93
|
export declare function executeTest(cwd: string): Promise<CommandResult<TestData>>;
|
|
63
94
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"p-timeout": "^7.0.1",
|
|
45
45
|
"vite": "^8.2.1",
|
|
46
46
|
"zod": "^4.4.3",
|
|
47
|
-
"@alexkroman1/aai
|
|
48
|
-
"@alexkroman1/aai": "
|
|
49
|
-
"@alexkroman1/aai-
|
|
47
|
+
"@alexkroman1/aai": "9.0.0",
|
|
48
|
+
"@alexkroman1/aai-runtime": "9.0.0",
|
|
49
|
+
"@alexkroman1/aai-ui": "9.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"playwright": "^1.62.1",
|