@alexkroman1/aai-cli 6.11.0 → 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 +40 -19
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +1 -1
  28. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  29. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  30. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  31. package/dist/templates/call-audit/agent.test.ts +57 -204
  32. package/dist/templates/call-audit/agent.ts +32 -19
  33. package/dist/templates/call-audit/client.tsx +17 -54
  34. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  35. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  36. package/dist/templates/call-audit/workflows/media.ts +2 -12
  37. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  38. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  39. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  40. package/dist/templates/dispatch-center/client.tsx +239 -129
  41. package/dist/templates/dispatch-center/shared.ts +99 -1
  42. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  43. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  44. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  45. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  46. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  47. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  48. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  49. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  50. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  51. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  52. package/dist/templates/health-assistant/agent.test.ts +22 -4
  53. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  54. package/dist/templates/infocom-adventure/client.tsx +246 -164
  55. package/dist/templates/link-digest/agent.test.ts +24 -19
  56. package/dist/templates/link-digest/client.tsx +47 -61
  57. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  58. package/dist/templates/night-owl/agent.test.ts +70 -19
  59. package/dist/templates/night-owl/agent.ts +5 -0
  60. package/dist/templates/night-owl/client.tsx +56 -56
  61. package/dist/templates/night-owl/shared.ts +24 -0
  62. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  63. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  64. package/dist/templates/pizza-ordering/client.tsx +9 -26
  65. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  66. package/dist/templates/plan-and-execute/agent.ts +1 -1
  67. package/dist/templates/plan-and-execute/client.tsx +12 -15
  68. package/dist/templates/plan-and-execute/shared.ts +71 -2
  69. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  70. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  71. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  72. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  73. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  74. package/dist/templates/podcast-digest/agent.ts +139 -0
  75. package/dist/templates/podcast-digest/client.tsx +154 -0
  76. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  77. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  78. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  79. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  80. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  81. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  82. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  83. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  84. package/dist/templates/redline/agent.test.ts +17 -15
  85. package/dist/templates/redline/client.tsx +12 -12
  86. package/dist/templates/redline/workflows/redline.ts +19 -31
  87. package/dist/templates/research-workflow/agent.test.ts +60 -59
  88. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  89. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  90. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  91. package/dist/templates/retail/agent.test.ts +26 -23
  92. package/dist/templates/retail/client.tsx +226 -117
  93. package/dist/templates/retail/registry.test.ts +38 -6
  94. package/dist/templates/retail/store.test.ts +82 -15
  95. package/dist/templates/retail/store.ts +174 -47
  96. package/dist/templates/retail/system-prompt.md +11 -2
  97. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  98. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  99. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  100. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  101. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  102. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  105. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  106. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  107. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  108. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  109. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  110. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  111. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  112. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  113. package/dist/templates/solo-rpg/client.tsx +38 -37
  114. package/dist/templates/solo-rpg/shared.ts +145 -19
  115. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  116. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  117. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  118. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  119. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  120. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  121. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  122. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  123. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  124. package/dist/templates/spoken-summary/agent.ts +30 -17
  125. package/dist/templates/spoken-summary/client.tsx +10 -50
  126. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  127. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  128. package/dist/templates/support-line/agent.test.ts +11 -16
  129. package/dist/templates/support-line/agent.ts +1 -1
  130. package/dist/templates/support-line/client.tsx +9 -9
  131. package/dist/templates/support-line/nodes.ts +100 -0
  132. package/dist/templates/support-line/procedure.ts +407 -0
  133. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  134. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  135. package/dist/templates/transcription-workflow/agent.ts +1 -1
  136. package/dist/templates/transcription-workflow/client.tsx +17 -42
  137. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  138. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  139. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  140. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  141. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  142. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  143. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  144. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  145. package/dist/templates/travel-concierge/client.tsx +11 -23
  146. package/dist/templates/travel-concierge/routing.ts +34 -15
  147. package/dist/templates/travel-concierge/shared.ts +70 -3
  148. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  149. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  153. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  154. package/dist/worker-bundler.mjs +1 -1
  155. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  156. package/dist/workflow-bundler.d.ts +6 -1
  157. package/dist/workflow.d.ts +1 -1
  158. package/package.json +5 -4
  159. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  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
