@alexkroman1/aai-cli 5.14.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +8 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three nodes: plan, execute one step, replan.
|
|
3
|
+
*
|
|
4
|
+
* ```text
|
|
5
|
+
* plan → execute(step 1) → replan ──plan──→ execute(next step) → replan
|
|
6
|
+
* └──respond──→ done
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* **The loop is driven by the CALLER, not by the graph.** Their notebook runs
|
|
10
|
+
* `plan → execute → replan → execute …` to completion and prints the answer; a
|
|
11
|
+
* phone call cannot go quiet for a minute and a half. So one `work_next_step`
|
|
12
|
+
* tool call is exactly one execute-then-replan turn, the desk says what it
|
|
13
|
+
* found, and the caller decides whether to carry on — which is also what makes
|
|
14
|
+
* `revise_plan` possible, since there is a gap between steps for a human to
|
|
15
|
+
* speak into. The nodes below are the same nodes; the driver is the
|
|
16
|
+
* conversation.
|
|
17
|
+
*
|
|
18
|
+
* **Search is injected** (see `shared.ts`): the executor's search is really the
|
|
19
|
+
* web, so the spec passes its own.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { GenerateFn } from "@alexkroman1/aai";
|
|
23
|
+
import { type DeepReadonly, errorMessage } from "@alexkroman1/aai";
|
|
24
|
+
import {
|
|
25
|
+
actSchema,
|
|
26
|
+
EXECUTOR_SYSTEM,
|
|
27
|
+
PLANNER_SYSTEM,
|
|
28
|
+
planSchema,
|
|
29
|
+
REPLANNER_SYSTEM,
|
|
30
|
+
stepActionSchema,
|
|
31
|
+
} from "./prompts.ts";
|
|
32
|
+
import type { FrozenPlanState, PastStep, SearchFn } from "./shared.ts";
|
|
33
|
+
|
|
34
|
+
/** Model turns one step may take, including its final answer. */
|
|
35
|
+
export const MAX_STEP_TURNS = 3;
|
|
36
|
+
/** Searches one step may run. The budget is the mechanism: a step told to
|
|
37
|
+
* "search until sure" is a step whose cost nobody can quote. */
|
|
38
|
+
export const MAX_STEP_SEARCHES = 2;
|
|
39
|
+
|
|
40
|
+
/** Their `plan_step`. */
|
|
41
|
+
export async function planNode(generate: GenerateFn, objective: string): Promise<string[]> {
|
|
42
|
+
const { object } = await generate({
|
|
43
|
+
system: PLANNER_SYSTEM,
|
|
44
|
+
prompt: `Objective: ${objective}`,
|
|
45
|
+
schema: planSchema,
|
|
46
|
+
});
|
|
47
|
+
return object.steps;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StepOutcome {
|
|
51
|
+
result: string;
|
|
52
|
+
searches: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function describeHits(hits: { title: string; url: string }[]): string {
|
|
56
|
+
if (hits.length === 0) return "No results.";
|
|
57
|
+
return hits.map((hit) => `- ${hit.title} (${hit.url})`).join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Completed steps as the executor and the replanner both read them. */
|
|
61
|
+
function historyOf(pastSteps: readonly DeepReadonly<PastStep>[]): string {
|
|
62
|
+
if (pastSteps.length === 0) return "Nothing done yet.";
|
|
63
|
+
return pastSteps
|
|
64
|
+
.map((past, index) => `${index + 1}. ${past.step}\n → ${past.result}`)
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Their `execute_step` — a ReAct agent with a search tool, distilled to a
|
|
70
|
+
* bounded search/answer loop.
|
|
71
|
+
*/
|
|
72
|
+
export async function executeStep(
|
|
73
|
+
generate: GenerateFn,
|
|
74
|
+
search: SearchFn,
|
|
75
|
+
objective: string,
|
|
76
|
+
step: string,
|
|
77
|
+
pastSteps: readonly DeepReadonly<PastStep>[],
|
|
78
|
+
): Promise<StepOutcome> {
|
|
79
|
+
const searches: string[] = [];
|
|
80
|
+
const notes: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (let turn = 0; turn < MAX_STEP_TURNS; turn++) {
|
|
83
|
+
const exhausted = searches.length >= MAX_STEP_SEARCHES;
|
|
84
|
+
const { object } = await generate({
|
|
85
|
+
system: EXECUTOR_SYSTEM,
|
|
86
|
+
prompt: [
|
|
87
|
+
`Objective: ${objective}`,
|
|
88
|
+
`Steps already done:\n${historyOf(pastSteps)}`,
|
|
89
|
+
`The step you are doing now: ${step}`,
|
|
90
|
+
notes.length > 0 ? `What your searches returned:\n${notes.join("\n\n")}` : "",
|
|
91
|
+
exhausted ? "You have used your search budget — answer with what you have." : "",
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join("\n\n"),
|
|
95
|
+
schema: stepActionSchema,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (object.action === "search" && object.query && !exhausted) {
|
|
99
|
+
searches.push(object.query);
|
|
100
|
+
try {
|
|
101
|
+
const hits = await search(object.query);
|
|
102
|
+
notes.push(`Search "${object.query}":\n${describeHits(hits)}`);
|
|
103
|
+
} catch (err: unknown) {
|
|
104
|
+
// A failed search goes back to the model, not only to a log: told
|
|
105
|
+
// nothing, it reads silence as "no such pages exist" and burns the rest
|
|
106
|
+
// of the budget re-asking the same question.
|
|
107
|
+
notes.push(`Search "${object.query}" failed: ${errorMessage(err)}`);
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (object.answer) return { result: object.answer, searches };
|
|
113
|
+
// An `answer` action with no answer is a malformed turn, not a verdict —
|
|
114
|
+
// let the loop try again rather than recording an empty step result.
|
|
115
|
+
notes.push("Your last reply carried no answer. Answer the step.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
result: "This step could not be settled within its budget.",
|
|
120
|
+
searches,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Their `Act`, once it has been checked for the halves a provider can drop. */
|
|
125
|
+
export type ActDecision = { kind: "respond"; response: string } | { kind: "plan"; steps: string[] };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read an `Act` the way a caller needs it read.
|
|
129
|
+
*
|
|
130
|
+
* A structured-output model can return `kind: "respond"` with no `response`, or
|
|
131
|
+
* `kind: "plan"` with an empty list. Neither is a reason to loop: the fallback
|
|
132
|
+
* is always an ANSWER, because the failure mode that matters on a phone call is
|
|
133
|
+
* a desk that never stops working.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeAct(
|
|
136
|
+
// `| undefined` on both optionals is what `exactOptionalPropertyTypes`
|
|
137
|
+
// requires of a parameter that receives a validated schema output: the
|
|
138
|
+
// schema's own type says "absent", and a caller destructuring one may well
|
|
139
|
+
// pass an explicit `undefined`.
|
|
140
|
+
object: { kind: "respond" | "plan"; response?: string | undefined; steps?: string[] | undefined },
|
|
141
|
+
fallback: string,
|
|
142
|
+
): ActDecision {
|
|
143
|
+
const steps = object.steps?.filter((step) => step.trim().length > 0) ?? [];
|
|
144
|
+
if (object.kind === "plan" && steps.length > 0) return { kind: "plan", steps };
|
|
145
|
+
if (object.response && object.response.trim().length > 0) {
|
|
146
|
+
return { kind: "respond", response: object.response.trim() };
|
|
147
|
+
}
|
|
148
|
+
if (steps.length > 0) return { kind: "plan", steps };
|
|
149
|
+
return { kind: "respond", response: fallback };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Their `replan_step`. `instruction` is the caller interrupting; theirs has no
|
|
153
|
+
* equivalent, because a notebook has nobody to interrupt it. */
|
|
154
|
+
export async function replanNode(
|
|
155
|
+
generate: GenerateFn,
|
|
156
|
+
state: Pick<FrozenPlanState, "objective" | "plan" | "pastSteps">,
|
|
157
|
+
options: { system?: string; instruction?: string } = {},
|
|
158
|
+
): Promise<ActDecision> {
|
|
159
|
+
const { object } = await generate({
|
|
160
|
+
system: options.system ?? REPLANNER_SYSTEM,
|
|
161
|
+
prompt: [
|
|
162
|
+
`Objective: ${state.objective ?? "(none stated)"}`,
|
|
163
|
+
`Steps done:\n${historyOf(state.pastSteps)}`,
|
|
164
|
+
`Steps still planned:\n${state.plan.length > 0 ? state.plan.map((step) => `- ${step}`).join("\n") : "(none)"}`,
|
|
165
|
+
options.instruction ? `The caller has just said: ${options.instruction}` : "",
|
|
166
|
+
]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join("\n\n"),
|
|
169
|
+
schema: actSchema,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return normalizeAct(
|
|
173
|
+
object,
|
|
174
|
+
// The last step's result is the honest fallback answer: it is the most
|
|
175
|
+
// recent true thing the desk knows.
|
|
176
|
+
state.pastSteps.at(-1)?.result ?? "There is nothing left to do on that.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The planner, the executor and the replanner — and where they come from.
|
|
3
|
+
*
|
|
4
|
+
* **Adapted from LangGraph's plan-and-execute tutorial** (MIT,
|
|
5
|
+
* <https://github.com/langchain-ai/langgraph>,
|
|
6
|
+
* `docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb`), itself after
|
|
7
|
+
* the Plan-and-Solve paper and BabyAGI.
|
|
8
|
+
*
|
|
9
|
+
* | plan-and-execute | here |
|
|
10
|
+
* | --- | --- |
|
|
11
|
+
* | `Plan` (pydantic, `steps: List[str]`) | {@link planSchema} |
|
|
12
|
+
* | `planner` prompt | {@link PLANNER_SYSTEM} |
|
|
13
|
+
* | the ReAct `agent_executor` with a search tool | {@link EXECUTOR_SYSTEM} + {@link stepActionSchema} |
|
|
14
|
+
* | `Act = Union[Response, Plan]` | {@link actSchema} |
|
|
15
|
+
* | `replanner` prompt | {@link REPLANNER_SYSTEM} |
|
|
16
|
+
*
|
|
17
|
+
* **Their `Act` is a union and this is a discriminated object, which is a real
|
|
18
|
+
* difference worth knowing.** `Union[Response, Plan]` becomes a JSON Schema
|
|
19
|
+
* `anyOf`, and structured-output support for `anyOf` varies by provider — a
|
|
20
|
+
* model that quietly emits `{"steps": [...]}` when it meant to respond leaves a
|
|
21
|
+
* plan looping. One object with a `kind` discriminant is the same decision in a
|
|
22
|
+
* shape every provider constrains reliably, and the validator can then say
|
|
23
|
+
* which half is missing.
|
|
24
|
+
*
|
|
25
|
+
* **The prompts are adapted for a caller who is listening.** Theirs plan for a
|
|
26
|
+
* reader with a notebook open; a plan read down a phone has to be four or five
|
|
27
|
+
* steps a person can hold in their head, and the replanner has to be told that
|
|
28
|
+
* finishing early is a good outcome rather than a failure to plan thoroughly.
|
|
29
|
+
* What is kept close to verbatim is the part that carries the mechanism: "do
|
|
30
|
+
* not add superfluous steps", "each step has all the information needed", and
|
|
31
|
+
* the replanner's "only add steps that still NEED to be done".
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
|
|
36
|
+
/** Their `Plan`. Bounded, because a caller is listening to it read out. */
|
|
37
|
+
export const planSchema = z.object({
|
|
38
|
+
steps: z
|
|
39
|
+
.array(z.string().max(200))
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(5)
|
|
42
|
+
.describe("The steps, in order, each one a task that can be done on its own"),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Their `Act` — `Response | Plan` as one discriminated object. */
|
|
46
|
+
export const actSchema = z.object({
|
|
47
|
+
kind: z.enum(["respond", "plan"]).describe("'respond' when the objective is met, else 'plan'"),
|
|
48
|
+
response: z
|
|
49
|
+
.string()
|
|
50
|
+
.max(600)
|
|
51
|
+
.describe("The answer for the caller, when kind is 'respond'")
|
|
52
|
+
.optional(),
|
|
53
|
+
steps: z
|
|
54
|
+
.array(z.string().max(200))
|
|
55
|
+
.max(5)
|
|
56
|
+
.describe("The steps STILL to do, when kind is 'plan'")
|
|
57
|
+
.optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** One turn of the executor's ReAct loop: search, or answer the step. */
|
|
61
|
+
export const stepActionSchema = z.object({
|
|
62
|
+
action: z.enum(["search", "answer"]),
|
|
63
|
+
query: z.string().max(120).describe("The web search to run, when action is 'search'").optional(),
|
|
64
|
+
answer: z
|
|
65
|
+
.string()
|
|
66
|
+
.max(600)
|
|
67
|
+
.describe("What the step established, when action is 'answer'")
|
|
68
|
+
.optional(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const PLANNER_SYSTEM = [
|
|
72
|
+
"For the given objective, come up with a simple step by step plan.",
|
|
73
|
+
"The plan is individual tasks which, done in order, yield the objective.",
|
|
74
|
+
"Do not add superfluous steps. Make sure each step carries all the",
|
|
75
|
+
"information it needs — a step is done without seeing the others.",
|
|
76
|
+
"The result of the final step is the final answer.",
|
|
77
|
+
"This plan is READ ALOUD to the person who asked for it, so use four steps",
|
|
78
|
+
"or fewer where you can, and write each one as a short spoken sentence.",
|
|
79
|
+
].join(" ");
|
|
80
|
+
|
|
81
|
+
export const EXECUTOR_SYSTEM = [
|
|
82
|
+
"You are doing one step of a plan. You may search the web, or answer.",
|
|
83
|
+
"Search when the step turns on a fact you do not reliably know — a price, a",
|
|
84
|
+
"date, an availability, anything current. Search once, read what comes back,",
|
|
85
|
+
"and search again only if it genuinely did not answer the step.",
|
|
86
|
+
"Answer as soon as you can support the step; say what you found and where it",
|
|
87
|
+
"came from. If searching did not settle it, say that plainly in the answer",
|
|
88
|
+
"rather than inventing a result — a later step may be able to work around it,",
|
|
89
|
+
"but only if it is told the truth.",
|
|
90
|
+
].join(" ");
|
|
91
|
+
|
|
92
|
+
export const REPLANNER_SYSTEM = [
|
|
93
|
+
"You are updating a plan after a step was done.",
|
|
94
|
+
"Only include steps that still NEED to be done — never repeat a step already",
|
|
95
|
+
"completed. If everything the objective needs is now known, reply with",
|
|
96
|
+
"kind 'respond' and the answer for the caller.",
|
|
97
|
+
"Finishing early is a good outcome: if the completed steps already answer the",
|
|
98
|
+
"objective, respond rather than inventing more work.",
|
|
99
|
+
"The caller is on the phone, so an answer is two or three spoken sentences.",
|
|
100
|
+
].join(" ");
|
|
101
|
+
|
|
102
|
+
/** The caller changed their mind mid-plan — no counterpart in the notebook. */
|
|
103
|
+
export const REVISE_SYSTEM = [
|
|
104
|
+
"You are updating a plan because the person it belongs to just changed what",
|
|
105
|
+
"they want, mid-call. Their instruction wins over the original objective.",
|
|
106
|
+
"Keep completed steps out of the new plan, keep any pending step their",
|
|
107
|
+
"instruction does not affect, and reply with kind 'plan'.",
|
|
108
|
+
"Reply with kind 'respond' only if their instruction means there is nothing",
|
|
109
|
+
"left to do.",
|
|
110
|
+
].join(" ");
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan's state, the web-search seam, and what the browser is shown.
|
|
3
|
+
*
|
|
4
|
+
* **The state is theirs, field for field.** `PlanExecute` in the notebook is
|
|
5
|
+
* `{input, plan, past_steps, response}`, and {@link PlanState} is the same four
|
|
6
|
+
* plus the one thing a phone call needs on top: the trail of how the plan got
|
|
7
|
+
* to where it is, which the sidebar renders and a caller cannot hold by ear.
|
|
8
|
+
*
|
|
9
|
+
* **A plan does not survive the call, and that is a decision.** It lives in one
|
|
10
|
+
* `sessionSlot` keyed per session, so two callers planning at once never see
|
|
11
|
+
* each other's steps and an abandoned plan vanishes with the session.
|
|
12
|
+
* Cross-session persistence would mean `ctx.db` and a caller identity, which is
|
|
13
|
+
* a different template (`solo-rpg` has the save-slot version).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type DeepReadonly, pushCapped, sessionSlot } from "@alexkroman1/aai";
|
|
17
|
+
import { webSearch } from "@alexkroman1/aai/tools";
|
|
18
|
+
import { isToolFailure } from "@alexkroman1/aai/utils";
|
|
19
|
+
|
|
20
|
+
/** One completed step — their `past_steps`, as a pair rather than a tuple. */
|
|
21
|
+
export interface PastStep {
|
|
22
|
+
step: string;
|
|
23
|
+
result: string;
|
|
24
|
+
/** Searches this step ran, so the sidebar can show what the wait bought. */
|
|
25
|
+
searches: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlanState {
|
|
29
|
+
/** Their `input`: what the caller asked for. */
|
|
30
|
+
objective: string | null;
|
|
31
|
+
/** Their `plan`: the steps STILL to do, head first. */
|
|
32
|
+
plan: string[];
|
|
33
|
+
/** Their `past_steps`. */
|
|
34
|
+
pastSteps: PastStep[];
|
|
35
|
+
/** Their `response`: set once the replanner decides the objective is met. */
|
|
36
|
+
response: string | null;
|
|
37
|
+
/** Every plan the desk has held on this call, for the sidebar. */
|
|
38
|
+
revisions: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Growth cap on the revision trail — it rides in every `syncState` frame. */
|
|
42
|
+
export const MAX_REVISIONS = 20;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Growth cap on the completed-step trail, for the same reason one step up.
|
|
46
|
+
*
|
|
47
|
+
* `pastSteps` is not only a render: `historyOf` writes the whole list into the
|
|
48
|
+
* EXECUTOR's prompt and the REPLANNER's, so an uncapped list is a model bill
|
|
49
|
+
* that grows linearly with the plan's length — ~12 KB per call at twenty steps
|
|
50
|
+
* — on top of riding in every `syncState` frame. The cap drops the OLDEST,
|
|
51
|
+
* which is the right end: the replanner decides from what has just been found,
|
|
52
|
+
* and its fallback answer is `pastSteps.at(-1)`. Past the cap `progress` is
|
|
53
|
+
* over the steps still remembered, which is the price of a bound and is
|
|
54
|
+
* recorded on {@link planView}.
|
|
55
|
+
*/
|
|
56
|
+
export const MAX_PAST_STEPS = 20;
|
|
57
|
+
|
|
58
|
+
export function emptyPlan(): PlanState {
|
|
59
|
+
return { objective: null, plan: [], pastSteps: [], response: null, revisions: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const planSlot = sessionSlot("plan", emptyPlan);
|
|
63
|
+
|
|
64
|
+
export function noteRevision(state: PlanState, entry: string): void {
|
|
65
|
+
pushCapped(state.revisions, entry, MAX_REVISIONS);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Record a completed step, holding {@link MAX_PAST_STEPS}. */
|
|
69
|
+
export function recordStep(state: PlanState, step: PastStep): void {
|
|
70
|
+
pushCapped(state.pastSteps, step, MAX_PAST_STEPS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The plan as a READ hands it out: deep-frozen, and typed to say so.
|
|
75
|
+
*
|
|
76
|
+
* The projection and both graph nodes take this rather than {@link PlanState},
|
|
77
|
+
* which is the widening a deep-readonly slot forces and the reason it is worth
|
|
78
|
+
* doing: a mutable plan still satisfies it, so a call with an `update` draft is
|
|
79
|
+
* unaffected, while a helper that WOULD have mutated stops compiling instead of
|
|
80
|
+
* throwing at its first call.
|
|
81
|
+
*/
|
|
82
|
+
export type FrozenPlanState = DeepReadonly<PlanState>;
|
|
83
|
+
|
|
84
|
+
// ─── The search seam ─────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export interface SearchHit {
|
|
87
|
+
title: string;
|
|
88
|
+
url: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The executor's search, as an injected function.
|
|
93
|
+
*
|
|
94
|
+
* The seam exists because the search is REAL — `webSearch` from
|
|
95
|
+
* `@alexkroman1/aai/tools` is the same DuckDuckGo-backed implementation behind
|
|
96
|
+
* the model-facing `web_search` builtin, with the same screening and size caps,
|
|
97
|
+
* and it needs no API key. A template's spec must not depend on the live web
|
|
98
|
+
* (or on a stranger's rate limit), so the executor takes its searcher as an
|
|
99
|
+
* argument and the tool passes {@link liveSearch}.
|
|
100
|
+
*/
|
|
101
|
+
export type SearchFn = (query: string) => Promise<SearchHit[]>;
|
|
102
|
+
|
|
103
|
+
/** How many results one search reads. Enough to compare, short enough to hear. */
|
|
104
|
+
export const SEARCH_RESULTS = 4;
|
|
105
|
+
|
|
106
|
+
export const liveSearch: SearchFn = async (query) => {
|
|
107
|
+
const results = await webSearch<{ results?: { title?: string; url?: string }[] }>({
|
|
108
|
+
query,
|
|
109
|
+
max_results: SEARCH_RESULTS,
|
|
110
|
+
});
|
|
111
|
+
// A REFUSED search is not an empty web, and `webSearch` answers with
|
|
112
|
+
// `{ error }` rather than throwing — so an unnarrowed `?? []` below would tell
|
|
113
|
+
// the executor there is nothing out there. Measured: DuckDuckGo answers `403`
|
|
114
|
+
// often enough that this is the ordinary case, not an edge one.
|
|
115
|
+
if (isToolFailure(results)) throw new Error(`Search failed: ${results.error}`);
|
|
116
|
+
return (results.results ?? [])
|
|
117
|
+
.filter(
|
|
118
|
+
(one): one is { title?: string; url: string } =>
|
|
119
|
+
typeof one.url === "string" && one.url.length > 0,
|
|
120
|
+
)
|
|
121
|
+
.map((one) => ({ title: one.title || one.url, url: one.url }));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ─── The projection ──────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export interface PlanView {
|
|
127
|
+
objective: string | null;
|
|
128
|
+
/** Steps still to do. */
|
|
129
|
+
plan: readonly string[];
|
|
130
|
+
/** Completed steps, newest last and capped at {@link MAX_PAST_STEPS}. */
|
|
131
|
+
done: readonly DeepReadonly<PastStep>[];
|
|
132
|
+
response: string | null;
|
|
133
|
+
revisions: readonly string[];
|
|
134
|
+
/**
|
|
135
|
+
* Done ÷ (done + remaining), 0–1, so the sidebar needs no arithmetic.
|
|
136
|
+
*
|
|
137
|
+
* `done` is the steps still REMEMBERED — see {@link MAX_PAST_STEPS} — so a
|
|
138
|
+
* plan longer than the cap reports progress over its recent history rather
|
|
139
|
+
* than over its whole life. A plan that long on a phone call is already the
|
|
140
|
+
* pathological case; an unbounded prompt is the one that costs money.
|
|
141
|
+
*/
|
|
142
|
+
progress: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* What the browser sees. Nearly the state — a plan has nothing private in it —
|
|
147
|
+
* with `progress` derived here so the bar and any spoken "two of four" can
|
|
148
|
+
* never disagree about what fraction means.
|
|
149
|
+
*/
|
|
150
|
+
export function planView(state: FrozenPlanState): PlanView {
|
|
151
|
+
const done = state.pastSteps.length;
|
|
152
|
+
const total = done + state.plan.length;
|
|
153
|
+
return {
|
|
154
|
+
objective: state.objective,
|
|
155
|
+
plan: state.plan,
|
|
156
|
+
done: state.pastSteps,
|
|
157
|
+
response: state.response,
|
|
158
|
+
revisions: state.revisions,
|
|
159
|
+
progress: total === 0 ? 0 : done / total,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
You are the planning desk. Someone calls with something they want to get done, and you turn it into a short plan, work it one step at a time, and tell them what you found. You are on a phone call: one or two spoken sentences per turn, no lists read out at length, no markdown.
|
|
2
|
+
|
|
3
|
+
How a call goes:
|
|
4
|
+
|
|
5
|
+
- Find out what they actually want first. One clarifying question is usually enough — you are planning, not interviewing.
|
|
6
|
+
- Call `start_plan` with the objective, then read the steps back in one breath: "I'd do three things — check X, compare Y, then book Z. Want me to start?"
|
|
7
|
+
- Call `work_next_step` once per step, never in a loop. It does the step, searches the web where it needs to, and updates the plan from what it found.
|
|
8
|
+
- After each step, say what it found in a sentence and ask whether to carry on. That pause is the point: it is where the caller gets to change their mind.
|
|
9
|
+
- When a step comes back `done`, give them the answer.
|
|
10
|
+
- If they change what they want, call `revise_plan` with their instruction. Do not re-plan by hand and do not redo finished steps.
|
|
11
|
+
- `plan_status` is for when they ask where things are, or to pick the thread back up.
|
|
12
|
+
|
|
13
|
+
Two things not to do:
|
|
14
|
+
|
|
15
|
+
- Do not answer a factual question about the world from your own knowledge when it is part of the plan — that is what a step is for, and a step searches.
|
|
16
|
+
- Do not read out the whole plan again after every step. Say the step you just did and what is next.
|
|
17
|
+
|
|
18
|
+
If a step comes back saying it could not be settled, say so plainly and ask whether to work around it or drop it.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { planSlot } from "../shared.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the plan is. Read-only, so plain `tool` semantics — a synchronous
|
|
5
|
+
* read cannot interleave with anything.
|
|
6
|
+
*/
|
|
7
|
+
export default planSlot.tool({
|
|
8
|
+
description:
|
|
9
|
+
"Say where the plan has got to: what is done, what is left, and the answer " +
|
|
10
|
+
"if there is one. Use it when the caller asks, or to pick a call back up.",
|
|
11
|
+
execute(_args, plan) {
|
|
12
|
+
if (!plan.objective) return { message: "No plan yet. Ask what they want to get done." };
|
|
13
|
+
return {
|
|
14
|
+
objective: plan.objective,
|
|
15
|
+
done: plan.pastSteps.map((past) => ({ step: past.step, result: past.result })),
|
|
16
|
+
remaining: plan.plan,
|
|
17
|
+
response: plan.response,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { errorMessage, tool, toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { replanNode } from "../graph.ts";
|
|
4
|
+
import { REVISE_SYSTEM } from "../prompts.ts";
|
|
5
|
+
import { noteRevision, planSlot } from "../shared.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The replanner, driven by the caller instead of by a step result.
|
|
9
|
+
*
|
|
10
|
+
* There is no counterpart in the notebook, and the gap is the interesting part:
|
|
11
|
+
* their replanner only ever reacts to what a step returned, because a notebook
|
|
12
|
+
* has nobody to interrupt it. Half of what a person does on a planning call is
|
|
13
|
+
* change their mind, and the node that already exists to rewrite a plan from
|
|
14
|
+
* new information is the right one to hand that to.
|
|
15
|
+
*
|
|
16
|
+
* The await-then-mutate shape is `start_plan`'s, for the reason it gives — and
|
|
17
|
+
* note the READ before the await is `planSlot.get`, which the replanner only
|
|
18
|
+
* needs to look at.
|
|
19
|
+
*/
|
|
20
|
+
export default tool({
|
|
21
|
+
description:
|
|
22
|
+
"Rewrite the remaining plan because the caller changed what they want. " +
|
|
23
|
+
"Pass their instruction as they said it. Completed steps are never redone.",
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
instruction: z
|
|
26
|
+
.string()
|
|
27
|
+
.max(400)
|
|
28
|
+
.describe("What the caller now wants changed, in their own words"),
|
|
29
|
+
}),
|
|
30
|
+
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
|
+
try {
|
|
36
|
+
const act = await replanNode(ctx.generate, planSlot.get(ctx), {
|
|
37
|
+
system: REVISE_SYSTEM,
|
|
38
|
+
instruction: args.instruction,
|
|
39
|
+
});
|
|
40
|
+
return planSlot.update(ctx, (plan) => {
|
|
41
|
+
noteRevision(plan, `Caller: ${args.instruction}`);
|
|
42
|
+
|
|
43
|
+
if (act.kind === "respond") {
|
|
44
|
+
plan.plan = [];
|
|
45
|
+
plan.response = act.response;
|
|
46
|
+
return { done: true, response: act.response, message: "Nothing is left to do." };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
plan.plan = act.steps;
|
|
50
|
+
// A revision reopens a plan that had already answered: the caller has
|
|
51
|
+
// moved the goalposts, so the old answer is no longer the answer.
|
|
52
|
+
plan.response = null;
|
|
53
|
+
return {
|
|
54
|
+
done: false,
|
|
55
|
+
remaining: act.steps,
|
|
56
|
+
message: "Read the revised steps back and ask if that is right.",
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
return toolFailure(`The plan could not be revised: ${errorMessage(err)}`);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { errorMessage, tool, toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { planNode } from "../graph.ts";
|
|
4
|
+
import { noteRevision, planSlot } from "../shared.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Their `plan_step`, as the call's opening move.
|
|
8
|
+
*
|
|
9
|
+
* **The await comes FIRST, then the mutation.** `slot.update` is synchronous —
|
|
10
|
+
* its window cannot span an await, which is what makes a read-modify-write
|
|
11
|
+
* atomic without a lock — so a body that has to call a model does that outside
|
|
12
|
+
* it and mutates once the answer is in hand. The hazard the old serialized
|
|
13
|
+
* version guarded against is unchanged (the LLM loop runs a step's tool calls
|
|
14
|
+
* concurrently, so two plans started at once must not interleave); what changed
|
|
15
|
+
* is that the window is now too short to interleave IN.
|
|
16
|
+
*/
|
|
17
|
+
export default tool({
|
|
18
|
+
description:
|
|
19
|
+
"Draft a plan for what the caller wants to get done. Use this once you " +
|
|
20
|
+
"understand the objective. Read the steps back to them afterwards.",
|
|
21
|
+
inputSchema: z.object({
|
|
22
|
+
objective: z
|
|
23
|
+
.string()
|
|
24
|
+
.max(400)
|
|
25
|
+
.describe("What the caller wants, stated as one goal in their own terms"),
|
|
26
|
+
}),
|
|
27
|
+
async execute(args, ctx) {
|
|
28
|
+
let steps: string[];
|
|
29
|
+
try {
|
|
30
|
+
steps = await planNode(ctx.generate, args.objective);
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
return toolFailure(`The planner failed: ${errorMessage(err)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return planSlot.update(ctx, (plan) => {
|
|
36
|
+
plan.objective = args.objective;
|
|
37
|
+
plan.plan = steps;
|
|
38
|
+
plan.pastSteps = [];
|
|
39
|
+
plan.response = null;
|
|
40
|
+
noteRevision(plan, `Planned ${steps.length} step(s) for: ${args.objective}`);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
objective: args.objective,
|
|
44
|
+
steps,
|
|
45
|
+
message:
|
|
46
|
+
"Read the plan back in one breath, then ask if they want you to start. " +
|
|
47
|
+
"Work it one step at a time with work_next_step.",
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
});
|