@xingular/harness 2.1.1 → 3.0.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/CHANGELOG.md +18 -0
- package/NOTICE.md +22 -0
- package/README.md +70 -37
- package/dist/cli.js +6572 -6700
- package/dist/terminal-pi.js +51 -38
- package/docs/terminal-tui.md +50 -12
- package/package.json +2 -2
- package/src/accounts/accounts.ts +4 -3
- package/src/capabilities/api.ts +1 -1
- package/src/cli.ts +7 -3
- package/src/core/bus.test.ts +15 -1
- package/src/core/bus.ts +39 -0
- package/src/core/caller.ts +34 -0
- package/src/core/fs.ts +17 -1
- package/src/harness.ts +55 -66
- package/src/{memory → history}/recall.test.ts +1 -1
- package/src/{memory → history}/recall.ts +1 -1
- package/src/{memory → history}/trajectory.test.ts +4 -4
- package/src/{memory → history}/trajectory.ts +1 -1
- package/src/hub/hub.test.ts +4 -5
- package/src/hub/hub.ts +2 -2
- package/src/index.ts +28 -14
- package/src/mcp/connections.ts +1 -1
- package/src/mcp/service.ts +1 -1
- package/src/pi/proxy.test.ts +68 -0
- package/src/pi/runtime.test.ts +61 -0
- package/src/pi/runtime.ts +50 -0
- package/src/plugins/api.ts +11 -9
- package/src/plugins/builtin.ts +22 -40
- package/src/plugins/catalogue.test.ts +13 -41
- package/src/plugins/catalogue.ts +3 -32
- package/src/plugins/discover.ts +2 -1
- package/src/plugins/firstParty.test.ts +1 -1
- package/src/plugins/hooks.test.ts +21 -71
- package/src/plugins/hooks.ts +6 -7
- package/src/plugins/identity.ts +16 -2
- package/src/plugins/install.test.ts +1 -1
- package/src/plugins/install.ts +2 -1
- package/src/plugins/inventory.ts +19 -18
- package/src/plugins/plugins.ts +38 -2
- package/src/prompt/compile.test.ts +82 -193
- package/src/prompt/compile.ts +98 -363
- package/src/refine/cli.ts +114 -0
- package/src/refine/demo.ts +124 -0
- package/src/refine/evidence.ts +349 -0
- package/src/refine/host.ts +102 -0
- package/src/refine/ledger.ts +104 -0
- package/src/refine/library.ts +192 -0
- package/src/refine/pi.test.ts +90 -0
- package/src/refine/pi.ts +218 -0
- package/src/refine/prompts.ts +430 -0
- package/src/refine/recursion.ts +353 -0
- package/src/refine/refine.test.ts +140 -0
- package/src/refine/runner.ts +205 -0
- package/src/refine/selection.ts +291 -0
- package/src/refine/service.ts +194 -0
- package/src/refine/trajectory.ts +71 -0
- package/src/refine/types.ts +91 -0
- package/src/server/methods.ts +137 -123
- package/src/server/server.test.ts +37 -30
- package/src/sessions/contracts.ts +7 -2
- package/src/sessions/conversation.test.ts +2 -27
- package/src/sessions/planMode.test.ts +8 -1
- package/src/sessions/planMode.ts +17 -9
- package/src/sessions/policy.ts +1 -1
- package/src/sessions/pricing.ts +3 -1
- package/src/sessions/questionBatches.test.ts +15 -3
- package/src/sessions/registry.ts +13 -1
- package/src/sessions/runStore.ts +3 -1
- package/src/sessions/runner.ts +10 -5
- package/src/sessions/sessions.test.ts +60 -13
- package/src/sessions/sessions.ts +43 -18
- package/src/sessions/subagentService.test.ts +22 -1
- package/src/sessions/subagentService.ts +14 -1
- package/src/sessions/subagents.ts +2 -0
- package/src/terminal/cli.ts +24 -48
- package/src/terminal/logs.test.ts +26 -0
- package/src/terminal/logs.ts +67 -0
- package/src/terminal/piExtension.lifecycle.test.ts +1 -1
- package/src/terminal/piExtension.ts +3 -3
- package/src/terminal/pluginControl.test.ts +7 -8
- package/src/terminal/socket.ts +3 -3
- package/src/terminal/terminal.test.ts +41 -39
- package/src/terminal/terminals.ts +31 -13
- package/src/terminal/tui/application.test.ts +58 -0
- package/src/terminal/tui/application.ts +15 -4
- package/src/terminal/tui/clipboardFiles.test.ts +94 -0
- package/src/terminal/tui/clipboardFiles.ts +86 -0
- package/src/terminal/tui/commands.test.ts +13 -1
- package/src/terminal/tui/commands.ts +16 -0
- package/src/terminal/tui/logPanel.test.ts +61 -0
- package/src/terminal/tui/logPanel.ts +175 -0
- package/src/terminal/tui/piAdapter.ts +7 -4
- package/src/terminal/tui/questionPicker.ts +168 -0
- package/src/terminal/tui/questions.test.ts +86 -19
- package/src/terminal/tui/questions.ts +27 -22
- package/src/terminal/tui/runtime.ts +2 -0
- package/src/tools/askUser.test.ts +23 -2
- package/src/tools/askUser.ts +15 -5
- package/src/tools/definitions.ts +49 -132
- package/src/tools/dispatch.ts +78 -0
- package/src/tools/harnessTools.ts +23 -0
- package/src/tools/spawnSubagent.ts +3 -3
- package/src/tools/types.ts +3 -1
- package/src/tree/components.ts +221 -0
- package/src/tree/materialise.ts +122 -0
- package/src/tree/seed.ts +123 -0
- package/src/tree/store.ts +193 -0
- package/src/tree/surface.ts +135 -0
- package/src/tree/tree.test.ts +126 -0
- package/src/tree/tree.ts +346 -0
- package/src/tree/trust.ts +86 -0
- package/src/web/tools.ts +27 -5
- package/src/web/web.test.ts +26 -1
- package/src/wiki/notes.ts +313 -0
- package/src/memory/api.test.ts +0 -82
- package/src/memory/api.ts +0 -303
- package/src/memory/blocks.test.ts +0 -78
- package/src/memory/blocks.ts +0 -408
- package/src/memory/caller.ts +0 -69
- package/src/memory/curation.test.ts +0 -206
- package/src/memory/curation.ts +0 -64
- package/src/memory/dispatch.ts +0 -64
- package/src/memory/facts.test.ts +0 -41
- package/src/memory/facts.ts +0 -248
- package/src/memory/graph.ts +0 -103
- package/src/memory/health.test.ts +0 -130
- package/src/memory/health.ts +0 -74
- package/src/memory/maintainer.ts +0 -272
- package/src/memory/maintenance.test.ts +0 -192
- package/src/memory/maintenance.ts +0 -679
- package/src/memory/migrate.ts +0 -79
- package/src/memory/operations.ts +0 -150
- package/src/memory/proposals.test.ts +0 -168
- package/src/memory/proposals.ts +0 -584
- package/src/memory/references.ts +0 -77
- package/src/memory/runContext.ts +0 -93
- package/src/memory/store.test.ts +0 -66
- package/src/memory/store.ts +0 -385
- package/src/memory/wiki.test.ts +0 -47
- package/src/memory/wiki.ts +0 -423
- package/src/memory/wikiIndex.test.ts +0 -88
- package/src/memory/wikiIndex.ts +0 -117
- package/src/plugins/proposal.ts +0 -121
- package/src/skills/registry.test.ts +0 -104
- package/src/skills/registry.ts +0 -413
- package/src/tools/maintenance.ts +0 -29
- package/src/tools/memoryTools.ts +0 -22
- package/src/tools/pluginPropose.ts +0 -41
- package/src/tools/registryWrite.test.ts +0 -101
- package/src/tools/registryWrite.ts +0 -182
- /package/src/{memory → core}/secrets.ts +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Refinements as the harness offers them: start one in the background, follow it, stop it, read
|
|
2
|
+
// what it produced, and adopt its chosen harness. The method itself is in `runner.ts`.
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import type { Bus } from '../core/bus'
|
|
8
|
+
import { HarnessError, invalid, notFound } from '../core/errors'
|
|
9
|
+
import { readJsonOr, safeId } from '../core/fs'
|
|
10
|
+
import * as store from '../tree/store'
|
|
11
|
+
import { PI_SURFACE } from '../tree/surface'
|
|
12
|
+
import { diffSummary, readTree, type TreeDiff } from '../tree/tree'
|
|
13
|
+
import type { TreeTrust } from '../tree/trust'
|
|
14
|
+
import { DEFAULT_REFINE_HOST, type RefineHost } from './host'
|
|
15
|
+
import { Library, libraryAreas, libraryScopes } from './library'
|
|
16
|
+
import { PiExecutor, PiImprover, type ImproverSettings, type RefineRuntime } from './pi'
|
|
17
|
+
import { checkWorkPrompts } from './prompts'
|
|
18
|
+
import { readSummary, refineDir, runRefinement, type RefineEvent, type RefineSummary } from './runner'
|
|
19
|
+
import type { Executor, Improver, RecursionMode, RefineTask } from './types'
|
|
20
|
+
|
|
21
|
+
export interface StartInput {
|
|
22
|
+
task: RefineTask
|
|
23
|
+
k?: number
|
|
24
|
+
mode?: RecursionMode
|
|
25
|
+
/** The model and account improver, selector and reflector sessions use. Defaults to the account default. */
|
|
26
|
+
improver?: ImproverSettings
|
|
27
|
+
/** Start from this version of the scope instead of its head. */
|
|
28
|
+
baseVersion?: string | null
|
|
29
|
+
id?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CandidateView {
|
|
33
|
+
label: string
|
|
34
|
+
kind: 'base' | 'candidate' | 'final'
|
|
35
|
+
ok: boolean
|
|
36
|
+
delivered: boolean
|
|
37
|
+
runId: string
|
|
38
|
+
fingerprint: string | null
|
|
39
|
+
diff: TreeDiff | null
|
|
40
|
+
files: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RefinementsOptions {
|
|
44
|
+
root: () => string
|
|
45
|
+
bus: Bus
|
|
46
|
+
runtime: RefineRuntime
|
|
47
|
+
host: () => RefineHost | null
|
|
48
|
+
trust: TreeTrust
|
|
49
|
+
/** Replace the Pi executor and improver, for tests and the demo. */
|
|
50
|
+
executor?: (id: string, dir: string, signal: AbortSignal) => Executor
|
|
51
|
+
improver?: (id: string, dir: string, settings: ImproverSettings, signal: AbortSignal) => Improver
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Refinements {
|
|
55
|
+
private readonly active = new Map<string, { controller: AbortController; done: Promise<RefineSummary | null> }>()
|
|
56
|
+
|
|
57
|
+
constructor(private readonly options: RefinementsOptions) {}
|
|
58
|
+
|
|
59
|
+
/** Starts a refinement and returns at once; progress arrives as `refine.changed` and `refine.event` notices. */
|
|
60
|
+
start(input: StartInput): { id: string } {
|
|
61
|
+
const root = this.options.root()
|
|
62
|
+
const task = input.task
|
|
63
|
+
if (!task || typeof task.id !== 'string' || !task.id.trim() || typeof task.prompt !== 'string' || !task.prompt.trim()) throw invalid('A refinement needs a task with an id and a request.')
|
|
64
|
+
safeId(task.scope)
|
|
65
|
+
if (task.prompts) checkWorkPrompts(task.prompts)
|
|
66
|
+
const k = input.k ?? 1
|
|
67
|
+
const mode = input.mode ?? 'parallel'
|
|
68
|
+
if (mode !== 'parallel' && mode !== 'sequential') throw invalid('mode is parallel or sequential.')
|
|
69
|
+
const base = input.baseVersion ? { tree: store.readVersion(root, task.scope, input.baseVersion) } : store.headTree(root, task.scope)
|
|
70
|
+
if (!base) throw new HarnessError('harness_missing', `The harness '${task.scope}' has no version yet.`, 'Create or seed it first.')
|
|
71
|
+
const id = input.id ? safeId(input.id) : `${new Date().toISOString().slice(0, 10)}-${randomUUID().slice(0, 8)}`
|
|
72
|
+
if (this.active.has(id)) throw invalid('That refinement is already running.')
|
|
73
|
+
const dir = refineDir(root, id)
|
|
74
|
+
const controller = new AbortController()
|
|
75
|
+
const host = this.options.host() ?? DEFAULT_REFINE_HOST
|
|
76
|
+
const executor = this.options.executor?.(id, dir, controller.signal) ?? new PiExecutor(this.options.runtime, host, id, dir, controller.signal)
|
|
77
|
+
const improver = this.options.improver?.(id, dir, input.improver ?? {}, controller.signal) ?? new PiImprover(this.options.runtime, dir, input.improver ?? {}, controller.signal)
|
|
78
|
+
const emit = (event: RefineEvent) => {
|
|
79
|
+
if (event.type === 'summary') this.options.bus.emit({ type: 'refine.changed', id, scope: task.scope, status: event.summary.status })
|
|
80
|
+
else this.options.bus.emit({ type: 'refine.event', id, scope: task.scope, event })
|
|
81
|
+
}
|
|
82
|
+
const done = runRefinement({ root, id, task, k, mode, executor, improver, baseTree: base.tree, onEvent: emit, signal: controller.signal })
|
|
83
|
+
.catch((error) => {
|
|
84
|
+
this.options.bus.emit({ type: 'refine.changed', id, scope: task.scope, status: 'failed', error: error instanceof Error ? error.message : String(error) })
|
|
85
|
+
return null
|
|
86
|
+
})
|
|
87
|
+
.finally(() => this.active.delete(id))
|
|
88
|
+
this.active.set(id, { controller, done })
|
|
89
|
+
return { id }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Waits for a running refinement; resolves at once for one that is not running. */
|
|
93
|
+
async wait(id: string): Promise<RefineSummary | null> {
|
|
94
|
+
return (await this.active.get(id)?.done) ?? readSummary(this.options.root(), id)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get(id: string): RefineSummary {
|
|
98
|
+
const summary = readSummary(this.options.root(), safeId(id))
|
|
99
|
+
if (!summary) throw notFound('That refinement')
|
|
100
|
+
return summary
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
running(): string[] {
|
|
104
|
+
return [...this.active.keys()]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every recorded refinement, newest first, optionally for one scope. */
|
|
108
|
+
list(options: { scope?: string | null; limit?: number } = {}): RefineSummary[] {
|
|
109
|
+
const dir = join(this.options.root(), 'refine')
|
|
110
|
+
if (!existsSync(dir)) return []
|
|
111
|
+
const out: RefineSummary[] = []
|
|
112
|
+
for (const name of readdirSync(dir).sort().reverse()) {
|
|
113
|
+
if (name === 'library') continue
|
|
114
|
+
const summary = readSummary(this.options.root(), name)
|
|
115
|
+
if (summary && (!options.scope || summary.scope === options.scope)) out.push(summary)
|
|
116
|
+
}
|
|
117
|
+
return out.sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, options.limit ?? 100)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async cancel(id: string): Promise<void> {
|
|
121
|
+
const running = this.active.get(id)
|
|
122
|
+
if (!running) return
|
|
123
|
+
running.controller.abort()
|
|
124
|
+
await running.done
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Every execution of a refinement: what harness it ran, how it differs from the base, what it produced. */
|
|
128
|
+
candidates(id: string): CandidateView[] {
|
|
129
|
+
const root = this.options.root()
|
|
130
|
+
const summary = this.get(id)
|
|
131
|
+
const dir = refineDir(root, id)
|
|
132
|
+
const baseTree = readTree(PI_SURFACE, join(dir, 'harnesses', 'base'))
|
|
133
|
+
const finals = new Set(summary.recursion?.finals ?? [])
|
|
134
|
+
const labels = existsSync(join(dir, 'rollouts')) ? readdirSync(join(dir, 'rollouts')).sort() : []
|
|
135
|
+
return labels.map((label) => {
|
|
136
|
+
const record = readJsonOr<{ status?: string; files?: string[] }>(join(dir, 'rollouts', label, 'rollout.json'), {})
|
|
137
|
+
const base = summary.base.find((entry) => entry.label === label)
|
|
138
|
+
const harnessDir = join(dir, 'harnesses', label)
|
|
139
|
+
const tree = existsSync(harnessDir) ? readTree(PI_SURFACE, harnessDir) : null
|
|
140
|
+
const files = record.files ?? []
|
|
141
|
+
return {
|
|
142
|
+
label,
|
|
143
|
+
kind: label.startsWith('base') ? 'base' : finals.has(label) ? 'final' : 'candidate',
|
|
144
|
+
ok: base ? base.ok : record.status ? record.status === 'completed' : true,
|
|
145
|
+
delivered: base ? base.delivered : files.length > 0,
|
|
146
|
+
runId: `${id}-${label}`,
|
|
147
|
+
fingerprint: tree ? store.fingerprint(tree) : null,
|
|
148
|
+
diff: tree && !label.startsWith('base') ? diffSummary(PI_SURFACE, baseTree, tree) : null,
|
|
149
|
+
files
|
|
150
|
+
} satisfies CandidateView
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Makes a refinement's chosen harness (or one of its candidates, by label) the scope's head.
|
|
156
|
+
* Plugin code in it must already be trusted.
|
|
157
|
+
*/
|
|
158
|
+
adopt(id: string, options: { label?: string | null } = {}): store.Head {
|
|
159
|
+
const root = this.options.root()
|
|
160
|
+
const summary = this.get(id)
|
|
161
|
+
const label = options.label ?? summary.selection?.chosenLabel
|
|
162
|
+
if (!label) throw invalid('This refinement has nothing to adopt.')
|
|
163
|
+
const tree = readTree(PI_SURFACE, join(refineDir(root, id), 'harnesses', safeId(label)))
|
|
164
|
+
if (!Object.keys(tree).length) throw notFound('That candidate harness')
|
|
165
|
+
const untrusted = this.options.trust.review(tree).filter((plugin) => !plugin.trusted)
|
|
166
|
+
if (untrusted.length) throw new HarnessError('plugin_untrusted', `Trust the plugin code first: ${untrusted.map((plugin) => plugin.path).join(', ')}.`, 'Review the plugin files on the Harness page, trust them, then adopt.')
|
|
167
|
+
store.commit(root, summary.scope, tree, { kind: 'refinement', refineId: id, label, note: summary.selection?.why?.slice(0, 500) })
|
|
168
|
+
this.options.bus.emit({ type: 'harnesses.changed', scope: summary.scope })
|
|
169
|
+
return store.head(root, summary.scope)!
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
library(scope: string): { area: string; entries: ReturnType<Library['list']> }[] {
|
|
173
|
+
const root = this.options.root()
|
|
174
|
+
return libraryAreas(root, scope).map((area) => ({ area, entries: new Library(root, scope, area).list() }))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
libraryEntry(scope: string, area: string, task: string): { experience: string | null; tree: Record<string, string> } {
|
|
178
|
+
const library = new Library(this.options.root(), scope, area)
|
|
179
|
+
return { experience: library.experienceText(task), tree: library.treeOf(task) }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
forget(scope: string, area: string, tasks: string[]): void {
|
|
183
|
+
new Library(this.options.root(), scope, area).forget(tasks)
|
|
184
|
+
this.options.bus.emit({ type: 'library.changed', scope })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
scopes(): string[] {
|
|
188
|
+
return libraryScopes(this.options.root())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async close(): Promise<void> {
|
|
192
|
+
for (const id of this.running()) await this.cancel(id)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// A run's recorded events in the shared trajectory grammar an improver reads:
|
|
2
|
+
// [USER] / [ASSISTANT] / [ACTION] / [OBSERVATION] / [ERROR]. Every event is kept; only a single
|
|
3
|
+
// observation longer than `obsTrunc` is clipped, with an explicit marker.
|
|
4
|
+
import type { RunEvent } from '../sessions/contracts'
|
|
5
|
+
|
|
6
|
+
const TRUNC = (n: number) => `…(+${n} chars — render truncation)`
|
|
7
|
+
|
|
8
|
+
function clip(text: string, limit: number): string {
|
|
9
|
+
const clean = text.replace(/\0/g, '')
|
|
10
|
+
return limit > 0 && clean.length > limit ? `${clean.slice(0, limit)}${TRUNC(clean.length - limit)}` : clean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function renderEvents(events: readonly RunEvent[], options: { obsTrunc: number; argsTrunc?: number }): string {
|
|
14
|
+
const out: string[] = []
|
|
15
|
+
const argsTrunc = options.argsTrunc ?? 2000
|
|
16
|
+
for (const event of events) {
|
|
17
|
+
const payload = (event.payload ?? {}) as Record<string, unknown>
|
|
18
|
+
switch (event.type) {
|
|
19
|
+
case 'message.completed':
|
|
20
|
+
if (typeof payload.text === 'string' && payload.text.trim()) out.push(`[${payload.role === 'user' ? 'USER' : 'ASSISTANT'}]\n${clip(payload.text, options.obsTrunc)}`)
|
|
21
|
+
break
|
|
22
|
+
case 'tool.started':
|
|
23
|
+
out.push(`[ACTION] ${String(payload.name ?? 'tool')}${typeof payload.input === 'string' && payload.input ? ` ${clip(payload.input, argsTrunc)}` : ''}`)
|
|
24
|
+
break
|
|
25
|
+
case 'tool.completed': {
|
|
26
|
+
const text = typeof payload.output === 'string' ? payload.output : ''
|
|
27
|
+
out.push(`${payload.ok === false ? '[ERROR]' : '[OBSERVATION]'} ${String(payload.name ?? 'tool')}${typeof payload.durationMs === 'number' ? ` (${Math.round(payload.durationMs)} ms)` : ''}\n${clip(text, options.obsTrunc)}${payload.outputTruncated ? '\n(the tool output was already bounded when it was recorded)' : ''}`)
|
|
28
|
+
break
|
|
29
|
+
}
|
|
30
|
+
case 'retry.started':
|
|
31
|
+
out.push(`[ERROR] model request failed, retrying (${String(payload.attempt)}/${String(payload.maxAttempts)}): ${String(payload.error ?? '')}`)
|
|
32
|
+
break
|
|
33
|
+
case 'compaction.ended':
|
|
34
|
+
out.push(`[ACTION] context compacted (${String(payload.reason ?? '')})${typeof payload.tokensBefore === 'number' ? `: ${payload.tokensBefore} → ${String(payload.tokensAfter ?? '?')} tokens` : ''}`)
|
|
35
|
+
break
|
|
36
|
+
case 'approval.resolved':
|
|
37
|
+
if (payload.decision === 'denied') out.push('[ERROR] a tool call was refused')
|
|
38
|
+
break
|
|
39
|
+
case 'extension.notice':
|
|
40
|
+
if (typeof payload.text === 'string') out.push(`[OBSERVATION] notice: ${clip(payload.text, 2000)}`)
|
|
41
|
+
break
|
|
42
|
+
case 'failed':
|
|
43
|
+
out.push(`[ERROR] run failed: ${String(payload.message ?? payload.code ?? '')}`)
|
|
44
|
+
break
|
|
45
|
+
case 'status.changed':
|
|
46
|
+
if (payload.status === 'interrupted') out.push('[ERROR] run interrupted')
|
|
47
|
+
break
|
|
48
|
+
default:
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out.length ? out.join('\n\n') : '(no trajectory: the run recorded no messages or tool use)'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The last assistant message of a run. */
|
|
56
|
+
export function finalText(events: readonly RunEvent[]): string {
|
|
57
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
58
|
+
const payload = events[i]!.payload as Record<string, unknown> | undefined
|
|
59
|
+
if (events[i]!.type === 'message.completed' && payload?.role === 'assistant' && typeof payload.text === 'string' && payload.text.trim()) return payload.text
|
|
60
|
+
}
|
|
61
|
+
return ''
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The run's cost so far, from its last usage report. */
|
|
65
|
+
export function costOf(events: readonly RunEvent[]): number {
|
|
66
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
67
|
+
const payload = events[i]!.payload as Record<string, unknown> | undefined
|
|
68
|
+
if (events[i]!.type === 'usage.updated' && typeof payload?.costUsd === 'number') return payload.costUsd
|
|
69
|
+
}
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The seams of refinement. The method (recursion, selection, reflection, library) needs only
|
|
2
|
+
// these: something that runs a harness on a task (an Executor) and something that runs an
|
|
3
|
+
// improver session over an evidence folder (an Improver). The harness supplies Pi-backed ones;
|
|
4
|
+
// tests and the demo supply deterministic ones.
|
|
5
|
+
//
|
|
6
|
+
// Shaped after Harness² (Apache-2.0, Google LLC; `core/spec.py`, `substrate/base.py`,
|
|
7
|
+
// `core/proposer.py`).
|
|
8
|
+
import type { Tree } from '../tree/surface'
|
|
9
|
+
import type { WorkPrompts } from './prompts'
|
|
10
|
+
|
|
11
|
+
export type RecursionMode = 'parallel' | 'sequential'
|
|
12
|
+
|
|
13
|
+
/** One piece of work to refine on. */
|
|
14
|
+
export interface RefineTask {
|
|
15
|
+
/** Stable id of the task; names its folder and its library entry. */
|
|
16
|
+
id: string
|
|
17
|
+
/** The request, as the agent received it, with any expected answer stripped. */
|
|
18
|
+
prompt: string
|
|
19
|
+
/** The harness scope: whose tree this is and whose library it reads and grows. */
|
|
20
|
+
scope: string
|
|
21
|
+
/** A grouping inside the scope's library. Defaults to the scope. */
|
|
22
|
+
domain?: string
|
|
23
|
+
/** Material the work was done from, served to improvers and the selector as `source/`. */
|
|
24
|
+
sourceDir?: string | null
|
|
25
|
+
/** Paths that must never reach a model (answer keys and the like), as globs. */
|
|
26
|
+
leakGlobs?: string[]
|
|
27
|
+
/** The text about the work the prompts interpolate. */
|
|
28
|
+
prompts?: WorkPrompts
|
|
29
|
+
/** Whether a harness here may carry subagents. Default true. */
|
|
30
|
+
allowSubagents?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the method knows about one finished execution. Never carries a score. */
|
|
34
|
+
export interface Rollout {
|
|
35
|
+
label: string
|
|
36
|
+
runId: string
|
|
37
|
+
/** A folder that holds this execution's evidence. */
|
|
38
|
+
dir: string
|
|
39
|
+
ok: boolean
|
|
40
|
+
delivered: boolean
|
|
41
|
+
costUsd: number
|
|
42
|
+
wallMs: number
|
|
43
|
+
error: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Executor {
|
|
47
|
+
/**
|
|
48
|
+
* Runs one execution of `task` with the harness in `harnessDir` (null: the base harness) and
|
|
49
|
+
* returns what it produced. A failure is returned as `ok: false`, never thrown.
|
|
50
|
+
*/
|
|
51
|
+
execute(task: RefineTask, harnessDir: string | null, label: string, runId: string): Promise<Rollout>
|
|
52
|
+
/** The execution in the shared `[ACTION]/[OBSERVATION]/[ERROR]` grammar. */
|
|
53
|
+
trajectory(rollout: Rollout, options: { obsTrunc: number }): string
|
|
54
|
+
/** filename → readable text, one entry per artifact produced. */
|
|
55
|
+
deliverableFiles(rollout: Rollout): Record<string, string>
|
|
56
|
+
/** Extra validation of a proposed tree for this task; throw with every problem listed. */
|
|
57
|
+
validateTree?(task: RefineTask, tree: Tree): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ImproverReply {
|
|
61
|
+
label: string
|
|
62
|
+
text: string
|
|
63
|
+
costUsd: number
|
|
64
|
+
attempts: number
|
|
65
|
+
cached: boolean
|
|
66
|
+
/** The harvested folder, when the call asked for one; null when it did not come back. */
|
|
67
|
+
workspace: string | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ImproverError extends Error {
|
|
71
|
+
constructor(message: string, readonly attempts = 0, readonly costUsd = 0) {
|
|
72
|
+
super(message)
|
|
73
|
+
this.name = 'ImproverError'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ImproverCall {
|
|
78
|
+
/** The instruction; evidence stays on disk under `context/`. */
|
|
79
|
+
mission: string
|
|
80
|
+
/** The folder copied into the session as `context/`. */
|
|
81
|
+
contextDir: string
|
|
82
|
+
/** A unique label for the audit trail and cache. */
|
|
83
|
+
label: string
|
|
84
|
+
/** A path inside the session's folder to copy out before it is removed, e.g. `context/harness`. */
|
|
85
|
+
harvest?: string
|
|
86
|
+
signal?: AbortSignal
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Improver {
|
|
90
|
+
call(input: ImproverCall): Promise<ImproverReply>
|
|
91
|
+
}
|