@spendgraph/workflows 0.3.3 → 0.4.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 CHANGED
@@ -24,7 +24,7 @@ result.route; // which of the four it earned
24
24
  purpose: the prompt client pulls the stored wording and records what the call
25
25
  cost, and the model client makes it.
26
26
 
27
- ## Four routes, chosen in code
27
+ ## Three routes, chosen in code
28
28
 
29
29
  Stage 0 is a router prompt that returns five signals — difficulty, stakes,
30
30
  confidence, ambiguity, tool need — and never a route. `routeFor` in
@@ -34,7 +34,6 @@ an eval run without touching a prompt.
34
34
  | route | what it runs | when |
35
35
  | --- | --- | --- |
36
36
  | `direct` | 6C alone | easy, unambiguous, already known |
37
- | `retrieve` | one tool call, then 6C | easy and unambiguous, but the answer is elsewhere |
38
37
  | `standard` | frame, split, plan, execute, check, close | everything in between |
39
38
  | `full` | standard plus four audits | high stakes, expert difficulty, or real ambiguity |
40
39
 
@@ -124,6 +123,12 @@ Bill both or you undercharge every run that touched the web: `web_search`
124
123
  reports Perplexity's four token counts and `deep_recall` reports micro-USD,
125
124
  and stage 4 records zero tokens of its own.
126
125
 
126
+ ## The signals
127
+
128
+ Stage 0 estimates five numbers and never a route; [SIGNALS.md](./SIGNALS.md) is the
129
+ prompt block that produces them, kept here because the authoritative copy is a
130
+ versioned prompt on the server that nothing in this repository can gate.
131
+
127
132
  ## The ledger
128
133
 
129
134
  Every stage files its result, its rollouts and its tokens in one place. That
@@ -201,7 +206,7 @@ exercised against the server:
201
206
  produced its evidence.
202
207
 
203
208
  `examples/` runs each route against the real prompts for a few pennies. Start
204
- with `04-retrieve.mjs`: it is the cheapest, and it exercises triage, the
209
+ with `05-standard.mjs`: it is the cheapest that looks anything up, and it exercises triage, the
205
210
  dispatcher, the ledger and 6C. The routes that look something up need one of
206
211
  the two lookups configured — `LOCUSGRAPH_AGENT_SECRET` for `deep_recall`, or
207
212
  `PERPLEXITY_API_KEY` for `web_search` — and say so rather than half-running.
