@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,411 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import type { GenerateFn, ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import {
|
|
5
|
+
createToolContext,
|
|
6
|
+
runTool,
|
|
7
|
+
stubGenerate,
|
|
8
|
+
withDiscoveredTools,
|
|
9
|
+
} from "@alexkroman1/aai/testing";
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import authoredAgent from "./agent.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
15
|
+
*
|
|
16
|
+
* The glob is written HERE rather than reached for from a shared helper because
|
|
17
|
+
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
18
|
+
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
19
|
+
* the file containing it either way. This is the pattern a user writes.
|
|
20
|
+
*/
|
|
21
|
+
const agentDef = withDiscoveredTools(
|
|
22
|
+
authoredAgent,
|
|
23
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
import { executeStep, MAX_STEP_SEARCHES, normalizeAct, planNode } from "./graph.ts";
|
|
27
|
+
import { EXECUTOR_SYSTEM, PLANNER_SYSTEM, REPLANNER_SYSTEM, REVISE_SYSTEM } from "./prompts.ts";
|
|
28
|
+
import type { SearchFn } from "./shared.ts";
|
|
29
|
+
import { MAX_PAST_STEPS, planSlot, planView } from "./shared.ts";
|
|
30
|
+
|
|
31
|
+
// ─── A scripted model ────────────────────────────────────────────────────────
|
|
32
|
+
//
|
|
33
|
+
// Each node is one `ctx.generate` call carrying its own system prompt, so
|
|
34
|
+
// `stubGenerate` — whose script IS keyed by system prompt — drives the whole
|
|
35
|
+
// loop with no model and no network. `calls` is every call the fake took, which
|
|
36
|
+
// is how the "a failed search goes back to the model" assertion is made: it
|
|
37
|
+
// asserts on the PROMPT the next turn carried.
|
|
38
|
+
|
|
39
|
+
interface Script {
|
|
40
|
+
steps?: string[];
|
|
41
|
+
/** One entry per executor turn: search that query, or answer with that text. */
|
|
42
|
+
turns?: ({ search: string } | { answer: string })[];
|
|
43
|
+
/** One entry per replan/revise call. */
|
|
44
|
+
acts?: { kind: "respond" | "plan"; response?: string; steps?: string[] }[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scriptedModel(script: Script = {}) {
|
|
48
|
+
const turns = [...(script.turns ?? [{ answer: "Settled it." }])];
|
|
49
|
+
const acts = [...(script.acts ?? [])];
|
|
50
|
+
// The replanner and the reviser are the same node with a different brief, so
|
|
51
|
+
// they share one queue — which is what the "revise then carry on" test rests on.
|
|
52
|
+
const act = () => ({ object: acts.shift() ?? { kind: "respond", response: "All done." } });
|
|
53
|
+
|
|
54
|
+
return stubGenerate({
|
|
55
|
+
[PLANNER_SYSTEM]: { object: { steps: script.steps ?? ["Only step"] } },
|
|
56
|
+
[EXECUTOR_SYSTEM]: () => {
|
|
57
|
+
const turn = turns.shift() ?? { answer: "Settled it." };
|
|
58
|
+
return "search" in turn
|
|
59
|
+
? { object: { action: "search", query: turn.search } }
|
|
60
|
+
: { object: { action: "answer", answer: turn.answer } };
|
|
61
|
+
},
|
|
62
|
+
[REPLANNER_SYSTEM]: act,
|
|
63
|
+
[REVISE_SYSTEM]: act,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A searcher that never touches the network. The tools use `liveSearch`, so
|
|
68
|
+
* every tool-level test below scripts the executor to answer without one. */
|
|
69
|
+
function fakeSearch(hits: Record<string, { title: string; url: string }[]>): {
|
|
70
|
+
search: SearchFn;
|
|
71
|
+
queries: string[];
|
|
72
|
+
} {
|
|
73
|
+
const queries: string[] = [];
|
|
74
|
+
const search: SearchFn = async (query) => {
|
|
75
|
+
queries.push(query);
|
|
76
|
+
const found = hits[query];
|
|
77
|
+
if (!found) throw new Error("search backend unavailable");
|
|
78
|
+
return found;
|
|
79
|
+
};
|
|
80
|
+
return { search, queries };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function makeCtx(generate: GenerateFn, sessionId?: string) {
|
|
84
|
+
return createToolContext({ generate, ...(sessionId ? { sessionId } : {}) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup
|
|
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);
|
|
92
|
+
|
|
93
|
+
function stateOf(ctx: ToolContext) {
|
|
94
|
+
return planSlot.get(ctx);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── 1. The nodes ────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe("planNode", () => {
|
|
100
|
+
test("returns the steps the planner produced", async () => {
|
|
101
|
+
const { generate, calls } = scriptedModel({ steps: ["Check prices", "Book it"] });
|
|
102
|
+
expect(await planNode(generate, "get me to Lisbon in May")).toEqual([
|
|
103
|
+
"Check prices",
|
|
104
|
+
"Book it",
|
|
105
|
+
]);
|
|
106
|
+
expect(calls[0]?.prompt).toContain("get me to Lisbon in May");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("executeStep", () => {
|
|
111
|
+
test("searches, reads the results, then answers — and reports what it searched", async () => {
|
|
112
|
+
const { generate, calls } = scriptedModel({
|
|
113
|
+
turns: [{ search: "lisbon flights may" }, { answer: "Flights are around 180 return." }],
|
|
114
|
+
});
|
|
115
|
+
const { search, queries } = fakeSearch({
|
|
116
|
+
"lisbon flights may": [{ title: "Fares to Lisbon", url: "https://example.test/fares" }],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const outcome = await executeStep(generate, search, "get to Lisbon", "Check prices", []);
|
|
120
|
+
expect(queries).toEqual(["lisbon flights may"]);
|
|
121
|
+
expect(outcome.result).toContain("180");
|
|
122
|
+
expect(outcome.searches).toEqual(["lisbon flights may"]);
|
|
123
|
+
// The results are what the second turn reasons over, not a note in a log.
|
|
124
|
+
expect(calls[1]?.prompt).toContain("https://example.test/fares");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a failed search goes back to the model rather than only to a log", async () => {
|
|
128
|
+
// Told nothing, the model reads silence as "no such pages exist" and burns
|
|
129
|
+
// the rest of its budget re-asking the same question.
|
|
130
|
+
const { generate, calls } = scriptedModel({
|
|
131
|
+
turns: [{ search: "unindexed thing" }, { answer: "Could not confirm that." }],
|
|
132
|
+
});
|
|
133
|
+
const { search } = fakeSearch({});
|
|
134
|
+
|
|
135
|
+
const outcome = await executeStep(generate, search, "objective", "Check the thing", []);
|
|
136
|
+
expect(calls[1]?.prompt).toContain("search backend unavailable");
|
|
137
|
+
expect(outcome.result).toBe("Could not confirm that.");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("the search budget is a bound, not a suggestion", async () => {
|
|
141
|
+
const { generate, calls } = scriptedModel({
|
|
142
|
+
// Three searches asked for, two allowed.
|
|
143
|
+
turns: [{ search: "a" }, { search: "b" }, { search: "c" }],
|
|
144
|
+
});
|
|
145
|
+
const { search, queries } = fakeSearch({
|
|
146
|
+
a: [{ title: "A", url: "https://example.test/a" }],
|
|
147
|
+
b: [{ title: "B", url: "https://example.test/b" }],
|
|
148
|
+
c: [{ title: "C", url: "https://example.test/c" }],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const outcome = await executeStep(generate, search, "objective", "step", []);
|
|
152
|
+
expect(queries).toHaveLength(MAX_STEP_SEARCHES);
|
|
153
|
+
// The last turn is told the budget is gone, which is what turns a search
|
|
154
|
+
// loop into an answer.
|
|
155
|
+
expect(calls.at(-1)?.prompt).toContain("search budget");
|
|
156
|
+
expect(outcome.result).toBe("This step could not be settled within its budget.");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("normalizeAct", () => {
|
|
161
|
+
test("a plan with steps is a plan", () => {
|
|
162
|
+
expect(normalizeAct({ kind: "plan", steps: ["Do X"] }, "fallback")).toEqual({
|
|
163
|
+
kind: "plan",
|
|
164
|
+
steps: ["Do X"],
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("'respond' with no response falls back rather than looping", () => {
|
|
169
|
+
// The failure mode that matters on a phone call is a desk that never stops.
|
|
170
|
+
expect(normalizeAct({ kind: "respond" }, "the last thing we know")).toEqual({
|
|
171
|
+
kind: "respond",
|
|
172
|
+
response: "the last thing we know",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("'plan' with no usable steps is read as an answer", () => {
|
|
177
|
+
expect(normalizeAct({ kind: "plan", steps: [" "] }, "nothing left")).toEqual({
|
|
178
|
+
kind: "respond",
|
|
179
|
+
response: "nothing left",
|
|
180
|
+
});
|
|
181
|
+
expect(normalizeAct({ kind: "plan", response: "here it is" }, "nothing left")).toEqual({
|
|
182
|
+
kind: "respond",
|
|
183
|
+
response: "here it is",
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── 2. The tools ────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
describe("start_plan", () => {
|
|
191
|
+
test("stores the objective and the steps", async () => {
|
|
192
|
+
const { generate } = scriptedModel({ steps: ["Check prices", "Compare hotels", "Book"] });
|
|
193
|
+
const ctx = makeCtx(generate);
|
|
194
|
+
const result = (await run("start_plan", { objective: "a weekend in Lisbon" }, ctx)) as {
|
|
195
|
+
steps: string[];
|
|
196
|
+
};
|
|
197
|
+
expect(result.steps).toHaveLength(3);
|
|
198
|
+
|
|
199
|
+
const state = stateOf(ctx);
|
|
200
|
+
expect(state.objective).toBe("a weekend in Lisbon");
|
|
201
|
+
expect(state.plan).toEqual(["Check prices", "Compare hotels", "Book"]);
|
|
202
|
+
expect(state.pastSteps).toEqual([]);
|
|
203
|
+
expect(state.revisions[0]).toContain("Planned 3 step(s)");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a broken model call is reported rather than thrown at the turn", async () => {
|
|
207
|
+
// The default `createToolContext` generate rejects — a bad key looks the
|
|
208
|
+
// same from here.
|
|
209
|
+
const ctx = createToolContext({});
|
|
210
|
+
expect(await run("start_plan", { objective: "anything" }, ctx)).toMatchObject({
|
|
211
|
+
error: expect.stringContaining("planner failed"),
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("work_next_step", () => {
|
|
217
|
+
test("refuses before there is a plan", async () => {
|
|
218
|
+
const { generate } = scriptedModel();
|
|
219
|
+
const ctx = makeCtx(generate);
|
|
220
|
+
expect(await run("work_next_step", {}, ctx)).toEqual({
|
|
221
|
+
error: "There is no plan yet — use start_plan first.",
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("does the head step, records it, and takes the replanner's next plan", async () => {
|
|
226
|
+
const { generate } = scriptedModel({
|
|
227
|
+
steps: ["Check prices", "Compare hotels"],
|
|
228
|
+
turns: [{ answer: "Fares are about 180 return." }],
|
|
229
|
+
acts: [{ kind: "plan", steps: ["Compare hotels"] }],
|
|
230
|
+
});
|
|
231
|
+
const ctx = makeCtx(generate);
|
|
232
|
+
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
233
|
+
|
|
234
|
+
const first = (await run("work_next_step", {}, ctx)) as {
|
|
235
|
+
done: boolean;
|
|
236
|
+
step: string;
|
|
237
|
+
result: string;
|
|
238
|
+
remaining: string[];
|
|
239
|
+
};
|
|
240
|
+
expect(first.done).toBe(false);
|
|
241
|
+
expect(first.step).toBe("Check prices");
|
|
242
|
+
expect(first.result).toContain("180");
|
|
243
|
+
expect(first.remaining).toEqual(["Compare hotels"]);
|
|
244
|
+
|
|
245
|
+
const state = stateOf(ctx);
|
|
246
|
+
expect(state.pastSteps).toHaveLength(1);
|
|
247
|
+
expect(state.plan).toEqual(["Compare hotels"]);
|
|
248
|
+
expect(state.response).toBeNull();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("a 'respond' act finishes the plan and clears what is left", async () => {
|
|
252
|
+
const { generate } = scriptedModel({
|
|
253
|
+
steps: ["Check prices", "Compare hotels"],
|
|
254
|
+
turns: [{ answer: "Fares are about 180 return." }],
|
|
255
|
+
acts: [{ kind: "respond", response: "Go in May — flights are about 180 return." }],
|
|
256
|
+
});
|
|
257
|
+
const ctx = makeCtx(generate);
|
|
258
|
+
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
259
|
+
const result = (await run("work_next_step", {}, ctx)) as { done: boolean; response: string };
|
|
260
|
+
|
|
261
|
+
expect(result.done).toBe(true);
|
|
262
|
+
expect(result.response).toContain("180");
|
|
263
|
+
const state = stateOf(ctx);
|
|
264
|
+
// The replanner deciding early is a good outcome, so the pending step goes.
|
|
265
|
+
expect(state.plan).toEqual([]);
|
|
266
|
+
expect(state.response).toBe(result.response);
|
|
267
|
+
|
|
268
|
+
// A finished plan is not worked again.
|
|
269
|
+
expect(await run("work_next_step", {}, ctx)).toMatchObject({ done: true });
|
|
270
|
+
expect(stateOf(ctx).pastSteps).toHaveLength(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("the completed-step trail is capped, so the executor's prompt cannot grow forever", async () => {
|
|
274
|
+
// `historyOf` renders every past step into the executor's prompt AND the
|
|
275
|
+
// replanner's, so an uncapped list is a model bill that grows linearly with
|
|
276
|
+
// the plan — the reason `recordStep` holds MAX_PAST_STEPS.
|
|
277
|
+
const total = MAX_PAST_STEPS + 3;
|
|
278
|
+
const { generate } = scriptedModel({
|
|
279
|
+
steps: Array.from({ length: total }, (_, i) => `Step ${i + 1}`),
|
|
280
|
+
turns: Array.from({ length: total }, (_, i) => ({ answer: `Found ${i + 1}.` })),
|
|
281
|
+
acts: Array.from({ length: total }, (_, i) => ({
|
|
282
|
+
kind: "plan" as const,
|
|
283
|
+
steps: Array.from({ length: total - i - 1 }, (_, j) => `Step ${i + j + 2}`),
|
|
284
|
+
})),
|
|
285
|
+
});
|
|
286
|
+
const ctx = makeCtx(generate);
|
|
287
|
+
await run("start_plan", { objective: "a long one" }, ctx);
|
|
288
|
+
for (let i = 0; i < total; i++) await run("work_next_step", {}, ctx);
|
|
289
|
+
|
|
290
|
+
const state = stateOf(ctx);
|
|
291
|
+
expect(state.pastSteps).toHaveLength(MAX_PAST_STEPS);
|
|
292
|
+
// The OLDEST go: the replanner decides from what was just found, and its
|
|
293
|
+
// fallback answer is the last entry.
|
|
294
|
+
expect(state.pastSteps[0]?.step).toBe(`Step ${total - MAX_PAST_STEPS + 1}`);
|
|
295
|
+
expect(state.pastSteps.at(-1)?.step).toBe(`Step ${total}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("two independent contexts never share a plan", async () => {
|
|
299
|
+
// What this really checks: the state lives in the SLOT and not in a
|
|
300
|
+
// module-level variable. `createToolContext()` hands each call its own
|
|
301
|
+
// detached slot store, so the isolation is per CONTEXT — two distinct
|
|
302
|
+
// session ids would prove nothing extra, and `sessionSlot` could stop
|
|
303
|
+
// keying by session with this still passing.
|
|
304
|
+
const { generate } = scriptedModel({ steps: ["Only step"] });
|
|
305
|
+
const first = makeCtx(generate);
|
|
306
|
+
const second = makeCtx(generate);
|
|
307
|
+
|
|
308
|
+
await run("start_plan", { objective: "mine" }, first);
|
|
309
|
+
expect(stateOf(second).objective).toBeNull();
|
|
310
|
+
expect(await run("work_next_step", {}, second)).toMatchObject({
|
|
311
|
+
error: "There is no plan yet — use start_plan first.",
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("revise_plan", () => {
|
|
317
|
+
test("rewrites what is left, keeps what is done, and reopens a finished plan", async () => {
|
|
318
|
+
const { generate, calls } = scriptedModel({
|
|
319
|
+
steps: ["Check Lisbon prices", "Book Lisbon"],
|
|
320
|
+
turns: [{ answer: "Lisbon is about 180 return." }],
|
|
321
|
+
acts: [
|
|
322
|
+
{ kind: "respond", response: "Lisbon in May, about 180." },
|
|
323
|
+
{ kind: "plan", steps: ["Check Porto prices"] },
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
const ctx = makeCtx(generate);
|
|
327
|
+
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
328
|
+
await run("work_next_step", {}, ctx);
|
|
329
|
+
expect(stateOf(ctx).response).not.toBeNull();
|
|
330
|
+
|
|
331
|
+
const revised = (await run("revise_plan", { instruction: "make it Porto instead" }, ctx)) as {
|
|
332
|
+
done: boolean;
|
|
333
|
+
remaining: string[];
|
|
334
|
+
};
|
|
335
|
+
expect(revised.done).toBe(false);
|
|
336
|
+
expect(revised.remaining).toEqual(["Check Porto prices"]);
|
|
337
|
+
|
|
338
|
+
const state = stateOf(ctx);
|
|
339
|
+
// The old answer is no longer the answer — the caller moved the goalposts.
|
|
340
|
+
expect(state.response).toBeNull();
|
|
341
|
+
// Completed work survives a revision; that is what "only add steps that
|
|
342
|
+
// still NEED to be done" means.
|
|
343
|
+
expect(state.pastSteps).toHaveLength(1);
|
|
344
|
+
expect(state.revisions.at(-1)).toContain("make it Porto instead");
|
|
345
|
+
// The caller's words reach the replanner, which is the whole node.
|
|
346
|
+
expect(calls.at(-1)?.prompt).toContain("make it Porto instead");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("refuses before there is a plan", async () => {
|
|
350
|
+
const { generate } = scriptedModel();
|
|
351
|
+
const ctx = makeCtx(generate);
|
|
352
|
+
expect(await run("revise_plan", { instruction: "change it" }, ctx)).toEqual({
|
|
353
|
+
error: "There is no plan to revise — use start_plan first.",
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("plan_status", () => {
|
|
359
|
+
test("reports done, remaining and the answer", async () => {
|
|
360
|
+
const { generate } = scriptedModel({
|
|
361
|
+
steps: ["Check prices", "Book"],
|
|
362
|
+
turns: [{ answer: "About 180 return." }],
|
|
363
|
+
acts: [{ kind: "plan", steps: ["Book"] }],
|
|
364
|
+
});
|
|
365
|
+
const ctx = makeCtx(generate);
|
|
366
|
+
expect(await run("plan_status", {}, ctx)).toEqual({
|
|
367
|
+
message: "No plan yet. Ask what they want to get done.",
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
371
|
+
await run("work_next_step", {}, ctx);
|
|
372
|
+
expect(await run("plan_status", {}, ctx)).toMatchObject({
|
|
373
|
+
objective: "a weekend in Lisbon",
|
|
374
|
+
remaining: ["Book"],
|
|
375
|
+
response: null,
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ─── 3. The projection contract with client.tsx ─────────────────────────────
|
|
381
|
+
|
|
382
|
+
describe("planView projection", () => {
|
|
383
|
+
test("an untouched call projects an empty plan, not undefined", () => {
|
|
384
|
+
// Exactly the value client.tsx hoists as its fallback.
|
|
385
|
+
expect(planSlot.projection(planView)(undefined)).toEqual({
|
|
386
|
+
objective: null,
|
|
387
|
+
plan: [],
|
|
388
|
+
done: [],
|
|
389
|
+
response: null,
|
|
390
|
+
revisions: [],
|
|
391
|
+
progress: 0,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("progress is derived once, so the bar and any spoken count agree", async () => {
|
|
396
|
+
const { generate } = scriptedModel({
|
|
397
|
+
steps: ["One", "Two", "Three"],
|
|
398
|
+
turns: [{ answer: "Done one." }],
|
|
399
|
+
acts: [{ kind: "plan", steps: ["Two", "Three"] }],
|
|
400
|
+
});
|
|
401
|
+
const ctx = makeCtx(generate);
|
|
402
|
+
await run("start_plan", { objective: "three things" }, ctx);
|
|
403
|
+
expect(planView(stateOf(ctx)).progress).toBe(0);
|
|
404
|
+
|
|
405
|
+
await run("work_next_step", {}, ctx);
|
|
406
|
+
const view = planView(stateOf(ctx));
|
|
407
|
+
expect(view.progress).toBeCloseTo(1 / 3, 5);
|
|
408
|
+
expect(view.done[0]?.step).toBe("One");
|
|
409
|
+
expect(view.plan).toEqual(["Two", "Three"]);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { agent } from "@alexkroman1/aai";
|
|
2
|
+
import { planSlot, planView } from "./shared.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A planning desk you can phone: plan-and-execute, with the caller in the loop.
|
|
6
|
+
* `prompts.ts` carries the attribution, `graph.ts` the three nodes, `shared.ts`
|
|
7
|
+
* the state (which is their `PlanExecute`, field for field).
|
|
8
|
+
*
|
|
9
|
+
* **The steps do real work.** `work_next_step` runs a bounded search/answer loop
|
|
10
|
+
* on `webSearch` — the same DuckDuckGo-backed implementation behind the
|
|
11
|
+
* `web_search` builtin, no API key — so a plan about anything current is worked
|
|
12
|
+
* against what is actually out there rather than against what a model
|
|
13
|
+
* remembers. That is the difference between this and a template that "plans" by
|
|
14
|
+
* asking a model to imagine having looked something up.
|
|
15
|
+
*/
|
|
16
|
+
export default agent({
|
|
17
|
+
name: "Planning Desk",
|
|
18
|
+
// The plan exists before the first tool call, so a resumed connection has
|
|
19
|
+
// something to project.
|
|
20
|
+
// The plan, its progress and its revision trail, pushed after every tool call.
|
|
21
|
+
syncState: planSlot.projection(planView),
|
|
22
|
+
greeting: "Planning desk. Tell me what you're trying to get done and I'll work out the steps.",
|
|
23
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
+
import { AutoScroll, client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
|
|
3
|
+
import type { PlanView } from "./shared.ts";
|
|
4
|
+
import { planSlot, planView } from "./shared.ts";
|
|
5
|
+
|
|
6
|
+
const EMPTY_PLAN: PlanView = planSlot.projection(planView)(undefined);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The plan, ticking off.
|
|
10
|
+
*
|
|
11
|
+
* A plan is the one thing in this template that is genuinely hard to hold by
|
|
12
|
+
* ear — four steps, two of them done, one rewritten since it was first read
|
|
13
|
+
* out. The sidebar is where that lives; the call is where the decisions happen.
|
|
14
|
+
*/
|
|
15
|
+
function PlanSidebar() {
|
|
16
|
+
const theme = useTheme();
|
|
17
|
+
const plan = useAgentState<PlanView>(EMPTY_PLAN);
|
|
18
|
+
|
|
19
|
+
if (!plan.objective) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
|
22
|
+
<span className="text-4xl">🗂️</span>
|
|
23
|
+
<p className="text-sm opacity-60" style={{ color: theme.text }}>
|
|
24
|
+
Say what you want to get done and the plan appears here.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex h-full min-h-0 flex-col gap-4 p-4" style={{ color: theme.text }}>
|
|
32
|
+
<div className="flex flex-col gap-2">
|
|
33
|
+
<h3 className="text-sm font-bold uppercase tracking-wide opacity-60">Objective</h3>
|
|
34
|
+
<p className="text-sm">{plan.objective}</p>
|
|
35
|
+
<div className="h-1.5 w-full rounded-full" style={{ background: theme.surface }}>
|
|
36
|
+
<div
|
|
37
|
+
className="h-1.5 rounded-full transition-all"
|
|
38
|
+
style={{ width: `${Math.round(plan.progress * 100)}%`, background: theme.primary }}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
<p className="text-xs opacity-50">
|
|
42
|
+
{plan.done.length} done · {plan.plan.length} to go
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<AutoScroll
|
|
47
|
+
scrollClassName="min-h-0 overflow-y-auto"
|
|
48
|
+
contentClassName="flex flex-col gap-2 pr-1"
|
|
49
|
+
>
|
|
50
|
+
{plan.done.map((past) => (
|
|
51
|
+
<div key={past.step} className="rounded-lg p-3" style={{ background: theme.surface }}>
|
|
52
|
+
<p className="text-sm">
|
|
53
|
+
<span style={{ color: theme.primary }}>✓</span> {past.step}
|
|
54
|
+
</p>
|
|
55
|
+
<p className="mt-1 text-xs opacity-70">{past.result}</p>
|
|
56
|
+
{past.searches.length > 0 && (
|
|
57
|
+
<p className="mt-1 text-[11px] opacity-40">searched: {past.searches.join(" · ")}</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
{plan.plan.map((step, index) => (
|
|
62
|
+
// Steps are short spoken sentences and can legitimately repeat across
|
|
63
|
+
// a revision, so the position is part of the identity.
|
|
64
|
+
<div
|
|
65
|
+
key={`${index}-${step}`}
|
|
66
|
+
className="rounded-lg p-3 text-sm opacity-60"
|
|
67
|
+
style={{ border: `1px dashed ${theme.border}` }}
|
|
68
|
+
>
|
|
69
|
+
<span className="opacity-50">{plan.done.length + index + 1}.</span> {step}
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</AutoScroll>
|
|
73
|
+
|
|
74
|
+
{plan.response && (
|
|
75
|
+
<div
|
|
76
|
+
className="rounded-lg p-3"
|
|
77
|
+
style={{ background: theme.surface, border: `1px solid ${theme.primary}` }}
|
|
78
|
+
>
|
|
79
|
+
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Answer</p>
|
|
80
|
+
<p className="mt-1 text-sm">{plan.response}</p>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{plan.revisions.length > 0 && (
|
|
85
|
+
<details className="text-xs opacity-60">
|
|
86
|
+
<summary className="cursor-pointer">Plan history ({plan.revisions.length})</summary>
|
|
87
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
88
|
+
{plan.revisions.map((entry, index) => (
|
|
89
|
+
// Append-only and capped, so the index is stable for its lifetime.
|
|
90
|
+
<p key={`${index}-${entry}`}>{entry}</p>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</details>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
client({
|
|
100
|
+
name: "Planning Desk",
|
|
101
|
+
sidebar: PlanSidebar,
|
|
102
|
+
theme: {
|
|
103
|
+
bg: "#12100e",
|
|
104
|
+
primary: "#d9a441",
|
|
105
|
+
text: "#f3efe7",
|
|
106
|
+
surface: "#1e1a16",
|
|
107
|
+
border: "#2c2620",
|
|
108
|
+
},
|
|
109
|
+
tools: {
|
|
110
|
+
start_plan: { icon: "\u{1F5C2}", label: "Drafting the plan" },
|
|
111
|
+
work_next_step: { icon: "\u{1F50E}", label: "Working a step" },
|
|
112
|
+
revise_plan: { icon: "\u{270F}", label: "Revising the plan" },
|
|
113
|
+
plan_status: { icon: "\u{2139}", label: "Checking the plan" },
|
|
114
|
+
},
|
|
115
|
+
});
|