@alexkroman1/aai-cli 6.11.0 → 8.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/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
4
|
import {
|
|
5
5
|
createToolContext,
|
|
6
|
-
|
|
6
|
+
okPosition,
|
|
7
7
|
stubGenerate,
|
|
8
|
+
toolRunner,
|
|
8
9
|
withDiscoveredTools,
|
|
9
10
|
} from "@alexkroman1/aai/testing";
|
|
10
11
|
import { describe, expect, test } from "vitest";
|
|
@@ -23,10 +24,10 @@ const agentDef = withDiscoveredTools(
|
|
|
23
24
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
24
25
|
);
|
|
25
26
|
|
|
26
|
-
import { executeStep, MAX_STEP_SEARCHES, normalizeAct, planNode } from "./
|
|
27
|
+
import { executeStep, MAX_STEP_SEARCHES, normalizeAct, planNode } from "./procedure.ts";
|
|
27
28
|
import { EXECUTOR_SYSTEM, PLANNER_SYSTEM, REPLANNER_SYSTEM, REVISE_SYSTEM } from "./prompts.ts";
|
|
28
29
|
import type { SearchFn } from "./shared.ts";
|
|
29
|
-
import { MAX_PAST_STEPS, planProjection, planSlot, planView } from "./shared.ts";
|
|
30
|
+
import { MAX_PAST_STEPS, planFlow, planProjection, planSlot, planView } from "./shared.ts";
|
|
30
31
|
|
|
31
32
|
// ─── A scripted model ────────────────────────────────────────────────────────
|
|
32
33
|
//
|
|
@@ -80,15 +81,11 @@ function fakeSearch(hits: Record<string, { title: string; url: string }[]>): {
|
|
|
80
81
|
return { search, queries };
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* and its "no such tool" message are `runTool`'s (`@alexkroman1/aai/testing`);
|
|
89
|
-
* what is local is only which agent they run against. */
|
|
90
|
-
const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
|
|
91
|
-
runTool(agentDef, name, args, ctx);
|
|
84
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup,
|
|
85
|
+
* its "no such tool" message and the args-or-context shape are all
|
|
86
|
+
* `toolRunner`'s (`@alexkroman1/aai/testing`); what is local is only which
|
|
87
|
+
* agent it runs against. */
|
|
88
|
+
const run = toolRunner(agentDef);
|
|
92
89
|
|
|
93
90
|
function stateOf(ctx: ToolContext) {
|
|
94
91
|
return planSlot.get(ctx);
|
|
@@ -190,7 +187,7 @@ describe("normalizeAct", () => {
|
|
|
190
187
|
describe("start_plan", () => {
|
|
191
188
|
test("stores the objective and the steps", async () => {
|
|
192
189
|
const { generate } = scriptedModel({ steps: ["Check prices", "Compare hotels", "Book"] });
|
|
193
|
-
const ctx =
|
|
190
|
+
const ctx = createToolContext({ generate });
|
|
194
191
|
const result = (await run("start_plan", { objective: "a weekend in Lisbon" }, ctx)) as {
|
|
195
192
|
steps: string[];
|
|
196
193
|
};
|
|
@@ -214,12 +211,20 @@ describe("start_plan", () => {
|
|
|
214
211
|
});
|
|
215
212
|
|
|
216
213
|
describe("work_next_step", () => {
|
|
217
|
-
test("
|
|
214
|
+
test("is refused before there is a plan, by the flow rather than by the body", async () => {
|
|
218
215
|
const { generate } = scriptedModel();
|
|
219
|
-
const ctx =
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
const ctx = createToolContext({ generate });
|
|
217
|
+
// The gate is `when: "working"`, so the refusal names the state the call is
|
|
218
|
+
// actually in and quotes that state's instruction — which is what the model
|
|
219
|
+
// needs in order to do the right thing on its own next turn.
|
|
220
|
+
expect(await run("work_next_step", ctx)).toMatchObject({
|
|
221
|
+
error: expect.stringContaining('this conversation is at "idle"'),
|
|
222
222
|
});
|
|
223
|
+
expect(await run("work_next_step", ctx)).toMatchObject({
|
|
224
|
+
error: expect.stringContaining("use start_plan"),
|
|
225
|
+
});
|
|
226
|
+
// Refused means the body did not run: nothing was claimed off the plan.
|
|
227
|
+
expect(stateOf(ctx).pastSteps).toEqual([]);
|
|
223
228
|
});
|
|
224
229
|
|
|
225
230
|
test("does the head step, records it, and takes the replanner's next plan", async () => {
|
|
@@ -228,19 +233,21 @@ describe("work_next_step", () => {
|
|
|
228
233
|
turns: [{ answer: "Fares are about 180 return." }],
|
|
229
234
|
acts: [{ kind: "plan", steps: ["Compare hotels"] }],
|
|
230
235
|
});
|
|
231
|
-
const ctx =
|
|
236
|
+
const ctx = createToolContext({ generate });
|
|
232
237
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
233
238
|
|
|
234
|
-
const first =
|
|
235
|
-
|
|
239
|
+
const first = okPosition<{
|
|
240
|
+
finished: boolean;
|
|
236
241
|
step: string;
|
|
237
242
|
result: string;
|
|
238
243
|
remaining: string[];
|
|
239
|
-
};
|
|
240
|
-
expect(first.
|
|
241
|
-
expect(first.step).toBe("Check prices");
|
|
242
|
-
expect(first.result).toContain("180");
|
|
243
|
-
expect(first.remaining).toEqual(["Compare hotels"]);
|
|
244
|
+
}>(await run("work_next_step", ctx));
|
|
245
|
+
expect(first.result.finished).toBe(false);
|
|
246
|
+
expect(first.result.step).toBe("Check prices");
|
|
247
|
+
expect(first.result.result).toContain("180");
|
|
248
|
+
expect(first.result.remaining).toEqual(["Compare hotels"]);
|
|
249
|
+
// Still working: nothing was concluded, so the flow has not moved on.
|
|
250
|
+
expect(first.state).toBe("working");
|
|
244
251
|
|
|
245
252
|
const state = stateOf(ctx);
|
|
246
253
|
expect(state.pastSteps).toHaveLength(1);
|
|
@@ -254,19 +261,29 @@ describe("work_next_step", () => {
|
|
|
254
261
|
turns: [{ answer: "Fares are about 180 return." }],
|
|
255
262
|
acts: [{ kind: "respond", response: "Go in May — flights are about 180 return." }],
|
|
256
263
|
});
|
|
257
|
-
const ctx =
|
|
264
|
+
const ctx = createToolContext({ generate });
|
|
258
265
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
259
|
-
const
|
|
266
|
+
const answered = okPosition<{ finished: boolean; response: string }>(
|
|
267
|
+
await run("work_next_step", ctx),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(answered.result.finished).toBe(true);
|
|
271
|
+
expect(answered.result.response).toContain("180");
|
|
272
|
+
// A response is what sends ANSWERED — the flow and the data agree because
|
|
273
|
+
// one tool call moved both.
|
|
274
|
+
expect(answered.state).toBe("answered");
|
|
275
|
+
expect(planFlow.position(ctx).state).toBe("answered");
|
|
260
276
|
|
|
261
|
-
expect(result.done).toBe(true);
|
|
262
|
-
expect(result.response).toContain("180");
|
|
263
277
|
const state = stateOf(ctx);
|
|
264
278
|
// The replanner deciding early is a good outcome, so the pending step goes.
|
|
265
279
|
expect(state.plan).toEqual([]);
|
|
266
|
-
expect(state.response).toBe(result.response);
|
|
280
|
+
expect(state.response).toBe(answered.result.response);
|
|
267
281
|
|
|
268
|
-
// A finished plan is not worked again
|
|
269
|
-
|
|
282
|
+
// A finished plan is not worked again, and now it CANNOT be: the tool is
|
|
283
|
+
// gated out of `answered` rather than returning a done-shaped result.
|
|
284
|
+
expect(await run("work_next_step", ctx)).toMatchObject({
|
|
285
|
+
error: expect.stringContaining('this conversation is at "answered"'),
|
|
286
|
+
});
|
|
270
287
|
expect(stateOf(ctx).pastSteps).toHaveLength(1);
|
|
271
288
|
});
|
|
272
289
|
|
|
@@ -283,9 +300,9 @@ describe("work_next_step", () => {
|
|
|
283
300
|
steps: Array.from({ length: total - i - 1 }, (_, j) => `Step ${i + j + 2}`),
|
|
284
301
|
})),
|
|
285
302
|
});
|
|
286
|
-
const ctx =
|
|
303
|
+
const ctx = createToolContext({ generate });
|
|
287
304
|
await run("start_plan", { objective: "a long one" }, ctx);
|
|
288
|
-
for (let i = 0; i < total; i++) await run("work_next_step",
|
|
305
|
+
for (let i = 0; i < total; i++) await run("work_next_step", ctx);
|
|
289
306
|
|
|
290
307
|
const state = stateOf(ctx);
|
|
291
308
|
expect(state.pastSteps).toHaveLength(MAX_PAST_STEPS);
|
|
@@ -302,13 +319,15 @@ describe("work_next_step", () => {
|
|
|
302
319
|
// session ids would prove nothing extra, and `sessionSlot` could stop
|
|
303
320
|
// keying by session with this still passing.
|
|
304
321
|
const { generate } = scriptedModel({ steps: ["Only step"] });
|
|
305
|
-
const first =
|
|
306
|
-
const second =
|
|
322
|
+
const first = createToolContext({ generate });
|
|
323
|
+
const second = createToolContext({ generate });
|
|
307
324
|
|
|
308
325
|
await run("start_plan", { objective: "mine" }, first);
|
|
309
326
|
expect(stateOf(second).objective).toBeNull();
|
|
310
|
-
|
|
311
|
-
|
|
327
|
+
// The FLOW is per-session too, not just the plan — `second` is still idle.
|
|
328
|
+
expect(planFlow.position(second).state).toBe("idle");
|
|
329
|
+
expect(await run("work_next_step", second)).toMatchObject({
|
|
330
|
+
error: expect.stringContaining('this conversation is at "idle"'),
|
|
312
331
|
});
|
|
313
332
|
});
|
|
314
333
|
});
|
|
@@ -323,17 +342,19 @@ describe("revise_plan", () => {
|
|
|
323
342
|
{ kind: "plan", steps: ["Check Porto prices"] },
|
|
324
343
|
],
|
|
325
344
|
});
|
|
326
|
-
const ctx =
|
|
345
|
+
const ctx = createToolContext({ generate });
|
|
327
346
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
328
|
-
await run("work_next_step",
|
|
347
|
+
await run("work_next_step", ctx);
|
|
329
348
|
expect(stateOf(ctx).response).not.toBeNull();
|
|
330
349
|
|
|
331
|
-
const revised =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
expect(revised.
|
|
336
|
-
|
|
350
|
+
const revised = okPosition<{ finished: boolean; remaining: string[] }>(
|
|
351
|
+
await run("revise_plan", { instruction: "make it Porto instead" }, ctx),
|
|
352
|
+
);
|
|
353
|
+
expect(revised.result.finished).toBe(false);
|
|
354
|
+
expect(revised.result.remaining).toEqual(["Check Porto prices"]);
|
|
355
|
+
// REOPENED: the one transition that goes backwards, and the reason
|
|
356
|
+
// `revise_plan` is legal in `answered` at all.
|
|
357
|
+
expect(revised.state).toBe("working");
|
|
337
358
|
|
|
338
359
|
const state = stateOf(ctx);
|
|
339
360
|
// The old answer is no longer the answer — the caller moved the goalposts.
|
|
@@ -346,13 +367,27 @@ describe("revise_plan", () => {
|
|
|
346
367
|
expect(calls.at(-1)?.prompt).toContain("make it Porto instead");
|
|
347
368
|
});
|
|
348
369
|
|
|
349
|
-
test("
|
|
370
|
+
test("is refused before there is a plan", async () => {
|
|
350
371
|
const { generate } = scriptedModel();
|
|
351
|
-
const ctx =
|
|
352
|
-
expect(await run("revise_plan", { instruction: "change it" }, ctx)).
|
|
353
|
-
error:
|
|
372
|
+
const ctx = createToolContext({ generate });
|
|
373
|
+
expect(await run("revise_plan", { instruction: "change it" }, ctx)).toMatchObject({
|
|
374
|
+
error: expect.stringContaining('this conversation is at "idle"'),
|
|
354
375
|
});
|
|
355
376
|
});
|
|
377
|
+
|
|
378
|
+
test("a revision that answers outright lands in `answered`", async () => {
|
|
379
|
+
const { generate } = scriptedModel({
|
|
380
|
+
steps: ["Check Lisbon prices"],
|
|
381
|
+
acts: [{ kind: "respond", response: "Nothing to do — you already booked it." }],
|
|
382
|
+
});
|
|
383
|
+
const ctx = createToolContext({ generate });
|
|
384
|
+
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
385
|
+
const revised = okPosition<{ finished: boolean }>(
|
|
386
|
+
await run("revise_plan", { instruction: "never mind, it is booked" }, ctx),
|
|
387
|
+
);
|
|
388
|
+
expect(revised.result.finished).toBe(true);
|
|
389
|
+
expect(revised.state).toBe("answered");
|
|
390
|
+
});
|
|
356
391
|
});
|
|
357
392
|
|
|
358
393
|
describe("plan_status", () => {
|
|
@@ -362,14 +397,20 @@ describe("plan_status", () => {
|
|
|
362
397
|
turns: [{ answer: "About 180 return." }],
|
|
363
398
|
acts: [{ kind: "plan", steps: ["Book"] }],
|
|
364
399
|
});
|
|
365
|
-
const ctx =
|
|
366
|
-
|
|
367
|
-
|
|
400
|
+
const ctx = createToolContext({ generate });
|
|
401
|
+
// Legal in every state, so it READS the position rather than being gated on
|
|
402
|
+
// one — and "no plan yet" is the flow's own answer, not a third derivation
|
|
403
|
+
// of `!objective`.
|
|
404
|
+
expect(await run("plan_status", ctx)).toMatchObject({
|
|
405
|
+
stage: "idle",
|
|
406
|
+
next: expect.stringContaining("start_plan"),
|
|
407
|
+
objective: null,
|
|
368
408
|
});
|
|
369
409
|
|
|
370
410
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
371
|
-
await run("work_next_step",
|
|
372
|
-
expect(await run("plan_status",
|
|
411
|
+
await run("work_next_step", ctx);
|
|
412
|
+
expect(await run("plan_status", ctx)).toMatchObject({
|
|
413
|
+
stage: "working",
|
|
373
414
|
objective: "a weekend in Lisbon",
|
|
374
415
|
remaining: ["Book"],
|
|
375
416
|
response: null,
|
|
@@ -399,11 +440,11 @@ describe("planView projection", () => {
|
|
|
399
440
|
turns: [{ answer: "Done one." }],
|
|
400
441
|
acts: [{ kind: "plan", steps: ["Two", "Three"] }],
|
|
401
442
|
});
|
|
402
|
-
const ctx =
|
|
443
|
+
const ctx = createToolContext({ generate });
|
|
403
444
|
await run("start_plan", { objective: "three things" }, ctx);
|
|
404
445
|
expect(planView(stateOf(ctx)).progress).toBe(0);
|
|
405
446
|
|
|
406
|
-
await run("work_next_step",
|
|
447
|
+
await run("work_next_step", ctx);
|
|
407
448
|
const view = planView(stateOf(ctx));
|
|
408
449
|
expect(view.progress).toBeCloseTo(1 / 3, 5);
|
|
409
450
|
expect(view.done[0]?.step).toBe("One");
|
|
@@ -3,7 +3,7 @@ import { planProjection } from "./shared.ts";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A planning desk you can phone: plan-and-execute, with the caller in the loop.
|
|
6
|
-
* `prompts.ts` carries the attribution, `
|
|
6
|
+
* `prompts.ts` carries the attribution, `procedure.ts` the three nodes, `shared.ts`
|
|
7
7
|
* the state (which is their `PlanExecute`, field for field).
|
|
8
8
|
*
|
|
9
9
|
* **The steps do real work.** `work_next_step` runs a bounded search/answer loop
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll, client, useAgentState
|
|
2
|
+
import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
3
|
import { planProjection } from "./shared.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,14 +10,13 @@ import { planProjection } from "./shared.ts";
|
|
|
10
10
|
* out. The sidebar is where that lives; the call is where the decisions happen.
|
|
11
11
|
*/
|
|
12
12
|
function PlanSidebar() {
|
|
13
|
-
const theme = useTheme();
|
|
14
13
|
const plan = useAgentState(planProjection);
|
|
15
14
|
|
|
16
15
|
if (!plan.objective) {
|
|
17
16
|
return (
|
|
18
17
|
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
|
19
18
|
<span className="text-4xl">🗂️</span>
|
|
20
|
-
<p className="text-sm opacity-60
|
|
19
|
+
<p className="text-sm opacity-60 text-aai-text">
|
|
21
20
|
Say what you want to get done and the plan appears here.
|
|
22
21
|
</p>
|
|
23
22
|
</div>
|
|
@@ -25,14 +24,16 @@ function PlanSidebar() {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
return (
|
|
28
|
-
<div className="flex h-full min-h-0 flex-col gap-4 p-4
|
|
27
|
+
<div className="flex h-full min-h-0 flex-col gap-4 p-4 text-aai-text">
|
|
29
28
|
<div className="flex flex-col gap-2">
|
|
30
29
|
<h3 className="text-sm font-bold uppercase tracking-wide opacity-60">Objective</h3>
|
|
31
30
|
<p className="text-sm">{plan.objective}</p>
|
|
32
|
-
<div className="h-1.5 w-full rounded-full
|
|
31
|
+
<div className="h-1.5 w-full rounded-full bg-aai-surface">
|
|
32
|
+
{/* The WIDTH is the one thing here that is genuinely computed, so it
|
|
33
|
+
stays inline; the colour is a token. */}
|
|
33
34
|
<div
|
|
34
|
-
className="h-1.5 rounded-full transition-all"
|
|
35
|
-
style={{ width: `${Math.round(plan.progress * 100)}
|
|
35
|
+
className="h-1.5 rounded-full transition-all bg-aai-primary"
|
|
36
|
+
style={{ width: `${Math.round(plan.progress * 100)}%` }}
|
|
36
37
|
/>
|
|
37
38
|
</div>
|
|
38
39
|
<p className="text-xs opacity-50">
|
|
@@ -45,9 +46,9 @@ function PlanSidebar() {
|
|
|
45
46
|
contentClassName="flex flex-col gap-2 pr-1"
|
|
46
47
|
>
|
|
47
48
|
{plan.done.map((past) => (
|
|
48
|
-
<div key={past.step} className="rounded-lg p-3
|
|
49
|
+
<div key={past.step} className="rounded-lg p-3 bg-aai-surface">
|
|
49
50
|
<p className="text-sm">
|
|
50
|
-
<span
|
|
51
|
+
<span className="text-aai-primary">✓</span> {past.step}
|
|
51
52
|
</p>
|
|
52
53
|
<p className="mt-1 text-xs opacity-70">{past.result}</p>
|
|
53
54
|
{past.searches.length > 0 && (
|
|
@@ -60,8 +61,7 @@ function PlanSidebar() {
|
|
|
60
61
|
// a revision, so the position is part of the identity.
|
|
61
62
|
<div
|
|
62
63
|
key={`${index}-${step}`}
|
|
63
|
-
className="rounded-lg p-3 text-sm opacity-60"
|
|
64
|
-
style={{ border: `1px dashed ${theme.border}` }}
|
|
64
|
+
className="rounded-lg p-3 text-sm opacity-60 border border-dashed border-aai-border"
|
|
65
65
|
>
|
|
66
66
|
<span className="opacity-50">{plan.done.length + index + 1}.</span> {step}
|
|
67
67
|
</div>
|
|
@@ -69,10 +69,7 @@ function PlanSidebar() {
|
|
|
69
69
|
</AutoScroll>
|
|
70
70
|
|
|
71
71
|
{plan.response && (
|
|
72
|
-
<div
|
|
73
|
-
className="rounded-lg p-3"
|
|
74
|
-
style={{ background: theme.surface, border: `1px solid ${theme.primary}` }}
|
|
75
|
-
>
|
|
72
|
+
<div className="rounded-lg p-3 bg-aai-surface border border-aai-primary">
|
|
76
73
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Answer</p>
|
|
77
74
|
<p className="mt-1 text-sm">{plan.response}</p>
|
|
78
75
|
</div>
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
* a different template (`solo-rpg` has the save-slot version).
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type DeepReadonly,
|
|
18
|
+
type DialogPosition,
|
|
19
|
+
type DialogSpec,
|
|
20
|
+
dialog,
|
|
21
|
+
isToolFailure,
|
|
22
|
+
pushCapped,
|
|
23
|
+
sessionSlot,
|
|
24
|
+
} from "@alexkroman1/aai";
|
|
17
25
|
import { webSearch } from "@alexkroman1/aai/tools";
|
|
18
|
-
import { isToolFailure } from "@alexkroman1/aai/utils";
|
|
19
26
|
|
|
20
27
|
/** One completed step — their `past_steps`, as a pair rather than a tuple. */
|
|
21
28
|
export interface PastStep {
|
|
@@ -61,6 +68,68 @@ export function emptyPlan(): PlanState {
|
|
|
61
68
|
|
|
62
69
|
export const planSlot = sessionSlot("plan", emptyPlan);
|
|
63
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The plan's LIFECYCLE, as a declared machine rather than a guard per tool.
|
|
73
|
+
*
|
|
74
|
+
* Three of the four tools used to open with the same shape — `if
|
|
75
|
+
* (!plan.objective) return toolFailure("There is no plan yet …")`, and
|
|
76
|
+
* `work_next_step` carried a second one for the already-answered case. Those are
|
|
77
|
+
* not data checks, they are the question "where is this conversation", and
|
|
78
|
+
* getting one wrong is silent: the tool runs, the model reads a plausible
|
|
79
|
+
* result, and the caller is told about a plan that does not exist. A fifth tool
|
|
80
|
+
* would have had to remember both.
|
|
81
|
+
*
|
|
82
|
+
* `when` is that check now, and it is the SDK's rather than this template's — so
|
|
83
|
+
* the refusal names where the call actually is and quotes the instruction below,
|
|
84
|
+
* which is what lets the model recover on its own turn instead of apologizing.
|
|
85
|
+
*
|
|
86
|
+
* The states are the notebook's own, read off `PlanExecute`: no `input` yet,
|
|
87
|
+
* an `input` with steps left, and a `response`. `PLANNED` is accepted from all
|
|
88
|
+
* three because `start_plan` is always legal — a caller may re-plan from
|
|
89
|
+
* scratch at any point, which is the one transition that is not a progression.
|
|
90
|
+
*/
|
|
91
|
+
const planSpec = {
|
|
92
|
+
initial: "idle",
|
|
93
|
+
states: {
|
|
94
|
+
idle: {
|
|
95
|
+
instruction:
|
|
96
|
+
"There is no plan yet. Find out what the caller wants to get done, then use start_plan.",
|
|
97
|
+
on: { PLANNED: "working" },
|
|
98
|
+
},
|
|
99
|
+
working: {
|
|
100
|
+
instruction:
|
|
101
|
+
"Work the plan one step at a time with work_next_step, reporting after each step.",
|
|
102
|
+
on: { ANSWERED: "answered", PLANNED: "working" },
|
|
103
|
+
},
|
|
104
|
+
answered: {
|
|
105
|
+
instruction:
|
|
106
|
+
"The plan is finished — give the caller the answer. Use revise_plan if they change their mind.",
|
|
107
|
+
on: { REOPENED: "working", PLANNED: "working" },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
} as const satisfies DialogSpec;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The flow. Its own slot key, because a flow stores an actor snapshot and
|
|
114
|
+
* {@link planSlot} stores the plan — the position and the payload are two
|
|
115
|
+
* things, and one tool call moves both.
|
|
116
|
+
*/
|
|
117
|
+
export const planFlow = dialog("planFlow", planSpec);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* How the stage reads to a caller, from the flow's own position.
|
|
121
|
+
*
|
|
122
|
+
* A helper over a {@link DialogPosition} rather than over {@link PlanState}, which
|
|
123
|
+
* is the point: "where is this call" is the machine's answer, and deriving it a
|
|
124
|
+
* second time from the plan's fields is what the three removed guards were
|
|
125
|
+
* doing. `plan_status` reads this, and so would any prompt that wants to say it
|
|
126
|
+
* aloud.
|
|
127
|
+
*/
|
|
128
|
+
export function stageLabel(at: DialogPosition): string {
|
|
129
|
+
if (at.state === "idle") return "no plan yet";
|
|
130
|
+
return at.state === "answered" ? "finished" : "in progress";
|
|
131
|
+
}
|
|
132
|
+
|
|
64
133
|
export function noteRevision(state: PlanState, entry: string): void {
|
|
65
134
|
pushCapped(state.revisions, entry, MAX_REVISIONS);
|
|
66
135
|
}
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { planFlow, planSlot, stageLabel } from "../shared.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Where the plan is. Read-only, so plain `tool` semantics — a synchronous
|
|
5
|
-
*
|
|
5
|
+
* Where the plan is. Read-only, so plain `tool` semantics — a synchronous read
|
|
6
|
+
* cannot interleave with anything.
|
|
7
|
+
*
|
|
8
|
+
* **Legal in every state, which is why it is not a `planFlow.tool`**, and it is
|
|
9
|
+
* the one tool that reports the flow's own POSITION: `stage` and `next` come
|
|
10
|
+
* from the machine rather than from a second reading of the plan's fields, so
|
|
11
|
+
* "no plan yet" is the same fact here as the refusal `work_next_step` would
|
|
12
|
+
* give. It used to derive that from `!plan.objective`, which was a third copy of
|
|
13
|
+
* the same question.
|
|
6
14
|
*/
|
|
7
|
-
export default
|
|
15
|
+
export default tool({
|
|
8
16
|
description:
|
|
9
17
|
"Say where the plan has got to: what is done, what is left, and the answer " +
|
|
10
18
|
"if there is one. Use it when the caller asks, or to pick a call back up.",
|
|
11
|
-
execute(_args,
|
|
12
|
-
|
|
19
|
+
execute(_args, ctx) {
|
|
20
|
+
const at = planFlow.position(ctx);
|
|
21
|
+
const plan = planSlot.get(ctx);
|
|
13
22
|
return {
|
|
23
|
+
stage: at.state,
|
|
24
|
+
reads: stageLabel(at),
|
|
25
|
+
next: at.instruction,
|
|
14
26
|
objective: plan.objective,
|
|
15
27
|
done: plan.pastSteps.map((past) => ({ step: past.step, result: past.result })),
|
|
16
28
|
remaining: plan.plan,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { errorMessage,
|
|
1
|
+
import { errorMessage, toolFailure } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { replanNode } from "../
|
|
3
|
+
import { replanNode } from "../procedure.ts";
|
|
4
4
|
import { REVISE_SYSTEM } from "../prompts.ts";
|
|
5
|
-
import { noteRevision, planSlot } from "../shared.ts";
|
|
5
|
+
import { noteRevision, planFlow, planSlot } from "../shared.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The replanner, driven by the caller instead of by a step result.
|
|
@@ -16,8 +16,14 @@ import { noteRevision, planSlot } from "../shared.ts";
|
|
|
16
16
|
* The await-then-mutate shape is `start_plan`'s, for the reason it gives — and
|
|
17
17
|
* note the READ before the await is `planSlot.get`, which the replanner only
|
|
18
18
|
* needs to look at.
|
|
19
|
+
*
|
|
20
|
+
* **Legal in `working` AND `answered`, which is the whole point of it.** A
|
|
21
|
+
* finished plan is exactly what a caller most often wants changed, so this is
|
|
22
|
+
* the one tool that reopens one — `REOPENED` takes the flow back to `working`,
|
|
23
|
+
* and the body's clearing of `plan.response` is the same decision at the data
|
|
24
|
+
* level. Its `!objective` guard is gone: `when` is that check now.
|
|
19
25
|
*/
|
|
20
|
-
export default tool({
|
|
26
|
+
export default planFlow.tool({
|
|
21
27
|
description:
|
|
22
28
|
"Rewrite the remaining plan because the caller changed what they want. " +
|
|
23
29
|
"Pass their instruction as they said it. Completed steps are never redone.",
|
|
@@ -27,11 +33,8 @@ export default tool({
|
|
|
27
33
|
.max(400)
|
|
28
34
|
.describe("What the caller now wants changed, in their own words"),
|
|
29
35
|
}),
|
|
36
|
+
when: ["working", "answered"],
|
|
30
37
|
async execute(args, ctx) {
|
|
31
|
-
if (!planSlot.get(ctx).objective) {
|
|
32
|
-
return toolFailure("There is no plan to revise — use start_plan first.");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
38
|
try {
|
|
36
39
|
const act = await replanNode(ctx.generate, planSlot.get(ctx), {
|
|
37
40
|
system: REVISE_SYSTEM,
|
|
@@ -43,7 +46,7 @@ export default tool({
|
|
|
43
46
|
if (act.kind === "respond") {
|
|
44
47
|
plan.plan = [];
|
|
45
48
|
plan.response = act.response;
|
|
46
|
-
return {
|
|
49
|
+
return { finished: true, response: act.response, message: "Nothing is left to do." };
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
plan.plan = act.steps;
|
|
@@ -51,7 +54,7 @@ export default tool({
|
|
|
51
54
|
// moved the goalposts, so the old answer is no longer the answer.
|
|
52
55
|
plan.response = null;
|
|
53
56
|
return {
|
|
54
|
-
|
|
57
|
+
finished: false,
|
|
55
58
|
remaining: act.steps,
|
|
56
59
|
message: "Read the revised steps back and ask if that is right.",
|
|
57
60
|
};
|
|
@@ -60,4 +63,9 @@ export default tool({
|
|
|
60
63
|
return toolFailure(`The plan could not be revised: ${errorMessage(err)}`);
|
|
61
64
|
}
|
|
62
65
|
},
|
|
66
|
+
// Below `execute` deliberately — see the note on `work_next_step`'s own
|
|
67
|
+
// `sendFrom`: this body's return type is itself inferred, so a `sendFrom`
|
|
68
|
+
// written first has nothing to be contextually typed against.
|
|
69
|
+
sendFrom: (outcome) =>
|
|
70
|
+
outcome.finished ? ({ type: "ANSWERED" } as const) : ({ type: "REOPENED" } as const),
|
|
63
71
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { errorMessage, tool, toolFailure } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { planNode } from "../
|
|
4
|
-
import { noteRevision, planSlot } from "../shared.ts";
|
|
3
|
+
import { planNode } from "../procedure.ts";
|
|
4
|
+
import { noteRevision, planFlow, planSlot } from "../shared.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Their `plan_step`, as the call's opening move.
|
|
@@ -13,6 +13,12 @@ import { noteRevision, planSlot } from "../shared.ts";
|
|
|
13
13
|
* version guarded against is unchanged (the LLM loop runs a step's tool calls
|
|
14
14
|
* concurrently, so two plans started at once must not interleave); what changed
|
|
15
15
|
* is that the window is now too short to interleave IN.
|
|
16
|
+
*
|
|
17
|
+
* **This one is an ordinary `tool()` and NOT a `planFlow.tool`, deliberately.**
|
|
18
|
+
* `PLANNED` is accepted in all three states — re-planning from scratch is always
|
|
19
|
+
* legal — so a `when` listing every state would be a gate that gates nothing,
|
|
20
|
+
* paying the wrapper for it. It sends the event itself instead, which is what
|
|
21
|
+
* `dialog.send` is public for.
|
|
16
22
|
*/
|
|
17
23
|
export default tool({
|
|
18
24
|
description:
|
|
@@ -32,6 +38,11 @@ export default tool({
|
|
|
32
38
|
return toolFailure(`The planner failed: ${errorMessage(err)}`);
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
// The flow moves first: `work_next_step` gates on `working`, and a plan
|
|
42
|
+
// written to the slot while the flow still said `idle` would be refused by
|
|
43
|
+
// its own next tool call.
|
|
44
|
+
planFlow.send(ctx, { type: "PLANNED" });
|
|
45
|
+
|
|
35
46
|
return planSlot.update(ctx, (plan) => {
|
|
36
47
|
plan.objective = args.objective;
|
|
37
48
|
plan.plan = steps;
|