package/SIGNALS.md ADDED
@@ -0,0 +1,104 @@
1
+ # SIGNALS
2
+
3
+ Stage 0 estimates five numbers about a request. `routeFor` turns them into a
4
+ route. This file is the copy of that prompt block that lives in the repository —
5
+ the authoritative one is a stored, versioned prompt on the spendgraph server
6
+ (`triage-router-v1-81c2cc`), which nothing here can gate. A revision promoted
7
+ there takes effect on every consumer without a deploy, so a change made and not
8
+ recorded here is a change nobody can find later.
9
+
10
+ ## What each signal is for
11
+
12
+ | signal | what it measures | who reads it |
13
+ | --- | --- | --- |
14
+ | `difficulty` | how hard the task is for a strong model | `routeFor` — hard and unsure buys `full` |
15
+ | `stakes` | blast radius if the answer is wrong | `routeFor` — high or critical buys `full` and the red team |
16
+ | `confidence` | whether the model already knows the answer | `routeFor` — under 0.7 leaves `direct` |
17
+ | `ambiguity` | how underspecified the **wording** is | `routeFor` — 0.6 and over buys `full` |
18
+ | `tool_need` | whether an external tool is required | `routeFor` — 0.3 and over leaves `direct` |
19
+
20
+ The prompt is told it does not choose the route, and it never sees the tool bus.
21
+ `tool_need` says a tool is wanted and never which one: stage 2 decides that, per
22
+ sub-question, and stage 4 dispatches what it named.
23
+
24
+ ## The block, as promoted
25
+
26
+ ```
27
+ Estimate the following five signals for the request. Judge each independently.
28
+
29
+ 1. difficulty — how hard the task is for a strong LLM with standard tools. Choose ONE: trivial | easy | medium | hard | expert | research
30
+
31
+ 2. stakes — blast radius if the answer is wrong: how costly the error is and how reversible. Judge by consequence, not by topic (the same arithmetic is low stakes in homework, critical inside a medication dose). Choose ONE:
32
+ - low: an error is easily noticed and costless to redo
33
+ - medium: an error wastes real time or money but is recoverable
34
+ - high: an error causes significant harm, cost, or exposure (legal, medical, security, production infrastructure)
35
+ - critical: an error is irreversible or endangers safety, funds, or a live incident
36
+
37
+ 3. confidence — probability from 0.00 to 1.00 that you already know enough to answer correctly without external information or extended reasoning. This is a calibrated estimate and will be scored against outcomes; do not inflate it.
38
+
39
+ 4. ambiguity — how underspecified the request is, from 0.00 to 1.00. Anchors: 0.00 = fully specified; 0.10 = minor wording issues only; 0.30 = one material unknown; 0.60 = a key referent cannot be resolved (location, entity, requirement); 0.90 = the intent itself is unclear.
40
+
41
+ This signal measures the WORDING, never your access to the answer. Two consequences, and both affect ambiguity alone — judge confidence and tool_need exactly as you otherwise would:
42
+
43
+ - A request is not ambiguous because you lack the data to answer it. If the wording is clear and the missing piece is information you would look up — the user's own notes, a repository, a live price — score 0.10 or below and let tool_need carry the gap. "What is my current workout split?" is fully specified.
44
+ - A request about your own capabilities or about this conversation is fully specified even though it names no external task; score 0.10 or below. This does not extend to content the user has stored: what is in their knowledge base is a lookup like any other.
45
+
46
+ 5. tool_need — probability from 0.00 to 1.00 that external tools (search, retrieval, code execution, APIs, live data) are required for a correct answer.
47
+ ```
48
+
49
+ ## Why item 4 says all that
50
+
51
+ Both clauses are there because a live run cost real money without them.
52
+
53
+ **"What can you do for me?"** scored 0.90 ambiguity and took `full`: sixteen
54
+ stages and 96k tokens to describe the assistant. The anchors read "names no
55
+ task" as "intent unclear", and nothing distinguished the two.
56
+
57
+ The first fix said a request about *"you, this assistant, or the conversation"*
58
+ is fully specified. That was too wide. **"What is inside my knowledge base?"**
59
+ then scored `tool_need: 0` and took `direct` — the model reported it already
60
+ knew what the user had stored, and answered from nothing. Worse than expensive:
61
+ wrong.
62
+
63
+ **"What is my current workout split?"** scored 1.00 ambiguity and took `full`.
64
+ The model could not resolve *which* split, so it called the question unclear —
65
+ but the question is perfectly clear and the gap is data. That is `tool_need`,
66
+ which it scored 0.90 at the same time. Every question about the user's own
67
+ stored knowledge — the whole point of a knowledge-base product — was buying the
68
+ most expensive route.
69
+
70
+ Hence the framing the block now leads with: ambiguity is about the **wording**,
71
+ and the two clauses say so explicitly rather than by example alone.
72
+
73
+ ## What it does not fix
74
+
75
+ **"Which version of vitest is this repo pinned to?"** still scores ~0.70 and
76
+ takes `full`. The model reads "this repo" as a referent it cannot resolve, which
77
+ is fair when the question arrives with no session context. Inside a real chat it
78
+ has that context. Tuning the wording until this one passes standalone would fit
79
+ the prompt to a test rig rather than to the product.
80
+
81
+ ## Testing a revision
82
+
83
+ `evals/dataset/routing.json` gates `routeFor`, not this prompt — every row
84
+ supplies hand-labelled estimates, so a prompt change passes it unchanged. The
85
+ only way to check a revision is to run it:
86
+
87
+ ```sh
88
+ node examples/triage/01-triage.mjs # QUESTION=… for your own
89
+ ```
90
+
91
+ The five that matter, and what they should produce:
92
+
93
+ | question | expected |
94
+ | --- | --- |
95
+ | What can you do for me? | ambiguity ≤ 0.10 → `direct` |
96
+ | What is inside my knowledge base? | tool_need high → `standard` |
97
+ | What is my current workout split? | ambiguity ≤ 0.10, tool_need high → `standard` |
98
+ | Make our agent better. | ambiguity ~0.85 → `full` |
99
+ | Should we migrate every production workload onto one provider? | `full` |
100
+
101
+ The fourth is the control. It also names no task, and it must keep taking `full`
102
+ — it names a task and withholds the success criterion, which is what 3B and 3C
103
+ exist for. A revision that makes chit-chat cheap by making that one cheap too
104
+ has broken the thing the long route is for.
@@ -45,7 +45,7 @@ export { dropPoint, parseStage, resumeAt } from "./run/redo.js";
45
45
  export { STAGE_OF_SLUG } from "./slugs.js";
