@spendgraph/workflows 0.2.1 → 0.3.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.
- package/README.md +51 -0
- package/dist/noesis/execute/execute.js +2 -0
- package/dist/noesis/execute/index.d.ts +2 -0
- package/dist/noesis/execute/index.js +1 -0
- package/dist/noesis/execute/spend.d.ts +36 -0
- package/dist/noesis/execute/spend.js +79 -0
- package/dist/noesis/execute/types.d.ts +2 -0
- package/dist/noesis/index.d.ts +2 -2
- package/dist/noesis/index.js +1 -1
- package/dist/noesis/run/index.d.ts +2 -2
- package/dist/noesis/run/index.js +1 -1
- package/dist/noesis/run/ledger.d.ts +49 -1
- package/dist/noesis/run/ledger.js +19 -0
- package/dist/noesis/run/loop.js +4 -3
- package/dist/noesis/run/options.d.ts +11 -0
- package/dist/noesis/run/options.js +18 -0
- package/dist/noesis/run/plan.js +4 -3
- package/dist/noesis/run/produce.js +8 -3
- package/dist/noesis/run/result.js +1 -2
- package/dist/noesis/run/run.js +5 -0
- package/dist/noesis/run/stages/classify.js +2 -1
- package/dist/noesis/run/state.d.ts +5 -0
- package/dist/noesis/run/state.js +5 -0
- package/dist/noesis/run/verify.js +5 -4
- package/dist/noesis/types.d.ts +17 -5
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -70,6 +70,57 @@ answer. Stage 5 pairs 5A's claims back to 5B's blind answers, and it has to be
|
|
|
70
70
|
code: whatever does it sees both the claim and the answer written without it,
|
|
71
71
|
and a model given both is no longer a blind check of anything.
|
|
72
72
|
|
|
73
|
+
## Choosing models
|
|
74
|
+
|
|
75
|
+
`stage` sets the model every stage runs on. `stages` overrides it by name, for
|
|
76
|
+
the stages worth paying more or less for.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await run(client, llm, question, {
|
|
80
|
+
stage: { model: "claude-haiku-4-5" },
|
|
81
|
+
stages: {
|
|
82
|
+
plan_candidates: { model: "claude-sonnet-4-6" },
|
|
83
|
+
red_team: { model: "claude-opus-4-8" },
|
|
84
|
+
answer: { model: "claude-sonnet-4-6" },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The keys are the stage names from the table above with the spaces closed up —
|
|
90
|
+
`triage`, `question_brief`, `plan_candidates`, `red_team`, `answer` — and
|
|
91
|
+
`STAGE_KEYS` maps every id to its own. `triage` and `answer` also have
|
|
92
|
+
`opts.triage` and `opts.answer`, which
|
|
93
|
+
take more than a model — a current task, and the answer's escalating tiers —
|
|
94
|
+
and win where both are set.
|
|
95
|
+
|
|
96
|
+
**`params` does not follow a model it was not written for.** A stage naming its
|
|
97
|
+
own `model` gets its own `params` or none: Haiku takes `temperature`, Sonnet 5
|
|
98
|
+
rejects it outright, and one shared request body fails on whichever it was not
|
|
99
|
+
written for. A stage overriding only `params` still merges over `stage`.
|
|
100
|
+
|
|
101
|
+
## What a run cost
|
|
102
|
+
|
|
103
|
+
`usage` is the model spend: six numbers rather than two, summed across every
|
|
104
|
+
stage and every retry, split `byStage` and `byModel` because the answer can
|
|
105
|
+
cascade. A back edge refunds nothing.
|
|
106
|
+
|
|
107
|
+
`usage.tools` is what the tools reported, and it is deliberately **not** folded
|
|
108
|
+
in — a tool call is somebody else's API at somebody else's rates and is no
|
|
109
|
+
rollout of ours.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
result.usage.inputTokens; // the model side
|
|
113
|
+
result.usage.byModel["claude-sonnet-4-6"];
|
|
114
|
+
|
|
115
|
+
result.usage.tools.calls; // the tool side
|
|
116
|
+
result.usage.tools.costMicros; // where a tool priced itself
|
|
117
|
+
result.usage.tools.byTool.web_search; // tokens, keyed by the tool that ran
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Bill both or you undercharge every run that touched the web: `web_search`
|
|
121
|
+
reports Perplexity's four token counts and `deep_recall` reports micro-USD,
|
|
122
|
+
and stage 4 records zero tokens of its own.
|
|
123
|
+
|
|
73
124
|
## The ledger
|
|
74
125
|
|
|
75
126
|
Every stage files its result, its rollouts and its tokens in one place. That
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { order } from "./order.js";
|
|
2
|
+
import { toolSpendOf } from "./spend.js";
|
|
2
3
|
export const DEFAULT_TOOLS = {
|
|
3
4
|
calculator: { name: "calculate", arg: "expression" },
|
|
4
5
|
deeprecall: { name: "deep_recall", arg: "question" },
|
|
@@ -63,6 +64,7 @@ export async function execute(bus, plan, opts = {}) {
|
|
|
63
64
|
return {
|
|
64
65
|
answers,
|
|
65
66
|
unresolved,
|
|
67
|
+
spend: toolSpendOf(answers, wiring),
|
|
66
68
|
toolMs: answers.reduce((total, answer) => total + answer.latencyMs, 0),
|
|
67
69
|
};
|
|
68
70
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { DEFAULT_TOOLS, execute } from "./execute.js";
|
|
2
2
|
export { order } from "./order.js";
|
|
3
3
|
export type { ExecuteOptions, Execution, SubAnswer, Waves, Wiring } from "./types.js";
|
|
4
|
+
export type { ToolSpend, ToolUsage } from "./spend.js";
|
|
5
|
+
export { addToolSpend, NO_TOOL_SPEND, readToolUsage, toolSpendOf } from "./spend.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SubAnswer, Wiring } from "./types.js";
|
|
2
|
+
import type { SubQuestionTool } from "../producers/decompose/index.js";
|
|
3
|
+
/** What one tool reported it cost, keyed by the tool that reported it. */
|
|
4
|
+
export interface ToolSpend {
|
|
5
|
+
calls: number;
|
|
6
|
+
/** Summed where a tool priced itself. Undefined where none did. */
|
|
7
|
+
costMicros?: number;
|
|
8
|
+
byTool: Record<string, ToolUsage>;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolUsage {
|
|
11
|
+
calls: number;
|
|
12
|
+
model?: string;
|
|
13
|
+
inputTokens: number;
|
|
14
|
+
outputTokens: number;
|
|
15
|
+
citationTokens: number;
|
|
16
|
+
reasoningTokens: number;
|
|
17
|
+
costMicros?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const NO_TOOL_SPEND: ToolSpend;
|
|
20
|
+
/**
|
|
21
|
+
* What a tool result says it cost, read by duck typing.
|
|
22
|
+
*
|
|
23
|
+
* A tool is somebody else's API and prices itself however it likes: `web_search`
|
|
24
|
+
* reports Perplexity's four token counts, `deep_recall` reports micro-USD. Both
|
|
25
|
+
* are read where present and nothing is invented where absent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readToolUsage(result: unknown): Omit<ToolUsage, "calls"> | null;
|
|
28
|
+
/**
|
|
29
|
+
* Every tool call in one execution, summed under the tool that actually ran.
|
|
30
|
+
*
|
|
31
|
+
* Keyed by the bus tool's name rather than the sub-question's, because that is
|
|
32
|
+
* the one a rate applies to and the one the bus offered.
|
|
33
|
+
*/
|
|
34
|
+
export declare function toolSpendOf(answers: SubAnswer[], wiring?: Partial<Record<SubQuestionTool, Wiring>>): ToolSpend;
|
|
35
|
+
/** Two executions' tool spend, added. A retry does not refund the first attempt. */
|
|
36
|
+
export declare function addToolSpend(a: ToolSpend, b: ToolSpend): ToolSpend;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const NO_TOOL_SPEND = { calls: 0, byTool: {} };
|
|
2
|
+
const numberAt = (source, field) => {
|
|
3
|
+
const value = source[field];
|
|
4
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
5
|
+
};
|
|
6
|
+
export function readToolUsage(result) {
|
|
7
|
+
if (!result || typeof result !== "object")
|
|
8
|
+
return null;
|
|
9
|
+
const shape = result;
|
|
10
|
+
const usage = (shape.usage ?? {});
|
|
11
|
+
const tokens = {
|
|
12
|
+
inputTokens: numberAt(usage, "inputTokens"),
|
|
13
|
+
outputTokens: numberAt(usage, "outputTokens"),
|
|
14
|
+
citationTokens: numberAt(usage, "citationTokens"),
|
|
15
|
+
reasoningTokens: numberAt(usage, "reasoningTokens"),
|
|
16
|
+
};
|
|
17
|
+
const priced = typeof shape.costMicroUsd === "number" ? shape.costMicroUsd : undefined;
|
|
18
|
+
const model = typeof shape.model === "string" ? shape.model : undefined;
|
|
19
|
+
const spent = tokens.inputTokens + tokens.outputTokens + tokens.citationTokens + tokens.reasoningTokens;
|
|
20
|
+
if (spent === 0 && priced === undefined)
|
|
21
|
+
return null;
|
|
22
|
+
return { ...tokens, ...(model ? { model } : {}), ...(priced !== undefined ? { costMicros: priced } : {}) };
|
|
23
|
+
}
|
|
24
|
+
export function toolSpendOf(answers, wiring = {}) {
|
|
25
|
+
const byTool = {};
|
|
26
|
+
let calls = 0;
|
|
27
|
+
let costMicros;
|
|
28
|
+
for (const answer of answers) {
|
|
29
|
+
const one = readToolUsage(answer.result);
|
|
30
|
+
if (!one)
|
|
31
|
+
continue;
|
|
32
|
+
calls++;
|
|
33
|
+
const named = wiring[answer.tool]?.name ?? answer.tool;
|
|
34
|
+
const prior = byTool[named] ?? {
|
|
35
|
+
calls: 0,
|
|
36
|
+
inputTokens: 0,
|
|
37
|
+
outputTokens: 0,
|
|
38
|
+
citationTokens: 0,
|
|
39
|
+
reasoningTokens: 0,
|
|
40
|
+
};
|
|
41
|
+
const merged = {
|
|
42
|
+
calls: prior.calls + 1,
|
|
43
|
+
inputTokens: prior.inputTokens + one.inputTokens,
|
|
44
|
+
outputTokens: prior.outputTokens + one.outputTokens,
|
|
45
|
+
citationTokens: prior.citationTokens + one.citationTokens,
|
|
46
|
+
reasoningTokens: prior.reasoningTokens + one.reasoningTokens,
|
|
47
|
+
...(one.model ?? prior.model ? { model: one.model ?? prior.model } : {}),
|
|
48
|
+
};
|
|
49
|
+
if (one.costMicros !== undefined || prior.costMicros !== undefined) {
|
|
50
|
+
merged.costMicros = (prior.costMicros ?? 0) + (one.costMicros ?? 0);
|
|
51
|
+
costMicros = (costMicros ?? 0) + (one.costMicros ?? 0);
|
|
52
|
+
}
|
|
53
|
+
byTool[named] = merged;
|
|
54
|
+
}
|
|
55
|
+
return { calls, byTool, ...(costMicros !== undefined ? { costMicros } : {}) };
|
|
56
|
+
}
|
|
57
|
+
export function addToolSpend(a, b) {
|
|
58
|
+
const byTool = { ...a.byTool };
|
|
59
|
+
for (const [tool, one] of Object.entries(b.byTool)) {
|
|
60
|
+
const prior = byTool[tool];
|
|
61
|
+
byTool[tool] = prior
|
|
62
|
+
? {
|
|
63
|
+
calls: prior.calls + one.calls,
|
|
64
|
+
inputTokens: prior.inputTokens + one.inputTokens,
|
|
65
|
+
outputTokens: prior.outputTokens + one.outputTokens,
|
|
66
|
+
citationTokens: prior.citationTokens + one.citationTokens,
|
|
67
|
+
reasoningTokens: prior.reasoningTokens + one.reasoningTokens,
|
|
68
|
+
...(one.model ?? prior.model ? { model: one.model ?? prior.model } : {}),
|
|
69
|
+
...(prior.costMicros !== undefined || one.costMicros !== undefined
|
|
70
|
+
? { costMicros: (prior.costMicros ?? 0) + (one.costMicros ?? 0) }
|
|
71
|
+
: {}),
|
|
72
|
+
}
|
|
73
|
+
: one;
|
|
74
|
+
}
|
|
75
|
+
const costMicros = a.costMicros !== undefined || b.costMicros !== undefined
|
|
76
|
+
? (a.costMicros ?? 0) + (b.costMicros ?? 0)
|
|
77
|
+
: undefined;
|
|
78
|
+
return { calls: a.calls + b.calls, byTool, ...(costMicros !== undefined ? { costMicros } : {}) };
|
|
79
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolSpend } from "./spend.js";
|
|
1
2
|
import type { Emit } from "../events.js";
|
|
2
3
|
import type { SubQuestion, SubQuestionTool } from "../producers/decompose/index.js";
|
|
3
4
|
/** Which tool answers a sub-question, and the argument its question fills. */
|
|
@@ -27,6 +28,7 @@ export interface Execution {
|
|
|
27
28
|
answers: SubAnswer[];
|
|
28
29
|
/** Ids that could never run: a cycle, or a dependency nothing created. */
|
|
29
30
|
unresolved: string[];
|
|
31
|
+
spend: ToolSpend;
|
|
30
32
|
/** Time spent inside tools. Not wall clock — a wave runs together. */
|
|
31
33
|
toolMs: number;
|
|
32
34
|
}
|
package/dist/noesis/index.d.ts
CHANGED
|
@@ -38,8 +38,8 @@ export { invoke as reflection } from "./repair/reflection/index.js";
|
|
|
38
38
|
export { routeFor } from "./route.js";
|
|
39
39
|
export type { Difficulty, Stakes, TriageEstimates, TriageInput, TriageOptions, TriageResult, } from "./router/triage/index.js";
|
|
40
40
|
export { invoke as triage, TriageReplyError } from "./router/triage/index.js";
|
|
41
|
-
export type { Entry, Escalation, Ledger, NoesisStream, Recorded, StageId, StageUsage, } from "./run/index.js";
|
|
42
|
-
export { NoToolsError, newLedger, RouteFailedError, RouteNotBuiltError, run, runStream, STAGE_NAMES, STAGES, } from "./run/index.js";
|
|
41
|
+
export type { Entry, Escalation, Ledger, NoesisStream, Recorded, StageId, StageKey, StageName, StageUsage, } from "./run/index.js";
|
|
42
|
+
export { NoToolsError, newLedger, RouteFailedError, RouteNotBuiltError, run, runStream, STAGE_KEYS, STAGE_NAMES, STAGES, } from "./run/index.js";
|
|
43
43
|
export { dropPoint, parseStage, resumeAt } from "./run/redo.js";
|
|
44
44
|
export { STAGE_OF_SLUG } from "./slugs.js";
|
|
45
45
|
export type { StageOptions, StageOutcome } from "./stage.js";
|
package/dist/noesis/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export { answer, invoke as toulmin, NoAnswerError, render, ToulminReplyError, }
|
|
|
19
19
|
export { invoke as reflection } from "./repair/reflection/index.js";
|
|
20
20
|
export { routeFor } from "./route.js";
|
|
21
21
|
export { invoke as triage, TriageReplyError } from "./router/triage/index.js";
|
|
22
|
-
export { NoToolsError, newLedger, RouteFailedError, RouteNotBuiltError, run, runStream, STAGE_NAMES, STAGES, } from "./run/index.js";
|
|
22
|
+
export { NoToolsError, newLedger, RouteFailedError, RouteNotBuiltError, run, runStream, STAGE_KEYS, STAGE_NAMES, STAGES, } from "./run/index.js";
|
|
23
23
|
export { dropPoint, parseStage, resumeAt } from "./run/redo.js";
|
|
24
24
|
export { STAGE_OF_SLUG } from "./slugs.js";
|
|
25
25
|
export { runStage, StageReplyError } from "./stage.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { NoToolsError, RouteFailedError, RouteNotBuiltError } from "./errors.js";
|
|
2
|
-
export type { Entry, Escalation, Ledger, Recorded, StageId, StageUsage } from "./ledger.js";
|
|
3
|
-
export { newLedger, STAGE_NAMES, STAGES } from "./ledger.js";
|
|
2
|
+
export type { Entry, Escalation, Ledger, Recorded, StageId, StageKey, StageName, StageUsage } from "./ledger.js";
|
|
3
|
+
export { newLedger, STAGE_KEYS, STAGE_NAMES, STAGES } from "./ledger.js";
|
|
4
4
|
export { run } from "./run.js";
|
|
5
5
|
export type { NoesisStream } from "./stream.js";
|
|
6
6
|
export { runStream } from "./stream.js";
|
package/dist/noesis/run/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { NoToolsError, RouteFailedError, RouteNotBuiltError } from "./errors.js";
|
|
2
|
-
export { newLedger, STAGE_NAMES, STAGES } from "./ledger.js";
|
|
2
|
+
export { newLedger, STAGE_KEYS, STAGE_NAMES, STAGES } from "./ledger.js";
|
|
3
3
|
export { run } from "./run.js";
|
|
4
4
|
export { runStream } from "./stream.js";
|
|
@@ -4,7 +4,55 @@ import { type PartialUsage, type Usage } from "../usage.js";
|
|
|
4
4
|
export declare const STAGES: readonly ["0", "1", "2", "3A", "3B", "3C", "4", "4A", "4B", "5A", "5B", "5", "5C", "6A", "6B", "6C", "R"];
|
|
5
5
|
export type StageId = (typeof STAGES)[number];
|
|
6
6
|
/** What a stage is called where a person or a model has to read it. */
|
|
7
|
-
export declare const STAGE_NAMES:
|
|
7
|
+
export declare const STAGE_NAMES: {
|
|
8
|
+
readonly "0": "triage";
|
|
9
|
+
readonly "1": "question brief";
|
|
10
|
+
readonly "2": "decomposition";
|
|
11
|
+
readonly "3A": "plan candidates";
|
|
12
|
+
readonly "3B": "premortem";
|
|
13
|
+
readonly "3C": "cqot gate";
|
|
14
|
+
readonly "4": "execution";
|
|
15
|
+
readonly "4A": "think checkpoint";
|
|
16
|
+
readonly "4B": "discriminating test";
|
|
17
|
+
readonly "5A": "verification questions";
|
|
18
|
+
readonly "5B": "verification answers";
|
|
19
|
+
readonly "5": "verification";
|
|
20
|
+
readonly "5C": "standards review";
|
|
21
|
+
readonly "6A": "closure checklist";
|
|
22
|
+
readonly "6B": "red team";
|
|
23
|
+
readonly "6C": "answer";
|
|
24
|
+
readonly R: "root cause";
|
|
25
|
+
};
|
|
26
|
+
/** A stage by the name it announces itself under. */
|
|
27
|
+
export type StageName = (typeof STAGE_NAMES)[StageId];
|
|
28
|
+
/**
|
|
29
|
+
* What a stage is called where it is a key rather than prose.
|
|
30
|
+
*
|
|
31
|
+
* `STAGE_NAMES` with the spaces closed up, kept apart from it because those
|
|
32
|
+
* names are read back out of the artifacts by the answer and cannot be renamed
|
|
33
|
+
* for a config's convenience.
|
|
34
|
+
*/
|
|
35
|
+
export declare const STAGE_KEYS: {
|
|
36
|
+
readonly "0": "triage";
|
|
37
|
+
readonly "1": "question_brief";
|
|
38
|
+
readonly "2": "decomposition";
|
|
39
|
+
readonly "3A": "plan_candidates";
|
|
40
|
+
readonly "3B": "premortem";
|
|
41
|
+
readonly "3C": "cqot_gate";
|
|
42
|
+
readonly "4": "execution";
|
|
43
|
+
readonly "4A": "think_checkpoint";
|
|
44
|
+
readonly "4B": "discriminating_test";
|
|
45
|
+
readonly "5A": "verification_questions";
|
|
46
|
+
readonly "5B": "verification_answers";
|
|
47
|
+
readonly "5": "verification";
|
|
48
|
+
readonly "5C": "standards_review";
|
|
49
|
+
readonly "6A": "closure_checklist";
|
|
50
|
+
readonly "6B": "red_team";
|
|
51
|
+
readonly "6C": "answer";
|
|
52
|
+
readonly R: "root_cause";
|
|
53
|
+
};
|
|
54
|
+
/** A stage by the key a config names it under. */
|
|
55
|
+
export type StageKey = (typeof STAGE_KEYS)[StageId];
|
|
8
56
|
/** What one stage left behind, and what it cost. */
|
|
9
57
|
export interface Entry extends Usage {
|
|
10
58
|
stage: StageId;
|
|
@@ -38,6 +38,25 @@ export const STAGE_NAMES = {
|
|
|
38
38
|
"6C": "answer",
|
|
39
39
|
R: "root cause",
|
|
40
40
|
};
|
|
41
|
+
export const STAGE_KEYS = {
|
|
42
|
+
"0": "triage",
|
|
43
|
+
"1": "question_brief",
|
|
44
|
+
"2": "decomposition",
|
|
45
|
+
"3A": "plan_candidates",
|
|
46
|
+
"3B": "premortem",
|
|
47
|
+
"3C": "cqot_gate",
|
|
48
|
+
"4": "execution",
|
|
49
|
+
"4A": "think_checkpoint",
|
|
50
|
+
"4B": "discriminating_test",
|
|
51
|
+
"5A": "verification_questions",
|
|
52
|
+
"5B": "verification_answers",
|
|
53
|
+
"5": "verification",
|
|
54
|
+
"5C": "standards_review",
|
|
55
|
+
"6A": "closure_checklist",
|
|
56
|
+
"6B": "red_team",
|
|
57
|
+
"6C": "answer",
|
|
58
|
+
R: "root_cause",
|
|
59
|
+
};
|
|
41
60
|
const DEFAULT_MAX_RETRIES = 5;
|
|
42
61
|
const rolloutsOf = (result) => "rolloutIds" in result ? result.rolloutIds : [result.rolloutId];
|
|
43
62
|
export function newLedger(maxRetries = DEFAULT_MAX_RETRIES, emit) {
|
package/dist/noesis/run/loop.js
CHANGED
|
@@ -4,6 +4,7 @@ import { invoke as standardsReview } from "../gates/standards-review/index.js";
|
|
|
4
4
|
import { STAGE_NAMES } from "./ledger.js";
|
|
5
5
|
import { dropPoint, resumeAt } from "./redo.js";
|
|
6
6
|
import { checks, reflect } from "./verify.js";
|
|
7
|
+
import { stageOptionsFor } from "./options.js";
|
|
7
8
|
async function stepBack(client, llm, question, opts, state, gates, gate, reason, hint) {
|
|
8
9
|
if (!state.ledger.retry(gate)) {
|
|
9
10
|
state.ledger.escalate(gate, `Out of retries. ${gate} last said: ${reason}`);
|
|
@@ -16,7 +17,7 @@ async function stepBack(client, llm, question, opts, state, gates, gate, reason,
|
|
|
16
17
|
}
|
|
17
18
|
async function judge(client, llm, opts, state, checked, gates) {
|
|
18
19
|
if (gates.standards) {
|
|
19
|
-
const review = state.ledger.record("5C", await standardsReview(client, llm, { question: checked, ...opts.context }, opts
|
|
20
|
+
const review = state.ledger.record("5C", await standardsReview(client, llm, { question: checked, ...opts.context }, stageOptionsFor(opts, "5C")));
|
|
20
21
|
if (review.overall !== "accept") {
|
|
21
22
|
return {
|
|
22
23
|
stage: "5C",
|
|
@@ -25,7 +26,7 @@ async function judge(client, llm, opts, state, checked, gates) {
|
|
|
25
26
|
}
|
|
26
27
|
state.emit?.({ type: "gate.passed", stage: "5C", name: STAGE_NAMES["5C"] });
|
|
27
28
|
}
|
|
28
|
-
const closure = state.ledger.record("6A", await closureChecklist(client, llm, { question: checked, ...opts.context }, opts
|
|
29
|
+
const closure = state.ledger.record("6A", await closureChecklist(client, llm, { question: checked, ...opts.context }, stageOptionsFor(opts, "6A")));
|
|
29
30
|
if (closure.gate !== "close") {
|
|
30
31
|
return {
|
|
31
32
|
stage: "6A",
|
|
@@ -36,7 +37,7 @@ async function judge(client, llm, opts, state, checked, gates) {
|
|
|
36
37
|
}
|
|
37
38
|
state.emit?.({ type: "gate.passed", stage: "6A", name: STAGE_NAMES["6A"] });
|
|
38
39
|
if (gates.redTeam) {
|
|
39
|
-
const attacked = state.ledger.record("6B", await redTeam(client, llm, { question: checked, ...opts.context }, opts
|
|
40
|
+
const attacked = state.ledger.record("6B", await redTeam(client, llm, { question: checked, ...opts.context }, stageOptionsFor(opts, "6B")));
|
|
40
41
|
if (attacked.verdict !== "survives") {
|
|
41
42
|
return { stage: "6B", reason: `${attacked.verdict}: ${attacked.would_break_it}` };
|
|
42
43
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnswerOptions } from "../producers/toulmin/index.js";
|
|
2
|
+
import type { StageOptions } from "../stage.js";
|
|
3
|
+
import type { NoesisOptions } from "../types.js";
|
|
4
|
+
import { type StageId } from "./ledger.js";
|
|
5
|
+
/**
|
|
6
|
+
* What one stage runs on: `stages[id]` over `stage`, with `params` dropped
|
|
7
|
+
* rather than inherited when the stage names a model of its own.
|
|
8
|
+
*/
|
|
9
|
+
export declare function stageOptionsFor(opts: NoesisOptions, id: StageId): StageOptions;
|
|
10
|
+
/** The answer's tiers, defaulting to whatever `stages.answer` named. */
|
|
11
|
+
export declare function answerOptions(opts: NoesisOptions): AnswerOptions;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { STAGE_KEYS } from "./ledger.js";
|
|
2
|
+
export function stageOptionsFor(opts, id) {
|
|
3
|
+
const base = opts.stage ?? {};
|
|
4
|
+
const per = opts.stages?.[STAGE_KEYS[id]];
|
|
5
|
+
if (!per)
|
|
6
|
+
return base;
|
|
7
|
+
const ownModel = per.model !== undefined && per.model !== base.model;
|
|
8
|
+
const params = ownModel ? per.params : { ...base.params, ...per.params };
|
|
9
|
+
const rest = Object.fromEntries(Object.entries({ ...base, ...per }).filter(([field]) => field !== "params"));
|
|
10
|
+
return params && Object.keys(params).length > 0 ? { ...rest, params } : rest;
|
|
11
|
+
}
|
|
12
|
+
export function answerOptions(opts) {
|
|
13
|
+
const base = opts.answer ?? {};
|
|
14
|
+
const sixC = opts.stages?.[STAGE_KEYS["6C"]];
|
|
15
|
+
if (base.tiers || !sixC?.model)
|
|
16
|
+
return base;
|
|
17
|
+
return { ...base, tiers: [{ model: sixC.model, ...(sixC.params ? { params: sixC.params } : {}) }] };
|
|
18
|
+
}
|
package/dist/noesis/run/plan.js
CHANGED
|
@@ -2,20 +2,21 @@ import { invoke as cqotGate } from "../gates/cqot-gate/index.js";
|
|
|
2
2
|
import { invoke as premortem } from "../gates/premortem/index.js";
|
|
3
3
|
import { invoke as planCandidates } from "../producers/plan-candidates/index.js";
|
|
4
4
|
import { soFar } from "./produce.js";
|
|
5
|
+
import { stageOptionsFor } from "./options.js";
|
|
5
6
|
export async function plan(client, llm, question, opts, state, audits) {
|
|
6
7
|
for (;;) {
|
|
7
8
|
state.ledger.dropFrom("3A");
|
|
8
|
-
state.ledger.record("3A", await planCandidates(client, llm, { question: soFar(state, question), ...opts.context }, opts
|
|
9
|
+
state.ledger.record("3A", await planCandidates(client, llm, { question: soFar(state, question), ...opts.context }, stageOptionsFor(opts, "3A")));
|
|
9
10
|
if (!audits)
|
|
10
11
|
return;
|
|
11
|
-
const dead = state.ledger.record("3B", await premortem(client, llm, { question: soFar(state, question), ...opts.context }, opts
|
|
12
|
+
const dead = state.ledger.record("3B", await premortem(client, llm, { question: soFar(state, question), ...opts.context }, stageOptionsFor(opts, "3B")));
|
|
12
13
|
if (dead.fatal || dead.verdict === "replan") {
|
|
13
14
|
if (state.ledger.retry("3B"))
|
|
14
15
|
continue;
|
|
15
16
|
state.ledger.escalate("3B", "Out of retries: the premortem keeps killing the plan.");
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
|
-
const gate = state.ledger.record("3C", await cqotGate(client, llm, { question: soFar(state, question), ...opts.context }, opts
|
|
19
|
+
const gate = state.ledger.record("3C", await cqotGate(client, llm, { question: soFar(state, question), ...opts.context }, stageOptionsFor(opts, "3C")));
|
|
19
20
|
if (gate.gate === "pass")
|
|
20
21
|
return;
|
|
21
22
|
if (state.ledger.retry("3C"))
|
|
@@ -6,9 +6,12 @@ import { invoke as brief } from "../producers/qn-brief/index.js";
|
|
|
6
6
|
import { digest } from "./artifacts.js";
|
|
7
7
|
import { NoToolsError } from "./errors.js";
|
|
8
8
|
import { plan } from "./plan.js";
|
|
9
|
+
import { stageOptionsFor } from "./options.js";
|
|
10
|
+
import { NO_TOOL_SPEND } from "../execute/spend.js";
|
|
11
|
+
import { recordToolSpend } from "./state.js";
|
|
9
12
|
export const soFar = (state, question) => JSON.stringify({ question, ...digest(state.ledger.artifacts()) }, null, 2);
|
|
10
13
|
export async function frame(client, llm, question, opts, state) {
|
|
11
|
-
const brief1 = state.ledger.record("1", await brief(client, llm, { question, ...opts.context }, opts
|
|
14
|
+
const brief1 = state.ledger.record("1", await brief(client, llm, { question, ...opts.context }, stageOptionsFor(opts, "1")));
|
|
12
15
|
let asked = question;
|
|
13
16
|
if (!brief1.is_right_question && brief1.better_question && opts.onAsk) {
|
|
14
17
|
state.emit?.({
|
|
@@ -25,13 +28,14 @@ export async function frame(client, llm, question, opts, state) {
|
|
|
25
28
|
state.emit?.({ type: "ask.answered", replacement: replacement ?? null });
|
|
26
29
|
asked = replacement ?? question;
|
|
27
30
|
}
|
|
28
|
-
const split = state.ledger.record("2", await decompose(client, llm, { question: asked, ...opts.context }, opts
|
|
31
|
+
const split = state.ledger.record("2", await decompose(client, llm, { question: asked, ...opts.context }, stageOptionsFor(opts, "2")));
|
|
29
32
|
return { asked, split };
|
|
30
33
|
}
|
|
31
34
|
export async function work(client, llm, question, opts, state, split) {
|
|
32
35
|
if (!opts.tools)
|
|
33
36
|
throw new NoToolsError();
|
|
34
37
|
const execution = await execute(opts.tools, split.sub_questions, { emit: state.emit });
|
|
38
|
+
recordToolSpend(state, execution.spend);
|
|
35
39
|
state.ledger.record("4", {
|
|
36
40
|
data: execution,
|
|
37
41
|
rolloutIds: [],
|
|
@@ -46,7 +50,7 @@ export async function work(client, llm, question, opts, state, split) {
|
|
|
46
50
|
findings: draft.findings.length,
|
|
47
51
|
missing: draft.missing.map((one) => ({ id: one.id, reason: one.reason })),
|
|
48
52
|
});
|
|
49
|
-
const checkpoint = state.ledger.record("4A", await thinkCheckpoint(client, llm, { question: soFar(state, question), ...opts.context }, opts
|
|
53
|
+
const checkpoint = state.ledger.record("4A", await thinkCheckpoint(client, llm, { question: soFar(state, question), ...opts.context }, stageOptionsFor(opts, "4A")));
|
|
50
54
|
if (checkpoint.next_action === "replan")
|
|
51
55
|
return { draft, action: "replan" };
|
|
52
56
|
if (checkpoint.next_action === "escalate")
|
|
@@ -61,6 +65,7 @@ export async function produce(client, llm, question, opts, state, audits) {
|
|
|
61
65
|
return assemble(asked, split, {
|
|
62
66
|
answers: [],
|
|
63
67
|
unresolved: split.sub_questions.map((sub) => sub.id),
|
|
68
|
+
spend: NO_TOOL_SPEND,
|
|
64
69
|
toolMs: 0,
|
|
65
70
|
});
|
|
66
71
|
}
|
|
@@ -11,6 +11,7 @@ function usageOfRun(state) {
|
|
|
11
11
|
...state.ledger.usage(),
|
|
12
12
|
byStage,
|
|
13
13
|
byModel: state.ledger.byModel(),
|
|
14
|
+
tools: state.toolSpend,
|
|
14
15
|
calls: byStage.reduce((total, stage) => total + stage.calls, 0),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
@@ -36,8 +37,6 @@ export function finish(decided, state, latencyMs) {
|
|
|
36
37
|
rolloutIds: ledger.rolloutIds,
|
|
37
38
|
unattributed: state.unattributed ?? [],
|
|
38
39
|
usage,
|
|
39
|
-
inputTokens: usage.inputTokens,
|
|
40
|
-
outputTokens: usage.outputTokens,
|
|
41
40
|
latencyMs,
|
|
42
41
|
};
|
|
43
42
|
}
|
package/dist/noesis/run/run.js
CHANGED
|
@@ -10,6 +10,11 @@ const watched = (opts, emit) => ({
|
|
|
10
10
|
triage: { ...opts.triage, emit },
|
|
11
11
|
stage: { ...opts.stage, emit },
|
|
12
12
|
answer: { ...opts.answer, emit },
|
|
13
|
+
...(opts.stages
|
|
14
|
+
? {
|
|
15
|
+
stages: Object.fromEntries(Object.entries(opts.stages).map(([id, one]) => [id, { ...one, emit }])),
|
|
16
|
+
}
|
|
17
|
+
: {}),
|
|
13
18
|
});
|
|
14
19
|
export async function run(client, llm, question, options = {}) {
|
|
15
20
|
const startedAt = Date.now();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { routeFor } from "../../route.js";
|
|
2
2
|
import { invoke as triage } from "../../router/triage/index.js";
|
|
3
3
|
import { nextTask } from "../state.js";
|
|
4
|
+
import { stageOptionsFor } from "../options.js";
|
|
4
5
|
export const classifyStage = (client, llm, question, opts, state) => async () => {
|
|
5
|
-
const stage = await triage(client, llm, { ...opts.triage, currentTask: state.currentTask, question }, { ...opts.triage });
|
|
6
|
+
const stage = await triage(client, llm, { ...opts.triage, currentTask: state.currentTask, question }, { ...stageOptionsFor(opts, "0"), ...opts.triage });
|
|
6
7
|
state.estimates = state.ledger.record("0", { ...stage, data: stage.estimates });
|
|
7
8
|
state.routing = routeFor(stage.estimates);
|
|
8
9
|
state.currentTask = nextTask(stage.estimates, state.currentTask);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ToolSpend } from "../execute/spend.js";
|
|
1
2
|
import type { Emit } from "../events.js";
|
|
2
3
|
import type { AnswerResult } from "../producers/toulmin/index.js";
|
|
3
4
|
import type { TriageEstimates } from "../router/triage/index.js";
|
|
@@ -5,6 +6,8 @@ import type { Routing } from "../types.js";
|
|
|
5
6
|
import { type Ledger } from "./ledger.js";
|
|
6
7
|
/** What each branch writes down, since `route()` hands back only its output. */
|
|
7
8
|
export interface RunState {
|
|
9
|
+
/** What the tools reported they cost, which no stage's tokens include. */
|
|
10
|
+
toolSpend: ToolSpend;
|
|
8
11
|
/** Every stage's result and what it cost, in one place. */
|
|
9
12
|
ledger: Ledger;
|
|
10
13
|
/** Where the run layer says what it is doing. Undefined when nobody listens. */
|
|
@@ -19,6 +22,8 @@ export interface RunState {
|
|
|
19
22
|
unattributed?: string[];
|
|
20
23
|
}
|
|
21
24
|
export declare const newRunState: (currentTask?: string, emit?: Emit, maxRetries?: number) => RunState;
|
|
25
|
+
/** Adds one execution's tool spend to the run's. A retry refunds nothing. */
|
|
26
|
+
export declare function recordToolSpend(state: RunState, spend: ToolSpend): void;
|
|
22
27
|
/**
|
|
23
28
|
* The open task after triage has read the request: the router's summary when it
|
|
24
29
|
* calls the request new, otherwise the task carried in.
|
package/dist/noesis/run/state.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { addToolSpend, NO_TOOL_SPEND } from "../execute/spend.js";
|
|
1
2
|
import { newLedger } from "./ledger.js";
|
|
2
3
|
export const newRunState = (currentTask, emit, maxRetries) => ({
|
|
4
|
+
toolSpend: NO_TOOL_SPEND,
|
|
3
5
|
ledger: newLedger(maxRetries, emit),
|
|
4
6
|
emit,
|
|
5
7
|
currentTask,
|
|
6
8
|
});
|
|
9
|
+
export function recordToolSpend(state, spend) {
|
|
10
|
+
state.toolSpend = addToolSpend(state.toolSpend, spend);
|
|
11
|
+
}
|
|
7
12
|
export function nextTask(estimates, current) {
|
|
8
13
|
return estimates.is_new_task ? estimates.summary : (current ?? estimates.summary);
|
|
9
14
|
}
|
|
@@ -7,11 +7,12 @@ import { invoke as reflection } from "../repair/reflection/index.js";
|
|
|
7
7
|
import { blind, compare, render as renderChecks } from "../verify/index.js";
|
|
8
8
|
import { soFar } from "./produce.js";
|
|
9
9
|
import { parseStage, resumeAt } from "./redo.js";
|
|
10
|
+
import { answerOptions, stageOptionsFor } from "./options.js";
|
|
10
11
|
export async function checks(client, llm, opts, state, draft) {
|
|
11
12
|
const drafted = renderDraft(draft);
|
|
12
|
-
const questions = state.ledger.record("5A", await coveQuestions(client, llm, { question: drafted, ...opts.context }, opts
|
|
13
|
+
const questions = state.ledger.record("5A", await coveQuestions(client, llm, { question: drafted, ...opts.context }, stageOptionsFor(opts, "5A")));
|
|
13
14
|
const answers = questions.questions.length
|
|
14
|
-
? state.ledger.record("5B", await coveAnswers(client, llm, { question: blind(questions), ...opts.context }, opts
|
|
15
|
+
? state.ledger.record("5B", await coveAnswers(client, llm, { question: blind(questions), ...opts.context }, stageOptionsFor(opts, "5B")))
|
|
15
16
|
: { answers: [] };
|
|
16
17
|
const verification = compare(questions, answers, opts.verify?.confidenceFloor);
|
|
17
18
|
state.emit?.({
|
|
@@ -34,7 +35,7 @@ export async function checks(client, llm, opts, state, draft) {
|
|
|
34
35
|
return `${drafted}\n\n${renderChecks(verification)}`;
|
|
35
36
|
}
|
|
36
37
|
export async function reflect(client, llm, question, opts, state, gate, reason, hint) {
|
|
37
|
-
const looked = state.ledger.record("R", await reflection(client, llm, { question: `${soFar(state, question)}\n\nThe gate said: ${reason}`, ...opts.context }, opts
|
|
38
|
+
const looked = state.ledger.record("R", await reflection(client, llm, { question: `${soFar(state, question)}\n\nThe gate said: ${reason}`, ...opts.context }, stageOptionsFor(opts, "R")));
|
|
38
39
|
const redoFrom = parseStage(looked.redo_from_stage) ?? parseStage(hint ?? "");
|
|
39
40
|
state.emit?.({
|
|
40
41
|
type: "reflect.pointed",
|
|
@@ -46,7 +47,7 @@ export async function reflect(client, llm, question, opts, state, gate, reason,
|
|
|
46
47
|
return redoFrom;
|
|
47
48
|
}
|
|
48
49
|
export async function compile(client, llm, question, opts, state) {
|
|
49
|
-
const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, opts
|
|
50
|
+
const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, answerOptions(opts));
|
|
50
51
|
state.answered = answered;
|
|
51
52
|
const loose = unattributed(answered.answer, state.ledger.byStage().map((one) => one.stage));
|
|
52
53
|
state.unattributed = loose;
|
package/dist/noesis/types.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ToolBus } from "@spendgraph/tools";
|
|
2
|
+
import type { ToolSpend } from "./execute/spend.js";
|
|
2
3
|
import type { Asked } from "./context.js";
|
|
3
4
|
import type { NoesisEvent } from "./events.js";
|
|
4
5
|
import type { SubQuestionTool } from "./producers/decompose/index.js";
|
|
5
6
|
import type { AnswerOptions, AnswerResult } from "./producers/toulmin/index.js";
|
|
6
7
|
import type { TriageEstimates, TriageInput, TriageOptions } from "./router/triage/index.js";
|
|
7
|
-
import type { Escalation, StageUsage } from "./run/ledger.js";
|
|
8
|
+
import type { Escalation, StageKey, StageUsage } from "./run/ledger.js";
|
|
8
9
|
import type { StageOptions } from "./stage.js";
|
|
9
10
|
import type { Usage } from "./usage.js";
|
|
10
11
|
/**
|
|
@@ -78,6 +79,13 @@ export interface NoesisOptions {
|
|
|
78
79
|
context?: Omit<Asked, "question">;
|
|
79
80
|
/** Model and params for every stage between triage and the answer. */
|
|
80
81
|
stage?: StageOptions;
|
|
82
|
+
/**
|
|
83
|
+
* Per-stage overrides, keyed by the stage's key, winning over `stage`.
|
|
84
|
+
*
|
|
85
|
+
* `params` is not inherited by a stage that names its own `model`: sampling
|
|
86
|
+
* knobs are per model, and a body written for one is rejected by another.
|
|
87
|
+
*/
|
|
88
|
+
stages?: Partial<Record<StageKey, StageOptions>>;
|
|
81
89
|
/**
|
|
82
90
|
* Reached when a stage wants a person before it can go on.
|
|
83
91
|
*
|
|
@@ -152,13 +160,17 @@ export interface NoesisResult {
|
|
|
152
160
|
* each inside its own output under the stage `4` artifact.
|
|
153
161
|
*/
|
|
154
162
|
usage: RunUsage;
|
|
155
|
-
/** @deprecated Read `usage.inputTokens`. Kept so existing callers still work. */
|
|
156
|
-
inputTokens: number;
|
|
157
|
-
/** @deprecated Read `usage.outputTokens`. Kept so existing callers still work. */
|
|
158
|
-
outputTokens: number;
|
|
159
163
|
latencyMs: number;
|
|
160
164
|
}
|
|
161
165
|
export interface RunUsage extends Usage {
|
|
166
|
+
/**
|
|
167
|
+
* What the tools reported, which the numbers above deliberately exclude.
|
|
168
|
+
*
|
|
169
|
+
* A tool call is somebody else's API at somebody else's rates and is no
|
|
170
|
+
* rollout of ours, so it is never folded into the model spend — but it is
|
|
171
|
+
* real money, so it is reachable rather than buried in an artifact.
|
|
172
|
+
*/
|
|
173
|
+
tools: ToolSpend;
|
|
162
174
|
/** Summed over every time the loop ran each stage. */
|
|
163
175
|
byStage: StageUsage[];
|
|
164
176
|
byModel: Record<string, Usage>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spendgraph/workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Ready-made workflows assembled from the spendgraph packages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@spendgraph/harness": "^0.
|
|
37
|
-
"@spendgraph/llms": "^0.
|
|
38
|
-
"@spendgraph/prompt": "^0.
|
|
39
|
-
"@spendgraph/tools": "^0.
|
|
36
|
+
"@spendgraph/harness": "^0.3.0",
|
|
37
|
+
"@spendgraph/llms": "^0.3.0",
|
|
38
|
+
"@spendgraph/prompt": "^0.3.0",
|
|
39
|
+
"@spendgraph/tools": "^0.3.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@locusgraph/client": "^0.8.1",
|
|
50
|
-
"@spendgraph/evals": "^0.
|
|
50
|
+
"@spendgraph/evals": "^0.3.0",
|
|
51
51
|
"typescript": "^5"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|