@@ -0,0 +1,407 @@
1
+ /**
2
+ * The corrective-RAG loop, as a `procedure()`.
3
+ *
4
+ * This is the port of the self-RAG / CRAG graph (see `prompts.ts` for the
5
+ * attribution), and it is now the same SHAPE as the thing it ports: their
6
+ * control flow is edges between nodes, and so is this.
7
+ *
8
+ * ```text
9
+ * retrieve → grade_documents → decide_to_generate
10
+ * ├─ no relevant docs → transform_query → retrieve
11
+ * └─ generate → grade_generation_v_documents
12
+ * ├─ not grounded → generate (once)
13
+ * └─ grade_generation_v_question
14
+ * ├─ not useful → transform_query
15
+ * └─ done
16
+ * ```
17
+ *
18
+ * **It used to be a `while` loop with the node names preserved in a trace**, so
19
+ * that a run stayed readable as the graph it came from. That worked and cost two
20
+ * things worth getting back. The node names were a STRING passed to a `step()`
21
+ * helper beside the code that did the work, so nothing stopped the two drifting;
22
+ * here a node IS a state and the trace is written by that state's `entry`, so a
23
+ * renamed node cannot keep an old label. And the routing was spread across a
24
+ * `continue`, a nested `for`, and four early `return`s — the exact structure a
25
+ * statechart exists to make declarative. `xstate` is a dependency of the SDK, so
26
+ * this costs the template nothing to reach for.
27
+ *
28
+ * **The budget is the mechanism, not the prompt.** A procedure with a
29
+ * `transform_query` edge can cycle, and their notebooks bound it with a recursion
30
+ * limit that raises on hit. A caller is holding the line, so the bound here is
31
+ * two attempts and one regeneration — {@link MAX_ATTEMPTS} — expressed as GUARDS
32
+ * (`canRetry`, `canRegenerate`) rather than as loop conditions. Running out is an
33
+ * ANSWER (`exhausted`, the caller is offered a ticket) rather than an error: an
34
+ * agent that cannot say "I don't have that documented" is the failure the whole
35
+ * grading apparatus exists to prevent, so it has to be a reachable state — and
36
+ * here it is literally one.
37
+ *
38
+ * **Documents are graded CONCURRENTLY.** Their loop is sequential because a
39
+ * notebook does not care; four serial grader calls is four round trips of dead
40
+ * air on a phone. The grades are independent, so the fan-out is free — it lives
41
+ * inside the `gradeDocuments` node (`nodes.ts`), not in the routing.
42
+ *
43
+ * **`procedure()` is what runs it, and no actor appears in this template.** The two
44
+ * SDK machine primitives are for different jobs and the split is worth knowing:
45
+ * a `dialog()` is where a CONVERSATION is (persisted in a slot, moved one event at
46
+ * a time by the caller's turns), and a `procedure()` is one unit of WORK inside a
47
+ * single tool call (never stored, driving itself through invoked actors). This is
48
+ * the second, so its context may hold a `GenerateFn` that no stored slot could.
49
+ *
50
+ * `procedure().run` is also what makes the loop interruptible: it takes
51
+ * `ctx.signal`, so a caller who barges in on the second of nine model calls is
52
+ * not charged for the remaining seven.
53
+ */
54
+
55
+ import { type GenerateFn, omitUndefined, procedure } from "@alexkroman1/aai";
56
+ import { assign, fromPromise, setup } from "xstate";
57
+ import {
58
+ generateAnswer,
59
+ gradeDocuments,
60
+ gradeGrounded,
61
+ gradeUseful,
62
+ transformQuery,
63
+ type Verdict,
64
+ } from "./nodes.ts";
65
+ import type { AnswerTrace, Doc, GradedDoc, TraceStep } from "./shared.ts";
66
+ import { retrieve } from "./shared.ts";
67
+
68
+ /** Retrieve-and-answer attempts, i.e. one query rewrite. */
69
+ export const MAX_ATTEMPTS = 2;
70
+ /** Regenerations after a "not grounded" verdict, within one attempt. */
71
+ export const MAX_REGENERATIONS = 1;
72
+
73
+ /**
74
+ * What the machine carries: the trace it is building, plus the two counters the
75
+ * guards read and the documents the current attempt is answering from.
76
+ *
77
+ * `generate` rides in here because this machine is never stored — see the module
78
+ * doc. A {@link dialog}'s context has to survive `structuredClone`; this one only
79
+ * has to survive one tool call.
80
+ */
81
+ interface Ctx extends AnswerTrace {
82
+ generate: GenerateFn;
83
+ /** 1-based, bounded by {@link MAX_ATTEMPTS}. */
84
+ attempt: number;
85
+ /** Regenerations spent WITHIN this attempt; reset by `transform_query`. */
86
+ regenerations: number;
87
+ /** What this attempt's query retrieved, before grading. */
88
+ retrieved: Doc[];
89
+ /** The documents this attempt retrieved and the grader passed. */
90
+ relevant: Doc[];
91
+ }
92
+
93
+ /**
94
+ * One trace entry, as a `{ type: "note", params }` action.
95
+ *
96
+ * A parameterized action declared INSIDE `setup` rather than a helper that
97
+ * returns a bare `assign`: an action built outside carries no knowledge of this
98
+ * machine's actors, so `exactOptionalPropertyTypes` refuses it wherever the
99
+ * machine expects one of its own. `params` may be a function, which is what lets
100
+ * a detail string read the context it is describing.
101
+ */
102
+ function noteAt(node: string, detail: string): { type: "note"; params: NoteParams } {
103
+ return { type: "note", params: { node, detail } };
104
+ }
105
+
106
+ /** The same, for a detail computed from the context the entry describes. */
107
+ function noteFrom(node: string, detail: (context: Ctx) => string) {
108
+ return {
109
+ type: "note" as const,
110
+ params: ({ context }: { context: Ctx }): NoteParams => ({ node, detail: detail(context) }),
111
+ };
112
+ }
113
+
114
+ interface NoteParams {
115
+ node: string;
116
+ detail: string;
117
+ }
118
+
119
+ const machine = setup({
120
+ types: {} as {
121
+ context: Ctx;
122
+ input: { generate: GenerateFn; question: string };
123
+ output: AnswerTrace;
124
+ },
125
+ actors: {
126
+ gradeDocuments: fromPromise(
127
+ async ({ input }: { input: { ctx: Ctx; docs: Doc[] } }): Promise<GradedDoc[]> =>
128
+ await gradeDocuments(input.ctx.generate, input.ctx.question, input.docs),
129
+ ),
130
+ generateAnswer: fromPromise(
131
+ async ({ input }: { input: { ctx: Ctx } }): Promise<string> =>
132
+ await generateAnswer(input.ctx.generate, input.ctx.question, input.ctx.relevant),
133
+ ),
134
+ gradeGrounded: fromPromise(
135
+ async ({ input }: { input: { ctx: Ctx } }): Promise<Verdict> =>
136
+ await gradeGrounded(input.ctx.generate, input.ctx.relevant, input.ctx.answer ?? ""),
137
+ ),
138
+ gradeUseful: fromPromise(
139
+ async ({ input }: { input: { ctx: Ctx } }): Promise<Verdict> =>
140
+ await gradeUseful(input.ctx.generate, input.ctx.question, input.ctx.answer ?? ""),
141
+ ),
142
+ transformQuery: fromPromise(
143
+ async ({ input }: { input: { ctx: Ctx } }): Promise<string> =>
144
+ await transformQuery(input.ctx.generate, input.ctx.question),
145
+ ),
146
+ },
147
+ actions: {
148
+ note: assign({
149
+ steps: ({ context }, params: NoteParams): TraceStep[] => [
150
+ ...context.steps,
151
+ { node: params.node, detail: params.detail },
152
+ ],
153
+ }),
154
+ },
155
+ guards: {
156
+ /** Their `decide_to_generate`: is there anything to answer FROM? */
157
+ hasRelevant: ({ context }) => context.relevant.length > 0,
158
+ canRetry: ({ context }) => context.attempt < MAX_ATTEMPTS,
159
+ canRegenerate: ({ context }) => context.regenerations < MAX_REGENERATIONS,
160
+ },
161
+ // A grader's verdict is read INLINE (`event.output.pass`) rather than through
162
+ // a named guard: a guard in `setup` is typed against the machine's whole event
163
+ // union, so it cannot see that this particular transition is a done-event.
164
+ }).createMachine({
165
+ id: "correctiveRag",
166
+ initial: "retrieve",
167
+ context: ({ input }) => ({
168
+ generate: input.generate,
169
+ question: input.question,
170
+ query: input.question,
171
+ rewrites: 0,
172
+ steps: [],
173
+ docs: [],
174
+ answer: null,
175
+ grounded: null,
176
+ useful: null,
177
+ exhausted: false,
178
+ attempt: 1,
179
+ regenerations: 0,
180
+ retrieved: [],
181
+ relevant: [],
182
+ }),
183
+ states: {
184
+ /**
185
+ * Retrieval is LEXICAL and synchronous (`shared.ts`), so it is an entry
186
+ * action rather than an invoked actor — there is nothing to await.
187
+ */
188
+ retrieve: {
189
+ entry: [
190
+ // Assigned ONCE and read from context by everything downstream: the
191
+ // grader's input, the relevance filter and the trace all have to be
192
+ // talking about the same retrieval, and calling `retrieve` again per
193
+ // reader is how they come to disagree.
194
+ assign({
195
+ retrieved: ({ context }) => retrieve(context.query).map((one) => one.doc),
196
+ relevant: () => [],
197
+ docs: () => [],
198
+ }),
199
+ // Actions run in order and an `assign` is visible to the next one, so
200
+ // this reads the retrieval above rather than redoing it.
201
+ noteFrom("retrieve", (ctx) => `${ctx.retrieved.length} document(s) for "${ctx.query}"`),
202
+ ],
203
+ always: "gradeDocuments",
204
+ },
205
+
206
+ gradeDocuments: {
207
+ invoke: {
208
+ src: "gradeDocuments",
209
+ input: ({ context }) => ({ ctx: context, docs: context.retrieved }),
210
+ onDone: {
211
+ target: "decideToGenerate",
212
+ actions: [
213
+ assign({
214
+ docs: ({ event }) => event.output,
215
+ relevant: ({ context, event }) =>
216
+ context.retrieved.filter(
217
+ (doc) => event.output.find((graded) => graded.id === doc.id)?.relevant,
218
+ ),
219
+ }),
220
+ noteFrom(
221
+ "grade_documents",
222
+ (ctx) => `${ctx.relevant.length} of ${ctx.docs.length} relevant`,
223
+ ),
224
+ ],
225
+ },
226
+ },
227
+ },
228
+
229
+ /**
230
+ * Nothing relevant means the RETRIEVAL was wrong, not that the answer is
231
+ * unknown — so rewrite and try again before giving up. Pure routing: three
232
+ * guarded edges and no work of its own.
233
+ */
234
+ decideToGenerate: {
235
+ always: [
236
+ { guard: "hasRelevant", target: "generate" },
237
+ { guard: "canRetry", target: "transformQuery" },
238
+ {
239
+ target: "exhausted",
240
+ actions: [
241
+ assign({ exhausted: () => true }),
242
+ noteAt("decide_to_generate", "no relevant documents, and no attempts left"),
243
+ ],
244
+ },
245
+ ],
246
+ },
247
+
248
+ generate: {
249
+ invoke: {
250
+ src: "generateAnswer",
251
+ input: ({ context }) => ({ ctx: context }),
252
+ onDone: {
253
+ target: "gradeGrounded",
254
+ actions: [
255
+ assign({ answer: ({ event }) => event.output }),
256
+ noteFrom("generate", (ctx) =>
257
+ ctx.regenerations > 0
258
+ ? "regenerated"
259
+ : `answered from ${ctx.relevant.map((doc) => doc.id).join(", ")}`,
260
+ ),
261
+ ],
262
+ },
263
+ },
264
+ },
265
+
266
+ /**
267
+ * Is it grounded? A regeneration is worth one shot — the same documents,
268
+ * asked again. Two answers the grader rejected is a signal about the
269
+ * DOCUMENTS rather than about the phrasing, so the second refusal is final
270
+ * and the answer is withheld: an ungrounded answer is never spoken.
271
+ */
272
+ gradeGrounded: {
273
+ invoke: {
274
+ src: "gradeGrounded",
275
+ input: ({ context }) => ({ ctx: context }),
276
+ onDone: [
277
+ {
278
+ guard: ({ event }) => event.output.pass,
279
+ target: "gradeUseful",
280
+ actions: [
281
+ assign({ grounded: () => true }),
282
+ noteAt("grade_generation_v_documents", "grounded"),
283
+ ],
284
+ },
285
+ {
286
+ guard: "canRegenerate",
287
+ target: "generate",
288
+ actions: [
289
+ noteAt("grade_generation_v_documents", "not grounded"),
290
+ assign({ regenerations: ({ context }) => context.regenerations + 1 }),
291
+ ],
292
+ },
293
+ {
294
+ target: "ungrounded",
295
+ actions: [
296
+ assign({ grounded: () => false, answer: () => null, exhausted: () => true }),
297
+ noteAt("grade_generation_v_documents", "still not grounded"),
298
+ ],
299
+ },
300
+ ],
301
+ },
302
+ },
303
+
304
+ /**
305
+ * Grounded is not the same as useful. A beside-the-point answer is rewritten
306
+ * once and, out of attempts, RETURNED with its verdict — it is still true,
307
+ * and the tool tells the model to offer a ticket alongside it.
308
+ */
309
+ gradeUseful: {
310
+ invoke: {
311
+ src: "gradeUseful",
312
+ input: ({ context }) => ({ ctx: context }),
313
+ onDone: [
314
+ {
315
+ guard: ({ event }) => event.output.pass,
316
+ target: "done",
317
+ actions: [
318
+ assign({ useful: () => true }),
319
+ noteAt("grade_generation_v_question", "useful"),
320
+ ],
321
+ },
322
+ {
323
+ guard: "canRetry",
324
+ target: "transformQuery",
325
+ actions: [
326
+ assign({ useful: () => false }),
327
+ noteAt("grade_generation_v_question", "not useful"),
328
+ ],
329
+ },
330
+ {
331
+ target: "exhausted",
332
+ actions: [
333
+ assign({ useful: () => false, exhausted: () => true }),
334
+ noteAt("grade_generation_v_question", "not useful, and no attempts left"),
335
+ ],
336
+ },
337
+ ],
338
+ },
339
+ },
340
+
341
+ /**
342
+ * The corrective edge. It resets the attempt's verdicts as well as its
343
+ * counters: a rewrite reopens the question, so an answer graded against the
344
+ * OLD query must not survive into the new attempt's trace.
345
+ */
346
+ transformQuery: {
347
+ invoke: {
348
+ src: "transformQuery",
349
+ input: ({ context }) => ({ ctx: context }),
350
+ onDone: {
351
+ target: "retrieve",
352
+ actions: [
353
+ assign({
354
+ query: ({ event }) => event.output,
355
+ rewrites: ({ context }) => context.rewrites + 1,
356
+ attempt: ({ context }) => context.attempt + 1,
357
+ regenerations: () => 0,
358
+ answer: () => null,
359
+ grounded: () => null,
360
+ useful: () => null,
361
+ }),
362
+ noteFrom("transform_query", (ctx) => `retrying as "${ctx.query}"`),
363
+ ],
364
+ },
365
+ },
366
+ },
367
+
368
+ done: { type: "final" },
369
+ exhausted: { type: "final" },
370
+ ungrounded: { type: "final" },
371
+ },
372
+ output: ({ context }) => ({
373
+ question: context.question,
374
+ query: context.query,
375
+ rewrites: context.rewrites,
376
+ steps: context.steps,
377
+ docs: context.docs,
378
+ answer: context.answer,
379
+ grounded: context.grounded,
380
+ useful: context.useful,
381
+ exhausted: context.exhausted,
382
+ }),
383
+ });
384
+
385
+ const rag = procedure(machine);
386
+
387
+ /**
388
+ * Run the procedure for one caller question.
389
+ *
390
+ * Never throws for a bad ANSWER — every way of failing to answer is a final
391
+ * STATE, and the trace says which one. Two things DO throw, and both are the
392
+ * tool's to report: a broken model call (an invoked actor's rejection has no
393
+ * `onError` here, so it stops the machine) and an aborted run.
394
+ *
395
+ * `signal` is optional only so the procedure stays drivable from a spec that has no
396
+ * context; a tool body should always pass `ctx.signal`.
397
+ */
398
+ export async function runCorrectiveRag(
399
+ generate: GenerateFn,
400
+ question: string,
401
+ signal?: AbortSignal,
402
+ ): Promise<AnswerTrace> {
403
+ // `omitUndefined` rather than a conditional spread: `ProcedureRunOptions.signal`
404
+ // is optional, and under `exactOptionalPropertyTypes` a present-and-undefined
405
+ // key is not the same as an absent one.
406
+ return await rag.run({ generate, question }, omitUndefined({ signal }));
407
+ }
@@ -1,6 +1,6 @@
1
- import { errorMessage, tool, toolFailure } from "@alexkroman1/aai";
1
+ import { errorMessage, ProcedureNotFinishedError, tool, toolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { runCorrectiveRag } from "../graph.ts";
3
+ import { runCorrectiveRag } from "../procedure.ts";
4
4
  import { recordQuestion, supportSlot } from "../shared.ts";
5
5
 
6
6
  /**
@@ -26,9 +26,22 @@ export default tool({
26
26
  async execute(args, ctx) {
27
27
  let trace: Awaited<ReturnType<typeof runCorrectiveRag>>;
28
28
  try {
29
- trace = await runCorrectiveRag(ctx.generate, args.question);
29
+ // `ctx.signal` is what stops the graph on a barge-in: this loop is five
30
+ // to nine model calls, and a caller who interrupts on the second should
31
+ // not be charged for the rest.
32
+ trace = await runCorrectiveRag(ctx.generate, args.question, ctx.signal);
30
33
  } catch (err: unknown) {
31
- // A broken model call is the tool's to report: the model can tell the
34
+ // An INTERRUPTED lookup is not a broken one, and the difference is worth
35
+ // a sentence: `ctx.signal` aborts on a barge-in AND on this call's own
36
+ // timeout, and telling the model the knowledge base failed would have it
37
+ // apologize for an outage that did not happen.
38
+ if (err instanceof ProcedureNotFinishedError) {
39
+ return toolFailure(
40
+ "That lookup was cut short before it finished. Offer to look again, " +
41
+ "or to log a ticket with log_ticket.",
42
+ );
43
+ }
44
+ // A broken model call IS the tool's to report: the model can tell the
32
45
  // caller the lookup failed, which is a better turn than silence.
33
46
  return toolFailure(`The knowledge base lookup failed: ${errorMessage(err)}`);
34
47
  }