46
46
  export type { StageOptions, StageOutcome } from "./stage.js";
47
47
  export { runStage, StageReplyError } from "./stage.js";
48
- export type { Ask, AskedOfYou, NoesisOptions, NoesisResult, RetrieveOptions, Route, Routing, RunUsage, Status, VerifyOptions, } from "./types.js";
48
+ export type { Ask, AskedOfYou, NoesisOptions, NoesisResult, Route, Routing, RunUsage, Status, VerifyOptions, } from "./types.js";
49
49
  export type { PartialUsage, Usage } from "./usage.js";
50
50
  export { addUsage, billableTokens, NO_USAGE, spend, usageOf } from "./usage.js";
51
51
  export type { Checked, Verification } from "./verify/index.js";
@@ -21,9 +21,8 @@ import type { Routing, TriageEstimates } from "./index.js";
21
21
  * exist for, and it takes `full` below the ambiguity bar that would catch it on
22
22
  * its own.
23
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.
24
+ * A clear question that still needs a tool takes `standard`: stage 2 is the only
25
+ * stage that decides which tool a sub-question wants, and a route that skipped
26
+ * it had to guess. Guessing sent every world-fact lookup at the knowledge base.
28
27
  */
29
28
  export declare function routeFor(estimates: TriageEstimates): Routing;
@@ -4,17 +4,12 @@ const CONFIDENT = 0.7;
4
4
  const NEEDS_TOOLS = 0.3;
5
5
  const VAGUE = 0.5;
6
6
  const UNSURE = 0.5;
7
- const NEARLY = 0.6;
8
7
  const HARD = new Set(["expert", "research"]);
9
8
  const EASY = new Set(["trivial", "easy"]);
10
9
  const SERIOUS = new Set(["high", "critical"]);
11
10
  export function routeFor(estimates) {
12
11
  const redTeam = SERIOUS.has(estimates.stakes);
13
12
  const clear = EASY.has(estimates.difficulty) && estimates.ambiguity < CLEAR;
14
- const lookup = clear && estimates.tool_need >= NEEDS_TOOLS;
15
- if (lookup && estimates.stakes !== "critical") {
16
- return { route: "retrieve", redTeam, reason: `a lookup — tool_need is ${estimates.tool_need}` };
17
- }
18
13
  if (redTeam) {
19
14
  return { route: "full", redTeam, reason: `stakes are ${estimates.stakes}` };
20
15
  }
@@ -32,11 +27,8 @@ export function routeFor(estimates) {
32
27
  reason: `ambiguity ${estimates.ambiguity} with confidence ${estimates.confidence}`,
33
28
  };
34
29
  }
35
- if (clear && estimates.confidence >= CONFIDENT) {
30
+ if (clear && estimates.confidence >= CONFIDENT && estimates.tool_need < NEEDS_TOOLS) {
36
31
  return { route: "direct", redTeam, reason: `${estimates.difficulty} and already known` };
37
32
  }
38
- if (clear && estimates.confidence >= NEARLY) {
39
- return { route: "retrieve", redTeam, reason: `${estimates.difficulty} but not certain` };
40
- }
41
33
  return { route: "standard", redTeam, reason: "neither trivial nor serious" };
42
34
  }
@@ -2,7 +2,7 @@ import { route } from "@spendgraph/harness";
2
2
  import { emitter } from "../events.js";
3
3
  import { notBuilt } from "./errors.js";
4
4
  import { finish } from "./result.js";
5
- import { classifyStage, directStage, fullStage, retrieveStage, standardStage, } from "./stages/index.js";
5
+ import { classifyStage, directStage, fullStage, standardStage, } from "./stages/index.js";
6
6
  import { newRunState } from "./state.js";
7
7
  const DEFAULT_MAX_RETRIES = 5;
