@spendgraph/workflows 0.3.0 → 0.3.2

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 CHANGED
@@ -96,7 +96,9 @@ and win where both are set.
96
96
  **`params` does not follow a model it was not written for.** A stage naming its
97
97
  own `model` gets its own `params` or none: Haiku takes `temperature`, Sonnet 5
98
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`.
99
+ written for. A stage overriding only `params` still merges over `stage` — except
100
+ `answer`, which escalates through tiers, so its `params` reach it only alongside
101
+ a `model`.
100
102
 
101
103
  ## What a run cost
102
104
 
@@ -2,4 +2,4 @@ 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
4
  export type { ToolSpend, ToolUsage } from "./spend.js";
5
- export { addToolSpend, NO_TOOL_SPEND, readToolUsage, toolSpendOf } from "./spend.js";
5
+ export { addToolSpend, NO_TOOL_SPEND, readToolUsage, recorded, toolSpendOf } from "./spend.js";
@@ -1,3 +1,3 @@
1
1
  export { DEFAULT_TOOLS, execute } from "./execute.js";
2
2
  export { order } from "./order.js";
3
- export { addToolSpend, NO_TOOL_SPEND, readToolUsage, toolSpendOf } from "./spend.js";
3
+ export { addToolSpend, NO_TOOL_SPEND, readToolUsage, recorded, toolSpendOf } from "./spend.js";
@@ -1,7 +1,8 @@
1
- import type { SubAnswer, Wiring } from "./types.js";
1
+ import type { Execution, SubAnswer, Wiring } from "./types.js";
2
2
  import type { SubQuestionTool } from "../producers/decompose/index.js";
3
3
  /** What one tool reported it cost, keyed by the tool that reported it. */
4
4
  export interface ToolSpend {
5
+ /** Tool calls that ran, whether or not the tool priced itself. */
5
6
  calls: number;
6
7
  /** Summed where a tool priced itself. Undefined where none did. */
7
8
  costMicros?: number;
@@ -34,3 +35,11 @@ export declare function readToolUsage(result: unknown): Omit<ToolUsage, "calls">
34
35
  export declare function toolSpendOf(answers: SubAnswer[], wiring?: Partial<Record<SubQuestionTool, Wiring>>): ToolSpend;
35
36
  /** Two executions' tool spend, added. A retry does not refund the first attempt. */
36
37
  export declare function addToolSpend(a: ToolSpend, b: ToolSpend): ToolSpend;
38
+ /**
39
+ * The execution as the ledger should keep it: everything but the billing.
40
+ *
41
+ * Stage 4's artifact is serialized into every later stage's prompt and into the
42
+ * answer's evidence, so what a tool cost would ride along as citable material
43
+ * and be paid for again in tokens at each stage that reads it.
44
+ */
45
+ export declare function recorded(execution: Execution): Omit<Execution, "spend">;
@@ -26,10 +26,11 @@ export function toolSpendOf(answers, wiring = {}) {
26
26
  let calls = 0;
27
27
  let costMicros;
28
28
  for (const answer of answers) {
29
+ if (answer.status === "answered")
30
+ calls++;
29
31
  const one = readToolUsage(answer.result);
30
32
  if (!one)
31
33
  continue;
32
- calls++;
33
34
  const named = wiring[answer.tool]?.name ?? answer.tool;
34
35
  const prior = byTool[named] ?? {
35
36
  calls: 0,
@@ -77,3 +78,10 @@ export function addToolSpend(a, b) {
77
78
  : undefined;
78
79
  return { calls: a.calls + b.calls, byTool, ...(costMicros !== undefined ? { costMicros } : {}) };
79
80
  }
81
+ export function recorded(execution) {
82
+ return {
83
+ answers: execution.answers,
84
+ unresolved: execution.unresolved,
85
+ toolMs: execution.toolMs,
86
+ };
87
+ }
@@ -6,7 +6,8 @@ export { assemble, render as renderDraft } from "./draft/index.js";
6
6
  export type { Emit, EventBase, NoesisEvent, NoesisEventInput } from "./events.js";
7
7
  export { emitter } from "./events.js";
8
8
  export type { ExecuteOptions, Execution, SubAnswer, Waves, Wiring, } from "./execute/index.js";
9
- export { DEFAULT_TOOLS, execute, order } from "./execute/index.js";
9
+ export type { ToolSpend, ToolUsage } from "./execute/index.js";
10
+ export { addToolSpend, DEFAULT_TOOLS, execute, NO_TOOL_SPEND, order, readToolUsage, recorded, toolSpendOf, } from "./execute/index.js";
10
11
  export type { ClosureChecklist, ClosureChecklistInput, ClosureChecklistOptions, ClosureChecklistResult, ClosureGate, } from "./gates/closure-checklist/index.js";
11
12
  export { invoke as closureChecklist } from "./gates/closure-checklist/index.js";
12
13
  export type { CqotGate, CqotGateInput, CqotGateOptions, CqotGateResult, Gate, GateQuestion, GateVerdict, } from "./gates/cqot-gate/index.js";
@@ -2,7 +2,7 @@ export { stageCited, unattributed } from "./attribution.js";
2
2
  export { asked } from "./context.js";
3
3
  export { assemble, render as renderDraft } from "./draft/index.js";
4
4
  export { emitter } from "./events.js";
5
- export { DEFAULT_TOOLS, execute, order } from "./execute/index.js";
5
+ export { addToolSpend, DEFAULT_TOOLS, execute, NO_TOOL_SPEND, order, readToolUsage, recorded, toolSpendOf, } from "./execute/index.js";
6
6
  export { invoke as closureChecklist } from "./gates/closure-checklist/index.js";
7
7
  export { invoke as cqotGate } from "./gates/cqot-gate/index.js";
8
8
  export { invoke as premortem } from "./gates/premortem/index.js";
@@ -51,8 +51,15 @@ export declare const STAGE_KEYS: {
51
51
  readonly "6C": "answer";
52
52
  readonly R: "root_cause";
53
53
  };
54
- /** A stage by the key a config names it under. */
55
- export type StageKey = (typeof STAGE_KEYS)[StageId];
54
+ /** Stages that dispatch or pair rather than call a model. */
55
+ type CodeOnly = "execution" | "verification" | "discriminating_test";
56
+ /**
57
+ * A stage by the key a config names it under.
58
+ *
59
+ * The code-only stages are excluded: naming a model for the tool dispatcher or
60
+ * the blind-check pairing would typecheck and do nothing.
61
+ */
62
+ export type StageKey = Exclude<(typeof STAGE_KEYS)[StageId], CodeOnly>;
56
63
  /** What one stage left behind, and what it cost. */
57
64
  export interface Entry extends Usage {
58
65
  stage: StageId;
@@ -156,3 +163,4 @@ export interface Ledger {
156
163
  * failing once each are the same amount of a run going nowhere.
157
164
  */
158
165
  export declare function newLedger(maxRetries?: number, emit?: Emit): Ledger;
166
+ export {};
@@ -7,5 +7,18 @@ import { type StageId } from "./ledger.js";
7
7
  * rather than inherited when the stage names a model of its own.
8
8
  */
9
9
  export declare function stageOptionsFor(opts: NoesisOptions, id: StageId): StageOptions;
10
- /** The answer's tiers, defaulting to whatever `stages.answer` named. */
10
+ /**
11
+ * Triage's options: `stages.triage` under `opts.triage`.
12
+ *
13
+ * `stage` is deliberately not a base here — it is the model for the stages
14
+ * between triage and the answer, and folding it in would send triage a body
15
+ * written for a different model.
16
+ */
17
+ export declare function triageOptions(opts: NoesisOptions): StageOptions;
18
+ /**
19
+ * The answer's tiers, defaulting to whatever `stages.answer` named.
20
+ *
21
+ * `params` reaches the answer only alongside a `model`, because the answer
22
+ * escalates through tiers and a tier carries its own params.
23
+ */
11
24
  export declare function answerOptions(opts: NoesisOptions): AnswerOptions;
@@ -9,6 +9,14 @@ export function stageOptionsFor(opts, id) {
9
9
  const rest = Object.fromEntries(Object.entries({ ...base, ...per }).filter(([field]) => field !== "params"));
10
10
  return params && Object.keys(params).length > 0 ? { ...rest, params } : rest;
11
11
  }
12
+ export function triageOptions(opts) {
13
+ const per = opts.stages?.[STAGE_KEYS["0"]] ?? {};
14
+ const own = opts.triage ?? {};
15
+ const ownModel = own.model !== undefined && own.model !== per.model;
16
+ const params = ownModel ? own.params : { ...per.params, ...own.params };
17
+ const rest = Object.fromEntries(Object.entries({ ...per, ...own }).filter(([field]) => field !== "params"));
18
+ return params && Object.keys(params).length > 0 ? { ...rest, params } : rest;
19
+ }
12
20
  export function answerOptions(opts) {
13
21
  const base = opts.answer ?? {};
14
22
  const sixC = opts.stages?.[STAGE_KEYS["6C"]];
@@ -8,6 +8,7 @@ import { NoToolsError } from "./errors.js";
8
8
  import { plan } from "./plan.js";
9
9
  import { stageOptionsFor } from "./options.js";
10
10
  import { NO_TOOL_SPEND } from "../execute/spend.js";
11
+ import { recorded } from "../execute/index.js";
11
12
  import { recordToolSpend } from "./state.js";
12
13
  export const soFar = (state, question) => JSON.stringify({ question, ...digest(state.ledger.artifacts()) }, null, 2);
13
14
  export async function frame(client, llm, question, opts, state) {
@@ -37,7 +38,7 @@ export async function work(client, llm, question, opts, state, split) {
37
38
  const execution = await execute(opts.tools, split.sub_questions, { emit: state.emit });
38
39
  recordToolSpend(state, execution.spend);
39
40
  state.ledger.record("4", {
40
- data: execution,
41
+ data: recorded(execution),
41
42
  rolloutIds: [],
42
43
  model: "",
43
44
  inputTokens: 0,
@@ -1,9 +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
+ import { triageOptions } from "../options.js";
5
5
  export const classifyStage = (client, llm, question, opts, state) => async () => {
6
- const stage = await triage(client, llm, { ...opts.triage, currentTask: state.currentTask, question }, { ...stageOptionsFor(opts, "0"), ...opts.triage });
6
+ const stage = await triage(client, llm, { ...opts.triage, currentTask: state.currentTask, question }, triageOptions(opts));
7
7
  state.estimates = state.ledger.record("0", { ...stage, data: stage.estimates });
8
8
  state.routing = routeFor(stage.estimates);
9
9
  state.currentTask = nextTask(stage.estimates, state.currentTask);
@@ -1,6 +1,7 @@
1
1
  import { answer } from "../../producers/toulmin/index.js";
2
+ import { answerOptions } from "../options.js";
2
3
  export const directStage = (client, llm, question, opts, state) => async () => {
3
- const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, opts.answer ?? {});
4
+ const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, answerOptions(opts));
4
5
  state.answered = answered;
5
6
  state.text = state.ledger.record("6C", { ...answered, data: answered.answer }).claim;
6
7
  return state.text;
@@ -1,6 +1,9 @@
1
1
  import { execute } from "../../execute/index.js";
2
2
  import { answer } from "../../producers/toulmin/index.js";
3
3
  import { NoToolsError } from "../errors.js";
4
+ import { answerOptions } from "../options.js";
5
+ import { recorded } from "../../execute/index.js";
6
+ import { recordToolSpend } from "../state.js";
4
7
  const DEFAULT_TOOL = "deeprecall";
5
8
  const lookup = (question, tool) => ({
6
9
  id: "q",
@@ -15,15 +18,16 @@ export const retrieveStage = (client, llm, question, opts, state) => async () =>
15
18
  if (!opts.tools)
16
19
  throw new NoToolsError();
17
20
  const execution = await execute(opts.tools, [lookup(question, opts.retrieve?.tool ?? DEFAULT_TOOL)], { emit: state.emit });
21
+ recordToolSpend(state, execution.spend);
18
22
  state.ledger.record("4", {
19
- data: execution,
23
+ data: recorded(execution),
20
24
  rolloutIds: [],
21
25
  model: "",
22
26
  inputTokens: 0,
23
27
  outputTokens: 0,
24
28
  latencyMs: execution.toolMs,
25
29
  });
26
- const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, opts.answer ?? {});
30
+ const answered = await answer(client, llm, { question, ...state.ledger.artifacts() }, answerOptions(opts));
27
31
  state.answered = answered;
28
32
  state.text = state.ledger.record("6C", { ...answered, data: answered.answer }).claim;
29
33
  return state.text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spendgraph/workflows",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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.3.0",
37
- "@spendgraph/llms": "^0.3.0",
38
- "@spendgraph/prompt": "^0.3.0",
39
- "@spendgraph/tools": "^0.3.0"
36
+ "@spendgraph/harness": "^0.3.2",
37
+ "@spendgraph/llms": "^0.3.2",
38
+ "@spendgraph/prompt": "^0.3.2",
39
+ "@spendgraph/tools": "^0.3.2"
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.3.0",
50
+ "@spendgraph/evals": "^0.3.2",
51
51
  "typescript": "^5"
52
52
  },
53
53
  "engines": {