@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +70 -37
  4. package/dist/cli.js +6572 -6700
  5. package/dist/terminal-pi.js +51 -38
  6. package/docs/terminal-tui.md +50 -12
  7. package/package.json +2 -2
  8. package/src/accounts/accounts.ts +4 -3
  9. package/src/capabilities/api.ts +1 -1
  10. package/src/cli.ts +7 -3
  11. package/src/core/bus.test.ts +15 -1
  12. package/src/core/bus.ts +39 -0
  13. package/src/core/caller.ts +34 -0
  14. package/src/core/fs.ts +17 -1
  15. package/src/harness.ts +55 -66
  16. package/src/{memory → history}/recall.test.ts +1 -1
  17. package/src/{memory → history}/recall.ts +1 -1
  18. package/src/{memory → history}/trajectory.test.ts +4 -4
  19. package/src/{memory → history}/trajectory.ts +1 -1
  20. package/src/hub/hub.test.ts +4 -5
  21. package/src/hub/hub.ts +2 -2
  22. package/src/index.ts +28 -14
  23. package/src/mcp/connections.ts +1 -1
  24. package/src/mcp/service.ts +1 -1
  25. package/src/pi/proxy.test.ts +68 -0
  26. package/src/pi/runtime.test.ts +61 -0
  27. package/src/pi/runtime.ts +50 -0
  28. package/src/plugins/api.ts +11 -9
  29. package/src/plugins/builtin.ts +22 -40
  30. package/src/plugins/catalogue.test.ts +13 -41
  31. package/src/plugins/catalogue.ts +3 -32
  32. package/src/plugins/discover.ts +2 -1
  33. package/src/plugins/firstParty.test.ts +1 -1
  34. package/src/plugins/hooks.test.ts +21 -71
  35. package/src/plugins/hooks.ts +6 -7
  36. package/src/plugins/identity.ts +16 -2
  37. package/src/plugins/install.test.ts +1 -1
  38. package/src/plugins/install.ts +2 -1
  39. package/src/plugins/inventory.ts +19 -18
  40. package/src/plugins/plugins.ts +38 -2
  41. package/src/prompt/compile.test.ts +82 -193
  42. package/src/prompt/compile.ts +98 -363
  43. package/src/refine/cli.ts +114 -0
  44. package/src/refine/demo.ts +124 -0
  45. package/src/refine/evidence.ts +349 -0
  46. package/src/refine/host.ts +102 -0
  47. package/src/refine/ledger.ts +104 -0
  48. package/src/refine/library.ts +192 -0
  49. package/src/refine/pi.test.ts +90 -0
  50. package/src/refine/pi.ts +218 -0
  51. package/src/refine/prompts.ts +430 -0
  52. package/src/refine/recursion.ts +353 -0
  53. package/src/refine/refine.test.ts +140 -0
  54. package/src/refine/runner.ts +205 -0
  55. package/src/refine/selection.ts +291 -0
  56. package/src/refine/service.ts +194 -0
  57. package/src/refine/trajectory.ts +71 -0
  58. package/src/refine/types.ts +91 -0
  59. package/src/server/methods.ts +137 -123
  60. package/src/server/server.test.ts +37 -30
  61. package/src/sessions/contracts.ts +7 -2
  62. package/src/sessions/conversation.test.ts +2 -27
  63. package/src/sessions/planMode.test.ts +8 -1
  64. package/src/sessions/planMode.ts +17 -9
  65. package/src/sessions/policy.ts +1 -1
  66. package/src/sessions/pricing.ts +3 -1
  67. package/src/sessions/questionBatches.test.ts +15 -3
  68. package/src/sessions/registry.ts +13 -1
  69. package/src/sessions/runStore.ts +3 -1
  70. package/src/sessions/runner.ts +10 -5
  71. package/src/sessions/sessions.test.ts +60 -13
  72. package/src/sessions/sessions.ts +43 -18
  73. package/src/sessions/subagentService.test.ts +22 -1
  74. package/src/sessions/subagentService.ts +14 -1
  75. package/src/sessions/subagents.ts +2 -0
  76. package/src/terminal/cli.ts +24 -48
  77. package/src/terminal/logs.test.ts +26 -0
  78. package/src/terminal/logs.ts +67 -0
  79. package/src/terminal/piExtension.lifecycle.test.ts +1 -1
  80. package/src/terminal/piExtension.ts +3 -3
  81. package/src/terminal/pluginControl.test.ts +7 -8
  82. package/src/terminal/socket.ts +3 -3
  83. package/src/terminal/terminal.test.ts +41 -39
  84. package/src/terminal/terminals.ts +31 -13
  85. package/src/terminal/tui/application.test.ts +58 -0
  86. package/src/terminal/tui/application.ts +15 -4
  87. package/src/terminal/tui/clipboardFiles.test.ts +94 -0
  88. package/src/terminal/tui/clipboardFiles.ts +86 -0
  89. package/src/terminal/tui/commands.test.ts +13 -1
  90. package/src/terminal/tui/commands.ts +16 -0
  91. package/src/terminal/tui/logPanel.test.ts +61 -0
  92. package/src/terminal/tui/logPanel.ts +175 -0
  93. package/src/terminal/tui/piAdapter.ts +7 -4
  94. package/src/terminal/tui/questionPicker.ts +168 -0
  95. package/src/terminal/tui/questions.test.ts +86 -19
  96. package/src/terminal/tui/questions.ts +27 -22
  97. package/src/terminal/tui/runtime.ts +2 -0
  98. package/src/tools/askUser.test.ts +23 -2
  99. package/src/tools/askUser.ts +15 -5
  100. package/src/tools/definitions.ts +49 -132
  101. package/src/tools/dispatch.ts +78 -0
  102. package/src/tools/harnessTools.ts +23 -0
  103. package/src/tools/spawnSubagent.ts +3 -3
  104. package/src/tools/types.ts +3 -1
  105. package/src/tree/components.ts +221 -0
  106. package/src/tree/materialise.ts +122 -0
  107. package/src/tree/seed.ts +123 -0
  108. package/src/tree/store.ts +193 -0
  109. package/src/tree/surface.ts +135 -0
  110. package/src/tree/tree.test.ts +126 -0
  111. package/src/tree/tree.ts +346 -0
  112. package/src/tree/trust.ts +86 -0
  113. package/src/web/tools.ts +27 -5
  114. package/src/web/web.test.ts +26 -1
  115. package/src/wiki/notes.ts +313 -0
  116. package/src/memory/api.test.ts +0 -82
  117. package/src/memory/api.ts +0 -303
  118. package/src/memory/blocks.test.ts +0 -78
  119. package/src/memory/blocks.ts +0 -408
  120. package/src/memory/caller.ts +0 -69
  121. package/src/memory/curation.test.ts +0 -206
  122. package/src/memory/curation.ts +0 -64
  123. package/src/memory/dispatch.ts +0 -64
  124. package/src/memory/facts.test.ts +0 -41
  125. package/src/memory/facts.ts +0 -248
  126. package/src/memory/graph.ts +0 -103
  127. package/src/memory/health.test.ts +0 -130
  128. package/src/memory/health.ts +0 -74
  129. package/src/memory/maintainer.ts +0 -272
  130. package/src/memory/maintenance.test.ts +0 -192
  131. package/src/memory/maintenance.ts +0 -679
  132. package/src/memory/migrate.ts +0 -79
  133. package/src/memory/operations.ts +0 -150
  134. package/src/memory/proposals.test.ts +0 -168
  135. package/src/memory/proposals.ts +0 -584
  136. package/src/memory/references.ts +0 -77
  137. package/src/memory/runContext.ts +0 -93
  138. package/src/memory/store.test.ts +0 -66
  139. package/src/memory/store.ts +0 -385
  140. package/src/memory/wiki.test.ts +0 -47
  141. package/src/memory/wiki.ts +0 -423
  142. package/src/memory/wikiIndex.test.ts +0 -88
  143. package/src/memory/wikiIndex.ts +0 -117
  144. package/src/plugins/proposal.ts +0 -121
  145. package/src/skills/registry.test.ts +0 -104
  146. package/src/skills/registry.ts +0 -413
  147. package/src/tools/maintenance.ts +0 -29
  148. package/src/tools/memoryTools.ts +0 -22
  149. package/src/tools/pluginPropose.ts +0 -41
  150. package/src/tools/registryWrite.test.ts +0 -101
  151. package/src/tools/registryWrite.ts +0 -182
  152. /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
+ }