@spendgraph/workflows 0.2.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 (210) hide show
  1. package/README.md +149 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/noesis/attribution.d.ts +24 -0
  5. package/dist/noesis/attribution.js +43 -0
  6. package/dist/noesis/context.d.ts +18 -0
  7. package/dist/noesis/context.js +16 -0
  8. package/dist/noesis/draft/draft.d.ts +18 -0
  9. package/dist/noesis/draft/draft.js +50 -0
  10. package/dist/noesis/draft/index.d.ts +2 -0
  11. package/dist/noesis/draft/index.js +1 -0
  12. package/dist/noesis/draft/types.d.ts +27 -0
  13. package/dist/noesis/draft/types.js +1 -0
  14. package/dist/noesis/events.d.ts +158 -0
  15. package/dist/noesis/events.js +24 -0
  16. package/dist/noesis/execute/execute.d.ts +22 -0
  17. package/dist/noesis/execute/execute.js +85 -0
  18. package/dist/noesis/execute/index.d.ts +3 -0
  19. package/dist/noesis/execute/index.js +2 -0
  20. package/dist/noesis/execute/order.d.ts +11 -0
  21. package/dist/noesis/execute/order.js +28 -0
  22. package/dist/noesis/execute/types.d.ts +37 -0
  23. package/dist/noesis/execute/types.js +1 -0
  24. package/dist/noesis/gates/closure-checklist/index.d.ts +3 -0
  25. package/dist/noesis/gates/closure-checklist/index.js +2 -0
  26. package/dist/noesis/gates/closure-checklist/invoke.d.ts +5 -0
  27. package/dist/noesis/gates/closure-checklist/invoke.js +6 -0
  28. package/dist/noesis/gates/closure-checklist/prompt.d.ts +5 -0
  29. package/dist/noesis/gates/closure-checklist/prompt.js +32 -0
  30. package/dist/noesis/gates/closure-checklist/types.d.ts +20 -0
  31. package/dist/noesis/gates/closure-checklist/types.js +1 -0
  32. package/dist/noesis/gates/cqot-gate/index.d.ts +3 -0
  33. package/dist/noesis/gates/cqot-gate/index.js +2 -0
  34. package/dist/noesis/gates/cqot-gate/invoke.d.ts +5 -0
  35. package/dist/noesis/gates/cqot-gate/invoke.js +6 -0
  36. package/dist/noesis/gates/cqot-gate/prompt.d.ts +5 -0
  37. package/dist/noesis/gates/cqot-gate/prompt.js +27 -0
  38. package/dist/noesis/gates/cqot-gate/types.d.ts +23 -0
  39. package/dist/noesis/gates/cqot-gate/types.js +1 -0
  40. package/dist/noesis/gates/premortem/index.d.ts +3 -0
  41. package/dist/noesis/gates/premortem/index.js +2 -0
  42. package/dist/noesis/gates/premortem/invoke.d.ts +5 -0
  43. package/dist/noesis/gates/premortem/invoke.js +6 -0
  44. package/dist/noesis/gates/premortem/prompt.d.ts +5 -0
  45. package/dist/noesis/gates/premortem/prompt.js +37 -0
  46. package/dist/noesis/gates/premortem/types.d.ts +28 -0
  47. package/dist/noesis/gates/premortem/types.js +1 -0
  48. package/dist/noesis/gates/red-team/index.d.ts +3 -0
  49. package/dist/noesis/gates/red-team/index.js +2 -0
  50. package/dist/noesis/gates/red-team/invoke.d.ts +5 -0
  51. package/dist/noesis/gates/red-team/invoke.js +6 -0
  52. package/dist/noesis/gates/red-team/prompt.d.ts +5 -0
  53. package/dist/noesis/gates/red-team/prompt.js +25 -0
  54. package/dist/noesis/gates/red-team/types.d.ts +21 -0
  55. package/dist/noesis/gates/red-team/types.js +1 -0
  56. package/dist/noesis/gates/standards-review/index.d.ts +3 -0
  57. package/dist/noesis/gates/standards-review/index.js +2 -0
  58. package/dist/noesis/gates/standards-review/invoke.d.ts +5 -0
  59. package/dist/noesis/gates/standards-review/invoke.js +6 -0
  60. package/dist/noesis/gates/standards-review/prompt.d.ts +5 -0
  61. package/dist/noesis/gates/standards-review/prompt.js +26 -0
  62. package/dist/noesis/gates/standards-review/types.d.ts +20 -0
  63. package/dist/noesis/gates/standards-review/types.js +1 -0
  64. package/dist/noesis/gates/think-checkpoint/index.d.ts +3 -0
  65. package/dist/noesis/gates/think-checkpoint/index.js +2 -0
  66. package/dist/noesis/gates/think-checkpoint/invoke.d.ts +5 -0
  67. package/dist/noesis/gates/think-checkpoint/invoke.js +6 -0
  68. package/dist/noesis/gates/think-checkpoint/prompt.d.ts +5 -0
  69. package/dist/noesis/gates/think-checkpoint/prompt.js +28 -0
  70. package/dist/noesis/gates/think-checkpoint/types.d.ts +21 -0
  71. package/dist/noesis/gates/think-checkpoint/types.js +1 -0
  72. package/dist/noesis/index.d.ts +51 -0
  73. package/dist/noesis/index.js +27 -0
  74. package/dist/noesis/producers/cove-answers/index.d.ts +3 -0
  75. package/dist/noesis/producers/cove-answers/index.js +2 -0
  76. package/dist/noesis/producers/cove-answers/invoke.d.ts +5 -0
  77. package/dist/noesis/producers/cove-answers/invoke.js +6 -0
  78. package/dist/noesis/producers/cove-answers/prompt.d.ts +5 -0
  79. package/dist/noesis/producers/cove-answers/prompt.js +27 -0
  80. package/dist/noesis/producers/cove-answers/types.d.ts +32 -0
  81. package/dist/noesis/producers/cove-answers/types.js +1 -0
  82. package/dist/noesis/producers/cove-questions/index.d.ts +3 -0
  83. package/dist/noesis/producers/cove-questions/index.js +2 -0
  84. package/dist/noesis/producers/cove-questions/invoke.d.ts +5 -0
  85. package/dist/noesis/producers/cove-questions/invoke.js +6 -0
  86. package/dist/noesis/producers/cove-questions/prompt.d.ts +5 -0
  87. package/dist/noesis/producers/cove-questions/prompt.js +26 -0
  88. package/dist/noesis/producers/cove-questions/types.d.ts +17 -0
  89. package/dist/noesis/producers/cove-questions/types.js +1 -0
  90. package/dist/noesis/producers/decompose/index.d.ts +3 -0
  91. package/dist/noesis/producers/decompose/index.js +2 -0
  92. package/dist/noesis/producers/decompose/invoke.d.ts +5 -0
  93. package/dist/noesis/producers/decompose/invoke.js +6 -0
  94. package/dist/noesis/producers/decompose/prompt.d.ts +22 -0
  95. package/dist/noesis/producers/decompose/prompt.js +68 -0
  96. package/dist/noesis/producers/decompose/types.d.ts +41 -0
  97. package/dist/noesis/producers/decompose/types.js +1 -0
  98. package/dist/noesis/producers/discriminating-test/index.d.ts +3 -0
  99. package/dist/noesis/producers/discriminating-test/index.js +2 -0
  100. package/dist/noesis/producers/discriminating-test/invoke.d.ts +5 -0
  101. package/dist/noesis/producers/discriminating-test/invoke.js +6 -0
  102. package/dist/noesis/producers/discriminating-test/prompt.d.ts +5 -0
  103. package/dist/noesis/producers/discriminating-test/prompt.js +67 -0
  104. package/dist/noesis/producers/discriminating-test/types.d.ts +32 -0
  105. package/dist/noesis/producers/discriminating-test/types.js +1 -0
  106. package/dist/noesis/producers/plan-candidates/index.d.ts +3 -0
  107. package/dist/noesis/producers/plan-candidates/index.js +2 -0
  108. package/dist/noesis/producers/plan-candidates/invoke.d.ts +5 -0
  109. package/dist/noesis/producers/plan-candidates/invoke.js +6 -0
  110. package/dist/noesis/producers/plan-candidates/prompt.d.ts +5 -0
  111. package/dist/noesis/producers/plan-candidates/prompt.js +46 -0
  112. package/dist/noesis/producers/plan-candidates/types.d.ts +32 -0
  113. package/dist/noesis/producers/plan-candidates/types.js +1 -0
  114. package/dist/noesis/producers/qn-brief/index.d.ts +3 -0
  115. package/dist/noesis/producers/qn-brief/index.js +2 -0
  116. package/dist/noesis/producers/qn-brief/invoke.d.ts +5 -0
  117. package/dist/noesis/producers/qn-brief/invoke.js +6 -0
  118. package/dist/noesis/producers/qn-brief/prompt.d.ts +12 -0
  119. package/dist/noesis/producers/qn-brief/prompt.js +65 -0
  120. package/dist/noesis/producers/qn-brief/types.d.ts +42 -0
  121. package/dist/noesis/producers/qn-brief/types.js +1 -0
  122. package/dist/noesis/producers/toulmin/answer.d.ts +18 -0
  123. package/dist/noesis/producers/toulmin/answer.js +87 -0
  124. package/dist/noesis/producers/toulmin/index.d.ts +5 -0
  125. package/dist/noesis/producers/toulmin/index.js +4 -0
  126. package/dist/noesis/producers/toulmin/invoke.d.ts +18 -0
  127. package/dist/noesis/producers/toulmin/invoke.js +53 -0
  128. package/dist/noesis/producers/toulmin/prompt.d.ts +12 -0
  129. package/dist/noesis/producers/toulmin/prompt.js +60 -0
  130. package/dist/noesis/producers/toulmin/render.d.ts +14 -0
  131. package/dist/noesis/producers/toulmin/render.js +82 -0
  132. package/dist/noesis/producers/toulmin/types.d.ts +126 -0
  133. package/dist/noesis/producers/toulmin/types.js +1 -0
  134. package/dist/noesis/question.d.ts +24 -0
  135. package/dist/noesis/question.js +37 -0
  136. package/dist/noesis/repair/reflection/index.d.ts +3 -0
  137. package/dist/noesis/repair/reflection/index.js +2 -0
  138. package/dist/noesis/repair/reflection/invoke.d.ts +5 -0
  139. package/dist/noesis/repair/reflection/invoke.js +6 -0
  140. package/dist/noesis/repair/reflection/prompt.d.ts +5 -0
  141. package/dist/noesis/repair/reflection/prompt.js +26 -0
  142. package/dist/noesis/repair/reflection/types.d.ts +17 -0
  143. package/dist/noesis/repair/reflection/types.js +1 -0
  144. package/dist/noesis/retry.d.ts +25 -0
  145. package/dist/noesis/retry.js +60 -0
  146. package/dist/noesis/route.d.ts +29 -0
  147. package/dist/noesis/route.js +69 -0
  148. package/dist/noesis/router/triage/index.d.ts +3 -0
  149. package/dist/noesis/router/triage/index.js +2 -0
  150. package/dist/noesis/router/triage/invoke.d.ts +31 -0
  151. package/dist/noesis/router/triage/invoke.js +76 -0
  152. package/dist/noesis/router/triage/prompt.d.ts +11 -0
  153. package/dist/noesis/router/triage/prompt.js +34 -0
  154. package/dist/noesis/router/triage/types.d.ts +70 -0
  155. package/dist/noesis/router/triage/types.js +1 -0
  156. package/dist/noesis/run/artifacts.d.ts +12 -0
  157. package/dist/noesis/run/artifacts.js +31 -0
  158. package/dist/noesis/run/errors.d.ts +38 -0
  159. package/dist/noesis/run/errors.js +55 -0
  160. package/dist/noesis/run/index.d.ts +6 -0
  161. package/dist/noesis/run/index.js +4 -0
  162. package/dist/noesis/run/ledger.d.ts +110 -0
  163. package/dist/noesis/run/ledger.js +184 -0
  164. package/dist/noesis/run/loop.d.ts +15 -0
  165. package/dist/noesis/run/loop.js +91 -0
  166. package/dist/noesis/run/plan.d.ts +18 -0
  167. package/dist/noesis/run/plan.js +39 -0
  168. package/dist/noesis/run/produce.d.ts +50 -0
  169. package/dist/noesis/run/produce.js +114 -0
  170. package/dist/noesis/run/redo.d.ts +24 -0
  171. package/dist/noesis/run/redo.js +41 -0
  172. package/dist/noesis/run/result.d.ts +5 -0
  173. package/dist/noesis/run/result.js +51 -0
  174. package/dist/noesis/run/run.d.ts +4 -0
  175. package/dist/noesis/run/run.js +73 -0
  176. package/dist/noesis/run/stages/classify.d.ts +7 -0
  177. package/dist/noesis/run/stages/classify.js +23 -0
  178. package/dist/noesis/run/stages/direct.d.ts +6 -0
  179. package/dist/noesis/run/stages/direct.js +8 -0
  180. package/dist/noesis/run/stages/full.d.ts +14 -0
  181. package/dist/noesis/run/stages/full.js +21 -0
  182. package/dist/noesis/run/stages/index.d.ts +5 -0
  183. package/dist/noesis/run/stages/index.js +5 -0
  184. package/dist/noesis/run/stages/retrieve.d.ts +13 -0
  185. package/dist/noesis/run/stages/retrieve.js +38 -0
  186. package/dist/noesis/run/stages/standard.d.ts +12 -0
  187. package/dist/noesis/run/stages/standard.js +19 -0
  188. package/dist/noesis/run/state.d.ts +26 -0
  189. package/dist/noesis/run/state.js +13 -0
  190. package/dist/noesis/run/stream.d.ts +28 -0
  191. package/dist/noesis/run/stream.js +33 -0
  192. package/dist/noesis/run/verify.d.ts +27 -0
  193. package/dist/noesis/run/verify.js +78 -0
  194. package/dist/noesis/slugs.d.ts +16 -0
  195. package/dist/noesis/slugs.js +46 -0
  196. package/dist/noesis/stage.d.ts +56 -0
  197. package/dist/noesis/stage.js +52 -0
  198. package/dist/noesis/started.d.ts +18 -0
  199. package/dist/noesis/started.js +39 -0
  200. package/dist/noesis/types.d.ts +167 -0
  201. package/dist/noesis/types.js +1 -0
  202. package/dist/noesis/usage.d.ts +49 -0
  203. package/dist/noesis/usage.js +56 -0
  204. package/dist/noesis/verify/index.d.ts +2 -0
  205. package/dist/noesis/verify/index.js +1 -0
  206. package/dist/noesis/verify/types.d.ts +54 -0
  207. package/dist/noesis/verify/types.js +1 -0
  208. package/dist/noesis/verify/verify.d.ts +36 -0
  209. package/dist/noesis/verify/verify.js +94 -0
  210. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @spendgraph/workflows