8
8
  const watched = (opts, emit) => ({
@@ -32,7 +32,6 @@ export async function run(client, llm, question, options = {}) {
32
32
  classify: classifyStage(client, llm, question, opts, state),
33
33
  routes: {
34
34
  direct: directStage(client, llm, question, opts, state),
35
- retrieve: retrieveStage(client, llm, question, opts, state),
36
35
  standard: standardStage(client, llm, question, opts, state),
37
36
  full: fullStage(client, llm, question, opts, state),
38
37
  },
@@ -1,5 +1,4 @@
1
1
  export { classifyStage } from "./classify.js";
2
2
  export { directStage } from "./direct.js";
3
3
  export { fullStage } from "./full.js";
4
- export { retrieveStage } from "./retrieve.js";
5
4
  export { standardStage } from "./standard.js";
@@ -1,5 +1,4 @@
1
1
  export { classifyStage } from "./classify.js";
2
2
  export { directStage } from "./direct.js";
3
3
  export { fullStage } from "./full.js";
4
- export { retrieveStage } from "./retrieve.js";
5
4
  export { standardStage } from "./standard.js";
@@ -2,7 +2,6 @@ import type { ToolBus } from "@spendgraph/tools";
2
2
  import type { ToolSpend } from "./execute/spend.js";
3
3
  import type { Asked } from "./context.js";
4
4
  import type { NoesisEvent } from "./events.js";
5
- import type { SubQuestionTool } from "./producers/decompose/index.js";
6
5
  import type { AnswerOptions, AnswerResult } from "./producers/toulmin/index.js";
7
6
  import type { TriageEstimates, TriageInput, TriageOptions } from "./router/triage/index.js";
8
7
  import type { Escalation, StageKey, StageUsage } from "./run/ledger.js";
@@ -14,7 +13,7 @@ import type { Usage } from "./usage.js";
14
13
  * Derived in code from the triage estimates, which is what the router prompt
15
14
  * itself says: it returns signals and never a route.
16
15
  */
17
- export type Route = "direct" | "retrieve" | "standard" | "full";
16
+ export type Route = "direct" | "standard" | "full";
18
17
  /** What the estimates decide, before any of it is acted on. */
19
18
  export interface Routing {
20
19
  route: Route;
@@ -39,16 +38,6 @@ export interface AskedOfYou {
39
38
  /** Why it says so. */
40
39
  reason: string;
41
40
  }
42
- export interface RetrieveOptions {
43
- /**
44
- * What a lookup reaches for. Default `"deeprecall"`.
45
- *
46
- * Triage says how much a question needs a tool and never which one, so the
47
- * route picks. The enum is ordered cheapest first and the knowledge base
48
- * comes before the open web, which is the same preference.
49
- */
50
- tool?: SubQuestionTool;
51
- }
52
41
  export interface VerifyOptions {
53
42
  /**
54
43
  * The confidence a blind check has to reach before it settles a claim.
@@ -108,7 +97,6 @@ export interface NoesisOptions {
108
97
  * person. Default 5, counted across every gate together.
109
98
  */
110
99
  maxRetries?: number;
111
- retrieve?: RetrieveOptions;
112
100
  /** How the blind checks in stage 5 are read. */
113
101
  verify?: VerifyOptions;
114
102
  answer?: AnswerOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spendgraph/workflows",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Ready-made workflows assembled from the spendgraph packages.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,13 +30,14 @@
30
30
  },
31
31
  "files": [
32
32
  "dist",
33
+ "SIGNALS.md",
33
34
  "README.md"
34
35
  ],
35
36
  "dependencies": {
36
- "@spendgraph/harness": "^0.3.3",
37
- "@spendgraph/llms": "^0.3.3",
38
- "@spendgraph/prompt": "^0.3.3",
39
- "@spendgraph/tools": "^0.3.3"
37
+ "@spendgraph/harness": "^0.4.0",
38
+ "@spendgraph/llms": "^0.4.0",
39
+ "@spendgraph/prompt": "^0.4.0",
40
+ "@spendgraph/tools": "^0.4.0"
40
41
  },
41
42
  "scripts": {
42
43
  "build": "tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments",
@@ -47,7 +48,7 @@
47
48
  },
48
49
  "devDependencies": {
49
50
  "@locusgraph/client": "^0.8.1",
50
- "@spendgraph/evals": "^0.3.3",
51
+ "@spendgraph/evals": "^0.4.0",
51
52
  "typescript": "^5"
52
53
  },
53
54
  "engines": {