Repository navigation
Expand file tree
/
Copy pathtool-ledger.ts
More file actions
340 lines (325 loc) · 13.9 KB
/
Copy pathtool-ledger.ts
File metadata and controls
340 lines (325 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* Forged-tool ledger types + prompt-block builder.
*
* The orchestrator keeps a per-tool history of every invocation across
* a run so the dashboard can show WHEN a tool was first forged, WHICH
* department invoked it, WHAT each invocation produced, and whether
* each appearance was a fresh forge, a re-forge, or a pure citation.
*
* These shapes + the "ALREADY-FORGED TOOLS" context block builder
* extracted here are pure data structures and a pure text generator —
* no IO, no side effects — so they move cleanly out of the
* turn-loop soup.
*
* @module paracosm/runtime/tool-ledger
*/
import type { OutcomeModifiers } from '../../engine/registries/effects.js';
/** A single invocation of a forged tool, appended to the tool's history. */
export interface ToolUseRecord {
turn: number;
time: number;
eventIndex: number;
eventTitle: string;
department: string;
/** What the tool produced this invocation (string-truncated to 400). */
output: string | null;
/** True when the LLM re-invoked forge_tool (vs cited an existing tool). */
isReforge: boolean;
/** Set when isReforge=true and the judge rejected the new attempt. */
rejected: boolean;
/** Judge confidence on this invocation (only meaningful for forge calls). */
confidence?: number;
}
/** Per-tool ledger entry. First-forge metadata + full history. */
export interface ForgedToolLedgerEntry {
firstForgedTurn: number;
firstForgedDepartment: string;
firstForgedEventIndex: number;
firstForgedEventTitle: string;
inputSchema?: unknown;
outputSchema?: unknown;
/** Append-only history of every invocation across the run. */
history: ToolUseRecord[];
}
/** Shorthand alias used across the orchestrator's closures. */
export type ForgedLedger = Map<string, ForgedToolLedgerEntry>;
/**
* Whether the reviewers approved a tool, from its ledger entry: one of its
* forges passed. A department that cites a tool without calling forge_tool
* writes a use that is not rejected, and that use approves nothing: a tool
* whose every forge was rejected stays rejected however often it is cited.
* A tool that first appears as such a use is a forge the run did not
* capture, which a department's report names, and that first use counts.
*/
function ledgerApproved(entry: ForgedToolLedgerEntry): boolean {
const first = entry.history[0];
if (first && !first.isReforge && !first.rejected) return true;
return entry.history.some(h => h.isReforge && !h.rejected);
}
/**
* Build the ALREADY-FORGED TOOLS block appended to every department's
* per-turn context. Tells the LLM which tools are available to reuse,
* shows each tool's last approved output so the LLM can cite a number
* without re-forging, and enforces a hard rule against re-forging by
* the same name.
*
* Only approved (non-rejected) tools appear. Capped at 20 most recent
* so the block stays under ~500 tokens even in long runs.
*
* Returns an empty string when no tools have been approved yet, so
* callers can concatenate unconditionally without an empty section
* appearing in turn 1 prompts.
*/
export function buildAvailableToolsBlock(ledger: ForgedLedger): string {
type Entry = { name: string; dept: string; title: string; output: string | null };
const approved: Entry[] = [];
for (const [name, entry] of ledger.entries()) {
if (!ledgerApproved(entry)) continue;
const lastApproved = [...entry.history].reverse().find(h => !h.rejected);
if (!lastApproved) continue;
approved.push({
name,
dept: entry.firstForgedDepartment,
title: entry.firstForgedEventTitle || name,
// Truncate aggressively so the block stays under a few hundred
// tokens even with 20 tools. 180 chars covers a compact JSON
// like {"score":42,"warnings":[]} which is what the LLM cites.
output: lastApproved.output ? lastApproved.output.slice(0, 180) : null,
});
}
if (approved.length === 0) return '';
const lines = approved.slice(-20).reverse().map(t => {
const head = `- ${t.name} (${t.dept}): ${t.title}`;
return t.output ? `${head}\n last output: ${t.output}` : head;
}).join('\n');
return `\n\nALREADY-FORGED TOOLS IN THIS SESSION:\n${lines}\n\nHARD RULE — how to reuse:\n- Preferred: call the tool for real with fresh inputs via call_forged_tool({"name":"<tool_name>","args":{...}}). This produces a new output using the approved, judge-reviewed code. Then cite the tool in "forgedToolsUsed" with the new output.\n- If the analysis is essentially unchanged from last invocation, cite the tool name in "forgedToolsUsed" and reference the last output from the list above in your summary — no tool call needed.\n- Do NOT call forge_tool with a name from this list. Re-forging costs −0.06 outcome bonus and −0.015 morale. Only forge a NEW tool when NO existing one applies. Each reuse (via call_forged_tool or citation) is +0.02 outcome bonus.\n`;
}
/** A forge as a department's forge_tool call captured it. */
export interface CapturedForgeForLedger {
name: string;
description: string;
mode: string;
approved: boolean;
confidence: number;
department: string;
inputSchema: unknown;
outputSchema: unknown;
output: unknown;
}
/** The event a run's forges were made in. */
export interface ForgeEvent {
turn: number;
time: number;
eventIndex: number;
eventTitle: string;
}
/**
* Take one event's captured forges into the run, in the order captured:
* each joins `allForges` with the event's turn, time and index.
*
* A department's report writes the ledger for the forges it made, and a
* department whose session threw after a forge (a transient provider error
* on its next model call) returns the empty report and writes nothing,
* though the engine holds the tool. A report also records a tool once per
* event, from its last capture, so a department that forged a tool, approved,
* and then forged it again, rejected, has a rejected record for it. So an
* approved forge whose department has no approved ledger record for this
* event is recorded here: as the tool's first forge when the ledger has no
* entry for it, else as a further forge in its history. Like a report, it
* records a tool once for its department and the event, from the
* department's last approved capture of it (its output, confidence and, for
* a first forge, schemas). A forge the report recorded as approved is not
* recorded again, and a rejected forge is no tool of the run either way.
*/
export function recordEventForges<F extends CapturedForgeForLedger>(
eventForges: readonly F[],
event: ForgeEvent,
ledger: ForgedLedger,
allForges: Array<F & { turn: number; time: number; eventIndex: number }>,
): void {
// Each department's last approved capture of each tool, in the order of
// its first approved capture.
const lastApproved = new Map<string, F>();
for (const forge of eventForges) {
allForges.push({ ...forge, turn: event.turn, time: event.time, eventIndex: event.eventIndex });
if (forge.approved) lastApproved.set(JSON.stringify([forge.department, forge.name]), forge);
}
for (const forge of lastApproved.values()) {
const known = ledger.get(forge.name);
const recorded = known?.history.some(h =>
h.turn === event.turn && h.eventIndex === event.eventIndex && h.department === forge.department && !h.rejected,
);
if (recorded) continue;
const entry: ForgedToolLedgerEntry = known ?? {
firstForgedTurn: event.turn,
firstForgedDepartment: forge.department,
firstForgedEventIndex: event.eventIndex,
firstForgedEventTitle: event.eventTitle,
inputSchema: forge.inputSchema,
outputSchema: forge.outputSchema,
history: [],
};
if (!known) ledger.set(forge.name, entry);
const output = forge.output != null
? (typeof forge.output === 'string' ? forge.output : JSON.stringify(forge.output))
: null;
entry.history.push({
turn: event.turn,
time: event.time,
eventIndex: event.eventIndex,
eventTitle: event.eventTitle,
department: forge.department,
output: output?.slice(0, 400) || null,
isReforge: true,
rejected: false,
confidence: forge.confidence,
});
}
}
/** One entry in the canonical Forged Toolbox that ships in the run output. */
export interface ForgedToolboxEntry {
name: string;
description: string;
mode: string;
firstForgedTurn: number;
firstForgedDepartment: string;
firstForgedEventTitle: string;
departments: string[];
/** Total uses minus the original forge. */
reuseCount: number;
/** Total uses including the original forge. */
totalUses: number;
/** How many of those uses were re-forge attempts. */
reforgeCount: number;
/** Re-forge attempts that the judge rejected. */
rejectedReforges: number;
approved: boolean;
confidence: number;
inputSchema: unknown;
outputSchema: unknown;
sampleOutput: unknown;
history: ToolUseRecord[];
}
/** Minimal shape of a captured forge event needed to hydrate toolbox entries. */
export interface ForgeCaptureForToolbox {
name: string;
description: string;
mode: string;
approved: boolean;
confidence: number;
}
/**
* Build the canonical Forged Toolbox from a completed run's ledger.
* Deduplicates by tool name, preserves first-forge metadata, and rolls
* up reuse/reforge/rejection counts from the full invocation history.
* Pure function — takes the ledger + the raw stream of captured forge
* events and returns a sorted, enriched array ready for serialization.
*/
export function buildForgedToolbox(
ledger: ForgedLedger,
allForges: readonly ForgeCaptureForToolbox[],
): ForgedToolboxEntry[] {
const out: ForgedToolboxEntry[] = [];
for (const [name, entry] of ledger) {
const firstForge = allForges.find(f => f.name === name);
const departments = new Set<string>();
let maxConfidence = 0;
let maxForgeConfidence = 0;
let sampleOutput: string | null = null;
let reforgeCount = 0;
let rejectedReforges = 0;
for (const h of entry.history) {
departments.add(h.department);
if (typeof h.confidence === 'number' && h.confidence > maxConfidence) maxConfidence = h.confidence;
if (h.output) sampleOutput = h.output;
if (h.isReforge) {
reforgeCount++;
if (h.rejected) rejectedReforges++;
if (typeof h.confidence === 'number' && h.confidence > maxForgeConfidence) maxForgeConfidence = h.confidence;
}
}
const approved = ledgerApproved(entry) || (firstForge?.approved ?? false);
const totalUses = entry.history.length;
out.push({
name,
description: firstForge?.description || name,
mode: firstForge?.mode || 'sandbox',
firstForgedTurn: entry.firstForgedTurn,
firstForgedDepartment: entry.firstForgedDepartment,
firstForgedEventTitle: entry.firstForgedEventTitle,
departments: [...departments],
reuseCount: Math.max(0, totalUses - 1),
totalUses,
reforgeCount,
rejectedReforges,
approved,
// Highest judge confidence across this tool's invocations. When
// no approval ever landed, confidence is 0 — never fabricate a
// default that misrepresents rejected tools as borderline. A
// rejected tool keeps its forges' confidence: the confidence a
// department wrote when it cited the tool is not the judge's.
confidence: approved
? maxConfidence || (firstForge?.approved ? firstForge.confidence : 0)
: maxForgeConfidence,
inputSchema: entry.inputSchema,
outputSchema: entry.outputSchema,
sampleOutput,
history: entry.history.slice(),
});
}
return out.sort((a, b) => a.firstForgedTurn - b.firstForgedTurn);
}
/**
* The tools a run has forged so far, one name each: the approved entries of
* the toolbox `buildForgedToolbox` builds from the same ledger, which is what
* the run's artifact ships as `forgedTools` and what the verdict counts. A
* tool used by three departments is one tool, and a forge attempt the
* reviewers rejected is none, also when a department cites it later. The
* stream's `toolsForged` and the director's "Tools forged so far" read this.
*/
export function forgedToolNames(
ledger: ForgedLedger,
allForges: readonly ForgeCaptureForToolbox[],
): string[] {
return buildForgedToolbox(ledger, allForges).filter(tool => tool.approved).map(tool => tool.name);
}
/** The tool modifiers an event's outcome reads; see {@link OutcomeModifiers}. */
export type EventToolModifiers = NonNullable<OutcomeModifiers['toolModifiers']>;
/**
* The tool modifiers of one event's outcome. `eventForges` are the forges
* the event's departments made during the event, in the order captured: an
* approved one is a new tool when the ledger first saw its name this turn
* (once per name), else a reuse; a rejected one is a failed forge.
* `totalToolsForRun` is the run-wide count the innovation bonus reads: the
* run's approved tools, each once, as {@link forgedToolNames} counts them for
* the stream, the director and the verdict. A tool whose every forge the
* reviewers rejected is none, so it raises no outcome.
*/
export function eventToolModifiers(
eventForges: readonly Pick<ForgeCaptureForToolbox, 'name' | 'approved'>[],
ledger: ForgedLedger,
allForges: readonly ForgeCaptureForToolbox[],
turn: number,
): EventToolModifiers {
let newCount = 0, reuseCount = 0, failCount = 0;
const newNamesThisEvent = new Set<string>();
for (const f of eventForges) {
if (!f.approved) { failCount++; continue; }
// First-time forges: name not seen before (firstForgedTurn === turn
// AND we haven't already counted it elsewhere this event).
const ledgerEntry = ledger.get(f.name);
const isFirstUse = ledgerEntry?.firstForgedTurn === turn && !newNamesThisEvent.has(f.name);
if (isFirstUse) {
newCount++;
newNamesThisEvent.add(f.name);
} else {
reuseCount++;
}
}
return {
newToolsThisEvent: newCount,
reuseCountThisEvent: reuseCount,
forgeFailures: failCount,
totalToolsForRun: forgedToolNames(ledger, allForges).length,
};
}