2
+
3
+ Ready-made workflows assembled from the other packages. One so far: **noesis**,
4
+ a question-answering loop that decides how much of itself a question is worth.
5
+
6
+ ```ts
7
+ import { run } from "@spendgraph/workflows";
8
+
9
+ const result = await run(client, llm, "Has the price changed since March?", {
10
+ tools: bus,
11
+ context: { now: new Date().toISOString(), locale: "IN / INR" },
12
+ });
13
+
14
+ result.text; // the answer, as a person reads it
15
+ result.status; // "answered" | "abstained" | "escalated"
16
+ result.route; // which of the four it earned
17
+ ```
18
+
19
+ `client` is a `PromptClient` and `llm` an `Llm`. The two are separate on
20
+ purpose: the prompt client pulls the stored wording and records what the call
21
+ cost, and the model client makes it.
22
+
23
+ ## Four routes, chosen in code
24
+
25
+ Stage 0 is a router prompt that returns five signals — difficulty, stakes,
26
+ confidence, ambiguity, tool need — and never a route. `routeFor` in
27
+ `src/noesis/route.ts` turns those into one, from thresholds you can move after
28
+ an eval run without touching a prompt.
29
+
30
+ | route | what it runs | when |
31
+ | --- | --- | --- |
32
+ | `direct` | 6C alone | easy, unambiguous, already known |
33
+ | `retrieve` | one tool call, then 6C | easy and unambiguous, but the answer is elsewhere |
34
+ | `standard` | frame, split, plan, execute, check, close | everything in between |
35
+ | `full` | standard plus four audits | high stakes, expert difficulty, or real ambiguity |
36
+
37
+ Stakes are tested first, because that is the one signal where being wrong is
38
+ expensive in only one direction: an easy question answered the long way costs
39
+ tokens, a critical one answered directly costs more than that.
40
+
41
+ ## The stages
42
+
43
+ | | | |
44
+ | --- | --- | --- |
45
+ | 0 | triage | five signals |
46
+ | 1 | question brief | the question interrogated, never answered |
47
+ | 2 | decompose | ordered sub-questions, at most seven, depth one |
48
+ | 3A | plan candidates | approaches, the one chosen, the steps that follow |
49
+ | 3B | premortem | the plan assumed dead, and what killed it |
50
+ | 3C | cqot gate | eight questions of the plan |
51
+ | 4 | execution | **code** — every sub-question through the tool its hint named |
52
+ | 4A | think checkpoint | predicted against actual, against 3A's own tolerance |
53
+ | 4B | discriminating test | the one test that tells the live hypotheses apart |
54
+ | 5A | cove questions | the verification questions the draft invites |
55
+ | 5B | cove answers | each answered blind |
56
+ | 5 | verification | **code** — 5A and 5B paired back together by id |
57
+ | 5C | standards review | the answer against eight standards |
58
+ | 6A | closure checklist | the last questions before it may close |
59
+ | 6B | red team | the answer attacked |
60
+ | 6C | toulmin | the artifacts compiled into a claim and its qualifier |
61
+ | R | reflection | the earliest wrong decision behind a failed gate |
62
+
63
+ Two of them are code and not model calls. Stage 4 dispatches and does not
64
+ reason, because `deep_recall` and `web_search` already return a synthesised
65
+ answer. Stage 5 pairs 5A's claims back to 5B's blind answers, and it has to be
66
+ code: whatever does it sees both the claim and the answer written without it,
67
+ and a model given both is no longer a blind check of anything.
68
+
69
+ ## The ledger
70
+
71
+ Every stage files its result, its rollouts and its tokens in one place. That
72
+ record is what 6C compiles from, keyed by stage id and name together — `"3A plan
73
+ candidates"` — because the model writes one of them back as
74
+ `grounds[].source_stage` and either has to lead to the entry.
75
+
76
+ It also carries the retry budget. A gate that will not let the answer through
77
+ spends one, runs R to find the earliest wrong decision, and cuts the ledger back
78
+ to where the loop restarts, so nothing stale is compiled into the answer. What
79
+ it cost stays: tokens were spent and rollouts were recorded, and going back does
80
+ not un-spend them. The nth retry escalates instead of looping again.
81
+
82
+ ## Asking, and stopping
83
+
84
+ Two different problems, so two mechanisms.
85
+
86
+ `onAsk` is asking. Stage 1 is the only stage that reaches for a person mid-run,
87
+ and only when it says the asker aimed at the wrong thing; a replacement question
88
+ restarts the framing with it, and with nobody to ask the run proceeds on the
89
+ question as put.
90
+
91
+ `result.status` is stopping. `answered` is an answer to act on. `abstained` is
92
+ 6C saying the grounds supported no claim. `escalated` is a gate, or a spent
93
+ retry budget, asking for a person — and it outranks an abstention, because a
94
+ gate that asked for somebody said so about the whole run. Both still carry
95
+ `text`, because saying what is not known is worth reading.
96
+
97
+ `onDeliver` runs once, after the loop, and only for `answered`. Anything that
98
+ renders a deliverable belongs there: nothing downstream reads it, no gate can
99
+ check it, every back edge would make it again, and generating a
100
+ finished-looking document out of an abstention is the worst thing this loop
101
+ could produce.
102
+
103
+ ## Where it sits
104
+
105
+ `@spendgraph/harness` holds the patterns — `refine`, `route`, `chain`,
106
+ `parallel`, `loop`, `orchestrate`, `cascade` — each general and unaware of any
107
+ particular job. This package holds finished workflows built out of them, each
108
+ solving one job end to end. Nothing that belongs to all seven ever lands here.
109
+
110
+ Nothing here talks to the database or holds a provider key. `run()` is handed
111
+ its clients and its `ToolBus`; where a workflow needs the server it goes through
112
+ `@spendgraph/sdk`.
113
+
114
+ ## Layout
115
+
116
+ ```
117
+ src/
118
+ index.ts the public surface
119
+ noesis/
120
+ route.ts the thresholds, and nothing else
121
+ stage.ts pull, ask through {question}, force the schema
122
+ question.ts askThrough — the unmigrated-prompt floor
123
+ router/ producers/ gates/ repair/ one directory per stage
124
+ execute/ draft/ verify/ the code stages, and what they render
125
+ run/ the ledger, the routes, and the gate loop
126
+ tests/ excluded from dist
127
+ evals/ scoring the loop — the free layers run in CI
128
+ TODO.md how it was built, brick by brick
129
+ GAPS.md what is wrong, what was fixed, what needs a decision
130
+ ```
131
+
132
+ ## Not yet met a real prompt
133
+
134
+ Every test runs against a fake `PromptClient` and `Llm`, which is what made
135
+ building this cheap. Two assumptions carry the design and neither has been
136
+ exercised against the server:
137
+
138
+ 1. **The stored prompts render `{question}`.** Where one does not, `askThrough`
139
+ overwrites the box and warns once on stderr. The run still succeeds and
140
+ quietly answers whatever was saved on the prompt, so a warning per stage on
141
+ the first live run is the signal, and publishing is the fix.
142
+ 2. **6C reads the artifact keys back**, so a citation names the stage that
143
+ produced its evidence.
144
+
145
+ `examples/` runs each route against the real prompts for a few pennies. Start
146
+ with `04-retrieve.mjs`: it is the cheapest, and it exercises triage, the
147
+ dispatcher, the ledger and 6C. The routes that look something up need one of
148
+ the two lookups configured — `LOCUSGRAPH_AGENT_SECRET` for `deep_recall`, or
149
+ `PERPLEXITY_API_KEY` for `web_search` — and say so rather than half-running.
@@ -0,0 +1 @@
1
+ export * from "./noesis/index.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./noesis/index.js";
@@ -0,0 +1,24 @@
1
+ import type { ToulminAnswer } from "./producers/toulmin/index.js";
2
+ import { type StageId } from "./run/ledger.js";
3
+ /**
4
+ * The stage a ground says it came from, or nothing.
5
+ *
6
+ * Lenient because the field is free text on a model reply, and "5B", "stage 5B"
7
+ * and "verification answers" are the same citation. It will not guess: a string
8
+ * that resolves to nothing is a ground that cannot be traced, which is the
9
+ * finding rather than a parsing problem to work around.
10
+ */
11
+ export declare function stageCited(source: string): StageId | undefined;
12
+ /**
13
+ * The grounds citing a stage this run never ran.
14
+ *
15
+ * 6C compiles `grounds[]` out of the artifacts and each one names its
16
+ * `source_stage`; the ledger knows which stages were actually paid for. A
17
+ * citation of anything else is the stage writing evidence rather than
18
+ * assembling it, and it is the cheapest check available that catches a
19
+ * fabricated answer without a judge and without a reference.
20
+ *
21
+ * Returns the `source_stage` strings as written, because what the model claimed
22
+ * is the useful thing to read back.
23
+ */
24
+ export declare function unattributed(answer: ToulminAnswer, ran: Iterable<StageId>): string[];
@@ -0,0 +1,43 @@
1
+ import { STAGE_NAMES, STAGES } from "./run/ledger.js";
2
+ const BY_NAME = new Map(Object.entries(STAGE_NAMES).map(([id, name]) => [name.toLowerCase(), id]));
3
+ const IDS = new Set(STAGES);
4
+ /**
5
+ * The stage a ground says it came from, or nothing.
6
+ *
7
+ * Lenient because the field is free text on a model reply, and "5B", "stage 5B"
8
+ * and "verification answers" are the same citation. It will not guess: a string
9
+ * that resolves to nothing is a ground that cannot be traced, which is the
10
+ * finding rather than a parsing problem to work around.
11
+ */
12
+ export function stageCited(source) {
13
+ const raw = source.trim();
14
+ const id = raw
15
+ .toUpperCase()
16
+ .replace(/^STAGE\s*/, "")
17
+ .replace(/[^0-9A-Z]/g, "");
18
+ if (IDS.has(id))
19
+ return id;
20
+ return BY_NAME.get(raw.toLowerCase());
21
+ }
22
+ /**
23
+ * The grounds citing a stage this run never ran.
24
+ *
25
+ * 6C compiles `grounds[]` out of the artifacts and each one names its
26
+ * `source_stage`; the ledger knows which stages were actually paid for. A
27
+ * citation of anything else is the stage writing evidence rather than
28
+ * assembling it, and it is the cheapest check available that catches a
29
+ * fabricated answer without a judge and without a reference.
30
+ *
31
+ * Returns the `source_stage` strings as written, because what the model claimed
32
+ * is the useful thing to read back.
33
+ */
34
+ export function unattributed(answer, ran) {
35
+ const filed = new Set(ran);
36
+ const grounds = answer.grounds ?? [];
37
+ return grounds
38
+ .filter((ground) => {
39
+ const stage = stageCited(ground.source_stage ?? "");
40
+ return !stage || !filed.has(stage);
41
+ })
42
+ .map((ground) => ground.source_stage);
43
+ }
@@ -0,0 +1,18 @@
1
+ /** The `now` and `locale` facts a stage is given rather than asked for. */
2
+ export interface Asked {
3
+ /** The request itself. */
4
+ question: string;
5
+ /** Current date and time with offset. The stage is told to trust it over its own sense of the present. */
6
+ now?: string;
7
+ /** Country, currency, units, language variant. It governs jurisdiction, not only geography. */
8
+ locale?: string;
9
+ }
10
+ /**
11
+ * The request with the `<context>` block ahead of it.
12
+ *
13
+ * The prompts name `now` and `locale` but never show the markup they expect
14
+ * them in, so this is the form they are given. An absent fact is omitted rather
15
+ * than sent empty: a missing one is recorded as something to find out, an empty
16
+ * one reads as a fact already in hand.
17
+ */
18
+ export declare function asked(input: Asked): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The request with the `<context>` block ahead of it.
3
+ *
4
+ * The prompts name `now` and `locale` but never show the markup they expect
5
+ * them in, so this is the form they are given. An absent fact is omitted rather
6
+ * than sent empty: a missing one is recorded as something to find out, an empty
7
+ * one reads as a fact already in hand.
8
+ */
9
+ export function asked(input) {
10
+ const facts = [
11
+ input.now ? `now: ${input.now}` : "",
12
+ input.locale ? `locale: ${input.locale}` : "",
13
+ ].filter(Boolean);
14
+ const question = input.question.trim();
15
+ return facts.length ? `<context>\n${facts.join("\n")}\n</context>\n\n${question}` : question;
16
+ }
@@ -0,0 +1,18 @@
1
+ import type { Execution } from "../execute/index.js";
2
+ import type { Decomposition } from "../producers/decompose/index.js";
3
+ import type { Draft } from "./types.js";
4
+ /**
5
+ * The sub-answers, paired back to the sub-questions that asked for them.
6
+ *
7
+ * A sub-question that produced nothing is named rather than dropped: 6A asks
8
+ * whether every one was answered, and a draft that quietly omits the failures
9
+ * would let it say yes.
10
+ */
11
+ export declare function assemble(question: string, decomposition: Decomposition, execution: Execution): Draft;
12
+ /**
13
+ * The draft as a stage reads it.
14
+ *
15
+ * Tagged the way `asked()` tags the context block, because every stage
16
+ * downstream takes this through `{question}` and has to tell the parts apart.
17
+ */
18
+ export declare function render(draft: Draft): string;
@@ -0,0 +1,50 @@
1
+ const NEVER_RAN = "It depended on something that was never answered.";
2
+ /**
3
+ * The sub-answers, paired back to the sub-questions that asked for them.
4
+ *
5
+ * A sub-question that produced nothing is named rather than dropped: 6A asks
6
+ * whether every one was answered, and a draft that quietly omits the failures
7
+ * would let it say yes.
8
+ */
9
+ export function assemble(question, decomposition, execution) {
10
+ const asked = new Map(decomposition.sub_questions.map((sub) => [sub.id, sub.question]));
11
+ const findings = [];
12
+ const missing = [];
13
+ for (const answer of execution.answers) {
14
+ if (answer.status === "answered") {
15
+ findings.push({ id: answer.id, question: answer.question, answer: answer.result });
16
+ }
17
+ else {
18
+ missing.push({
19
+ id: answer.id,
20
+ question: answer.question,
21
+ reason: answer.reason ?? `The sub-question ${answer.status}.`,
22
+ });
23
+ }
24
+ }
25
+ for (const id of execution.unresolved) {
26
+ missing.push({ id, question: asked.get(id) ?? id, reason: NEVER_RAN });
27
+ }
28
+ return { question, composition: decomposition.composition, findings, missing };
29
+ }
30
+ const block = (tag, body) => `<${tag}>\n${body}\n</${tag}>`;
31
+ const asText = (answer) => typeof answer === "string" ? answer : JSON.stringify(answer, null, 2);
32
+ /**
33
+ * The draft as a stage reads it.
34
+ *
35
+ * Tagged the way `asked()` tags the context block, because every stage
36
+ * downstream takes this through `{question}` and has to tell the parts apart.
37
+ */
38
+ export function render(draft) {
39
+ const parts = [
40
+ block("question", draft.question),
41
+ block("composition", draft.composition),
42
+ block("findings", draft.findings.length
43
+ ? draft.findings.map((f) => `${f.id}. ${f.question}\n${asText(f.answer)}`).join("\n\n")
44
+ : "Nothing was answered."),
45
+ ];
46
+ if (draft.missing.length) {
47
+ parts.push(block("unanswered", draft.missing.map((m) => `${m.id}. ${m.question} — ${m.reason}`).join("\n")));
48
+ }
49
+ return parts.join("\n\n");
50
+ }
@@ -0,0 +1,2 @@
1
+ export { assemble, render } from "./draft.js";
2
+ export type { Draft, Finding, Missing } from "./types.js";
@@ -0,0 +1 @@
1
+ export { assemble, render } from "./draft.js";
@@ -0,0 +1,27 @@
1
+ /** One sub-question and whatever answering it produced. */
2
+ export interface Finding {
3
+ id: string;
4
+ question: string;
5
+ /** The tool's output. Absent where the sub-question was never answered. */
6
+ answer?: unknown;
7
+ }
8
+ /** A sub-question that produced nothing, and why. */
9
+ export interface Missing {
10
+ id: string;
11
+ question: string;
12
+ reason: string;
13
+ }
14
+ /**
15
+ * What the producing half leaves for the verifying half.
16
+ *
17
+ * Assembled in code and not written by a model: nothing here is a claim anybody
18
+ * made, only what was asked and what came back. 5A is the first stage allowed
19
+ * to treat any of it as something to check.
20
+ */
21
+ export interface Draft {
22
+ question: string;
23
+ /** How stage 2 said the sub-answers combine. */
24
+ composition: string;
25
+ findings: Finding[];
26
+ missing: Missing[];
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ import type { TriageEstimates } from "./router/triage/index.js";
2
+ import type { StageId } from "./run/ledger.js";
3
+ import type { Route, Routing, Status } from "./types.js";
4
+ import type { Usage } from "./usage.js";
5
+ /** What every event carries, whatever else it says. */
6
+ export interface EventBase {
7
+ /** Monotonic within a run, so a consumer can order or de-duplicate. */
8
+ seq: number;
9
+ /** Milliseconds since the run started. */
10
+ at: number;
11
+ }
12
+ /**
13
+ * What the loop says while it runs.
14
+ *
15
+ * Named `noun.verb` in the past tense, because every one of these reports
16
+ * something that has already happened — there is nothing to cancel or confirm.
17
+ */
18
+ export type NoesisEvent = EventBase & ({
19
+ type: "run.started";
20
+ question: string;
21
+ hasTools: boolean;
22
+ retryBudget: number;
23
+ } | {
24
+ type: "run.routed";
25
+ route: Route;
26
+ routing: Routing;
27
+ estimates: TriageEstimates;
28
+ } | {
29
+ type: "run.escalated";
30
+ from: StageId;
31
+ reason: string;
32
+ } | {
33
+ type: "run.finished";
34
+ status: Status;
35
+ text: string;
36
+ route: Route;
37
+ usage: Usage;
38
+ rollouts: number;
39
+ latencyMs: number;
40
+ } | {
41
+ type: "stage.started";
42
+ stage: StageId;
43
+ name: string;
44
+ slug: string;
45
+ model?: string;
46
+ } | {
47
+ type: "stage.finished";
48
+ stage: StageId;
49
+ name: string;
50
+ /** The stage's own object, with long strings cut. */
51
+ artifact: unknown;
52
+ model: string;
53
+ rolloutIds: string[];
54
+ usage: Usage;
55
+ latencyMs: number;
56
+ } | {
57
+ type: "stage.failed";
58
+ stage: StageId;
59
+ kind: "reply" | "transport";
60
+ error: string;
61
+ } | {
62
+ type: "tools.planned";
63
+ waves: string[][];
64
+ unresolved: string[];
65
+ } | {
66
+ type: "tool.started";
67
+ id: string;
68
+ question: string;
69
+ tool: string;
70
+ wave: number;
71
+ } | {
72
+ type: "tool.finished";
73
+ id: string;
74
+ tool: string;
75
+ status: "answered" | "skipped" | "failed";
76
+ latencyMs: number;
77
+ reason?: string;
78
+ } | {
79
+ type: "draft.assembled";
80
+ findings: number;
81
+ missing: {
82
+ id: string;
83
+ reason: string;
84
+ }[];
85
+ } | {
86
+ type: "checks.paired";
87
+ checked: number;
88
+ settled: number;
89
+ unsettled: string[];
90
+ unanswered: string[];
91
+ unasked: string[];
92
+ /** Ids whose `checked_by` the run could not support, corrected to `knowledge`. */
93
+ overclaimed: string[];
94
+ } | {
95
+ type: "gate.passed";
96
+ stage: StageId;
97
+ name: string;
98
+ } | {
99
+ type: "gate.refused";
100
+ stage: StageId;
101
+ name: string;
102
+ reason: string;
103
+ hint?: string | null;
104
+ } | {
105
+ type: "retry.spent";
106
+ gate: StageId;
107
+ spent: number;
108
+ left: number;
109
+ budget: number;
110
+ /** False on the ask that ran the budget out — the run escalates instead. */
111
+ granted: boolean;
112
+ } | {
113
+ type: "reflect.pointed";
114
+ from: StageId;
115
+ redoFrom?: StageId;
116
+ resumeAt: "produce" | "verify";
117
+ lesson?: string;
118
+ } | {
119
+ type: "ledger.dropped";
120
+ from: StageId;
121
+ dropped: StageId[];
122
+ } | {
123
+ type: "ask.raised";
124
+ question: string;
125
+ better: string;
126
+ reason: string;
127
+ } | {
128
+ type: "ask.answered";
129
+ replacement: string | null;
130
+ } | {
131
+ type: "grounds.checked";
132
+ cited: number;
133
+ /** The `source_stage` values naming a stage the run never ran. */
134
+ unattributed: string[];
135
+ } | {
136
+ type: "delivered";
137
+ });
138
+ /**
139
+ * One event before the emitter stamps it.
140
+ *
141
+ * Distributed over the union rather than a plain `Omit`, which would collapse
142
+ * seventeen shapes into their common fields and lose the discriminant.
143
+ */
144
+ type Unstamped<T> = T extends unknown ? Omit<T, "seq" | "at"> : never;
145
+ export type NoesisEventInput = Unstamped<NoesisEvent>;
146
+ /** What a stage or the ledger calls to say something happened. */
147
+ export type Emit = (event: NoesisEventInput) => void;
148
+ /**
149
+ * One emitter per run, stamping order and elapsed time.
150
+ *
151
+ * Deliberately synchronous and deliberately swallowing: a listener that throws
152
+ * is a bug in the listener, and letting it take the run down would make
153
+ * watching a run more dangerous than not watching one. Returns `undefined`
154
+ * where nobody is listening, so every emit site can stay a `?.` call and cost
155
+ * nothing.
156
+ */
157
+ export declare function emitter(listener: ((event: NoesisEvent) => void) | undefined, now?: () => number): Emit | undefined;
158
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One emitter per run, stamping order and elapsed time.
3
+ *
4
+ * Deliberately synchronous and deliberately swallowing: a listener that throws
5
+ * is a bug in the listener, and letting it take the run down would make
6
+ * watching a run more dangerous than not watching one. Returns `undefined`
7
+ * where nobody is listening, so every emit site can stay a `?.` call and cost
8
+ * nothing.
9
+ */
10
+ export function emitter(listener, now = Date.now) {
11
+ if (!listener)
12
+ return undefined;
13
+ const startedAt = now();
14
+ let seq = 0;
15
+ return (event) => {
16
+ const stamped = { ...event, seq: seq++, at: now() - startedAt };
17
+ try {
18
+ listener(stamped);
19
+ }
20
+ catch {
21
+ // A listener that throws does not get to fail the run.
22
+ }
23
+ };
24
+ }
@@ -0,0 +1,22 @@
1
+ import type { ToolBus } from "@spendgraph/tools";
2
+ import type { SubQuestion, SubQuestionTool } from "../producers/decompose/index.js";
3
+ import type { ExecuteOptions, Execution, Wiring } from "./types.js";
4
+ /**
5
+ * What each hint reaches for, and where the question goes.
6
+ *
7
+ * `code_exec` and `get_documents` are unwired because nothing ships them —
8
+ * a sub-question asking for one is reported rather than quietly dropped.
9
+ */
10
+ export declare const DEFAULT_TOOLS: Partial<Record<SubQuestionTool, Wiring>>;
11
+ /**
12
+ * Stage 4. Every sub-question put through the tool its hint named.
13
+ *
14
+ * The dispatcher dispatches and does not reason: `deep_recall` and `web_search`
15
+ * already return a synthesised answer, and a sub-question with no tool is
16
+ * recorded as unanswered rather than guessed at.
17
+ *
18
+ * `done_when` is carried rather than acted on. It describes an observable
19
+ * condition, which is 4A's job to judge against what actually happened, not
20
+ * something this can evaluate.
21
+ */
22
+ export declare function execute(bus: ToolBus, plan: SubQuestion[], opts?: ExecuteOptions): Promise<Execution>;
@@ -0,0 +1,85 @@
1
+ import { order } from "./order.js";
2
+ /**
3
+ * What each hint reaches for, and where the question goes.
4
+ *
5
+ * `code_exec` and `get_documents` are unwired because nothing ships them —
6
+ * a sub-question asking for one is reported rather than quietly dropped.
7
+ */
8
+ export const DEFAULT_TOOLS = {
9
+ calculator: { name: "calculate", arg: "expression" },
10
+ deeprecall: { name: "deep_recall", arg: "question" },
11
+ web_search: { name: "web_search", arg: "query" },
12
+ fetch_url: { name: "http_request", arg: "url" },
13
+ };
14
+ const parsed = (output) => {
15
+ try {
16
+ return JSON.parse(output);
17
+ }
18
+ catch {
19
+ return output;
20
+ }
21
+ };
22
+ async function answerOne(bus, wiring, sub, wave, emit) {
23
+ const base = { id: sub.id, question: sub.question, tool: sub.tool, latencyMs: 0 };
24
+ const wired = wiring[sub.tool];
25
+ const done = (answer) => {
26
+ emit?.({
27
+ type: "tool.finished",
28
+ id: answer.id,
29
+ tool: wired?.name ?? answer.tool,
30
+ status: answer.status,
31
+ latencyMs: answer.latencyMs,
32
+ reason: answer.reason,
33
+ });
34
+ return answer;
35
+ };
36
+ if (!wired) {
37
+ return done({
38
+ ...base,
39
+ status: "skipped",
40
+ reason: sub.tool === "none"
41
+ ? "The stage named no tool, so there is nothing to call."
42
+ : `Nothing is wired for "${sub.tool}".`,
43
+ });
44
+ }
45
+ emit?.({
46
+ type: "tool.started",
47
+ id: sub.id,
48
+ question: sub.question,
49
+ tool: wired.name,
50
+ wave,
51
+ });
52
+ const result = await bus.invoke(wired.name, { [wired.arg]: sub.question });
53
+ return done(result.status === "failed"
54
+ ? { ...base, status: "failed", reason: result.error, latencyMs: result.latencyMs }
55
+ : { ...base, status: "answered", result: parsed(result.output), latencyMs: result.latencyMs });
56
+ }
57
+ /**
58
+ * Stage 4. Every sub-question put through the tool its hint named.
59
+ *
60
+ * The dispatcher dispatches and does not reason: `deep_recall` and `web_search`
61
+ * already return a synthesised answer, and a sub-question with no tool is
62
+ * recorded as unanswered rather than guessed at.
63
+ *
64
+ * `done_when` is carried rather than acted on. It describes an observable
65
+ * condition, which is 4A's job to judge against what actually happened, not
66
+ * something this can evaluate.
67
+ */
68
+ export async function execute(bus, plan, opts = {}) {
69
+ const wiring = { ...DEFAULT_TOOLS, ...opts.tools };
70
+ const { waves, unresolved } = order(plan);
71
+ const answers = [];
72
+ opts.emit?.({
73
+ type: "tools.planned",
74
+ waves: waves.map((wave) => wave.map((sub) => sub.id)),
75
+ unresolved,
76
+ });
77
+ for (const [index, wave] of waves.entries()) {
78
+ answers.push(...(await Promise.all(wave.map((sub) => answerOne(bus, wiring, sub, index, opts.emit)))));
79
+ }
80
+ return {
81
+ answers,
82
+ unresolved,
83
+ toolMs: answers.reduce((total, answer) => total + answer.latencyMs, 0),
84
+ };
85
+ }
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_TOOLS, execute } from "./execute.js";
2
+ export { order } from "./order.js";
3
+ export type { ExecuteOptions, Execution, SubAnswer, Waves, Wiring } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_TOOLS, execute } from "./execute.js";
2
+ export { order } from "./order.js";