@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 +8 -3
- package/SIGNALS.md +104 -0
- package/dist/noesis/index.d.ts +1 -1
- package/dist/noesis/route.d.ts +3 -4
- package/dist/noesis/route.js +1 -9
- package/dist/noesis/run/run.js +1 -2
- package/dist/noesis/run/stages/index.d.ts +0 -1
- package/dist/noesis/run/stages/index.js +0 -1
- package/dist/noesis/types.d.ts +1 -13
- package/package.json +7 -6
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
|
-
##
|
|
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 `
|
|
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.
|
package/dist/noesis/index.d.ts
CHANGED
|
@@ -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,
|
|
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";
|
package/dist/noesis/route.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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;
|
package/dist/noesis/route.js
CHANGED
|
@@ -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
|
}
|
package/dist/noesis/run/run.js
CHANGED
|
@@ -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,
|
|
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
|
},
|
package/dist/noesis/types.d.ts
CHANGED
|
@@ -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" | "
|
|
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
|
+
"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.
|
|
37
|
-
"@spendgraph/llms": "^0.
|
|
38
|
-
"@spendgraph/prompt": "^0.
|
|
39
|
-
"@spendgraph/tools": "^0.
|
|
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.
|
|
51
|
+
"@spendgraph/evals": "^0.4.0",
|
|
51
52
|
"typescript": "^5"
|
|
52
53
|
},
|
|
53
54
|
"engines": {
|