@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.
- package/README.md +149 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/noesis/attribution.d.ts +24 -0
- package/dist/noesis/attribution.js +43 -0
- package/dist/noesis/context.d.ts +18 -0
- package/dist/noesis/context.js +16 -0
- package/dist/noesis/draft/draft.d.ts +18 -0
- package/dist/noesis/draft/draft.js +50 -0
- package/dist/noesis/draft/index.d.ts +2 -0
- package/dist/noesis/draft/index.js +1 -0
- package/dist/noesis/draft/types.d.ts +27 -0
- package/dist/noesis/draft/types.js +1 -0
- package/dist/noesis/events.d.ts +158 -0
- package/dist/noesis/events.js +24 -0
- package/dist/noesis/execute/execute.d.ts +22 -0
- package/dist/noesis/execute/execute.js +85 -0
- package/dist/noesis/execute/index.d.ts +3 -0
- package/dist/noesis/execute/index.js +2 -0
- package/dist/noesis/execute/order.d.ts +11 -0
- package/dist/noesis/execute/order.js +28 -0
- package/dist/noesis/execute/types.d.ts +37 -0
- package/dist/noesis/execute/types.js +1 -0
- package/dist/noesis/gates/closure-checklist/index.d.ts +3 -0
- package/dist/noesis/gates/closure-checklist/index.js +2 -0
- package/dist/noesis/gates/closure-checklist/invoke.d.ts +5 -0
- package/dist/noesis/gates/closure-checklist/invoke.js +6 -0
- package/dist/noesis/gates/closure-checklist/prompt.d.ts +5 -0
- package/dist/noesis/gates/closure-checklist/prompt.js +32 -0
- package/dist/noesis/gates/closure-checklist/types.d.ts +20 -0
- package/dist/noesis/gates/closure-checklist/types.js +1 -0
- package/dist/noesis/gates/cqot-gate/index.d.ts +3 -0
- package/dist/noesis/gates/cqot-gate/index.js +2 -0
- package/dist/noesis/gates/cqot-gate/invoke.d.ts +5 -0
- package/dist/noesis/gates/cqot-gate/invoke.js +6 -0
- package/dist/noesis/gates/cqot-gate/prompt.d.ts +5 -0
- package/dist/noesis/gates/cqot-gate/prompt.js +27 -0
- package/dist/noesis/gates/cqot-gate/types.d.ts +23 -0
- package/dist/noesis/gates/cqot-gate/types.js +1 -0
- package/dist/noesis/gates/premortem/index.d.ts +3 -0
- package/dist/noesis/gates/premortem/index.js +2 -0
- package/dist/noesis/gates/premortem/invoke.d.ts +5 -0
- package/dist/noesis/gates/premortem/invoke.js +6 -0
- package/dist/noesis/gates/premortem/prompt.d.ts +5 -0
- package/dist/noesis/gates/premortem/prompt.js +37 -0
- package/dist/noesis/gates/premortem/types.d.ts +28 -0
- package/dist/noesis/gates/premortem/types.js +1 -0
- package/dist/noesis/gates/red-team/index.d.ts +3 -0
- package/dist/noesis/gates/red-team/index.js +2 -0
- package/dist/noesis/gates/red-team/invoke.d.ts +5 -0
- package/dist/noesis/gates/red-team/invoke.js +6 -0
- package/dist/noesis/gates/red-team/prompt.d.ts +5 -0
- package/dist/noesis/gates/red-team/prompt.js +25 -0
- package/dist/noesis/gates/red-team/types.d.ts +21 -0
- package/dist/noesis/gates/red-team/types.js +1 -0
- package/dist/noesis/gates/standards-review/index.d.ts +3 -0
- package/dist/noesis/gates/standards-review/index.js +2 -0
- package/dist/noesis/gates/standards-review/invoke.d.ts +5 -0
- package/dist/noesis/gates/standards-review/invoke.js +6 -0
- package/dist/noesis/gates/standards-review/prompt.d.ts +5 -0
- package/dist/noesis/gates/standards-review/prompt.js +26 -0
- package/dist/noesis/gates/standards-review/types.d.ts +20 -0
- package/dist/noesis/gates/standards-review/types.js +1 -0
- package/dist/noesis/gates/think-checkpoint/index.d.ts +3 -0
- package/dist/noesis/gates/think-checkpoint/index.js +2 -0
- package/dist/noesis/gates/think-checkpoint/invoke.d.ts +5 -0
- package/dist/noesis/gates/think-checkpoint/invoke.js +6 -0
- package/dist/noesis/gates/think-checkpoint/prompt.d.ts +5 -0
- package/dist/noesis/gates/think-checkpoint/prompt.js +28 -0
- package/dist/noesis/gates/think-checkpoint/types.d.ts +21 -0
- package/dist/noesis/gates/think-checkpoint/types.js +1 -0
- package/dist/noesis/index.d.ts +51 -0
- package/dist/noesis/index.js +27 -0
- package/dist/noesis/producers/cove-answers/index.d.ts +3 -0
- package/dist/noesis/producers/cove-answers/index.js +2 -0
- package/dist/noesis/producers/cove-answers/invoke.d.ts +5 -0
- package/dist/noesis/producers/cove-answers/invoke.js +6 -0
- package/dist/noesis/producers/cove-answers/prompt.d.ts +5 -0
- package/dist/noesis/producers/cove-answers/prompt.js +27 -0
- package/dist/noesis/producers/cove-answers/types.d.ts +32 -0
- package/dist/noesis/producers/cove-answers/types.js +1 -0
- package/dist/noesis/producers/cove-questions/index.d.ts +3 -0
- package/dist/noesis/producers/cove-questions/index.js +2 -0
- package/dist/noesis/producers/cove-questions/invoke.d.ts +5 -0
- package/dist/noesis/producers/cove-questions/invoke.js +6 -0
- package/dist/noesis/producers/cove-questions/prompt.d.ts +5 -0
- package/dist/noesis/producers/cove-questions/prompt.js +26 -0
- package/dist/noesis/producers/cove-questions/types.d.ts +17 -0
- package/dist/noesis/producers/cove-questions/types.js +1 -0
- package/dist/noesis/producers/decompose/index.d.ts +3 -0
- package/dist/noesis/producers/decompose/index.js +2 -0
- package/dist/noesis/producers/decompose/invoke.d.ts +5 -0
- package/dist/noesis/producers/decompose/invoke.js +6 -0
- package/dist/noesis/producers/decompose/prompt.d.ts +22 -0
- package/dist/noesis/producers/decompose/prompt.js +68 -0
- package/dist/noesis/producers/decompose/types.d.ts +41 -0
- package/dist/noesis/producers/decompose/types.js +1 -0
- package/dist/noesis/producers/discriminating-test/index.d.ts +3 -0
- package/dist/noesis/producers/discriminating-test/index.js +2 -0
- package/dist/noesis/producers/discriminating-test/invoke.d.ts +5 -0
- package/dist/noesis/producers/discriminating-test/invoke.js +6 -0
- package/dist/noesis/producers/discriminating-test/prompt.d.ts +5 -0
- package/dist/noesis/producers/discriminating-test/prompt.js +67 -0
- package/dist/noesis/producers/discriminating-test/types.d.ts +32 -0
- package/dist/noesis/producers/discriminating-test/types.js +1 -0
- package/dist/noesis/producers/plan-candidates/index.d.ts +3 -0
- package/dist/noesis/producers/plan-candidates/index.js +2 -0
- package/dist/noesis/producers/plan-candidates/invoke.d.ts +5 -0
- package/dist/noesis/producers/plan-candidates/invoke.js +6 -0
- package/dist/noesis/producers/plan-candidates/prompt.d.ts +5 -0
- package/dist/noesis/producers/plan-candidates/prompt.js +46 -0
- package/dist/noesis/producers/plan-candidates/types.d.ts +32 -0
- package/dist/noesis/producers/plan-candidates/types.js +1 -0
- package/dist/noesis/producers/qn-brief/index.d.ts +3 -0
- package/dist/noesis/producers/qn-brief/index.js +2 -0
- package/dist/noesis/producers/qn-brief/invoke.d.ts +5 -0
- package/dist/noesis/producers/qn-brief/invoke.js +6 -0
- package/dist/noesis/producers/qn-brief/prompt.d.ts +12 -0
- package/dist/noesis/producers/qn-brief/prompt.js +65 -0
- package/dist/noesis/producers/qn-brief/types.d.ts +42 -0
- package/dist/noesis/producers/qn-brief/types.js +1 -0
- package/dist/noesis/producers/toulmin/answer.d.ts +18 -0
- package/dist/noesis/producers/toulmin/answer.js +87 -0
- package/dist/noesis/producers/toulmin/index.d.ts +5 -0
- package/dist/noesis/producers/toulmin/index.js +4 -0
- package/dist/noesis/producers/toulmin/invoke.d.ts +18 -0
- package/dist/noesis/producers/toulmin/invoke.js +53 -0
- package/dist/noesis/producers/toulmin/prompt.d.ts +12 -0
- package/dist/noesis/producers/toulmin/prompt.js +60 -0
- package/dist/noesis/producers/toulmin/render.d.ts +14 -0
- package/dist/noesis/producers/toulmin/render.js +82 -0
- package/dist/noesis/producers/toulmin/types.d.ts +126 -0
- package/dist/noesis/producers/toulmin/types.js +1 -0
- package/dist/noesis/question.d.ts +24 -0
- package/dist/noesis/question.js +37 -0
- package/dist/noesis/repair/reflection/index.d.ts +3 -0
- package/dist/noesis/repair/reflection/index.js +2 -0
- package/dist/noesis/repair/reflection/invoke.d.ts +5 -0
- package/dist/noesis/repair/reflection/invoke.js +6 -0
- package/dist/noesis/repair/reflection/prompt.d.ts +5 -0
- package/dist/noesis/repair/reflection/prompt.js +26 -0
- package/dist/noesis/repair/reflection/types.d.ts +17 -0
- package/dist/noesis/repair/reflection/types.js +1 -0
- package/dist/noesis/retry.d.ts +25 -0
- package/dist/noesis/retry.js +60 -0
- package/dist/noesis/route.d.ts +29 -0
- package/dist/noesis/route.js +69 -0
- package/dist/noesis/router/triage/index.d.ts +3 -0
- package/dist/noesis/router/triage/index.js +2 -0
- package/dist/noesis/router/triage/invoke.d.ts +31 -0
- package/dist/noesis/router/triage/invoke.js +76 -0
- package/dist/noesis/router/triage/prompt.d.ts +11 -0
- package/dist/noesis/router/triage/prompt.js +34 -0
- package/dist/noesis/router/triage/types.d.ts +70 -0
- package/dist/noesis/router/triage/types.js +1 -0
- package/dist/noesis/run/artifacts.d.ts +12 -0
- package/dist/noesis/run/artifacts.js +31 -0
- package/dist/noesis/run/errors.d.ts +38 -0
- package/dist/noesis/run/errors.js +55 -0
- package/dist/noesis/run/index.d.ts +6 -0
- package/dist/noesis/run/index.js +4 -0
- package/dist/noesis/run/ledger.d.ts +110 -0
- package/dist/noesis/run/ledger.js +184 -0
- package/dist/noesis/run/loop.d.ts +15 -0
- package/dist/noesis/run/loop.js +91 -0
- package/dist/noesis/run/plan.d.ts +18 -0
- package/dist/noesis/run/plan.js +39 -0
- package/dist/noesis/run/produce.d.ts +50 -0
- package/dist/noesis/run/produce.js +114 -0
- package/dist/noesis/run/redo.d.ts +24 -0
- package/dist/noesis/run/redo.js +41 -0
- package/dist/noesis/run/result.d.ts +5 -0
- package/dist/noesis/run/result.js +51 -0
- package/dist/noesis/run/run.d.ts +4 -0
- package/dist/noesis/run/run.js +73 -0
- package/dist/noesis/run/stages/classify.d.ts +7 -0
- package/dist/noesis/run/stages/classify.js +23 -0
- package/dist/noesis/run/stages/direct.d.ts +6 -0
- package/dist/noesis/run/stages/direct.js +8 -0
- package/dist/noesis/run/stages/full.d.ts +14 -0
- package/dist/noesis/run/stages/full.js +21 -0
- package/dist/noesis/run/stages/index.d.ts +5 -0
- package/dist/noesis/run/stages/index.js +5 -0
- package/dist/noesis/run/stages/retrieve.d.ts +13 -0
- package/dist/noesis/run/stages/retrieve.js +38 -0
- package/dist/noesis/run/stages/standard.d.ts +12 -0
- package/dist/noesis/run/stages/standard.js +19 -0
- package/dist/noesis/run/state.d.ts +26 -0
- package/dist/noesis/run/state.js +13 -0
- package/dist/noesis/run/stream.d.ts +28 -0
- package/dist/noesis/run/stream.js +33 -0
- package/dist/noesis/run/verify.d.ts +27 -0
- package/dist/noesis/run/verify.js +78 -0
- package/dist/noesis/slugs.d.ts +16 -0
- package/dist/noesis/slugs.js +46 -0
- package/dist/noesis/stage.d.ts +56 -0
- package/dist/noesis/stage.js +52 -0
- package/dist/noesis/started.d.ts +18 -0
- package/dist/noesis/started.js +39 -0
- package/dist/noesis/types.d.ts +167 -0
- package/dist/noesis/types.js +1 -0
- package/dist/noesis/usage.d.ts +49 -0
- package/dist/noesis/usage.js +56 -0
- package/dist/noesis/verify/index.d.ts +2 -0
- package/dist/noesis/verify/index.js +1 -0
- package/dist/noesis/verify/types.d.ts +54 -0
- package/dist/noesis/verify/types.js +1 -0
- package/dist/noesis/verify/verify.d.ts +36 -0
- package/dist/noesis/verify/verify.js +94 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @spendgraph/workflows
|
|
2
|
+
|
|
3
|
+
Ready-made workflows assembled from the other packages. One so far: **noesis**,
|
|
4
|
+
a question-answering loop that decides how much of itself a question is worth.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { run } from "@spendgraph/workflows";
|
|
8
|
+
|
|
9
|
+
const result = await run(client, llm, "Has the price changed since March?", {
|
|
10
|
+
tools: bus,
|
|
11
|
+
context: { now: new Date().toISOString(), locale: "IN / INR" },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
result.text; // the answer, as a person reads it
|
|
15
|
+
result.status; // "answered" | "abstained" | "escalated"
|
|
16
|
+
result.route; // which of the four it earned
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`client` is a `PromptClient` and `llm` an `Llm`. The two are separate on
|
|
20
|
+
purpose: the prompt client pulls the stored wording and records what the call
|
|
21
|
+
cost, and the model client makes it.
|
|
22
|
+
|
|
23
|
+
## Four routes, chosen in code
|
|
24
|
+
|
|
25
|
+
Stage 0 is a router prompt that returns five signals — difficulty, stakes,
|
|
26
|
+
confidence, ambiguity, tool need — and never a route. `routeFor` in
|
|
27
|
+
`src/noesis/route.ts` turns those into one, from thresholds you can move after
|
|
28
|
+
an eval run without touching a prompt.
|
|
29
|
+
|
|
30
|
+
| route | what it runs | when |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `direct` | 6C alone | easy, unambiguous, already known |
|
|
33
|
+
| `retrieve` | one tool call, then 6C | easy and unambiguous, but the answer is elsewhere |
|
|
34
|
+
| `standard` | frame, split, plan, execute, check, close | everything in between |
|
|
35
|
+
| `full` | standard plus four audits | high stakes, expert difficulty, or real ambiguity |
|
|
36
|
+
|
|
37
|
+
Stakes are tested first, because that is the one signal where being wrong is
|
|
38
|
+
expensive in only one direction: an easy question answered the long way costs
|
|
39
|
+
tokens, a critical one answered directly costs more than that.
|
|
40
|
+
|
|
41
|
+
## The stages
|
|
42
|
+
|
|
43
|
+
| | | |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| 0 | triage | five signals |
|
|
46
|
+
| 1 | question brief | the question interrogated, never answered |
|
|
47
|
+
| 2 | decompose | ordered sub-questions, at most seven, depth one |
|
|
48
|
+
| 3A | plan candidates | approaches, the one chosen, the steps that follow |
|
|
49
|
+
| 3B | premortem | the plan assumed dead, and what killed it |
|
|
50
|
+
| 3C | cqot gate | eight questions of the plan |
|
|
51
|
+
| 4 | execution | **code** — every sub-question through the tool its hint named |
|
|
52
|
+
| 4A | think checkpoint | predicted against actual, against 3A's own tolerance |
|
|
53
|
+
| 4B | discriminating test | the one test that tells the live hypotheses apart |
|
|
54
|
+
| 5A | cove questions | the verification questions the draft invites |
|
|
55
|
+
| 5B | cove answers | each answered blind |
|
|
56
|
+
| 5 | verification | **code** — 5A and 5B paired back together by id |
|
|
57
|
+
| 5C | standards review | the answer against eight standards |
|
|
58
|
+
| 6A | closure checklist | the last questions before it may close |
|
|
59
|
+
| 6B | red team | the answer attacked |
|
|
60
|
+
| 6C | toulmin | the artifacts compiled into a claim and its qualifier |
|
|
61
|
+
| R | reflection | the earliest wrong decision behind a failed gate |
|
|
62
|
+
|
|
63
|
+
Two of them are code and not model calls. Stage 4 dispatches and does not
|
|
64
|
+
reason, because `deep_recall` and `web_search` already return a synthesised
|
|
65
|
+
answer. Stage 5 pairs 5A's claims back to 5B's blind answers, and it has to be
|
|
66
|
+
code: whatever does it sees both the claim and the answer written without it,
|
|
67
|
+
and a model given both is no longer a blind check of anything.
|
|
68
|
+
|
|
69
|
+
## The ledger
|
|
70
|
+
|
|
71
|
+
Every stage files its result, its rollouts and its tokens in one place. That
|
|
72
|
+
record is what 6C compiles from, keyed by stage id and name together — `"3A plan
|
|
73
|
+
candidates"` — because the model writes one of them back as
|
|
74
|
+
`grounds[].source_stage` and either has to lead to the entry.
|
|
75
|
+
|
|
76
|
+
It also carries the retry budget. A gate that will not let the answer through
|
|
77
|
+
spends one, runs R to find the earliest wrong decision, and cuts the ledger back
|
|
78
|
+
to where the loop restarts, so nothing stale is compiled into the answer. What
|
|
79
|
+
it cost stays: tokens were spent and rollouts were recorded, and going back does
|
|
80
|
+
not un-spend them. The nth retry escalates instead of looping again.
|
|
81
|
+
|
|
82
|
+
## Asking, and stopping
|
|
83
|
+
|
|
84
|
+
Two different problems, so two mechanisms.
|
|
85
|
+
|
|
86
|
+
`onAsk` is asking. Stage 1 is the only stage that reaches for a person mid-run,
|
|
87
|
+
and only when it says the asker aimed at the wrong thing; a replacement question
|
|
88
|
+
restarts the framing with it, and with nobody to ask the run proceeds on the
|
|
89
|
+
question as put.
|
|
90
|
+
|
|
91
|
+
`result.status` is stopping. `answered` is an answer to act on. `abstained` is
|
|
92
|
+
6C saying the grounds supported no claim. `escalated` is a gate, or a spent
|
|
93
|
+
retry budget, asking for a person — and it outranks an abstention, because a
|
|
94
|
+
gate that asked for somebody said so about the whole run. Both still carry
|
|
95
|
+
`text`, because saying what is not known is worth reading.
|
|
96
|
+
|
|
97
|
+
`onDeliver` runs once, after the loop, and only for `answered`. Anything that
|
|
98
|
+
renders a deliverable belongs there: nothing downstream reads it, no gate can
|
|
99
|
+
check it, every back edge would make it again, and generating a
|
|
100
|
+
finished-looking document out of an abstention is the worst thing this loop
|
|
101
|
+
could produce.
|
|
102
|
+
|
|
103
|
+
## Where it sits
|
|
104
|
+
|
|
105
|
+
`@spendgraph/harness` holds the patterns — `refine`, `route`, `chain`,
|
|
106
|
+
`parallel`, `loop`, `orchestrate`, `cascade` — each general and unaware of any
|
|
107
|
+
particular job. This package holds finished workflows built out of them, each
|
|
108
|
+
solving one job end to end. Nothing that belongs to all seven ever lands here.
|
|
109
|
+
|
|
110
|
+
Nothing here talks to the database or holds a provider key. `run()` is handed
|
|
111
|
+
its clients and its `ToolBus`; where a workflow needs the server it goes through
|
|
112
|
+
`@spendgraph/sdk`.
|
|
113
|
+
|
|
114
|
+
## Layout
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
src/
|
|
118
|
+
index.ts the public surface
|
|
119
|
+
noesis/
|
|
120
|
+
route.ts the thresholds, and nothing else
|
|
121
|
+
stage.ts pull, ask through {question}, force the schema
|
|
122
|
+
question.ts askThrough — the unmigrated-prompt floor
|
|
123
|
+
router/ producers/ gates/ repair/ one directory per stage
|
|
124
|
+
execute/ draft/ verify/ the code stages, and what they render
|
|
125
|
+
run/ the ledger, the routes, and the gate loop
|
|
126
|
+
tests/ excluded from dist
|
|
127
|
+
evals/ scoring the loop — the free layers run in CI
|
|
128
|
+
TODO.md how it was built, brick by brick
|
|
129
|
+
GAPS.md what is wrong, what was fixed, what needs a decision
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Not yet met a real prompt
|
|
133
|
+
|
|
134
|
+
Every test runs against a fake `PromptClient` and `Llm`, which is what made
|
|
135
|
+
building this cheap. Two assumptions carry the design and neither has been
|
|
136
|
+
exercised against the server:
|
|
137
|
+
|
|
138
|
+
1. **The stored prompts render `{question}`.** Where one does not, `askThrough`
|
|
139
|
+
overwrites the box and warns once on stderr. The run still succeeds and
|
|
140
|
+
quietly answers whatever was saved on the prompt, so a warning per stage on
|
|
141
|
+
the first live run is the signal, and publishing is the fix.
|
|
142
|
+
2. **6C reads the artifact keys back**, so a citation names the stage that
|
|
143
|
+
produced its evidence.
|
|
144
|
+
|
|
145
|
+
`examples/` runs each route against the real prompts for a few pennies. Start
|
|
146
|
+
with `04-retrieve.mjs`: it is the cheapest, and it exercises triage, the
|
|
147
|
+
dispatcher, the ledger and 6C. The routes that look something up need one of
|
|
148
|
+
the two lookups configured — `LOCUSGRAPH_AGENT_SECRET` for `deep_recall`, or
|
|
149
|
+
`PERPLEXITY_API_KEY` for `web_search` — and say so rather than half-running.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./noesis/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./noesis/index.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ToulminAnswer } from "./producers/toulmin/index.js";
|
|
2
|
+
import { type StageId } from "./run/ledger.js";
|
|
3
|
+
/**
|
|
4
|
+
* The stage a ground says it came from, or nothing.
|
|
5
|
+
*
|
|
6
|
+
* Lenient because the field is free text on a model reply, and "5B", "stage 5B"
|
|
7
|
+
* and "verification answers" are the same citation. It will not guess: a string
|
|
8
|
+
* that resolves to nothing is a ground that cannot be traced, which is the
|
|
9
|
+
* finding rather than a parsing problem to work around.
|
|
10
|
+
*/
|
|
11
|
+
export declare function stageCited(source: string): StageId | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The grounds citing a stage this run never ran.
|
|
14
|
+
*
|
|
15
|
+
* 6C compiles `grounds[]` out of the artifacts and each one names its
|
|
16
|
+
* `source_stage`; the ledger knows which stages were actually paid for. A
|
|
17
|
+
* citation of anything else is the stage writing evidence rather than
|
|
18
|
+
* assembling it, and it is the cheapest check available that catches a
|
|
19
|
+
* fabricated answer without a judge and without a reference.
|
|
20
|
+
*
|
|
21
|
+
* Returns the `source_stage` strings as written, because what the model claimed
|
|
22
|
+
* is the useful thing to read back.
|
|
23
|
+
*/
|
|
24
|
+
export declare function unattributed(answer: ToulminAnswer, ran: Iterable<StageId>): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { STAGE_NAMES, STAGES } from "./run/ledger.js";
|
|
2
|
+
const BY_NAME = new Map(Object.entries(STAGE_NAMES).map(([id, name]) => [name.toLowerCase(), id]));
|
|
3
|
+
const IDS = new Set(STAGES);
|
|
4
|
+
/**
|
|
5
|
+
* The stage a ground says it came from, or nothing.
|
|
6
|
+
*
|
|
7
|
+
* Lenient because the field is free text on a model reply, and "5B", "stage 5B"
|
|
8
|
+
* and "verification answers" are the same citation. It will not guess: a string
|
|
9
|
+
* that resolves to nothing is a ground that cannot be traced, which is the
|
|
10
|
+
* finding rather than a parsing problem to work around.
|
|
11
|
+
*/
|
|
12
|
+
export function stageCited(source) {
|
|
13
|
+
const raw = source.trim();
|
|
14
|
+
const id = raw
|
|
15
|
+
.toUpperCase()
|
|
16
|
+
.replace(/^STAGE\s*/, "")
|
|
17
|
+
.replace(/[^0-9A-Z]/g, "");
|
|
18
|
+
if (IDS.has(id))
|
|
19
|
+
return id;
|
|
20
|
+
return BY_NAME.get(raw.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The grounds citing a stage this run never ran.
|
|
24
|
+
*
|
|
25
|
+
* 6C compiles `grounds[]` out of the artifacts and each one names its
|
|
26
|
+
* `source_stage`; the ledger knows which stages were actually paid for. A
|
|
27
|
+
* citation of anything else is the stage writing evidence rather than
|
|
28
|
+
* assembling it, and it is the cheapest check available that catches a
|
|
29
|
+
* fabricated answer without a judge and without a reference.
|
|
30
|
+
*
|
|
31
|
+
* Returns the `source_stage` strings as written, because what the model claimed
|
|
32
|
+
* is the useful thing to read back.
|
|
33
|
+
*/
|
|
34
|
+
export function unattributed(answer, ran) {
|
|
35
|
+
const filed = new Set(ran);
|
|
36
|
+
const grounds = answer.grounds ?? [];
|
|
37
|
+
return grounds
|
|
38
|
+
.filter((ground) => {
|
|
39
|
+
const stage = stageCited(ground.source_stage ?? "");
|
|
40
|
+
return !stage || !filed.has(stage);
|
|
41
|
+
})
|
|
42
|
+
.map((ground) => ground.source_stage);
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The `now` and `locale` facts a stage is given rather than asked for. */
|
|
2
|
+
export interface Asked {
|
|
3
|
+
/** The request itself. */
|
|
4
|
+
question: string;
|
|
5
|
+
/** Current date and time with offset. The stage is told to trust it over its own sense of the present. */
|
|
6
|
+
now?: string;
|
|
7
|
+
/** Country, currency, units, language variant. It governs jurisdiction, not only geography. */
|
|
8
|
+
locale?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The request with the `<context>` block ahead of it.
|
|
12
|
+
*
|
|
13
|
+
* The prompts name `now` and `locale` but never show the markup they expect
|
|
14
|
+
* them in, so this is the form they are given. An absent fact is omitted rather
|
|
15
|
+
* than sent empty: a missing one is recorded as something to find out, an empty
|
|
16
|
+
* one reads as a fact already in hand.
|
|
17
|
+
*/
|
|
18
|
+
export declare function asked(input: Asked): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request with the `<context>` block ahead of it.
|
|
3
|
+
*
|
|
4
|
+
* The prompts name `now` and `locale` but never show the markup they expect
|
|
5
|
+
* them in, so this is the form they are given. An absent fact is omitted rather
|
|
6
|
+
* than sent empty: a missing one is recorded as something to find out, an empty
|
|
7
|
+
* one reads as a fact already in hand.
|
|
8
|
+
*/
|
|
9
|
+
export function asked(input) {
|
|
10
|
+
const facts = [
|
|
11
|
+
input.now ? `now: ${input.now}` : "",
|
|
12
|
+
input.locale ? `locale: ${input.locale}` : "",
|
|
13
|
+
].filter(Boolean);
|
|
14
|
+
const question = input.question.trim();
|
|
15
|
+
return facts.length ? `<context>\n${facts.join("\n")}\n</context>\n\n${question}` : question;
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Execution } from "../execute/index.js";
|
|
2
|
+
import type { Decomposition } from "../producers/decompose/index.js";
|
|
3
|
+
import type { Draft } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The sub-answers, paired back to the sub-questions that asked for them.
|
|
6
|
+
*
|
|
7
|
+
* A sub-question that produced nothing is named rather than dropped: 6A asks
|
|
8
|
+
* whether every one was answered, and a draft that quietly omits the failures
|
|
9
|
+
* would let it say yes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assemble(question: string, decomposition: Decomposition, execution: Execution): Draft;
|
|
12
|
+
/**
|
|
13
|
+
* The draft as a stage reads it.
|
|
14
|
+
*
|
|
15
|
+
* Tagged the way `asked()` tags the context block, because every stage
|
|
16
|
+
* downstream takes this through `{question}` and has to tell the parts apart.
|
|
17
|
+
*/
|
|
18
|
+
export declare function render(draft: Draft): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const NEVER_RAN = "It depended on something that was never answered.";
|
|
2
|
+
/**
|
|
3
|
+
* The sub-answers, paired back to the sub-questions that asked for them.
|
|
4
|
+
*
|
|
5
|
+
* A sub-question that produced nothing is named rather than dropped: 6A asks
|
|
6
|
+
* whether every one was answered, and a draft that quietly omits the failures
|
|
7
|
+
* would let it say yes.
|
|
8
|
+
*/
|
|
9
|
+
export function assemble(question, decomposition, execution) {
|
|
10
|
+
const asked = new Map(decomposition.sub_questions.map((sub) => [sub.id, sub.question]));
|
|
11
|
+
const findings = [];
|
|
12
|
+
const missing = [];
|
|
13
|
+
for (const answer of execution.answers) {
|
|
14
|
+
if (answer.status === "answered") {
|
|
15
|
+
findings.push({ id: answer.id, question: answer.question, answer: answer.result });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
missing.push({
|
|
19
|
+
id: answer.id,
|
|
20
|
+
question: answer.question,
|
|
21
|
+
reason: answer.reason ?? `The sub-question ${answer.status}.`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const id of execution.unresolved) {
|
|
26
|
+
missing.push({ id, question: asked.get(id) ?? id, reason: NEVER_RAN });
|
|
27
|
+
}
|
|
28
|
+
return { question, composition: decomposition.composition, findings, missing };
|
|
29
|
+
}
|
|
30
|
+
const block = (tag, body) => `<${tag}>\n${body}\n</${tag}>`;
|
|
31
|
+
const asText = (answer) => typeof answer === "string" ? answer : JSON.stringify(answer, null, 2);
|
|
32
|
+
/**
|
|
33
|
+
* The draft as a stage reads it.
|
|
34
|
+
*
|
|
35
|
+
* Tagged the way `asked()` tags the context block, because every stage
|
|
36
|
+
* downstream takes this through `{question}` and has to tell the parts apart.
|
|
37
|
+
*/
|
|
38
|
+
export function render(draft) {
|
|
39
|
+
const parts = [
|
|
40
|
+
block("question", draft.question),
|
|
41
|
+
block("composition", draft.composition),
|
|
42
|
+
block("findings", draft.findings.length
|
|
43
|
+
? draft.findings.map((f) => `${f.id}. ${f.question}\n${asText(f.answer)}`).join("\n\n")
|
|
44
|
+
: "Nothing was answered."),
|
|
45
|
+
];
|
|
46
|
+
if (draft.missing.length) {
|
|
47
|
+
parts.push(block("unanswered", draft.missing.map((m) => `${m.id}. ${m.question} — ${m.reason}`).join("\n")));
|
|
48
|
+
}
|
|
49
|
+
return parts.join("\n\n");
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { assemble, render } from "./draft.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** One sub-question and whatever answering it produced. */
|
|
2
|
+
export interface Finding {
|
|
3
|
+
id: string;
|
|
4
|
+
question: string;
|
|
5
|
+
/** The tool's output. Absent where the sub-question was never answered. */
|
|
6
|
+
answer?: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** A sub-question that produced nothing, and why. */
|
|
9
|
+
export interface Missing {
|
|
10
|
+
id: string;
|
|
11
|
+
question: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* What the producing half leaves for the verifying half.
|
|
16
|
+
*
|
|
17
|
+
* Assembled in code and not written by a model: nothing here is a claim anybody
|
|
18
|
+
* made, only what was asked and what came back. 5A is the first stage allowed
|
|
19
|
+
* to treat any of it as something to check.
|
|
20
|
+
*/
|
|
21
|
+
export interface Draft {
|
|
22
|
+
question: string;
|
|
23
|
+
/** How stage 2 said the sub-answers combine. */
|
|
24
|
+
composition: string;
|
|
25
|
+
findings: Finding[];
|
|
26
|
+
missing: Missing[];
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { TriageEstimates } from "./router/triage/index.js";
|
|
2
|
+
import type { StageId } from "./run/ledger.js";
|
|
3
|
+
import type { Route, Routing, Status } from "./types.js";
|
|
4
|
+
import type { Usage } from "./usage.js";
|
|
5
|
+
/** What every event carries, whatever else it says. */
|
|
6
|
+
export interface EventBase {
|
|
7
|
+
/** Monotonic within a run, so a consumer can order or de-duplicate. */
|
|
8
|
+
seq: number;
|
|
9
|
+
/** Milliseconds since the run started. */
|
|
10
|
+
at: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What the loop says while it runs.
|
|
14
|
+
*
|
|
15
|
+
* Named `noun.verb` in the past tense, because every one of these reports
|
|
16
|
+
* something that has already happened — there is nothing to cancel or confirm.
|
|
17
|
+
*/
|
|
18
|
+
export type NoesisEvent = EventBase & ({
|
|
19
|
+
type: "run.started";
|
|
20
|
+
question: string;
|
|
21
|
+
hasTools: boolean;
|
|
22
|
+
retryBudget: number;
|
|
23
|
+
} | {
|
|
24
|
+
type: "run.routed";
|
|
25
|
+
route: Route;
|
|
26
|
+
routing: Routing;
|
|
27
|
+
estimates: TriageEstimates;
|
|
28
|
+
} | {
|
|
29
|
+
type: "run.escalated";
|
|
30
|
+
from: StageId;
|
|
31
|
+
reason: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "run.finished";
|
|
34
|
+
status: Status;
|
|
35
|
+
text: string;
|
|
36
|
+
route: Route;
|
|
37
|
+
usage: Usage;
|
|
38
|
+
rollouts: number;
|
|
39
|
+
latencyMs: number;
|
|
40
|
+
} | {
|
|
41
|
+
type: "stage.started";
|
|
42
|
+
stage: StageId;
|
|
43
|
+
name: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
model?: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: "stage.finished";
|
|
48
|
+
stage: StageId;
|
|
49
|
+
name: string;
|
|
50
|
+
/** The stage's own object, with long strings cut. */
|
|
51
|
+
artifact: unknown;
|
|
52
|
+
model: string;
|
|
53
|
+
rolloutIds: string[];
|
|
54
|
+
usage: Usage;
|
|
55
|
+
latencyMs: number;
|
|
56
|
+
} | {
|
|
57
|
+
type: "stage.failed";
|
|
58
|
+
stage: StageId;
|
|
59
|
+
kind: "reply" | "transport";
|
|
60
|
+
error: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: "tools.planned";
|
|
63
|
+
waves: string[][];
|
|
64
|
+
unresolved: string[];
|
|
65
|
+
} | {
|
|
66
|
+
type: "tool.started";
|
|
67
|
+
id: string;
|
|
68
|
+
question: string;
|
|
69
|
+
tool: string;
|
|
70
|
+
wave: number;
|
|
71
|
+
} | {
|
|
72
|
+
type: "tool.finished";
|
|
73
|
+
id: string;
|
|
74
|
+
tool: string;
|
|
75
|
+
status: "answered" | "skipped" | "failed";
|
|
76
|
+
latencyMs: number;
|
|
77
|
+
reason?: string;
|
|
78
|
+
} | {
|
|
79
|
+
type: "draft.assembled";
|
|
80
|
+
findings: number;
|
|
81
|
+
missing: {
|
|
82
|
+
id: string;
|
|
83
|
+
reason: string;
|
|
84
|
+
}[];
|
|
85
|
+
} | {
|
|
86
|
+
type: "checks.paired";
|
|
87
|
+
checked: number;
|
|
88
|
+
settled: number;
|
|
89
|
+
unsettled: string[];
|
|
90
|
+
unanswered: string[];
|
|
91
|
+
unasked: string[];
|
|
92
|
+
/** Ids whose `checked_by` the run could not support, corrected to `knowledge`. */
|
|
93
|
+
overclaimed: string[];
|
|
94
|
+
} | {
|
|
95
|
+
type: "gate.passed";
|
|
96
|
+
stage: StageId;
|
|
97
|
+
name: string;
|
|
98
|
+
} | {
|
|
99
|
+
type: "gate.refused";
|
|
100
|
+
stage: StageId;
|
|
101
|
+
name: string;
|
|
102
|
+
reason: string;
|
|
103
|
+
hint?: string | null;
|
|
104
|
+
} | {
|
|
105
|
+
type: "retry.spent";
|
|
106
|
+
gate: StageId;
|
|
107
|
+
spent: number;
|
|
108
|
+
left: number;
|
|
109
|
+
budget: number;
|
|
110
|
+
/** False on the ask that ran the budget out — the run escalates instead. */
|
|
111
|
+
granted: boolean;
|
|
112
|
+
} | {
|
|
113
|
+
type: "reflect.pointed";
|
|
114
|
+
from: StageId;
|
|
115
|
+
redoFrom?: StageId;
|
|
116
|
+
resumeAt: "produce" | "verify";
|
|
117
|
+
lesson?: string;
|
|
118
|
+
} | {
|
|
119
|
+
type: "ledger.dropped";
|
|
120
|
+
from: StageId;
|
|
121
|
+
dropped: StageId[];
|
|
122
|
+
} | {
|
|
123
|
+
type: "ask.raised";
|
|
124
|
+
question: string;
|
|
125
|
+
better: string;
|
|
126
|
+
reason: string;
|
|
127
|
+
} | {
|
|
128
|
+
type: "ask.answered";
|
|
129
|
+
replacement: string | null;
|
|
130
|
+
} | {
|
|
131
|
+
type: "grounds.checked";
|
|
132
|
+
cited: number;
|
|
133
|
+
/** The `source_stage` values naming a stage the run never ran. */
|
|
134
|
+
unattributed: string[];
|
|
135
|
+
} | {
|
|
136
|
+
type: "delivered";
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* One event before the emitter stamps it.
|
|
140
|
+
*
|
|
141
|
+
* Distributed over the union rather than a plain `Omit`, which would collapse
|
|
142
|
+
* seventeen shapes into their common fields and lose the discriminant.
|
|
143
|
+
*/
|
|
144
|
+
type Unstamped<T> = T extends unknown ? Omit<T, "seq" | "at"> : never;
|
|
145
|
+
export type NoesisEventInput = Unstamped<NoesisEvent>;
|
|
146
|
+
/** What a stage or the ledger calls to say something happened. */
|
|
147
|
+
export type Emit = (event: NoesisEventInput) => void;
|
|
148
|
+
/**
|
|
149
|
+
* One emitter per run, stamping order and elapsed time.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately synchronous and deliberately swallowing: a listener that throws
|
|
152
|
+
* is a bug in the listener, and letting it take the run down would make
|
|
153
|
+
* watching a run more dangerous than not watching one. Returns `undefined`
|
|
154
|
+
* where nobody is listening, so every emit site can stay a `?.` call and cost
|
|
155
|
+
* nothing.
|
|
156
|
+
*/
|
|
157
|
+
export declare function emitter(listener: ((event: NoesisEvent) => void) | undefined, now?: () => number): Emit | undefined;
|
|
158
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One emitter per run, stamping order and elapsed time.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately synchronous and deliberately swallowing: a listener that throws
|
|
5
|
+
* is a bug in the listener, and letting it take the run down would make
|
|
6
|
+
* watching a run more dangerous than not watching one. Returns `undefined`
|
|
7
|
+
* where nobody is listening, so every emit site can stay a `?.` call and cost
|
|
8
|
+
* nothing.
|
|
9
|
+
*/
|
|
10
|
+
export function emitter(listener, now = Date.now) {
|
|
11
|
+
if (!listener)
|
|
12
|
+
return undefined;
|
|
13
|
+
const startedAt = now();
|
|
14
|
+
let seq = 0;
|
|
15
|
+
return (event) => {
|
|
16
|
+
const stamped = { ...event, seq: seq++, at: now() - startedAt };
|
|
17
|
+
try {
|
|
18
|
+
listener(stamped);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// A listener that throws does not get to fail the run.
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ToolBus } from "@spendgraph/tools";
|
|
2
|
+
import type { SubQuestion, SubQuestionTool } from "../producers/decompose/index.js";
|
|
3
|
+
import type { ExecuteOptions, Execution, Wiring } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* What each hint reaches for, and where the question goes.
|
|
6
|
+
*
|
|
7
|
+
* `code_exec` and `get_documents` are unwired because nothing ships them —
|
|
8
|
+
* a sub-question asking for one is reported rather than quietly dropped.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_TOOLS: Partial<Record<SubQuestionTool, Wiring>>;
|
|
11
|
+
/**
|
|
12
|
+
* Stage 4. Every sub-question put through the tool its hint named.
|
|
13
|
+
*
|
|
14
|
+
* The dispatcher dispatches and does not reason: `deep_recall` and `web_search`
|
|
15
|
+
* already return a synthesised answer, and a sub-question with no tool is
|
|
16
|
+
* recorded as unanswered rather than guessed at.
|
|
17
|
+
*
|
|
18
|
+
* `done_when` is carried rather than acted on. It describes an observable
|
|
19
|
+
* condition, which is 4A's job to judge against what actually happened, not
|
|
20
|
+
* something this can evaluate.
|
|
21
|
+
*/
|
|
22
|
+
export declare function execute(bus: ToolBus, plan: SubQuestion[], opts?: ExecuteOptions): Promise<Execution>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { order } from "./order.js";
|
|
2
|
+
/**
|
|
3
|
+
* What each hint reaches for, and where the question goes.
|
|
4
|
+
*
|
|
5
|
+
* `code_exec` and `get_documents` are unwired because nothing ships them —
|
|
6
|
+
* a sub-question asking for one is reported rather than quietly dropped.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_TOOLS = {
|
|
9
|
+
calculator: { name: "calculate", arg: "expression" },
|
|
10
|
+
deeprecall: { name: "deep_recall", arg: "question" },
|
|
11
|
+
web_search: { name: "web_search", arg: "query" },
|
|
12
|
+
fetch_url: { name: "http_request", arg: "url" },
|
|
13
|
+
};
|
|
14
|
+
const parsed = (output) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(output);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
async function answerOne(bus, wiring, sub, wave, emit) {
|
|
23
|
+
const base = { id: sub.id, question: sub.question, tool: sub.tool, latencyMs: 0 };
|
|
24
|
+
const wired = wiring[sub.tool];
|
|
25
|
+
const done = (answer) => {
|
|
26
|
+
emit?.({
|
|
27
|
+
type: "tool.finished",
|
|
28
|
+
id: answer.id,
|
|
29
|
+
tool: wired?.name ?? answer.tool,
|
|
30
|
+
status: answer.status,
|
|
31
|
+
latencyMs: answer.latencyMs,
|
|
32
|
+
reason: answer.reason,
|
|
33
|
+
});
|
|
34
|
+
return answer;
|
|
35
|
+
};
|
|
36
|
+
if (!wired) {
|
|
37
|
+
return done({
|
|
38
|
+
...base,
|
|
39
|
+
status: "skipped",
|
|
40
|
+
reason: sub.tool === "none"
|
|
41
|
+
? "The stage named no tool, so there is nothing to call."
|
|
42
|
+
: `Nothing is wired for "${sub.tool}".`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
emit?.({
|
|
46
|
+
type: "tool.started",
|
|
47
|
+
id: sub.id,
|
|
48
|
+
question: sub.question,
|
|
49
|
+
tool: wired.name,
|
|
50
|
+
wave,
|
|
51
|
+
});
|
|
52
|
+
const result = await bus.invoke(wired.name, { [wired.arg]: sub.question });
|
|
53
|
+
return done(result.status === "failed"
|
|
54
|
+
? { ...base, status: "failed", reason: result.error, latencyMs: result.latencyMs }
|
|
55
|
+
: { ...base, status: "answered", result: parsed(result.output), latencyMs: result.latencyMs });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Stage 4. Every sub-question put through the tool its hint named.
|
|
59
|
+
*
|
|
60
|
+
* The dispatcher dispatches and does not reason: `deep_recall` and `web_search`
|
|
61
|
+
* already return a synthesised answer, and a sub-question with no tool is
|
|
62
|
+
* recorded as unanswered rather than guessed at.
|
|
63
|
+
*
|
|
64
|
+
* `done_when` is carried rather than acted on. It describes an observable
|
|
65
|
+
* condition, which is 4A's job to judge against what actually happened, not
|
|
66
|
+
* something this can evaluate.
|
|
67
|
+
*/
|
|
68
|
+
export async function execute(bus, plan, opts = {}) {
|
|
69
|
+
const wiring = { ...DEFAULT_TOOLS, ...opts.tools };
|
|
70
|
+
const { waves, unresolved } = order(plan);
|
|
71
|
+
const answers = [];
|
|
72
|
+
opts.emit?.({
|
|
73
|
+
type: "tools.planned",
|
|
74
|
+
waves: waves.map((wave) => wave.map((sub) => sub.id)),
|
|
75
|
+
unresolved,
|
|
76
|
+
});
|
|
77
|
+
for (const [index, wave] of waves.entries()) {
|
|
78
|
+
answers.push(...(await Promise.all(wave.map((sub) => answerOne(bus, wiring, sub, index, opts.emit)))));
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
answers,
|
|
82
|
+
unresolved,
|
|
83
|
+
toolMs: answers.reduce((total, answer) => total + answer.latencyMs, 0),
|
|
84
|
+
};
|
|
85
|
+
}
|