@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,126 @@
1
+ import type { Emit } from "../../events.js";
2
+ import type { Usage } from "../../usage.js";
3
+ /** One piece of evidence, and the stage it came from. */
4
+ export interface Ground {
5
+ evidence: string;
6
+ source_stage: string;
7
+ }
8
+ /** The hedging, kept out of the claim and stated as scope plus a calibrated number. */
9
+ export interface Qualifier {
10
+ confidence: number;
11
+ holds_when: string;
12
+ does_not_apply_to: string;
13
+ }
14
+ /** An objection that survived red-teaming, and what would make it fatal. */
15
+ export interface Rebuttal {
16
+ objection: string;
17
+ defeats_claim_when: string;
18
+ }
19
+ /**
20
+ * Stage 6C. The answer, assembled from artifacts rather than written.
21
+ *
22
+ * `abstained` is a first-class outcome: true means the grounds supported no
23
+ * claim, and `claim` says what is not known and what would settle it.
24
+ */
25
+ export interface ToulminAnswer {
26
+ claim: string;
27
+ grounds: Ground[];
28
+ warrant: string;
29
+ qualifier: Qualifier;
30
+ rebuttals: Rebuttal[];
31
+ would_change_my_mind: string;
32
+ abstained: boolean;
33
+ }
34
+ /**
35
+ * What the stage compiles from.
36
+ *
37
+ * A string is sent as written; anything else is serialised. The prompt declares
38
+ * no fields, so this arrives as the user turn rather than as a substitution.
39
+ */
40
+ export type Artifacts = string | Record<string, unknown>;
41
+ export interface ToulminOptions {
42
+ /**
43
+ * Attempts for this call, counting the first. Default 2.
44
+ *
45
+ * Separate from `maxRetries`, the loop's budget for a gate sending work
46
+ * backwards. This one covers a call that produced nothing to think about, and
47
+ * the two must not share an allowance.
48
+ */
49
+ attempts?: number;
50
+ /** Overrides the model the `Llm` was built with. */
51
+ model?: string;
52
+ /** Where this stage says what it is doing. Set by `run()`. */
53
+ emit?: Emit;
54
+ /**
55
+ * Merged into the request body.
56
+ *
57
+ * Where `temperature` goes, and it is per model: Haiku 4.5 takes it, Sonnet 5
58
+ * rejects it outright.
59
+ */
60
+ params?: Record<string, unknown>;
61
+ }
62
+ export interface ToulminResult extends Usage {
63
+ answer: ToulminAnswer;
64
+ rolloutId: string;
65
+ model: string;
66
+ latencyMs: number;
67
+ /** What the server will price this tier at, still in flight. */
68
+ pricing?: Promise<number | undefined>;
69
+ }
70
+ /**
71
+ * One rung of the cascade.
72
+ *
73
+ * `params` is per model because it has to be: Haiku 4.5 takes `temperature`,
74
+ * Sonnet 5 rejects it outright, and one shared body would fail on whichever it
75
+ * was not written for.
76
+ */
77
+ export interface AnswerTier {
78
+ model: string;
79
+ params?: Record<string, unknown>;
80
+ }
81
+ export interface AnswerOptions {
82
+ /**
83
+ * Rungs in order, cheapest first. Defaults to the model the `Llm` carries, once.
84
+ *
85
+ * Name two to buy a second opinion. `params` is per tier because it has to
86
+ * be: models disagree about which sampling knobs they accept, and one shared
87
+ * request body fails on whichever tier it was not written for.
88
+ */
89
+ tiers?: AnswerTier[];
90
+ /** Where each tier says what it is doing. Set by `run()`. */
91
+ emit?: Emit;
92
+ /**
93
+ * Below this the answer escalates. Default 0.6.
94
+ *
95
+ * An abstention escalates whatever this is set to — a tier that could not
96
+ * answer has not answered, however calibrated it was about it.
97
+ */
98
+ minConfidence?: number;
99
+ /** Attempts per tier, counting the first. Default 2. */
100
+ attempts?: number;
101
+ /** Stop once the tiers have consumed this many tokens. */
102
+ maxTokens?: number;
103
+ /** Renders grounds and the warrant into the text. Default false. */
104
+ showGrounds?: boolean;
105
+ }
106
+ export interface AnswerResult extends Usage {
107
+ /** The reply, as a person reads it. */
108
+ text: string;
109
+ answer: ToulminAnswer;
110
+ /**
111
+ * Every tier's price, combined, still in flight.
112
+ *
113
+ * One promise rather than one per tier because the cascade is one stage in
114
+ * the ledger: an escalation is still 6C, and its cost is what 6C cost.
115
+ */
116
+ pricing?: Promise<number | undefined>;
117
+ /** The tier whose answer this is. */
118
+ model: string;
119
+ /** False when no tier cleared the bar and this is the last one's answer. */
120
+ accepted: boolean;
121
+ /** Tiers tried that did not supply the answer. */
122
+ escalated: string[];
123
+ /** One per tier that answered, in order. Each is its own recorded rollout. */
124
+ rolloutIds: string[];
125
+ latencyMs: number;
126
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ /** The variable every stored prompt takes its request through. */
2
+ export declare const QUESTION_FIELD = "question";
3
+ /**
4
+ * The messages to send, with the request in the turn the prompt renders.
5
+ *
6
+ * A prompt whose question box is `{question}` needs nothing from this: the
7
+ * value passed alongside fills it and the rendered turn already carries the
8
+ * request. Asked of the messages rather than of `prompt.fields`, because
9
+ * `applyVariables` substitutes any `{name}` it finds against the values it is
10
+ * given and never reads the field list — so a placeholder typed into the box
11
+ * works with no field declared, and would not show up in `fields` at all.
12
+ *
13
+ * Where the box holds something else — a question typed while testing, or
14
+ * nothing at all — the trailing turn is overwritten with the request and the
15
+ * stored text is not sent. That is what the placeholder would have done, and it
16
+ * keeps the shape the prompt expects: one system turn, one user turn. It warns
17
+ * once per prompt, because the alternative failure is silent: an unmigrated
18
+ * prompt would answer the question saved on it and return a well-formed result
19
+ * about the wrong thing.
20
+ */
21
+ export declare function askThrough<T extends {
22
+ role: string;
23
+ content: string;
24
+ }>(messages: T[], question: string, prompt: string): T[];
@@ -0,0 +1,37 @@
1
+ /** The variable every stored prompt takes its request through. */
2
+ export const QUESTION_FIELD = "question";
3
+ const warned = new Set();
4
+ function carries(messages, question) {
5
+ const last = messages[messages.length - 1];
6
+ return last?.role === "user" && last.content.includes(question);
7
+ }
8
+ /**
9
+ * The messages to send, with the request in the turn the prompt renders.
10
+ *
11
+ * A prompt whose question box is `{question}` needs nothing from this: the
12
+ * value passed alongside fills it and the rendered turn already carries the
13
+ * request. Asked of the messages rather than of `prompt.fields`, because
14
+ * `applyVariables` substitutes any `{name}` it finds against the values it is
15
+ * given and never reads the field list — so a placeholder typed into the box
16
+ * works with no field declared, and would not show up in `fields` at all.
17
+ *
18
+ * Where the box holds something else — a question typed while testing, or
19
+ * nothing at all — the trailing turn is overwritten with the request and the
20
+ * stored text is not sent. That is what the placeholder would have done, and it
21
+ * keeps the shape the prompt expects: one system turn, one user turn. It warns
22
+ * once per prompt, because the alternative failure is silent: an unmigrated
23
+ * prompt would answer the question saved on it and return a well-formed result
24
+ * about the wrong thing.
25
+ */
26
+ export function askThrough(messages, question, prompt) {
27
+ if (carries(messages, question))
28
+ return messages;
29
+ if (!warned.has(prompt)) {
30
+ warned.add(prompt);
31
+ console.warn(`[noesis] ${prompt} renders no {${QUESTION_FIELD}}, so the request was put in its question ` +
32
+ "box instead and the text saved there was not sent. Publish the prompt: pull() serves the " +
33
+ "published wording, and a migration only reaches the draft.");
34
+ }
35
+ const last = messages.length - 1;
36
+ return messages.map((m, i) => i === last && m.role === "user" ? { ...m, content: question } : m);
37
+ }
@@ -0,0 +1,3 @@
1
+ export { invoke } from "./invoke.js";
2
+ export { REFLECTION_SCHEMA, REFLECTION_SLUG } from "./prompt.js";
3
+ export type { Reflection, ReflectionInput, ReflectionOptions, ReflectionResult, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { invoke } from "./invoke.js";
2
+ export { REFLECTION_SCHEMA, REFLECTION_SLUG } from "./prompt.js";
@@ -0,0 +1,5 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { ReflectionInput, ReflectionOptions, ReflectionResult } from "./types.js";
4
+ /** Stage R. The earliest wrong decision behind a failure, not the place it surfaced. */
5
+ export declare function invoke(client: PromptClient, llm: Llm, input: ReflectionInput, opts?: ReflectionOptions): Promise<ReflectionResult>;
@@ -0,0 +1,6 @@
1
+ import { runStage } from "../../stage.js";
2
+ import { REFLECTION_SCHEMA, REFLECTION_SLUG } from "./prompt.js";
3
+ /** Stage R. The earliest wrong decision behind a failure, not the place it surfaced. */
4
+ export function invoke(client, llm, input, opts = {}) {
5
+ return runStage(client, llm, REFLECTION_SLUG, REFLECTION_SCHEMA, input, opts);
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { JsonSchema } from "@spendgraph/llms";
2
+ /** Stage R. Pulled by slug, so an edit needs no release here. */
3
+ export declare const REFLECTION_SLUG = "ctr-r-root-cause-reflection-7x0x90";
4
+ /** What the reply must be, as the prompt itself specifies it. */
5
+ export declare const REFLECTION_SCHEMA: JsonSchema;
@@ -0,0 +1,26 @@
1
+ /** Stage R. Pulled by slug, so an edit needs no release here. */
2
+ export const REFLECTION_SLUG = "ctr-r-root-cause-reflection-7x0x90";
3
+ /** What the reply must be, as the prompt itself specifies it. */
4
+ export const REFLECTION_SCHEMA = {
5
+ type: "object",
6
+ properties: {
7
+ symptom: { type: "string" },
8
+ earliest_faulty_stage: { type: "string" },
9
+ faulty_decision: { type: "string" },
10
+ symptom_equals_cause: { type: "boolean" },
11
+ redo_from_stage: { type: "string" },
12
+ do_differently: { type: "string" },
13
+ lesson: { type: "string" },
14
+ avoid_repeat_check: { type: "string" },
15
+ },
16
+ required: [
17
+ "symptom",
18
+ "earliest_faulty_stage",
19
+ "faulty_decision",
20
+ "symptom_equals_cause",
21
+ "redo_from_stage",
22
+ "do_differently",
23
+ "lesson",
24
+ "avoid_repeat_check",
25
+ ],
26
+ };
@@ -0,0 +1,17 @@
1
+ import type { Asked } from "../../context.js";
2
+ import type { StageOptions, StageOutcome } from "../../stage.js";
3
+ export interface Reflection {
4
+ symptom: string;
5
+ earliest_faulty_stage: string;
6
+ faulty_decision: string;
7
+ /** True only when the place it surfaced really is where it went wrong. */
8
+ symptom_equals_cause: boolean;
9
+ redo_from_stage: string;
10
+ do_differently: string;
11
+ /** Follows the form "In situations like X, strategy Y failed because Z." */
12
+ lesson: string;
13
+ avoid_repeat_check: string;
14
+ }
15
+ export type ReflectionInput = Asked;
16
+ export type ReflectionOptions = StageOptions;
17
+ export type ReflectionResult = StageOutcome<Reflection>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Whether the same call is worth making again.
3
+ *
4
+ * Two families qualify. A reply error means a schema-forced call came back with
5
+ * nothing to parse — the model declined to emit the tool call, which is a flake
6
+ * rather than a fault in the prompt, and the bounded budget covers the case
7
+ * where it is not. A transient transport error is the provider asking to be
8
+ * asked later.
9
+ */
10
+ export declare function retryable(err: unknown): boolean;
11
+ export interface AttemptOptions {
12
+ /** Total attempts including the first. Default 2. Set 1 to disable retrying. */
13
+ attempts?: number;
14
+ /** Called for every attempt that failed, retried or not. */
15
+ onFailure?: (err: unknown, willRetry: boolean) => void;
16
+ }
17
+ /**
18
+ * Runs a stage call, once more if the first failure looks worth repeating.
19
+ *
20
+ * This is deliberately not the gate retry budget. That one is spent when a gate
21
+ * refuses and reflection sends work backwards — the loop thinking again. This
22
+ * one covers a call that never produced anything to think about, and mixing the
23
+ * two would let a flaky provider eat the allowance reserved for reasoning.
24
+ */
25
+ export declare function attempting<T>(run: () => Promise<T>, opts?: AttemptOptions): Promise<T>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * How many times a stage call may be made, counting the first.
3
+ *
4
+ * Two, not more. A flake is usually gone on the second try, and a call that
5
+ * failed has often still been billed — so the budget is deliberately small and
6
+ * the classifier below deliberately narrow.
7
+ */
8
+ const DEFAULT_ATTEMPTS = 2;
9
+ /** Milliseconds before the next attempt, multiplied by the attempt number. */
10
+ const BACKOFF_MS = 300;
11
+ const pause = (ms) => new Promise((done) => setTimeout(done, ms));
12
+ /**
13
+ * Signals that a second attempt could plausibly land.
14
+ *
15
+ * Matched on the message because a stage is handed whatever the caller's client
16
+ * threw, and nothing here can assume a status code survived. Anything else is
17
+ * not retried: a 400 naming a model that does not exist and a 402 for credits
18
+ * that are not there fail the same way however many times they are sent, and
19
+ * spending three round trips to learn that is worse than failing once.
20
+ */
21
+ const TRANSIENT = /\b(429|500|502|503|504|522|524)\b|rate.?limit|overloaded|timed? ?out|timeout|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|fetch failed|network error/i;
22
+ /**
23
+ * Whether the same call is worth making again.
24
+ *
25
+ * Two families qualify. A reply error means a schema-forced call came back with
26
+ * nothing to parse — the model declined to emit the tool call, which is a flake
27
+ * rather than a fault in the prompt, and the bounded budget covers the case
28
+ * where it is not. A transient transport error is the provider asking to be
29
+ * asked later.
30
+ */
31
+ export function retryable(err) {
32
+ if (!(err instanceof Error))
33
+ return false;
34
+ if (err.name.endsWith("ReplyError") || err.name === "StructuredOutputError")
35
+ return true;
36
+ return TRANSIENT.test(err.message);
37
+ }
38
+ /**
39
+ * Runs a stage call, once more if the first failure looks worth repeating.
40
+ *
41
+ * This is deliberately not the gate retry budget. That one is spent when a gate
42
+ * refuses and reflection sends work backwards — the loop thinking again. This
43
+ * one covers a call that never produced anything to think about, and mixing the
44
+ * two would let a flaky provider eat the allowance reserved for reasoning.
45
+ */
46
+ export async function attempting(run, opts = {}) {
47
+ const total = Math.max(1, opts.attempts ?? DEFAULT_ATTEMPTS);
48
+ for (let attempt = 1;; attempt++) {
49
+ try {
50
+ return await run();
51
+ }
52
+ catch (err) {
53
+ const willRetry = attempt < total && retryable(err);
54
+ opts.onFailure?.(err, willRetry);
55
+ if (!willRetry)
56
+ throw err;
57
+ await pause(BACKOFF_MS * attempt);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,29 @@
1
+ import type { Routing, TriageEstimates } from "./index.js";
2
+ /**
3
+ * The estimates, turned into a route.
4
+ *
5
+ * Deliberately code and deliberately dull: the router prompt is told not to
6
+ * choose its own budget, and a threshold written here can be moved after an
7
+ * eval run without touching a prompt or re-running anything.
8
+ *
9
+ * A lookup is answered as a lookup, even when the stakes are high. `redTeam` is
10
+ * a flag rather than a route, so it still fires — the care is kept and the
11
+ * planning stages that have nothing to plan are not paid for. `critical` is the
12
+ * exception, and keeps the whole loop.
13
+ *
14
+ * Difficulty alone does not buy the audit set. The premortem and the standards
15
+ * review bite on risk and vagueness, not on how hard the reasoning is, so a
16
+ * hard question that is low-stakes, well-posed and already understood takes
17
+ * `standard`. Hard and *unsure* still takes `full`.
18
+ *
19
+ * Vagueness and doubt compound. Either one alone is survivable; a question that
20
+ * is both under-specified and one the model cannot place is what 3B and 3C
21
+ * exist for, and it takes `full` below the ambiguity bar that would catch it on
22
+ * its own.
23
+ *
24
+ * `retrieve` splits the two reasons a clear question fails `direct`: high tool
25
+ * need, and a confidence that only just misses. Both mean the model should not
26
+ * answer from memory, and neither makes it a hard question — looking it up is
27
+ * cheaper than planning it, and likelier to be right.
28
+ */
29
+ export declare function routeFor(estimates: TriageEstimates): Routing;
@@ -0,0 +1,69 @@
1
+ const AMBIGUOUS = 0.6;
2
+ const CLEAR = 0.3;
3
+ const CONFIDENT = 0.7;
4
+ const NEEDS_TOOLS = 0.3;
5
+ const VAGUE = 0.5;
6
+ const UNSURE = 0.5;
7
+ const NEARLY = 0.6;
8
+ const HARD = new Set(["expert", "research"]);
9
+ const EASY = new Set(["trivial", "easy"]);
10
+ const SERIOUS = new Set(["high", "critical"]);
11
+ /**
12
+ * The estimates, turned into a route.
13
+ *
14
+ * Deliberately code and deliberately dull: the router prompt is told not to
15
+ * choose its own budget, and a threshold written here can be moved after an
16
+ * eval run without touching a prompt or re-running anything.
17
+ *
18
+ * A lookup is answered as a lookup, even when the stakes are high. `redTeam` is
19
+ * a flag rather than a route, so it still fires — the care is kept and the
20
+ * planning stages that have nothing to plan are not paid for. `critical` is the
21
+ * exception, and keeps the whole loop.
22
+ *
23
+ * Difficulty alone does not buy the audit set. The premortem and the standards
24
+ * review bite on risk and vagueness, not on how hard the reasoning is, so a
25
+ * hard question that is low-stakes, well-posed and already understood takes
26
+ * `standard`. Hard and *unsure* still takes `full`.
27
+ *
28
+ * Vagueness and doubt compound. Either one alone is survivable; a question that
29
+ * is both under-specified and one the model cannot place is what 3B and 3C
30
+ * exist for, and it takes `full` below the ambiguity bar that would catch it on
31
+ * its own.
32
+ *
33
+ * `retrieve` splits the two reasons a clear question fails `direct`: high tool
34
+ * need, and a confidence that only just misses. Both mean the model should not
35
+ * answer from memory, and neither makes it a hard question — looking it up is
36
+ * cheaper than planning it, and likelier to be right.
37
+ */
38
+ export function routeFor(estimates) {
39
+ const redTeam = SERIOUS.has(estimates.stakes);
40
+ const clear = EASY.has(estimates.difficulty) && estimates.ambiguity < CLEAR;
41
+ const lookup = clear && estimates.tool_need >= NEEDS_TOOLS;
42
+ if (lookup && estimates.stakes !== "critical") {
43
+ return { route: "retrieve", redTeam, reason: `a lookup — tool_need is ${estimates.tool_need}` };
44
+ }
45
+ if (redTeam) {
46
+ return { route: "full", redTeam, reason: `stakes are ${estimates.stakes}` };
47
+ }
48
+ const capable = estimates.stakes === "low" && estimates.ambiguity < CLEAR && estimates.confidence >= CONFIDENT;
49
+ if (HARD.has(estimates.difficulty) && !capable) {
50
+ return { route: "full", redTeam, reason: `difficulty is ${estimates.difficulty}` };
51
+ }
52
+ if (estimates.ambiguity >= AMBIGUOUS) {
53
+ return { route: "full", redTeam, reason: `ambiguity is ${estimates.ambiguity}` };
54
+ }
55
+ if (estimates.ambiguity >= VAGUE && estimates.confidence < UNSURE) {
56
+ return {
57
+ route: "full",
58
+ redTeam,
59
+ reason: `ambiguity ${estimates.ambiguity} with confidence ${estimates.confidence}`,
60
+ };
61
+ }
62
+ if (clear && estimates.confidence >= CONFIDENT) {
63
+ return { route: "direct", redTeam, reason: `${estimates.difficulty} and already known` };
64
+ }
65
+ if (clear && estimates.confidence >= NEARLY) {
66
+ return { route: "retrieve", redTeam, reason: `${estimates.difficulty} but not certain` };
67
+ }
68
+ return { route: "standard", redTeam, reason: "neither trivial nor serious" };
69
+ }
@@ -0,0 +1,3 @@
1
+ export { attached, invoke, TriageReplyError } from "./invoke.js";
2
+ export { TRIAGE_SCHEMA, TRIAGE_SLUG } from "./prompt.js";
3
+ export type { Difficulty, Stakes, TriageEstimates, TriageInput, TriageOptions, TriageResult, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { attached, invoke, TriageReplyError } from "./invoke.js";
2
+ export { TRIAGE_SCHEMA, TRIAGE_SLUG } from "./prompt.js";
@@ -0,0 +1,31 @@
1
+ import type { Llm } from "@spendgraph/llms";
2
+ import type { PromptClient } from "@spendgraph/prompt";
3
+ import type { TriageInput, TriageOptions, TriageResult } from "./types.js";
4
+ /**
5
+ * An attachment slot, filled or explicitly empty.
6
+ *
7
+ * The prompt's question box wraps every request in an `<attachments>` block, so
8
+ * the tags are sent whether or not anything was attached. Left empty they read
9
+ * as an attachment the router cannot open, which it prices as ambiguity and
10
+ * tool need — enough on its own to push a plain question from `standard` to
11
+ * `full` and pay for the long route on every call that attaches nothing.
12
+ */
13
+ export declare function attached(content?: string): string;
14
+ /** The reply came back, but not as the object the router is supposed to return. */
15
+ export declare class TriageReplyError extends Error {
16
+ readonly raw: string;
17
+ constructor(raw: string);
18
+ }
19
+ /**
20
+ * Stage 0. Pulls the router, calls it, and hands back the parsed signals.
21
+ *
22
+ * The call is recorded once, as the prompt's rollout — the model client is told
23
+ * not to trace, or the same tokens land again as usage and every run prices
24
+ * double.
25
+ *
26
+ * An open task is sent as `<CURRENT_TASK>…</CURRENT_TASK>`. The prompt names
27
+ * the block but never shows its markup, so this is the form it is given — keep
28
+ * the two matching, or the router is pattern-matching on something it was never
29
+ * shown.
30
+ */
31
+ export declare function invoke(client: PromptClient, llm: Llm, input: TriageInput, opts?: TriageOptions): Promise<TriageResult>;
@@ -0,0 +1,76 @@
1
+ import { askThrough, QUESTION_FIELD } from "../../question.js";
2
+ import { attempting } from "../../retry.js";
3
+ import { announce, failed } from "../../started.js";
4
+ import { usageOf } from "../../usage.js";
5
+ import { TRIAGE_SCHEMA, TRIAGE_SLUG } from "./prompt.js";
6
+ function reTriage(currentTask) {
7
+ const task = currentTask?.trim();
8
+ return task ? [{ role: "user", content: `<CURRENT_TASK>\n${task}\n</CURRENT_TASK>` }] : [];
9
+ }
10
+ /** Stands in for an attachment nobody sent. */
11
+ const NOTHING_ATTACHED = "(none)";
12
+ /**
13
+ * An attachment slot, filled or explicitly empty.
14
+ *
15
+ * The prompt's question box wraps every request in an `<attachments>` block, so
16
+ * the tags are sent whether or not anything was attached. Left empty they read
17
+ * as an attachment the router cannot open, which it prices as ambiguity and
18
+ * tool need — enough on its own to push a plain question from `standard` to
19
+ * `full` and pay for the long route on every call that attaches nothing.
20
+ */
21
+ export function attached(content) {
22
+ return content?.trim() ? content : NOTHING_ATTACHED;
23
+ }
24
+ /** The reply came back, but not as the object the router is supposed to return. */
25
+ export class TriageReplyError extends Error {
26
+ raw;
27
+ constructor(raw) {
28
+ super("Triage returned no object matching its schema.");
29
+ this.raw = raw;
30
+ this.name = "TriageReplyError";
31
+ }
32
+ }
33
+ /**
34
+ * Stage 0. Pulls the router, calls it, and hands back the parsed signals.
35
+ *
36
+ * The call is recorded once, as the prompt's rollout — the model client is told
37
+ * not to trace, or the same tokens land again as usage and every run prices
38
+ * double.
39
+ *
40
+ * An open task is sent as `<CURRENT_TASK>…</CURRENT_TASK>`. The prompt names
41
+ * the block but never shows its markup, so this is the form it is given — keep
42
+ * the two matching, or the router is pattern-matching on something it was never
43
+ * shown.
44
+ */
45
+ export async function invoke(client, llm, input, opts = {}) {
46
+ announce(opts.emit, TRIAGE_SLUG, opts.model);
47
+ return attempting(async () => {
48
+ const prompt = await client.pull(TRIAGE_SLUG);
49
+ const question = input.question;
50
+ const values = {
51
+ file_content: attached(input.file_content),
52
+ image_content: attached(input.image_content),
53
+ [QUESTION_FIELD]: question,
54
+ };
55
+ let reply;
56
+ const outcome = await prompt.call(values, async ({ messages }) => {
57
+ reply = await llm.call(askThrough(messages, question, TRIAGE_SLUG), {
58
+ model: opts.model,
59
+ schema: TRIAGE_SCHEMA,
60
+ trace: false,
61
+ params: opts.params,
62
+ });
63
+ return reply;
64
+ }, { history: reTriage(input.currentTask) });
65
+ if (reply?.data === undefined)
66
+ throw new TriageReplyError(outcome.output);
67
+ return {
68
+ ...usageOf(outcome),
69
+ ...(outcome.pricing ? { pricing: outcome.pricing } : {}),
70
+ estimates: reply.data,
71
+ rolloutId: outcome.rolloutId,
72
+ model: outcome.model,
73
+ latencyMs: reply.latencyMs ?? 0,
74
+ };
75
+ }, { attempts: opts.attempts, onFailure: (err) => failed(opts.emit, TRIAGE_SLUG, err) });
76
+ }
@@ -0,0 +1,11 @@
1
+ import type { JsonSchema } from "@spendgraph/llms";
2
+ /** The stored router prompt. Pulled by slug, so an edit needs no release here. */
3
+ export declare const TRIAGE_SLUG = "triage-router-v1-81c2cc";
4
+ /**
5
+ * What the reply must be, as the prompt itself specifies it.
6
+ *
7
+ * `route`, `next_stage` and the rest are deliberately absent: the prompt forbids
8
+ * them and code derives them, so a model that returns one fails here rather
9
+ * than having it read downstream.
10
+ */
11
+ export declare const TRIAGE_SCHEMA: JsonSchema;
@@ -0,0 +1,34 @@
1
+ /** The stored router prompt. Pulled by slug, so an edit needs no release here. */
2
+ export const TRIAGE_SLUG = "triage-router-v1-81c2cc";
3
+ const UNIT = { type: "number", minimum: 0, maximum: 1 };
4
+ /**
5
+ * What the reply must be, as the prompt itself specifies it.
6
+ *
7
+ * `route`, `next_stage` and the rest are deliberately absent: the prompt forbids
8
+ * them and code derives them, so a model that returns one fails here rather
9
+ * than having it read downstream.
10
+ */
11
+ export const TRIAGE_SCHEMA = {
12
+ type: "object",
13
+ properties: {
14
+ is_new_task: { type: "boolean" },
15
+ difficulty: {
16
+ type: "string",
17
+ enum: ["trivial", "easy", "medium", "hard", "expert", "research"],
18
+ },
19
+ stakes: { type: "string", enum: ["low", "medium", "high", "critical"] },
20
+ confidence: UNIT,
21
+ ambiguity: UNIT,
22
+ tool_need: UNIT,
23
+ summary: { type: "string" },
24
+ },
25
+ required: [
26
+ "is_new_task",
27
+ "difficulty",
28
+ "stakes",
29
+ "confidence",
30
+ "ambiguity",
31
+ "tool_need",
32
+ "summary",
33
+ ],
34
+ };