@alexkroman1/aai-cli 6.10.1 → 7.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.
Files changed (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +93 -14
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +13 -4
  28. package/dist/scaffold/vite.config.ts +1 -1
  29. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  30. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  31. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  32. package/dist/templates/call-audit/agent.test.ts +818 -0
  33. package/dist/templates/call-audit/agent.ts +171 -0
  34. package/dist/templates/call-audit/client.tsx +198 -0
  35. package/dist/templates/call-audit/workflows/audit.ts +298 -0
  36. package/dist/templates/call-audit/workflows/ingest.ts +251 -0
  37. package/dist/templates/call-audit/workflows/media.ts +637 -0
  38. package/dist/templates/call-audit/workflows/summarize.ts +201 -0
  39. package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
  40. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  41. package/dist/templates/dispatch-center/client.tsx +239 -129
  42. package/dist/templates/dispatch-center/shared.ts +99 -1
  43. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  44. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  45. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  46. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  47. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  48. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  49. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  50. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  51. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  52. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  53. package/dist/templates/health-assistant/agent.test.ts +22 -4
  54. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  55. package/dist/templates/infocom-adventure/client.tsx +246 -164
  56. package/dist/templates/link-digest/agent.test.ts +24 -19
  57. package/dist/templates/link-digest/client.tsx +47 -61
  58. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  59. package/dist/templates/night-owl/agent.test.ts +70 -19
  60. package/dist/templates/night-owl/agent.ts +5 -0
  61. package/dist/templates/night-owl/client.tsx +56 -56
  62. package/dist/templates/night-owl/shared.ts +24 -0
  63. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  64. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  65. package/dist/templates/pizza-ordering/client.tsx +9 -26
  66. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  67. package/dist/templates/plan-and-execute/agent.ts +1 -1
  68. package/dist/templates/plan-and-execute/client.tsx +12 -15
  69. package/dist/templates/plan-and-execute/shared.ts +71 -2
  70. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  71. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  72. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  73. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  74. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  75. package/dist/templates/podcast-digest/agent.ts +139 -0
  76. package/dist/templates/podcast-digest/client.tsx +154 -0
  77. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  78. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  79. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  80. package/dist/templates/recap-workflow/agent.test.ts +107 -96
  81. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  82. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  83. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  84. package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
  85. package/dist/templates/redline/agent.test.ts +17 -15
  86. package/dist/templates/redline/client.tsx +12 -12
  87. package/dist/templates/redline/workflows/redline.ts +19 -31
  88. package/dist/templates/research-workflow/agent.test.ts +60 -59
  89. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  90. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  91. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  92. package/dist/templates/retail/agent.test.ts +26 -23
  93. package/dist/templates/retail/client.tsx +226 -117
  94. package/dist/templates/retail/registry.test.ts +38 -6
  95. package/dist/templates/retail/store.test.ts +82 -15
  96. package/dist/templates/retail/store.ts +174 -47
  97. package/dist/templates/retail/system-prompt.md +11 -2
  98. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  99. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  100. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  101. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  102. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  105. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  106. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  107. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  108. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  109. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  110. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  111. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  112. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  113. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  114. package/dist/templates/solo-rpg/client.tsx +38 -37
  115. package/dist/templates/solo-rpg/shared.ts +145 -19
  116. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  117. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  118. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  119. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  120. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  121. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  122. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  123. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  124. package/dist/templates/spoken-summary/agent.test.ts +314 -0
  125. package/dist/templates/spoken-summary/agent.ts +155 -0
  126. package/dist/templates/spoken-summary/client.tsx +185 -0
  127. package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
  128. package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
  129. package/dist/templates/support-line/agent.test.ts +11 -16
  130. package/dist/templates/support-line/agent.ts +1 -1
  131. package/dist/templates/support-line/client.tsx +9 -9
  132. package/dist/templates/support-line/nodes.ts +100 -0
  133. package/dist/templates/support-line/procedure.ts +407 -0
  134. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  135. package/dist/templates/transcription-workflow/agent.test.ts +246 -123
  136. package/dist/templates/transcription-workflow/agent.ts +21 -7
  137. package/dist/templates/transcription-workflow/client.tsx +17 -42
  138. package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
  139. package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
  140. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  141. package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
  142. package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
  143. package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
  144. package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
  145. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  146. package/dist/templates/travel-concierge/client.tsx +11 -23
  147. package/dist/templates/travel-concierge/routing.ts +34 -15
  148. package/dist/templates/travel-concierge/shared.ts +70 -3
  149. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  153. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  154. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  155. package/dist/worker-bundler.mjs +1 -1
  156. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  157. package/dist/workflow-bundler.d.ts +6 -1
  158. package/dist/workflow.d.ts +1 -1
  159. package/package.json +5 -4
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -13,9 +13,16 @@
13
13
  * a different template (`solo-rpg` has the save-slot version).
14
14
  */
15
15
 
16
- import { type DeepReadonly, pushCapped, sessionSlot } from "@alexkroman1/aai";
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 { planSlot } from "../shared.ts";
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
- * read cannot interleave with anything.
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 planSlot.tool({
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, plan) {
12
- if (!plan.objective) return { message: "No plan yet. Ask what they want to get done." };
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, tool, toolFailure } from "@alexkroman1/aai";
1
+ import { errorMessage, toolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { replanNode } from "../graph.ts";
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 { done: true, response: act.response, message: "Nothing is left to do." };
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
- done: false,
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 "../graph.ts";
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;
@@ -1,6 +1,26 @@
1
- import { errorMessage, isToolFailure, tool, toolFailure } from "@alexkroman1/aai";
2
- import { executeStep, replanNode } from "../graph.ts";
3
- import { liveSearch, noteRevision, type PastStep, planSlot, recordStep } from "../shared.ts";
1
+ import { errorMessage, type ToolFailure, toolFailure } from "@alexkroman1/aai";
2
+ import { executeStep, replanNode } from "../procedure.ts";
3
+ import {
4
+ liveSearch,
5
+ noteRevision,
6
+ type PastStep,
7
+ planFlow,
8
+ planSlot,
9
+ recordStep,
10
+ } from "../shared.ts";
11
+
12
+ /**
13
+ * What the claim window decided, as a DISCRIMINATED union.
14
+ *
15
+ * The three arms used to be told apart by `"done" in claimed`, which stopped
16
+ * working once the outcome had a declared type: union normalization gives every
17
+ * arm the other arms' keys as `?: never`, and `in` cannot rule out a key that is
18
+ * optional-never. A `kind` tag is both narrower and easier to read.
19
+ */
20
+ type Claim =
21
+ | { kind: "failed"; failure: ToolFailure }
22
+ | { kind: "dry" }
23
+ | { kind: "step"; step: string; objective: string; pastSteps: PastStep[] };
4
24
 
5
25
  /**
6
26
  * One turn of their execute→replan loop: do the head step, then let the
@@ -22,33 +42,54 @@ import { liveSearch, noteRevision, type PastStep, planSlot, recordStep } from ".
22
42
  * behind each other to get the same guarantee. A step whose work then FAILS is
23
43
  * put back, because a failed model call is worth retrying and a silently dropped
24
44
  * step leaves the caller with a plan that skipped something.
45
+ *
46
+ * **`when: "working"` replaced two hand-rolled guards** — the `!plan.objective`
47
+ * check and the already-answered one — and neither was a data check: both asked
48
+ * where the conversation was. See {@link planFlow}. The flow is what refuses now,
49
+ * naming the state and quoting its instruction, so this body only ever runs when
50
+ * there is a plan in progress.
51
+ *
52
+ * **`ANSWERED` is sent only when the replanner produced a RESPONSE.** A plan
53
+ * that merely ran out of steps stays in `working`: nothing was concluded, and
54
+ * the caller can still revise or add to it. Conflating the two would announce an
55
+ * answer that does not exist.
25
56
  */
26
- export default tool({
57
+ export default planFlow.tool({
27
58
  description:
28
59
  "Do the next step of the plan and report what it found. Call this once per " +
29
60
  "step — never in a loop. Say a short 'let me look into that' first, since " +
30
61
  "the step may take a few seconds.",
62
+ when: "working",
31
63
  async execute(_args, ctx) {
32
64
  // The whole read-and-claim, in one window nothing can interleave with.
33
- const claimed = planSlot.update(ctx, (plan) => {
34
- if (!plan.objective) return toolFailure("There is no plan yet use start_plan first.");
35
- if (plan.response) {
36
- return {
37
- done: true as const,
38
- response: plan.response,
39
- message: "The plan is already finished.",
40
- };
65
+ const claimed = planSlot.update(ctx, (plan): Claim => {
66
+ // `when: "working"` means there IS a plan, so this arm is unreachable by
67
+ // the flow's own guarantee — kept because the slot and the flow are two
68
+ // values, and a plan cleared by something else should refuse rather than
69
+ // claim a step off an empty objective.
70
+ if (!plan.objective) {
71
+ return { kind: "failed", failure: toolFailure("There is no plan yet — use start_plan.") };
41
72
  }
42
73
  const step = plan.plan.shift();
43
- if (!step) {
44
- return {
45
- done: true as const,
46
- message: "No steps are left. Ask the caller what they want next.",
47
- };
48
- }
49
- return { step, objective: plan.objective, pastSteps: [...plan.pastSteps] as PastStep[] };
74
+ if (!step) return { kind: "dry" };
75
+ return {
76
+ kind: "step",
77
+ step,
78
+ objective: plan.objective,
79
+ pastSteps: [...plan.pastSteps] as PastStep[],
80
+ };
50
81
  });
51
- if (isToolFailure(claimed) || "done" in claimed) return claimed;
82
+ if (claimed.kind === "failed") return claimed.failure;
83
+ if (claimed.kind === "dry") {
84
+ // `response: undefined` is spelled out rather than omitted: `sendFrom`
85
+ // below reads that field to decide whether to send ANSWERED, and a key
86
+ // missing from one arm of a union is not readable on the union at all.
87
+ return {
88
+ finished: true,
89
+ response: undefined,
90
+ message: "No steps are left. Ask the caller what they want next.",
91
+ };
92
+ }
52
93
  const { step, objective, pastSteps } = claimed;
53
94
 
54
95
  try {
@@ -69,7 +110,7 @@ export default tool({
69
110
  plan.plan = [];
70
111
  noteRevision(plan, `Finished after ${plan.pastSteps.length} step(s)`);
71
112
  return {
72
- done: true,
113
+ finished: true,
73
114
  step,
74
115
  result: outcome.result,
75
116
  searches: outcome.searches,
@@ -82,7 +123,7 @@ export default tool({
82
123
  plan.plan = act.steps;
83
124
  if (changed) noteRevision(plan, `Replanned to ${act.steps.length} step(s) after: ${step}`);
84
125
  return {
85
- done: false,
126
+ finished: false,
86
127
  step,
87
128
  result: outcome.result,
88
129
  searches: outcome.searches,
@@ -98,4 +139,11 @@ export default tool({
98
139
  return toolFailure(`That step could not be worked: ${errorMessage(err)}`);
99
140
  }
100
141
  },
142
+ // Written BELOW `execute` deliberately. `sendFrom`'s parameter is
143
+ // `Exclude<NoInfer<R>, ToolFailure>`, and `NoInfer` keeps it from bidding on
144
+ // `R` — but this body's own return type is itself an inference
145
+ // (`planSlot.update`'s), so with `sendFrom` first there is no candidate to
146
+ // contextually type it against and the parameter lands as `unknown`.
147
+ sendFrom: (outcome) =>
148
+ outcome.response === undefined ? undefined : ({ type: "ANSWERED" } as const),
101
149
  });