@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
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
parseSchemaInput,
|
|
35
35
|
schemaInputIssues,
|
|
36
36
|
toolRunner,
|
|
37
|
-
withDiscoveredTools,
|
|
38
37
|
} from "@alexkroman1/aai/testing";
|
|
39
38
|
import {
|
|
40
39
|
installStubStepFetch,
|
|
@@ -44,7 +43,6 @@ import {
|
|
|
44
43
|
import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
|
|
45
44
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
46
45
|
import { createHook, type Hook, sleep } from "workflow";
|
|
47
|
-
import authoredAgent from "./agent.ts";
|
|
48
46
|
import { recap } from "./shared.ts";
|
|
49
47
|
import {
|
|
50
48
|
askWhetherToKeep,
|
|
@@ -72,18 +70,8 @@ vi.mock("workflow", async (importActual) => ({
|
|
|
72
70
|
createHook: vi.fn(),
|
|
73
71
|
}));
|
|
74
72
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
* The glob is written HERE rather than reached for from a shared helper because
|
|
79
|
-
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
80
|
-
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
81
|
-
* the file containing it either way. This is the pattern a user writes.
|
|
82
|
-
*/
|
|
83
|
-
const agentDef = withDiscoveredTools(
|
|
84
|
-
authoredAgent,
|
|
85
|
-
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
86
|
-
);
|
|
73
|
+
/** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
|
|
74
|
+
import agentDef from "virtual:aai/agent";
|
|
87
75
|
|
|
88
76
|
/**
|
|
89
77
|
* Every tool here is driven through the agent's own table, by the name the model
|
|
@@ -151,10 +139,7 @@ describe("the agent declares its workflow", () => {
|
|
|
151
139
|
expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
|
|
152
140
|
});
|
|
153
141
|
|
|
154
|
-
test("
|
|
155
|
-
// The distinction this template exists on: a caller is on the line, so the
|
|
156
|
-
// run is a handoff. `link-digest` is the other shape.
|
|
157
|
-
expect(agentDef.page).toBeUndefined();
|
|
142
|
+
test("discovers every tool in tools/, by file name", () => {
|
|
158
143
|
// Discovered, not declared: every name here is a file in `tools/`.
|
|
159
144
|
expect(Object.keys(agentDef.tools).sort()).toEqual([
|
|
160
145
|
"cancel_recap",
|
|
@@ -65,11 +65,13 @@
|
|
|
65
65
|
* the run really submits the recording, really polls it, and really deletes it
|
|
66
66
|
* when it has to.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
68
|
+
* Runs are DURABLE on the platform with nothing to configure, so a recap parked
|
|
69
|
+
* on a callback survives a restart, a redeploy and an idle sandbox.
|
|
70
|
+
*
|
|
71
|
+
* A `DATABASE_URL` you supply moves the correlation-key index out of memory,
|
|
72
|
+
* which is what lets `find()` resolve that parked run by key afterwards. Under
|
|
73
|
+
* `aai dev` with none, both the runs and the index go with the process: the flow
|
|
74
|
+
* still runs, it just does not outlive the server.
|
|
73
75
|
*/
|
|
74
76
|
|
|
75
77
|
import { agent } from "@alexkroman1/aai";
|
|
@@ -42,19 +42,6 @@ const INPUT: RedlineInput = {
|
|
|
42
42
|
// ─── 1. The declaration ──────────────────────────────────────────────────────
|
|
43
43
|
|
|
44
44
|
describe("the agent declares itself a workflow app", () => {
|
|
45
|
-
test("its front door is a page, not a microphone", () => {
|
|
46
|
-
// The port's own decision: seven long-form model calls in sequence is not
|
|
47
|
-
// something anyone holds a phone for.
|
|
48
|
-
expect(agentDef.page).toBe("static");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("it declares no voice pipeline and no tools, because nothing talks", () => {
|
|
52
|
-
expect(agentDef.stt).toBeUndefined();
|
|
53
|
-
expect(agentDef.llm).toBeUndefined();
|
|
54
|
-
expect(agentDef.tts).toBeUndefined();
|
|
55
|
-
expect(agentDef.tools).toEqual({});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
45
|
test("under the name the page starts a run by", () => {
|
|
59
46
|
// `useWorkflowSubmit("redline")` in client.tsx names this key. Nothing else
|
|
60
47
|
// records it, so a rename here is a 400 there rather than a compile error.
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
* `ToolContext`, so it reads that key with `requireStepEnv` (inside
|
|
26
26
|
* `stepGenerate`); `requiredEnv` below is what makes a deploy check for it
|
|
27
27
|
* rather than letting the first run find out.
|
|
28
|
-
* - **
|
|
29
|
-
*
|
|
28
|
+
* - **Nothing for durability.** A deployed app's runs live on the platform. A
|
|
29
|
+
* `DATABASE_URL` (yours, if you set one) only moves the correlation-key index
|
|
30
|
+
* out of memory.
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import { workflow, workflowApp } from "@alexkroman1/aai";
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import "@alexkroman1/aai-ui/styles.css";
|
|
30
30
|
import { plural } from "@alexkroman1/aai/utils";
|
|
31
|
-
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
31
|
+
import type { WorkflowInputOf, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
32
32
|
import {
|
|
33
33
|
Form,
|
|
34
34
|
type FormValues,
|
|
@@ -62,10 +62,15 @@ const WORKFLOW = "redline";
|
|
|
62
62
|
* thing that otherwise gets half-done in three places. Blank lines go, so a
|
|
63
63
|
* trailing newline is not a requirement to cover "".
|
|
64
64
|
*/
|
|
65
|
-
export function toInput(values: FormValues):
|
|
65
|
+
export function toInput(values: FormValues): WorkflowInputOf<typeof redline> {
|
|
66
66
|
const raw = typeof values.mustCover === "string" ? values.mustCover : "";
|
|
67
|
+
// The scalars ride through as the form collected them — strings from the DOM,
|
|
68
|
+
// which the WORKFLOW's schema coerces and validates server-side. Only
|
|
69
|
+
// `mustCover` is reshaped here, because no control renders a `string[]`.
|
|
70
|
+
// The assertion is on the scalars alone and is what `submitForm` exists to
|
|
71
|
+
// avoid needing anywhere a page is not doing this reshaping deliberately.
|
|
67
72
|
return {
|
|
68
|
-
...values,
|
|
73
|
+
...(values as Omit<WorkflowInputOf<typeof redline>, "mustCover">),
|
|
69
74
|
mustCover: raw
|
|
70
75
|
.split("\n")
|
|
71
76
|
.map((line) => line.trim())
|
|
@@ -74,7 +79,7 @@ export function toInput(values: FormValues): FormValues {
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
function RedlineDesk() {
|
|
77
|
-
const { submit, run, pending, error, reset } = useWorkflowSubmit<
|
|
82
|
+
const { submit, run, pending, error, reset } = useWorkflowSubmit<typeof redline>(WORKFLOW);
|
|
78
83
|
|
|
79
84
|
return (
|
|
80
85
|
<main className="mx-auto flex max-w-2xl flex-col gap-8 p-8">
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
1
|
// An EVAL: does this desk actually behave? Run it with `aai eval`.
|
|
4
2
|
//
|
|
5
3
|
// `agent.test.ts` drives the four tools against a STUBBED `ctx.workflows` and
|
|
@@ -29,14 +27,6 @@
|
|
|
29
27
|
//
|
|
30
28
|
// And what no eval here can see at all: anything below the audio boundary —
|
|
31
29
|
// endpointing, barge-in, whether two sentences merged into one turn.
|
|
32
|
-
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
33
|
-
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
34
|
-
import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
|
|
35
|
-
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
36
|
-
import { expect } from "vitest";
|
|
37
|
-
import { z } from "zod";
|
|
38
|
-
import authoredAgent from "./agent.ts";
|
|
39
|
-
import { research } from "./shared.ts";
|
|
40
30
|
|
|
41
31
|
/**
|
|
42
32
|
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
@@ -50,10 +40,13 @@ import { research } from "./shared.ts";
|
|
|
50
40
|
* The glob is written here rather than reached for from a shared helper because
|
|
51
41
|
* this file SHIPS — see `agent.test.ts`.
|
|
52
42
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
import agentDef from "virtual:aai/agent";
|
|
44
|
+
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
45
|
+
import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
|
|
46
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
47
|
+
import { expect } from "vitest";
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
import { research } from "./shared.ts";
|
|
57
50
|
|
|
58
51
|
/**
|
|
59
52
|
* The key the run's steps read with `requireStepEnv`.
|
|
@@ -28,14 +28,12 @@ import {
|
|
|
28
28
|
type StubGatewayCall,
|
|
29
29
|
schemaInputIssues,
|
|
30
30
|
toolRunner,
|
|
31
|
-
withDiscoveredTools,
|
|
32
31
|
} from "@alexkroman1/aai/testing";
|
|
33
32
|
import { mockWorkflows, installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
34
33
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
35
34
|
import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
|
|
36
35
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
37
36
|
import { FatalError, RetryableError } from "workflow";
|
|
38
|
-
import authoredAgent from "./agent.ts";
|
|
39
37
|
import { research } from "./shared.ts";
|
|
40
38
|
import {
|
|
41
39
|
countSources,
|
|
@@ -63,18 +61,8 @@ vi.mock("@alexkroman1/aai/tools", () => ({
|
|
|
63
61
|
visitWebpage: vi.fn(async () => ({ content: "The page body." })),
|
|
64
62
|
}));
|
|
65
63
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
* The glob is written HERE rather than reached for from a shared helper because
|
|
70
|
-
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
71
|
-
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
72
|
-
* the file containing it either way. This is the pattern a user writes.
|
|
73
|
-
*/
|
|
74
|
-
const agentDef = withDiscoveredTools(
|
|
75
|
-
authoredAgent,
|
|
76
|
-
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
77
|
-
);
|
|
64
|
+
/** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
|
|
65
|
+
import agentDef from "virtual:aai/agent";
|
|
78
66
|
|
|
79
67
|
/**
|
|
80
68
|
* Every tool here is driven through the agent's own table, by the name the model
|
|
@@ -404,7 +392,7 @@ describe("the steps that research", () => {
|
|
|
404
392
|
|
|
405
393
|
const note = await investigate(brief, "Tool use");
|
|
406
394
|
|
|
407
|
-
expect(webSearch).toHaveBeenCalledWith({ query: "otter tool use",
|
|
395
|
+
expect(webSearch).toHaveBeenCalledWith({ query: "otter tool use", maxResults: 5 });
|
|
408
396
|
expect(visitWebpage).toHaveBeenCalledWith("https://otters.example/tools");
|
|
409
397
|
expect(note.findings).toContain("crack shellfish");
|
|
410
398
|
expect(note.sources).toEqual([{ title: "Otters", url: "https://otters.example/tools" }]);
|
|
@@ -69,11 +69,13 @@
|
|
|
69
69
|
* thing that changes under `aai dev` (the key has to be in `.env`, not just your
|
|
70
70
|
* shell).
|
|
71
71
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* Runs are DURABLE on the platform with nothing to configure — they live on the
|
|
73
|
+
* platform's own database and survive a restart, a redeploy and an idle sandbox.
|
|
74
|
+
*
|
|
75
|
+
* A `DATABASE_URL` you supply (a secret when deployed, `.env` under `aai dev`)
|
|
76
|
+
* buys the key index, which is what lets `find()` resolve a run by key across a
|
|
77
|
+
* restart. Under `aai dev` with none, the runs go with the process too —
|
|
78
|
+
* everything below still works, which is what lets you try it first.
|
|
77
79
|
*/
|
|
78
80
|
|
|
79
81
|
import { agent } from "@alexkroman1/aai";
|
|
@@ -65,7 +65,13 @@ import {
|
|
|
65
65
|
RESEARCH_SYSTEM,
|
|
66
66
|
} from "./prompts.ts";
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Angles investigated at once. The far side of every one is a rate limit.
|
|
70
|
+
*
|
|
71
|
+
* Inside the workflow world's default worker concurrency (three on the
|
|
72
|
+
* `DATABASE_URL` path), so this width is what really runs — see "The WINDOW is
|
|
73
|
+
* not the concurrency" in `mapConcurrent`.
|
|
74
|
+
*/
|
|
69
75
|
const ANGLE_CONCURRENCY = 2;
|
|
70
76
|
|
|
71
77
|
/**
|
|
@@ -424,7 +430,7 @@ async function search(query: string): Promise<{ summary: string; sources: Source
|
|
|
424
430
|
try {
|
|
425
431
|
const results = await webSearch<{ results?: { title?: string; url?: string }[] }>({
|
|
426
432
|
query,
|
|
427
|
-
|
|
433
|
+
maxResults: SEARCH_RESULTS,
|
|
428
434
|
});
|
|
429
435
|
// The `catch` below was written for exactly this and could not reach it:
|
|
430
436
|
// `webSearch` ANSWERS with `{ error }` rather than throwing, so a refused
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares, plus
|
|
3
|
+
* what `system-prompt.md` says.
|
|
4
|
+
*
|
|
5
|
+
* Driving the raw default export instead would measure a seventeen-tool desk
|
|
6
|
+
* with no tools and the FRAMEWORK DEFAULT prompt — and for this template that
|
|
7
|
+
* is the whole subject: the authenticate-first discipline, the three-step
|
|
8
|
+
* readback and the one-customer-per-call rule all live in that file.
|
|
9
|
+
*/
|
|
10
|
+
import retailAgent from "virtual:aai/agent";
|
|
4
11
|
import type { SessionEvent } from "@alexkroman1/aai/protocol";
|
|
5
|
-
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
6
12
|
// An EVAL: does this desk actually behave? Run it with `aai eval`.
|
|
7
13
|
//
|
|
8
14
|
// `agent.test.ts` drives each tool directly and asserts about its result;
|
|
@@ -29,27 +35,6 @@ import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-r
|
|
|
29
35
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
30
36
|
import { expect } from "vitest";
|
|
31
37
|
import { z } from "zod";
|
|
32
|
-
import authoredAgent from "./agent.ts";
|
|
33
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares, plus
|
|
37
|
-
* what `system-prompt.md` says.
|
|
38
|
-
*
|
|
39
|
-
* BOTH wrappers are load-bearing and neither is applied by `agent()` — they are
|
|
40
|
-
* applied by the BUILD (`aai build` enumerates `tools/` and resolves the prompt
|
|
41
|
-
* file), so an eval driving the raw default export would measure a
|
|
42
|
-
* seventeen-tool desk with no tools and the FRAMEWORK DEFAULT prompt. For this
|
|
43
|
-
* template that is the whole subject: the authenticate-first discipline, the
|
|
44
|
-
* three-step readback and the one-customer-per-call rule are all in that file.
|
|
45
|
-
*
|
|
46
|
-
* The glob is written here rather than reached for from a shared helper because
|
|
47
|
-
* this file SHIPS — see `registry.test.ts`.
|
|
48
|
-
*/
|
|
49
|
-
const retailAgent = withSystemPrompt(
|
|
50
|
-
withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
|
|
51
|
-
systemPrompt,
|
|
52
|
-
);
|
|
53
38
|
|
|
54
39
|
/** Harper Brown: exactly ONE pending order, which is what makes "cancel my
|
|
55
40
|
* pending order" unambiguous — `resolveOrder` refuses a reference that matches
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
1
|
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
4
|
-
import { createToolContext,
|
|
2
|
+
import { createToolContext, deployedAgent } from "@alexkroman1/aai/testing";
|
|
5
3
|
import { describe, expect, test } from "vitest";
|
|
6
|
-
import authoredAgent from "./agent.ts";
|
|
7
4
|
import { callFlow, retailSlot } from "./store.ts";
|
|
8
5
|
|
|
9
6
|
/** Tools that legitimately run before the caller is identified — the six
|
|
@@ -20,17 +17,23 @@ const PUBLIC_TOOLS = new Set([
|
|
|
20
17
|
]);
|
|
21
18
|
|
|
22
19
|
/**
|
|
23
|
-
* The def a DEPLOYED agent runs
|
|
20
|
+
* The def a DEPLOYED agent runs, lowered BY HAND — the one place in the
|
|
21
|
+
* templates that still does.
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* `virtual:aai/agent` is what every other spec imports and what a user should
|
|
24
|
+
* reach for. This file is the exception on purpose: its whole subject is the
|
|
25
|
+
* tool REGISTRY, so doing the discovery explicitly is the thing under test
|
|
26
|
+
* rather than setup around it. It is also the worked example for a project
|
|
27
|
+
* whose runner is not vitest, and so cannot register the plugin.
|
|
29
28
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
import authoredAgent from "./agent.ts";
|
|
30
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
31
|
+
|
|
32
|
+
const retailAgent = deployedAgent(authoredAgent, {
|
|
33
|
+
tools: import.meta.glob("./tools/*.ts", { eager: true }),
|
|
34
|
+
systemPrompt,
|
|
35
|
+
});
|
|
36
|
+
|
|
34
37
|
const registry = Object.entries(retailAgent.tools);
|
|
35
38
|
|
|
36
39
|
// `createToolContext()` rather than a cast: it carries a real slot store (the
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
1
|
// An EVAL: does the story machine actually hold? Run it with `aai eval`.
|
|
4
2
|
//
|
|
5
3
|
// `agent.test.ts` drives each tool directly against a context it made itself,
|
|
@@ -25,15 +23,6 @@
|
|
|
25
23
|
// What no eval here can see: anything below the audio boundary — endpointing,
|
|
26
24
|
// barge-in, a sentence split across two turns. Those need real paced audio.
|
|
27
25
|
|
|
28
|
-
import { withSystemPrompt } from "@alexkroman1/aai/manifest";
|
|
29
|
-
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
30
|
-
import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
31
|
-
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
32
|
-
import { expect } from "vitest";
|
|
33
|
-
import { z } from "zod";
|
|
34
|
-
import authoredAgent from "./agent.ts";
|
|
35
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
36
|
-
|
|
37
26
|
/**
|
|
38
27
|
* The def a DEPLOYED agent runs, assembled the way the build assembles it: the
|
|
39
28
|
* authored export, plus what `tools/` declares, plus `system-prompt.md`.
|
|
@@ -43,10 +32,11 @@ import systemPrompt from "./system-prompt.md?raw";
|
|
|
43
32
|
* eval run against the framework default prompt would measure an agent nobody
|
|
44
33
|
* deployed.
|
|
45
34
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
import agentDef from "virtual:aai/agent";
|
|
36
|
+
import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
37
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
38
|
+
import { expect } from "vitest";
|
|
39
|
+
import { z } from "zod";
|
|
50
40
|
|
|
51
41
|
/**
|
|
52
42
|
* What each tool this file drives answers, off the wire.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ToolContext, ToolDef, ToolInputSchema } from "@alexkroman1/aai";
|
|
2
2
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
3
3
|
import { createToolContext, ok } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test, vi } from "vitest";
|
|
@@ -18,44 +18,17 @@ import {
|
|
|
18
18
|
import actionRoll from "./tools/action_roll.ts";
|
|
19
19
|
import burnMomentum from "./tools/burn_momentum.ts";
|
|
20
20
|
import checkState from "./tools/check_state.ts";
|
|
21
|
-
import loadGame from "./tools/load_game.ts";
|
|
22
21
|
import oracle from "./tools/oracle.ts";
|
|
23
|
-
import saveGame from "./tools/save_game.ts";
|
|
24
22
|
import setupCharacter from "./tools/setup_character.ts";
|
|
25
23
|
import updateState from "./tools/update_state.ts";
|
|
26
24
|
|
|
27
25
|
// ── Test doubles ─────────────────────────────────────────────────────────────
|
|
28
26
|
|
|
29
|
-
/**
|
|
30
|
-
* Map-backed fake of the app database, implementing exactly the three SQL
|
|
31
|
-
* statements the shared save-slot helpers emit (create table / select /
|
|
32
|
-
* upsert). Values are stored parsed, the way a postgres driver returns jsonb.
|
|
33
|
-
*/
|
|
34
|
-
function makeDb(): { db: Db; rows: Map<string, unknown> } {
|
|
35
|
-
const rows = new Map<string, unknown>();
|
|
36
|
-
const db: Db = {
|
|
37
|
-
async query<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
38
|
-
if (sql.startsWith("create table if not exists app_state")) return [];
|
|
39
|
-
if (sql.startsWith("select value from app_state")) {
|
|
40
|
-
const key = params[0] as string;
|
|
41
|
-
return rows.has(key) ? ([{ value: structuredClone(rows.get(key)) }] as T[]) : [];
|
|
42
|
-
}
|
|
43
|
-
if (sql.startsWith("insert into app_state")) {
|
|
44
|
-
const [key, json] = params as [string, string];
|
|
45
|
-
rows.set(key, JSON.parse(json)); // $2::jsonb — parsed like postgres would
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
throw new Error(`unexpected SQL in test: ${sql}`);
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
return { db, rows };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
27
|
/** `send` is a spy rather than the recorder `createToolContext` installs,
|
|
55
28
|
* because this suite asserts call counts on it. Each call gets its own slot
|
|
56
29
|
* store, which is what makes two contexts two games. */
|
|
57
|
-
function makeCtx(
|
|
58
|
-
return createToolContext({
|
|
30
|
+
function makeCtx(): ToolContext {
|
|
31
|
+
return createToolContext({ send: vi.fn() });
|
|
59
32
|
}
|
|
60
33
|
|
|
61
34
|
const SETUP_ARGS = {
|
|
@@ -588,65 +561,13 @@ describe("update_state", () => {
|
|
|
588
561
|
expect(
|
|
589
562
|
params.parse({ addClockSegments: 6, updateNpcBond: 4, timeOfDay: "night" }),
|
|
590
563
|
).toBeTruthy();
|
|
591
|
-
|
|
592
|
-
const slotParams = saveGame.inputSchema!;
|
|
593
|
-
expect(() => slotParams.parse({ slot: "../../etc" })).toThrow();
|
|
594
|
-
expect(() => slotParams.parse({ slot: "a".repeat(40) })).toThrow();
|
|
595
|
-
expect(slotParams.parse({ slot: "chapter-2" })).toBeTruthy();
|
|
596
564
|
});
|
|
597
565
|
});
|
|
598
566
|
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const { db, rows } = makeDb();
|
|
604
|
-
|
|
605
|
-
// Session A plays and saves.
|
|
606
|
-
const sessionA = makeCtx(db);
|
|
607
|
-
const played = playingState();
|
|
608
|
-
played.playerName = "Kael";
|
|
609
|
-
played.sceneCount = 7;
|
|
610
|
-
seedPlaying(sessionA, played);
|
|
611
|
-
const saved = ok<{ saved: boolean; slot: string }>(
|
|
612
|
-
await saveGame.execute({ slot: "chapter-2" }, sessionA),
|
|
613
|
-
);
|
|
614
|
-
expect(saved.saved).toBe(true);
|
|
615
|
-
expect(saved.slot).toBe("chapter-2");
|
|
616
|
-
expect(rows.get("save:chapter-2")).toMatchObject({ playerName: "Kael", sceneCount: 7 });
|
|
617
|
-
|
|
618
|
-
// Session B (a fresh game slot, the same app db) resumes it.
|
|
619
|
-
const sessionB = makeCtx(db);
|
|
620
|
-
// `load_game` is a plain `tool()`, so its result is the union its body
|
|
621
|
-
// writes rather than a position envelope — the guard is what picks the
|
|
622
|
-
// arm, where the old `as Record<string, unknown>` picked neither.
|
|
623
|
-
const loaded = await loadGame.execute({ slot: "chapter-2" }, sessionB);
|
|
624
|
-
if (isToolFailure(loaded)) throw new Error(`load refused: ${loaded.error}`);
|
|
625
|
-
expect(loaded.loaded).toBe(true);
|
|
626
|
-
expect(loaded.playerName).toBe("Kael");
|
|
627
|
-
expect(loaded.sceneCount).toBe(7);
|
|
628
|
-
expect(gameSlot.get(sessionB).playerName).toBe("Kael");
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
test("loading a missing slot reports an error instead of resetting the game", async () => {
|
|
632
|
-
const ctx = makeCtx();
|
|
633
|
-
const result = await loadGame.execute({ slot: "nope" }, ctx);
|
|
634
|
-
expect(isToolFailure(result) && result.error).toMatch(/No save found/);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
test("saving twice to one slot upserts — the newer save wins", async () => {
|
|
638
|
-
const { db, rows } = makeDb();
|
|
639
|
-
const ctx = makeCtx(db);
|
|
640
|
-
seedPlaying(ctx);
|
|
641
|
-
await saveGame.execute({}, ctx); // autosave
|
|
642
|
-
gameSlot.update(ctx, (game) => {
|
|
643
|
-
game.sceneCount = 9;
|
|
644
|
-
});
|
|
645
|
-
await saveGame.execute({}, ctx);
|
|
646
|
-
expect(rows.size).toBe(1);
|
|
647
|
-
expect(rows.get("save:autosave")).toMatchObject({ sceneCount: 9 });
|
|
648
|
-
});
|
|
649
|
-
});
|
|
567
|
+
// A `save_game / load_game` suite stood here, driving cross-session persistence
|
|
568
|
+
// through a map-backed fake of `ctx.db`. Both tools are gone: `ctx.db` is gone,
|
|
569
|
+
// and a shipped template cannot reach a database (see `shared.ts`). This
|
|
570
|
+
// adventure is single-session now.
|
|
650
571
|
|
|
651
572
|
// ── the story flow ───────────────────────────────────────────────────────────
|
|
652
573
|
|
|
@@ -655,10 +576,11 @@ describe("the story flow", () => {
|
|
|
655
576
|
const ctx = makeCtx();
|
|
656
577
|
expect(storyFlow.position(ctx).state).toBe("awaitingSetup");
|
|
657
578
|
|
|
658
|
-
// All
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
// `load_game` would later restore over a real one
|
|
579
|
+
// All of these used to RUN before a character existed: `action_roll` rolled
|
|
580
|
+
// 2d6 against the stats of nobody and applied consequences to a game that was
|
|
581
|
+
// not there. (`save_game` was in this list too, writing an empty campaign to a
|
|
582
|
+
// slot `load_game` would later restore over a real one — both are gone with
|
|
583
|
+
// `ctx.db`.)
|
|
662
584
|
for (const call of [
|
|
663
585
|
actionRoll.execute(
|
|
664
586
|
{ move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "swing" },
|
|
@@ -666,7 +588,6 @@ describe("the story flow", () => {
|
|
|
666
588
|
),
|
|
667
589
|
updateState.execute({ location: "Nowhere" }, ctx),
|
|
668
590
|
callNoArgs(burnMomentum, ctx),
|
|
669
|
-
saveGame.execute({}, ctx),
|
|
670
591
|
]) {
|
|
671
592
|
const refusal = await call;
|
|
672
593
|
expect(isToolFailure(refusal)).toBe(true);
|
|
@@ -740,37 +661,9 @@ describe("the story flow", () => {
|
|
|
740
661
|
expect(restarted.state).toBe("playing.awaitingRoll");
|
|
741
662
|
});
|
|
742
663
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const sessionB = makeCtx(db);
|
|
750
|
-
expect(storyFlow.position(sessionB).state).toBe("awaitingSetup");
|
|
751
|
-
const loaded = await loadGame.execute({ slot: "resume" }, sessionB);
|
|
752
|
-
if (isToolFailure(loaded)) throw new Error(`load refused: ${loaded.error}`);
|
|
753
|
-
expect(loaded.state).toBe("playing.awaitingRoll");
|
|
754
|
-
|
|
755
|
-
// And the play tools are available in the resumed session.
|
|
756
|
-
ok(await updateState.execute({ location: "Back at the Docks" }, sessionB));
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
test("a save whose game was over resumes as over", async () => {
|
|
760
|
-
const { db } = makeDb();
|
|
761
|
-
const sessionA = makeCtx(db);
|
|
762
|
-
const dead = playingState();
|
|
763
|
-
dead.health = 0;
|
|
764
|
-
dead.spirit = 0;
|
|
765
|
-
dead.gameOver = true;
|
|
766
|
-
dead.crisisMode = true;
|
|
767
|
-
seedPlaying(sessionA, dead);
|
|
768
|
-
ok(await saveGame.execute({ slot: "ended" }, sessionA));
|
|
769
|
-
|
|
770
|
-
const sessionB = makeCtx(db);
|
|
771
|
-
const loaded = await loadGame.execute({ slot: "ended" }, sessionB);
|
|
772
|
-
if (isToolFailure(loaded)) throw new Error(`load refused: ${loaded.error}`);
|
|
773
|
-
expect(loaded.state).toBe("gameOver");
|
|
774
|
-
expect(storyFlow.position(sessionB).done).toBe(true);
|
|
775
|
-
});
|
|
664
|
+
// Two resume tests stood here — a saved game reopening in play, and one saved
|
|
665
|
+
// after the ending reopening as over. Both drove `save_game`/`load_game`, which
|
|
666
|
+
// are gone with `ctx.db`. What they proved about the FLOW (a resumed position is
|
|
667
|
+
// restored rather than recomputed) has no path left to exercise it: a session
|
|
668
|
+
// is the whole life of a game now.
|
|
776
669
|
});
|