@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
@@ -0,0 +1,39 @@
1
+ import { invoke as cqotGate } from "../gates/cqot-gate/index.js";
2
+ import { invoke as premortem } from "../gates/premortem/index.js";
3
+ import { invoke as planCandidates } from "../producers/plan-candidates/index.js";
4
+ import { soFar } from "./produce.js";
5
+ /**
6
+ * Stage 3A, and the audits only `full` pays for.
7
+ *
8
+ * Both long routes plan, because 4A judges each step against the `tolerance` 3A
9
+ * wrote for it and a run with no plan has nothing to check against. What `full`
10
+ * adds is 3B and 3C: the plan assumed dead, then gated.
11
+ *
12
+ * Chained rather than forked, because 3C points at the step at fault and 3B may
13
+ * have patched that step. Run side by side, 3C would gate a plan 3B had already
14
+ * said was wrong. A patch is prose, so `patch_and_proceed` proceeds and the
15
+ * premortem is filed — 3C reads the plan with the patches beside it rather than
16
+ * a plan nobody rewrote.
17
+ */
18
+ export async function plan(client, llm, question, opts, state, audits) {
19
+ for (;;) {
20
+ state.ledger.dropFrom("3A");
21
+ state.ledger.record("3A", await planCandidates(client, llm, { question: soFar(state, question), ...opts.context }, opts.stage));
22
+ if (!audits)
23
+ return;
24
+ const dead = state.ledger.record("3B", await premortem(client, llm, { question: soFar(state, question), ...opts.context }, opts.stage));
25
+ if (dead.fatal || dead.verdict === "replan") {
26
+ if (state.ledger.retry("3B"))
27
+ continue;
28
+ state.ledger.escalate("3B", "Out of retries: the premortem keeps killing the plan.");
29
+ return;
30
+ }
31
+ const gate = state.ledger.record("3C", await cqotGate(client, llm, { question: soFar(state, question), ...opts.context }, opts.stage));
32
+ if (gate.gate === "pass")
33
+ return;
34
+ if (state.ledger.retry("3C"))
35
+ continue;
36
+ state.ledger.escalate("3C", `Out of retries: the gate still blocks ${gate.blocking.join(", ")}.`);
37
+ return;
38
+ }
39
+ }
@@ -0,0 +1,50 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import { type Draft } from "../draft/index.js";
4
+ import type { Decomposition } from "../producers/decompose/index.js";
5
+ import type { NoesisOptions } from "../types.js";
6
+ import type { RunState } from "./state.js";
7
+ /**
8
+ * Everything the ledger holds so far, as a stage reads it.
9
+ *
10
+ * Cut through `digest`, because every caller here is reviewing decisions rather
11
+ * than citing evidence and one wide tool answer would otherwise be most of the
12
+ * prompt. 6C reads `ledger.artifacts()` itself, uncut.
13
+ */
14
+ export declare const soFar: (state: RunState, question: string) => string;
15
+ /**
16
+ * Stages 1 and 2: interrogate the question, then split it.
17
+ *
18
+ * Stage 1 is the one place a person is worth asking mid-run. It answers
19
+ * `is_right_question: false` when the asker aimed at the wrong thing, and
20
+ * splitting the wrong question is the most expensive way to find that out. With
21
+ * nobody to ask it proceeds on the question as put, and the brief says so in
22
+ * the ledger either way.
23
+ */
24
+ export declare function frame(client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState): Promise<{
25
+ asked: string;
26
+ split: Decomposition;
27
+ }>;
28
+ /**
29
+ * Stages 4 and 4A: answer the parts, then judge what came back against the plan.
30
+ *
31
+ * The draft is assembled either way. 4A decides whether it is worth reading:
32
+ * `continue` means the work is done, and the two that are not are the run's to
33
+ * act on.
34
+ */
35
+ export declare function work(client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState, split: Decomposition): Promise<{
36
+ draft: Draft;
37
+ action: "go" | "replan" | "escalate";
38
+ }>;
39
+ /**
40
+ * Everything up to the draft, and the checkpoint's word that it is worth having.
41
+ *
42
+ * The framing happens once. `replan` returns to 3A because a result outside the
43
+ * tolerance a step predicted is the plan being wrong, not the question — and
44
+ * re-framing would ask the person the same thing twice.
45
+ *
46
+ * A plan that never cleared its own audits stops here rather than executing:
47
+ * the run already knows it needs somebody, and the draft it hands back says
48
+ * nothing was answered rather than pretending otherwise.
49
+ */
50
+ export declare function produce(client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState, audits: boolean): Promise<Draft>;
@@ -0,0 +1,114 @@
1
+ import { assemble } from "../draft/index.js";
2
+ import { execute } from "../execute/index.js";
3
+ import { invoke as thinkCheckpoint } from "../gates/think-checkpoint/index.js";
4
+ import { invoke as decompose } from "../producers/decompose/index.js";
5
+ import { invoke as brief } from "../producers/qn-brief/index.js";
6
+ import { digest } from "./artifacts.js";
7
+ import { NoToolsError } from "./errors.js";
8
+ import { plan } from "./plan.js";
9
+ /**
10
+ * Everything the ledger holds so far, as a stage reads it.
11
+ *
12
+ * Cut through `digest`, because every caller here is reviewing decisions rather
13
+ * than citing evidence and one wide tool answer would otherwise be most of the
14
+ * prompt. 6C reads `ledger.artifacts()` itself, uncut.
15
+ */
16
+ export const soFar = (state, question) => JSON.stringify({ question, ...digest(state.ledger.artifacts()) }, null, 2);
17
+ /**
18
+ * Stages 1 and 2: interrogate the question, then split it.
19
+ *
20
+ * Stage 1 is the one place a person is worth asking mid-run. It answers
21
+ * `is_right_question: false` when the asker aimed at the wrong thing, and
22
+ * splitting the wrong question is the most expensive way to find that out. With
23
+ * nobody to ask it proceeds on the question as put, and the brief says so in
24
+ * the ledger either way.
25
+ */
26
+ export async function frame(client, llm, question, opts, state) {
27
+ const brief1 = state.ledger.record("1", await brief(client, llm, { question, ...opts.context }, opts.stage));
28
+ let asked = question;
29
+ if (!brief1.is_right_question && brief1.better_question && opts.onAsk) {
30
+ state.emit?.({
31
+ type: "ask.raised",
32
+ question,
33
+ better: brief1.better_question,
34
+ reason: brief1.purpose,
35
+ });
36
+ const replacement = await opts.onAsk({
37
+ question,
38
+ better: brief1.better_question,
39
+ reason: brief1.purpose,
40
+ });
41
+ state.emit?.({ type: "ask.answered", replacement: replacement ?? null });
42
+ asked = replacement ?? question;
43
+ }
44
+ const split = state.ledger.record("2", await decompose(client, llm, { question: asked, ...opts.context }, opts.stage));
45
+ return { asked, split };
46
+ }
47
+ /**
48
+ * Stages 4 and 4A: answer the parts, then judge what came back against the plan.
49
+ *
50
+ * The draft is assembled either way. 4A decides whether it is worth reading:
51
+ * `continue` means the work is done, and the two that are not are the run's to
52
+ * act on.
53
+ */
54
+ export async function work(client, llm, question, opts, state, split) {
55
+ if (!opts.tools)
56
+ throw new NoToolsError();
57
+ const execution = await execute(opts.tools, split.sub_questions, { emit: state.emit });
58
+ state.ledger.record("4", {
59
+ data: execution,
60
+ rolloutIds: [],
61
+ model: "",
62
+ inputTokens: 0,
63
+ outputTokens: 0,
64
+ latencyMs: execution.toolMs,
65
+ });
66
+ const draft = assemble(question, split, execution);
67
+ state.emit?.({
68
+ type: "draft.assembled",
69
+ findings: draft.findings.length,
70
+ missing: draft.missing.map((one) => ({ id: one.id, reason: one.reason })),
71
+ });
72
+ const checkpoint = state.ledger.record("4A", await thinkCheckpoint(client, llm, { question: soFar(state, question), ...opts.context }, opts.stage));
73
+ if (checkpoint.next_action === "replan")
74
+ return { draft, action: "replan" };
75
+ if (checkpoint.next_action === "escalate")
76
+ return { draft, action: "escalate" };
77
+ return { draft, action: "go" };
78
+ }
79
+ /**
80
+ * Everything up to the draft, and the checkpoint's word that it is worth having.
81
+ *
82
+ * The framing happens once. `replan` returns to 3A because a result outside the
83
+ * tolerance a step predicted is the plan being wrong, not the question — and
84
+ * re-framing would ask the person the same thing twice.
85
+ *
86
+ * A plan that never cleared its own audits stops here rather than executing:
87
+ * the run already knows it needs somebody, and the draft it hands back says
88
+ * nothing was answered rather than pretending otherwise.
89
+ */
90
+ export async function produce(client, llm, question, opts, state, audits) {
91
+ const { asked, split } = await frame(client, llm, question, opts, state);
92
+ for (;;) {
93
+ await plan(client, llm, asked, opts, state, audits);
94
+ if (state.ledger.escalated()) {
95
+ return assemble(asked, split, {
96
+ answers: [],
97
+ unresolved: split.sub_questions.map((sub) => sub.id),
98
+ toolMs: 0,
99
+ });
100
+ }
101
+ const { draft, action } = await work(client, llm, asked, opts, state, split);
102
+ if (action === "go")
103
+ return draft;
104
+ if (action === "escalate") {
105
+ state.ledger.escalate("4A", "The checkpoint asked for a person.");
106
+ return draft;
107
+ }
108
+ if (!state.ledger.retry("4A")) {
109
+ state.ledger.escalate("4A", "Out of retries: the plan kept missing its own tolerances.");
110
+ return draft;
111
+ }
112
+ state.ledger.dropFrom("3A");
113
+ }
114
+ }
@@ -0,0 +1,24 @@
1
+ import { type StageId } from "./ledger.js";
2
+ /**
3
+ * A stage id out of whatever a stage called it.
4
+ *
5
+ * `redo_from_stage` and `loop_back_to` are free strings, so they arrive as
6
+ * `"2"`, `"stage 3A"` or `"the decomposition"`. Longest first, or `"5A"` is
7
+ * read as `"5"`.
8
+ *
9
+ * Absent counts as unnameable. Both fields are `required` in their schemas and
10
+ * neither is checked on the way back, so a stage that omitted one hands this
11
+ * `undefined` — and a stage nobody can name redoes everything, which is the
12
+ * answer this already had for a name it could not parse.
13
+ */
14
+ export declare function parseStage(said: string | undefined): StageId | undefined;
15
+ /**
16
+ * Which half of the route has to run again.
17
+ *
18
+ * The draft is the seam: anything up to 4B was produced, and anything after it
19
+ * only read what the draft already held. A stage nobody could name redoes the
20
+ * lot, because the alternative is compiling something stale.
21
+ */
22
+ export declare function resumeAt(stage: StageId | undefined): "produce" | "verify";
23
+ /** Where the ledger is cut back to. The earliest thing a retry can redo is 1. */
24
+ export declare const dropPoint: (stage: StageId | undefined) => StageId;
@@ -0,0 +1,41 @@
1
+ import { STAGE_NAMES, STAGES } from "./ledger.js";
2
+ const LONGEST_FIRST = [...STAGES].sort((a, b) => b.length - a.length);
3
+ const boundary = (id) => new RegExp(`(^|[^a-z0-9])${id.toLowerCase()}([^a-z0-9]|$)`);
4
+ /**
5
+ * A stage id out of whatever a stage called it.
6
+ *
7
+ * `redo_from_stage` and `loop_back_to` are free strings, so they arrive as
8
+ * `"2"`, `"stage 3A"` or `"the decomposition"`. Longest first, or `"5A"` is
9
+ * read as `"5"`.
10
+ *
11
+ * Absent counts as unnameable. Both fields are `required` in their schemas and
12
+ * neither is checked on the way back, so a stage that omitted one hands this
13
+ * `undefined` — and a stage nobody can name redoes everything, which is the
14
+ * answer this already had for a name it could not parse.
15
+ */
16
+ export function parseStage(said) {
17
+ const text = said?.trim().toLowerCase();
18
+ if (!text)
19
+ return undefined;
20
+ const exact = LONGEST_FIRST.find((id) => text === id.toLowerCase());
21
+ if (exact)
22
+ return exact;
23
+ const mentioned = LONGEST_FIRST.find((id) => boundary(id).test(text));
24
+ if (mentioned)
25
+ return mentioned;
26
+ return STAGES.find((id) => text.includes(STAGE_NAMES[id]));
27
+ }
28
+ /**
29
+ * Which half of the route has to run again.
30
+ *
31
+ * The draft is the seam: anything up to 4B was produced, and anything after it
32
+ * only read what the draft already held. A stage nobody could name redoes the
33
+ * lot, because the alternative is compiling something stale.
34
+ */
35
+ export function resumeAt(stage) {
36
+ if (!stage)
37
+ return "produce";
38
+ return STAGES.indexOf(stage) <= STAGES.indexOf("4B") ? "produce" : "verify";
39
+ }
40
+ /** Where the ledger is cut back to. The earliest thing a retry can redo is 1. */
41
+ export const dropPoint = (stage) => !stage || stage === "0" ? "1" : stage;
@@ -0,0 +1,5 @@
1
+ import type { RouteResult } from "@spendgraph/harness";
2
+ import type { NoesisResult } from "../types.js";
3
+ import type { RunState } from "./state.js";
4
+ /** The state, once every branch has run, as a result. */
5
+ export declare function finish(decided: RouteResult, state: RunState, latencyMs: number): NoesisResult;
@@ -0,0 +1,51 @@
1
+ import { RouteFailedError, RouteNotBuiltError } from "./errors.js";
2
+ /**
3
+ * What the run is worth acting on.
4
+ *
5
+ * An escalation outranks an abstention: a gate that asked for a person said so
6
+ * about the whole run, and 6C abstaining afterwards is a consequence of it.
7
+ */
8
+ function statusOf(state) {
9
+ if (state.ledger.escalated())
10
+ return "escalated";
11
+ const answered = state.answered;
12
+ return answered && (answered.answer.abstained || !answered.accepted) ? "abstained" : "answered";
13
+ }
14
+ /** Everything the run was billed for, whole and split two ways. */
15
+ function usageOfRun(state) {
16
+ const byStage = state.ledger.byStage();
17
+ return {
18
+ ...state.ledger.usage(),
19
+ byStage,
20
+ byModel: state.ledger.byModel(),
21
+ calls: byStage.reduce((total, stage) => total + stage.calls, 0),
22
+ };
23
+ }
24
+ /** The state, once every branch has run, as a result. */
25
+ export function finish(decided, state, latencyMs) {
26
+ const { estimates, routing, answered, text, ledger } = state;
27
+ if (decided.status === "failed" || !estimates || !routing || !answered || !text) {
28
+ const named = routing?.route ?? decided.classified;
29
+ throw decided.route === null
30
+ ? new RouteNotBuiltError(named, estimates, routing, decided.error)
31
+ : new RouteFailedError(named, estimates, routing, decided.error);
32
+ }
33
+ const escalation = ledger.escalated();
34
+ const usage = usageOfRun(state);
35
+ return {
36
+ text,
37
+ status: statusOf(state),
38
+ ...(escalation ? { escalation } : {}),
39
+ currentTask: state.currentTask ?? estimates.summary,
40
+ route: routing.route,
41
+ routing,
42
+ estimates,
43
+ answer: answered,
44
+ rolloutIds: ledger.rolloutIds,
45
+ unattributed: state.unattributed ?? [],
46
+ usage,
47
+ inputTokens: usage.inputTokens,
48
+ outputTokens: usage.outputTokens,
49
+ latencyMs,
50
+ };
51
+ }
@@ -0,0 +1,4 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { NoesisOptions, NoesisResult } from "../types.js";
4
+ export declare function run(client: PromptClient, llm: Llm, question: string, options?: NoesisOptions): Promise<NoesisResult>;
@@ -0,0 +1,73 @@
1
+ import { route } from "@spendgraph/harness";
2
+ import { emitter } from "../events.js";
3
+ import { notBuilt } from "./errors.js";
4
+ import { finish } from "./result.js";
5
+ import { classifyStage, directStage, fullStage, retrieveStage, standardStage, } from "./stages/index.js";
6
+ import { newRunState } from "./state.js";
7
+ /**
8
+ * Question in, answer out.
9
+ *
10
+ * `onDeliver` runs once, after the loop, and only for an answer worth acting
11
+ * on. Anything that renders a deliverable belongs there rather than inside a
12
+ * branch: nothing downstream reads it, no gate can check it, every back edge
13
+ * would make it again, and generating a finished-looking document out of an
14
+ * abstention is the worst thing this loop could produce.
15
+ *
16
+ * Pass the previous run's `currentTask` back in through `opts.triage` to keep a
17
+ * session going: it is what lets triage answer `is_new_task` at all, and the
18
+ * result carries the task forward for the turn after this one.
19
+ */
20
+ const DEFAULT_MAX_RETRIES = 5;
21
+ /**
22
+ * The options every stage is handed, with the emitter already on them.
23
+ *
24
+ * Threading it here rather than at each stage call is what let fifteen stages
25
+ * start announcing themselves without one of them being edited: they all
26
+ * already forward `opts.stage`, `opts.triage` and `opts.answer` verbatim.
27
+ */
28
+ const watched = (opts, emit) => ({
29
+ ...opts,
30
+ triage: { ...opts.triage, emit },
31
+ stage: { ...opts.stage, emit },
32
+ answer: { ...opts.answer, emit },
33
+ });
34
+ export async function run(client, llm, question, options = {}) {
35
+ const startedAt = Date.now();
36
+ const emit = emitter(options.onEvent);
37
+ const opts = watched(options, emit);
38
+ const retryBudget = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
39
+ const state = newRunState(opts.triage?.currentTask, emit, retryBudget);
40
+ emit?.({
41
+ type: "run.started",
42
+ question,
43
+ hasTools: !!opts.tools,
44
+ retryBudget,
45
+ });
46
+ const decided = await route({ question }, {
47
+ classify: classifyStage(client, llm, question, opts, state),
48
+ routes: {
49
+ direct: directStage(client, llm, question, opts, state),
50
+ retrieve: retrieveStage(client, llm, question, opts, state),
51
+ standard: standardStage(client, llm, question, opts, state),
52
+ full: fullStage(client, llm, question, opts, state),
53
+ },
54
+ fallback: notBuilt("unknown"),
55
+ });
56
+ // Every price the stages left in flight, collected before the result is built.
57
+ await state.ledger.settle();
58
+ const result = finish(decided, state, Date.now() - startedAt);
59
+ emit?.({
60
+ type: "run.finished",
61
+ status: result.status,
62
+ text: result.text,
63
+ route: result.route,
64
+ usage: result.usage,
65
+ rollouts: result.rolloutIds.length,
66
+ latencyMs: result.latencyMs,
67
+ });
68
+ if (result.status === "answered") {
69
+ await opts.onDeliver?.(result);
70
+ emit?.({ type: "delivered" });
71
+ }
72
+ return result;
73
+ }
@@ -0,0 +1,7 @@
1
+ import type { Classification } from "@spendgraph/harness";
2
+ import type { Llm } from "@spendgraph/llms";
3
+ import type { PromptClient } from "@spendgraph/prompt";
4
+ import type { NoesisOptions } from "../../types.js";
5
+ import { type RunState } from "../state.js";
6
+ /** Stage 0, and the route its estimates earn. */
7
+ export declare const classifyStage: (client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState) => () => Promise<Classification>;
@@ -0,0 +1,23 @@
1
+ import { routeFor } from "../../route.js";
2
+ import { invoke as triage } from "../../router/triage/index.js";
3
+ import { nextTask } from "../state.js";
4
+ /** Stage 0, and the route its estimates earn. */
5
+ export const classifyStage = (client, llm, question, opts, state) => async () => {
6
+ const stage = await triage(client, llm, { ...opts.triage, currentTask: state.currentTask, question }, { ...opts.triage });
7
+ state.estimates = state.ledger.record("0", { ...stage, data: stage.estimates });
8
+ state.routing = routeFor(stage.estimates);
9
+ state.currentTask = nextTask(stage.estimates, state.currentTask);
10
+ state.emit?.({
11
+ type: "run.routed",
12
+ route: state.routing.route,
13
+ routing: state.routing,
14
+ estimates: stage.estimates,
15
+ });
16
+ return {
17
+ route: state.routing.route,
18
+ reason: state.routing.reason,
19
+ model: stage.model,
20
+ inputTokens: stage.inputTokens,
21
+ outputTokens: stage.outputTokens,
22
+ };
23
+ };
@@ -0,0 +1,6 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { NoesisOptions } from "../../types.js";
4
+ import type { RunState } from "../state.js";
5
+ /** The direct branch: the question and what triage made of it, straight to 6C. */
6
+ export declare const directStage: (client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState) => () => Promise<string>;
@@ -0,0 +1,8 @@
1
+ import { answer } from "../../producers/toulmin/index.js";
2
+ /** The direct branch: the question and what triage made of it, straight to 6C. */
3
+ export const directStage = (client, llm, question, opts, state) => async () => {
4
+ const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, opts.answer ?? {});
5
+ state.answered = answered;
6
+ state.text = state.ledger.record("6C", { ...answered, data: answered.answer }).claim;
7
+ return state.text;
8
+ };
@@ -0,0 +1,14 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { NoesisOptions } from "../../types.js";
4
+ import type { RunState } from "../state.js";
5
+ /**
6
+ * The full branch: everything standard does, plus the audits.
7
+ *
8
+ * 3B and 3C interrogate the plan before it runs; 5C and 6B interrogate the
9
+ * answer before it ships. Standard already plans, so what full adds is four
10
+ * audits and nothing else. 6B fires on `routing.redTeam` rather than on the
11
+ * route, because stakes are what earn it — a run sent here by difficulty or
12
+ * ambiguity has no attacker to pay for.
13
+ */
14
+ export declare const fullStage: (client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState) => () => Promise<string>;
@@ -0,0 +1,21 @@
1
+ import { settle } from "../loop.js";
2
+ import { produce } from "../produce.js";
3
+ import { compile } from "../verify.js";
4
+ /**
5
+ * The full branch: everything standard does, plus the audits.
6
+ *
7
+ * 3B and 3C interrogate the plan before it runs; 5C and 6B interrogate the
8
+ * answer before it ships. Standard already plans, so what full adds is four
9
+ * audits and nothing else. 6B fires on `routing.redTeam` rather than on the
10
+ * route, because stakes are what earn it — a run sent here by difficulty or
11
+ * ambiguity has no attacker to pay for.
12
+ */
13
+ export const fullStage = (client, llm, question, opts, state) => async () => {
14
+ const reproduce = () => produce(client, llm, question, opts, state, true);
15
+ await settle(client, llm, question, opts, state, await reproduce(), {
16
+ standards: true,
17
+ redTeam: state.routing?.redTeam ?? false,
18
+ reproduce,
19
+ });
20
+ return compile(client, llm, question, opts, state);
21
+ };
@@ -0,0 +1,5 @@
1
+ export { classifyStage } from "./classify.js";
2
+ export { directStage } from "./direct.js";
3
+ export { fullStage } from "./full.js";
4
+ export { retrieveStage } from "./retrieve.js";
5
+ export { standardStage } from "./standard.js";
@@ -0,0 +1,5 @@
1
+ export { classifyStage } from "./classify.js";
2
+ export { directStage } from "./direct.js";
3
+ export { fullStage } from "./full.js";
4
+ export { retrieveStage } from "./retrieve.js";
5
+ export { standardStage } from "./standard.js";
@@ -0,0 +1,13 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { NoesisOptions } from "../../types.js";
4
+ import type { RunState } from "../state.js";
5
+ /**
6
+ * The retrieve branch: one lookup, then 6C compiles what came back.
7
+ *
8
+ * No brief and no decomposition, because there is nothing to interrogate or
9
+ * split — the question is clear and the only reason the model cannot answer it
10
+ * is that the answer is somewhere else. A tool that finds nothing leaves 6C
11
+ * with no grounds, which is what abstention is for.
12
+ */
13
+ export declare const retrieveStage: (client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState) => () => Promise<string>;
@@ -0,0 +1,38 @@
1
+ import { execute } from "../../execute/index.js";
2
+ import { answer } from "../../producers/toulmin/index.js";
3
+ import { NoToolsError } from "../errors.js";
4
+ const DEFAULT_TOOL = "deeprecall";
5
+ const lookup = (question, tool) => ({
6
+ id: "q",
7
+ question,
8
+ depends_on: [],
9
+ verify_by: "the tool returned something to cite",
10
+ done_when: "the lookup came back",
11
+ kind: "read_only",
12
+ tool,
13
+ });
14
+ /**
15
+ * The retrieve branch: one lookup, then 6C compiles what came back.
16
+ *
17
+ * No brief and no decomposition, because there is nothing to interrogate or
18
+ * split — the question is clear and the only reason the model cannot answer it
19
+ * is that the answer is somewhere else. A tool that finds nothing leaves 6C
20
+ * with no grounds, which is what abstention is for.
21
+ */
22
+ export const retrieveStage = (client, llm, question, opts, state) => async () => {
23
+ if (!opts.tools)
24
+ throw new NoToolsError();
25
+ const execution = await execute(opts.tools, [lookup(question, opts.retrieve?.tool ?? DEFAULT_TOOL)], { emit: state.emit });
26
+ state.ledger.record("4", {
27
+ data: execution,
28
+ rolloutIds: [],
29
+ model: "",
30
+ inputTokens: 0,
31
+ outputTokens: 0,
32
+ latencyMs: execution.toolMs,
33
+ });
34
+ const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, opts.answer ?? {});
35
+ state.answered = answered;
36
+ state.text = state.ledger.record("6C", { ...answered, data: answered.answer }).claim;
37
+ return state.text;
38
+ };
@@ -0,0 +1,12 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { NoesisOptions } from "../../types.js";
4
+ import type { RunState } from "../state.js";
5
+ /**
6
+ * The standard branch: frame it, split it, answer the parts, then check them.
7
+ *
8
+ * It plans, because 4A judges each step against the tolerance 3A wrote for it.
9
+ * What it does not do is audit: no premortem or gate on the plan, and no
10
+ * standards review or red team on the answer. That is what `full` buys.
11
+ */
12
+ export declare const standardStage: (client: PromptClient, llm: Llm, question: string, opts: NoesisOptions, state: RunState) => () => Promise<string>;
@@ -0,0 +1,19 @@
1
+ import { settle } from "../loop.js";
2
+ import { produce } from "../produce.js";
3
+ import { compile } from "../verify.js";
4
+ /**
5
+ * The standard branch: frame it, split it, answer the parts, then check them.
6
+ *
7
+ * It plans, because 4A judges each step against the tolerance 3A wrote for it.
8
+ * What it does not do is audit: no premortem or gate on the plan, and no
9
+ * standards review or red team on the answer. That is what `full` buys.
10
+ */
11
+ export const standardStage = (client, llm, question, opts, state) => async () => {
12
+ const reproduce = () => produce(client, llm, question, opts, state, false);
13
+ await settle(client, llm, question, opts, state, await reproduce(), {
14
+ standards: false,
15
+ redTeam: false,
16
+ reproduce,
17
+ });
18
+ return compile(client, llm, question, opts, state);
19
+ };
@@ -0,0 +1,26 @@
1
+ import type { Emit } from "../events.js";
2
+ import type { AnswerResult } from "../producers/toulmin/index.js";
3
+ import type { TriageEstimates } from "../router/triage/index.js";
4
+ import type { Routing } from "../types.js";
5
+ import { type Ledger } from "./ledger.js";
6
+ /** What each branch writes down, since `route()` hands back only its output. */
7
+ export interface RunState {
8
+ /** Every stage's result and what it cost, in one place. */
9
+ ledger: Ledger;
10
+ /** Where the run layer says what it is doing. Undefined when nobody listens. */
11
+ emit?: Emit;
12
+ /** The open task, carried into the run and rewritten once triage has read it. */
13
+ currentTask?: string;
14
+ estimates?: TriageEstimates;
15
+ routing?: Routing;
16
+ answered?: AnswerResult;
17
+ text?: string;
18
+ /** `source_stage` values 6C cited that no stage in this run produced. */
19
+ unattributed?: string[];
20
+ }
21
+ export declare const newRunState: (currentTask?: string, emit?: Emit, maxRetries?: number) => RunState;
22
+ /**
23
+ * The open task after triage has read the request: the router's summary when it
24
+ * calls the request new, otherwise the task carried in.
25
+ */
26
+ export declare function nextTask(estimates: TriageEstimates, current?: string): string;
@@ -0,0 +1,13 @@
1
+ import { newLedger } from "./ledger.js";
2
+ export const newRunState = (currentTask, emit, maxRetries) => ({
3
+ ledger: newLedger(maxRetries, emit),
4
+ emit,
5
+ currentTask,
6
+ });
7
+ /**
8
+ * The open task after triage has read the request: the router's summary when it
9
+ * calls the request new, otherwise the task carried in.
10
+ */
11
+ export function nextTask(estimates, current) {
12
+ return estimates.is_new_task ? estimates.summary : (current ?? estimates.summary);
13
+ }