@owlmeans/agent 0.1.18-rc.39 → 0.1.18-rc.41
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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent/SKILL.md +96 -9
- package/build/model.d.ts.map +1 -1
- package/build/model.js +4 -1
- package/build/model.js.map +1 -1
- package/build/pipeline/index.d.ts +1 -0
- package/build/pipeline/index.d.ts.map +1 -1
- package/build/pipeline/index.js +1 -0
- package/build/pipeline/index.js.map +1 -1
- package/build/pipeline/results.d.ts +121 -0
- package/build/pipeline/results.d.ts.map +1 -0
- package/build/pipeline/results.js +572 -0
- package/build/pipeline/results.js.map +1 -0
- package/build/pipeline/runner.d.ts.map +1 -1
- package/build/pipeline/runner.js +170 -15
- package/build/pipeline/runner.js.map +1 -1
- package/build/pipeline/types.d.ts +140 -2
- package/build/pipeline/types.d.ts.map +1 -1
- package/build/stores/memory.d.ts +10 -1
- package/build/stores/memory.d.ts.map +1 -1
- package/build/stores/memory.js +0 -0
- package/build/stores/memory.js.map +1 -1
- package/build/stores/types.d.ts +23 -1
- package/build/stores/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/model.ts +4 -1
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/results.ts +827 -0
- package/src/pipeline/runner.ts +184 -15
- package/src/pipeline/types.ts +155 -2
- package/src/stores/memory.ts +0 -0
- package/src/stores/types.ts +24 -1
- package/tests/pipeline-compat.spec.ts +195 -0
- package/tests/pipeline-plugins.spec.ts +194 -0
- package/tests/pipeline-results.spec.ts +456 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CumulativeResultSource, DEFAULT_RESULT_SUMMARY_CHARS, DEFAULT_RESULTS_MAX_CHARS,
|
|
3
|
+
DEFAULT_RESULTS_WINDOW, RESULTS_EVERY_STEP, ResultViewMode,
|
|
4
|
+
compareResultOrder, factKey, orderPipelineSteps, pipelineAncestry, renderResultEntry,
|
|
5
|
+
renderResultFact, renderResultSummary, resultLabel, rootRunOf, sortFacts,
|
|
6
|
+
} from '@owlmeans/agent-common'
|
|
7
|
+
import type {
|
|
8
|
+
CumulativeResultEntry, CumulativeResultFact, CumulativeResultsSpec, PipelineSpec, PipelineState,
|
|
9
|
+
ResultSummarySpec, StepResultsSpec,
|
|
10
|
+
} from '@owlmeans/agent-common'
|
|
11
|
+
import { prefixHash } from '@owlmeans/llm'
|
|
12
|
+
import type { CumulativeResults } from '@owlmeans/llm-common'
|
|
13
|
+
import { createMemoryCumulativeResultStore } from '../stores/memory.js'
|
|
14
|
+
import type { CumulativeResultStore } from '../stores/types.js'
|
|
15
|
+
import type {
|
|
16
|
+
PipelineEnterMode, PipelineParentRef, PipelinePlugin, PipelineRunContext, StepResults,
|
|
17
|
+
VisibleResultEntry,
|
|
18
|
+
} from './types.js'
|
|
19
|
+
|
|
20
|
+
/** The plugin's alias. Seated under it, so wiring it twice replaces rather than doubles. */
|
|
21
|
+
export const CUMULATIVE_RESULTS_PLUGIN = 'cumulative-results'
|
|
22
|
+
|
|
23
|
+
/** The answer shape a summary is asked for when its declaration names none. */
|
|
24
|
+
export const DEFAULT_RESULT_SUMMARY_SCHEMA: Record<string, unknown> = {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: { summary: { type: 'string' } },
|
|
27
|
+
required: ['summary'],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a declaration resolver, a seed and a ledger resolver are told about the run. */
|
|
32
|
+
export interface ResultsRunInfo<S extends PipelineState, C> {
|
|
33
|
+
pipeline: string
|
|
34
|
+
spec: PipelineSpec
|
|
35
|
+
runId: string
|
|
36
|
+
scope: string
|
|
37
|
+
entityId?: string
|
|
38
|
+
deps: C
|
|
39
|
+
state: Readonly<S>
|
|
40
|
+
mode: PipelineEnterMode
|
|
41
|
+
parent?: PipelineParentRef
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What an extractor reads facts from. */
|
|
45
|
+
export interface ResultExtractInput<S extends PipelineState, C> {
|
|
46
|
+
pipeline: string
|
|
47
|
+
runId: string
|
|
48
|
+
step: string
|
|
49
|
+
/** Durable: the run's state, restored from its row on a resume. */
|
|
50
|
+
state: Readonly<S>
|
|
51
|
+
deps: C
|
|
52
|
+
/** Every file the entry answers for — its durable scope and whatever its step recorded. */
|
|
53
|
+
files: readonly string[]
|
|
54
|
+
/** What was recorded as touched on THIS pass — empty on a rebuild. */
|
|
55
|
+
changed: readonly string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ResultScopeInput<S extends PipelineState, C> = Omit<
|
|
59
|
+
ResultExtractInput<S, C>, 'files' | 'changed'
|
|
60
|
+
>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Facts from files. DETERMINISTIC — the same inputs give the same facts — and never a model
|
|
64
|
+
* call: an extractor is what makes a fact a fact, and a rebuild after a crash must find exactly
|
|
65
|
+
* what the first pass found.
|
|
66
|
+
*/
|
|
67
|
+
export type ResultExtractFn<S extends PipelineState, C> = (
|
|
68
|
+
input: ResultExtractInput<S, C>,
|
|
69
|
+
) => Promise<CumulativeResultFact[]> | CumulativeResultFact[]
|
|
70
|
+
|
|
71
|
+
export interface ResultExtractorSpec<S extends PipelineState, C> {
|
|
72
|
+
/**
|
|
73
|
+
* The files this extractor answers for, recomputed from DURABLE inputs — the state's keys, the
|
|
74
|
+
* file tree — and never from what a step happened to report. It is what a rebuild reads when
|
|
75
|
+
* nothing was recorded, so a step's facts can be found again after the process that ran it died.
|
|
76
|
+
*/
|
|
77
|
+
scope?: (input: ResultScopeInput<S, C>) => Promise<string[]> | string[]
|
|
78
|
+
extract: ResultExtractFn<S, C>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type ResultExtractor<S extends PipelineState, C> =
|
|
82
|
+
ResultExtractFn<S, C> | ResultExtractorSpec<S, C>
|
|
83
|
+
|
|
84
|
+
/** Facts known before any step of the run ran. Called on every entry of the run; deterministic. */
|
|
85
|
+
export type ResultSeed<S extends PipelineState, C> = (
|
|
86
|
+
run: ResultsRunInfo<S, C>,
|
|
87
|
+
) => Promise<CumulativeResultFact[]> | CumulativeResultFact[]
|
|
88
|
+
|
|
89
|
+
/** Everything a summarize callback needs, including a ready prompt. */
|
|
90
|
+
export interface ResultSummaryRequest<S extends PipelineState, C> extends ResultSummarySpec {
|
|
91
|
+
schema: Record<string, unknown>
|
|
92
|
+
maxChars: number
|
|
93
|
+
pipeline: string
|
|
94
|
+
runId: string
|
|
95
|
+
step: string
|
|
96
|
+
label: string
|
|
97
|
+
facts: readonly CumulativeResultFact[]
|
|
98
|
+
files: readonly string[]
|
|
99
|
+
state: Readonly<S>
|
|
100
|
+
deps: C
|
|
101
|
+
signal: AbortSignal
|
|
102
|
+
/** The instructions, the step's facts and its files, as one deterministic prompt. */
|
|
103
|
+
prompt: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Ask a model for a step's summary; return its schema-shaped answer.
|
|
108
|
+
*
|
|
109
|
+
* Typically `request => model.invoke(request.prompt, request.schema, { action })` on a model
|
|
110
|
+
* resolved for `request.role`. A throw is a warning and the entry goes without a summary.
|
|
111
|
+
*/
|
|
112
|
+
export type ResultSummarizer<S extends PipelineState, C> = (
|
|
113
|
+
request: ResultSummaryRequest<S, C>,
|
|
114
|
+
) => Promise<unknown>
|
|
115
|
+
|
|
116
|
+
export interface CumulativeResultsPluginOptions<S extends PipelineState, C> {
|
|
117
|
+
alias?: string
|
|
118
|
+
order?: number
|
|
119
|
+
/**
|
|
120
|
+
* The declaration, or a function of the run that returns one — or `null`/`false` to leave THIS
|
|
121
|
+
* run without results. Resolved once, on `enter`.
|
|
122
|
+
*/
|
|
123
|
+
spec:
|
|
124
|
+
| CumulativeResultsSpec
|
|
125
|
+
| ((run: ResultsRunInfo<S, C>) =>
|
|
126
|
+
CumulativeResultsSpec | null | false | undefined
|
|
127
|
+
| Promise<CumulativeResultsSpec | null | false | undefined>)
|
|
128
|
+
/** Named deterministic extractors, referenced by name from `StepResultsSpec.extractors`. */
|
|
129
|
+
extractors?: Record<string, ResultExtractor<S, C>>
|
|
130
|
+
/** Named facts available before any step ran. Visible to every step, in name order. */
|
|
131
|
+
seeds?: Record<string, ResultSeed<S, C>>
|
|
132
|
+
/**
|
|
133
|
+
* Where entries live. A composed pipeline must be given the SAME store as the pipeline it is a
|
|
134
|
+
* step of, or neither sees what the other produced. Default: an in-process memory store of this
|
|
135
|
+
* plugin instance.
|
|
136
|
+
*/
|
|
137
|
+
store?: CumulativeResultStore
|
|
138
|
+
/** Called only for a step whose declaration has a `summary`. */
|
|
139
|
+
summarize?: ResultSummarizer<S, C>
|
|
140
|
+
/** The ledger a run's entries belong to. Default: the parent's, else the root run's id. */
|
|
141
|
+
ledger?: (run: ResultsRunInfo<S, C>) => string
|
|
142
|
+
trace?: (line: string) => void
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface Recorded {
|
|
146
|
+
files: Set<string>
|
|
147
|
+
facts: CumulativeResultFact[]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface Item {
|
|
151
|
+
entry: CumulativeResultEntry
|
|
152
|
+
mode: ResultViewMode
|
|
153
|
+
/** Named as a full consumer by its producer — the last thing a budget touches. */
|
|
154
|
+
explicit: boolean
|
|
155
|
+
dropped: boolean
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface RunLedger<C> {
|
|
159
|
+
results: CumulativeResultsSpec
|
|
160
|
+
spec: PipelineSpec
|
|
161
|
+
runId: string
|
|
162
|
+
ledger: string
|
|
163
|
+
trail: number[]
|
|
164
|
+
index: Map<string, number>
|
|
165
|
+
entries: Map<string, CumulativeResultEntry>
|
|
166
|
+
upstream: readonly VisibleResultEntry[]
|
|
167
|
+
seeds: CumulativeResultEntry[]
|
|
168
|
+
views: Map<string, StepResults>
|
|
169
|
+
recorded: Map<string, Recorded>
|
|
170
|
+
deps: C
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface EntryParts {
|
|
174
|
+
facts: CumulativeResultFact[]
|
|
175
|
+
files: string[]
|
|
176
|
+
source: CumulativeResultSource
|
|
177
|
+
order: number[]
|
|
178
|
+
spec?: StepResultsSpec
|
|
179
|
+
partial?: boolean
|
|
180
|
+
summary?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const SEPARATOR = '\n\n'
|
|
184
|
+
|
|
185
|
+
const compare = (a: string, b: string): number => a < b ? -1 : a > b ? 1 : 0
|
|
186
|
+
|
|
187
|
+
const keyOf = (runId: string, step: string): string => `${runId}\u0000${step}`
|
|
188
|
+
|
|
189
|
+
const normalizePath = (path: string): string =>
|
|
190
|
+
path.trim().replace(/\\/g, '/').replace(/^(\.\/)+/, '').replace(/\/{2,}/g, '/')
|
|
191
|
+
|
|
192
|
+
const uniqueSorted = (paths: Iterable<string>): string[] =>
|
|
193
|
+
[...new Set([...paths].map(normalizePath).filter(path => path !== ''))].sort(compare)
|
|
194
|
+
|
|
195
|
+
const pathsOf = (facts: readonly CumulativeResultFact[]): string[] =>
|
|
196
|
+
facts.flatMap(fact => fact.path != null && fact.path !== '' ? [fact.path] : [])
|
|
197
|
+
|
|
198
|
+
/** First occurrence of each fact key wins — a step's own recorded facts come first. */
|
|
199
|
+
const dedupe = (facts: readonly CumulativeResultFact[]): CumulativeResultFact[] => {
|
|
200
|
+
const seen = new Set<string>()
|
|
201
|
+
|
|
202
|
+
return facts.filter(fact => {
|
|
203
|
+
const key = factKey(fact)
|
|
204
|
+
if (seen.has(key)) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
seen.add(key)
|
|
208
|
+
|
|
209
|
+
return true
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const copyFact = (fact: CumulativeResultFact): CumulativeResultFact => ({
|
|
214
|
+
...fact,
|
|
215
|
+
...(fact.members != null ? { members: [...fact.members] } : {}),
|
|
216
|
+
...(fact.attrs != null ? { attrs: { ...fact.attrs } } : {}),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Deep-frozen, because an entry is handed out by reference — to every view that shows it and to
|
|
221
|
+
* every caller of `entries()` — and one caller editing it in place would edit all of them.
|
|
222
|
+
*/
|
|
223
|
+
const freezeEntry = (entry: CumulativeResultEntry): CumulativeResultEntry => Object.freeze({
|
|
224
|
+
...entry,
|
|
225
|
+
order: Object.freeze([...entry.order]),
|
|
226
|
+
files: Object.freeze([...entry.files]),
|
|
227
|
+
facts: Object.freeze(entry.facts.map(fact => Object.freeze(copyFact(fact)))),
|
|
228
|
+
...(Array.isArray(entry.fullFor) ? { fullFor: Object.freeze([...entry.fullFor]) } : {}),
|
|
229
|
+
...(entry.omitFor != null ? { omitFor: Object.freeze([...entry.omitFor]) } : {}),
|
|
230
|
+
}) as CumulativeResultEntry
|
|
231
|
+
|
|
232
|
+
const hasExtractors = (spec: StepResultsSpec | undefined): boolean =>
|
|
233
|
+
(spec?.extractors?.length ?? 0) > 0
|
|
234
|
+
|
|
235
|
+
const extractFn = <S extends PipelineState, C>(extractor: ResultExtractor<S, C>): ResultExtractFn<S, C> =>
|
|
236
|
+
typeof extractor === 'function' ? extractor : extractor.extract
|
|
237
|
+
|
|
238
|
+
const summaryPrompt = (
|
|
239
|
+
spec: ResultSummarySpec, label: string, facts: readonly CumulativeResultFact[],
|
|
240
|
+
files: readonly string[], maxChars: number,
|
|
241
|
+
): string => [
|
|
242
|
+
spec.instructions.trim(),
|
|
243
|
+
`The step "${label}" has just finished. For the later steps of the same work, summarize what it `
|
|
244
|
+
+ 'decided or produced that the facts below do not already say. Do not repeat the facts and do not '
|
|
245
|
+
+ `guess at anything not shown. At most ${maxChars} characters.`,
|
|
246
|
+
`# Facts read from the files after the step finished\n${facts.map(renderResultFact).join('\n') || '(none)'}`,
|
|
247
|
+
`# Files the step answers for\n${files.map(file => `- ${file}`).join('\n') || '(none)'}`,
|
|
248
|
+
].join(SEPARATOR)
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* CUMULATIVE PIPELINE RESULTS: every step is told, in its prompt and through a typed query, what
|
|
252
|
+
* the steps before it produced — read from the files by code, not recalled by a model.
|
|
253
|
+
*
|
|
254
|
+
* - `enter` decides the LEDGER the run's entries belong to (its parent's for a composed run, else
|
|
255
|
+
* the root run's id). A fresh start or a restart forgets the run's own earlier entries; a
|
|
256
|
+
* continuation loads them and silently REBUILDS any that are missing from the finished steps'
|
|
257
|
+
* durable inputs — so a resume never depends on the ledger having survived, only the quality
|
|
258
|
+
* of what the next step is told does.
|
|
259
|
+
* - `beforeStep` cuts the step's VIEW once: every predecessor's entry, full within the window or
|
|
260
|
+
* when the producer names the step, names only otherwise, inside a character budget that drops
|
|
261
|
+
* the oldest entries to names only and then out — never cutting an entry's own text.
|
|
262
|
+
* - `afterStep` extracts the step's entry from its durable scope and what it recorded, leaves any
|
|
263
|
+
* fact an earlier entry already owns to that entry, re-extracts earlier entries whose files the
|
|
264
|
+
* step rewrote, and persists — all before the runner marks the step complete.
|
|
265
|
+
* - `passStep` gives a skipped step the entry its files still support, and an optional step that
|
|
266
|
+
* failed a partial one.
|
|
267
|
+
*
|
|
268
|
+
* Everything here is an enhancement. A store that fails, an extractor that throws or a summary
|
|
269
|
+
* that cannot be had costs a step some context and is logged; it never fails the run.
|
|
270
|
+
*/
|
|
271
|
+
export const cumulativeResultsPlugin = <S extends PipelineState, C>(
|
|
272
|
+
options: CumulativeResultsPluginOptions<S, C>,
|
|
273
|
+
): PipelinePlugin<S, C> => {
|
|
274
|
+
const alias = options.alias ?? CUMULATIVE_RESULTS_PLUGIN
|
|
275
|
+
const store = options.store ?? createMemoryCumulativeResultStore()
|
|
276
|
+
const trace = options.trace ?? (() => undefined)
|
|
277
|
+
const runs = new Map<string, RunLedger<C>>()
|
|
278
|
+
|
|
279
|
+
const warn = (what: string, e?: unknown): void => {
|
|
280
|
+
console.warn(`Cumulative results (${alias}): ${what}`, ...(e != null ? [e] : []))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const say = (run: RunLedger<C>, line: string): void => {
|
|
284
|
+
trace(`[results:${run.spec.alias}:${run.runId}] ${line}`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const orderOf = (run: RunLedger<C>, step: string): number[] =>
|
|
288
|
+
[...run.trail, run.index.get(step) ?? 0]
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The ledger as the store holds it — except this run's own entries, which only this plugin writes
|
|
292
|
+
* and whose in-memory copy is therefore never older than the stored one (a failed `put` leaves
|
|
293
|
+
* the store behind, never ahead). A store that cannot be read leaves what is held untouched.
|
|
294
|
+
*/
|
|
295
|
+
const reload = async (run: RunLedger<C>): Promise<void> => {
|
|
296
|
+
let listed: CumulativeResultEntry[]
|
|
297
|
+
try {
|
|
298
|
+
listed = await store.list(run.ledger)
|
|
299
|
+
} catch (e) {
|
|
300
|
+
warn(`could not read ledger ${run.ledger}`, e)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
const next = new Map<string, CumulativeResultEntry>()
|
|
304
|
+
for (const entry of listed) {
|
|
305
|
+
next.set(keyOf(entry.runId, entry.step), freezeEntry(entry))
|
|
306
|
+
}
|
|
307
|
+
for (const [key, entry] of run.entries) {
|
|
308
|
+
if (entry.runId === run.runId) {
|
|
309
|
+
next.set(key, entry)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
run.entries = next
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const persist = async (run: RunLedger<C>, entry: CumulativeResultEntry): Promise<void> => {
|
|
316
|
+
run.entries.set(keyOf(entry.runId, entry.step), entry)
|
|
317
|
+
try {
|
|
318
|
+
// A plain copy: a store adapter may annotate what it is given (an `_id`), and the held entry
|
|
319
|
+
// is frozen.
|
|
320
|
+
await store.put(JSON.parse(JSON.stringify(entry)) as CumulativeResultEntry)
|
|
321
|
+
} catch (e) {
|
|
322
|
+
warn(`could not store ${entry.label}; a resume will rebuild it`, e)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The step of THIS run an entry belongs to: its own step, or the step a composed run ran under. */
|
|
327
|
+
const anchorOf = (run: RunLedger<C>, entry: CumulativeResultEntry): string | null => {
|
|
328
|
+
if (entry.runId === run.runId) {
|
|
329
|
+
return entry.step
|
|
330
|
+
}
|
|
331
|
+
if (entry.runId.startsWith(`${run.runId}/`)) {
|
|
332
|
+
return entry.runId.slice(run.runId.length + 1).split('/')[0] ?? null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const modeFor = (
|
|
339
|
+
run: RunLedger<C>, entry: CumulativeResultEntry, consumer: string, depth: number,
|
|
340
|
+
): Pick<Item, 'mode' | 'explicit'> => {
|
|
341
|
+
if (entry.omitFor?.includes(consumer) === true) {
|
|
342
|
+
return { mode: ResultViewMode.Omitted, explicit: false }
|
|
343
|
+
}
|
|
344
|
+
if (entry.fullFor === RESULTS_EVERY_STEP) {
|
|
345
|
+
return { mode: ResultViewMode.Full, explicit: false }
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(entry.fullFor) && entry.fullFor.includes(consumer)) {
|
|
348
|
+
return { mode: ResultViewMode.Full, explicit: true }
|
|
349
|
+
}
|
|
350
|
+
const window = entry.window ?? run.results.window ?? DEFAULT_RESULTS_WINDOW
|
|
351
|
+
|
|
352
|
+
return { mode: depth <= window ? ResultViewMode.Full : ResultViewMode.Compact, explicit: false }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Every entry a step can see, oldest first, as its producer's declaration shows it: what the run
|
|
357
|
+
* was handed by the step it is composed under, the seeds, then every predecessor's entry and the
|
|
358
|
+
* entries of runs composed under a predecessor. Nothing else — a sibling branch has not
|
|
359
|
+
* necessarily run, so what it says cannot be what a step relies on.
|
|
360
|
+
*/
|
|
361
|
+
const visibleTo = (run: RunLedger<C>, step: string): Item[] => {
|
|
362
|
+
const depths = new Map(pipelineAncestry(run.spec, step).map(({ step: name, depth }) => [name, depth]))
|
|
363
|
+
const items: Item[] = [
|
|
364
|
+
...run.upstream.map(visible => ({
|
|
365
|
+
entry: visible.entry, mode: visible.mode, explicit: false, dropped: visible.dropped === true,
|
|
366
|
+
})),
|
|
367
|
+
...run.seeds.map(entry => ({ entry, mode: ResultViewMode.Full, explicit: false, dropped: false })),
|
|
368
|
+
]
|
|
369
|
+
for (const entry of run.entries.values()) {
|
|
370
|
+
const anchor = anchorOf(run, entry)
|
|
371
|
+
const depth = anchor != null ? depths.get(anchor) : undefined
|
|
372
|
+
if (depth != null) {
|
|
373
|
+
items.push({ entry, ...modeFor(run, entry, step, depth), dropped: false })
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return items.sort((a, b) =>
|
|
378
|
+
compareResultOrder(a.entry.order, b.entry.order) || compare(a.entry.label, b.entry.label))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const textOf = (item: Item): string =>
|
|
382
|
+
item.mode === ResultViewMode.Full
|
|
383
|
+
? item.entry.full
|
|
384
|
+
: item.mode === ResultViewMode.Compact ? item.entry.compact : ''
|
|
385
|
+
|
|
386
|
+
/** The step's view, inside the budget. See the plugin's own description for the order of cuts. */
|
|
387
|
+
const cut = (run: RunLedger<C>, step: string): StepResults => {
|
|
388
|
+
const items = visibleTo(run, step)
|
|
389
|
+
const maxChars = run.results.maxChars ?? DEFAULT_RESULTS_MAX_CHARS
|
|
390
|
+
const measure = (): number =>
|
|
391
|
+
items.map(textOf).filter(text => text !== '').join(SEPARATOR).length
|
|
392
|
+
|
|
393
|
+
let total = measure()
|
|
394
|
+
const downgrade = (explicit: boolean, from: ResultViewMode): void => {
|
|
395
|
+
for (const item of items) {
|
|
396
|
+
if (total <= maxChars) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
if (item.explicit !== explicit || item.mode !== from || textOf(item) === '') {
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
if (from === ResultViewMode.Full) {
|
|
403
|
+
item.mode = ResultViewMode.Compact
|
|
404
|
+
} else {
|
|
405
|
+
item.mode = ResultViewMode.Omitted
|
|
406
|
+
item.dropped = true
|
|
407
|
+
}
|
|
408
|
+
total = measure()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Oldest first, and whatever a producer has not named this step for goes before what it has.
|
|
412
|
+
downgrade(false, ResultViewMode.Full)
|
|
413
|
+
downgrade(false, ResultViewMode.Compact)
|
|
414
|
+
downgrade(true, ResultViewMode.Full)
|
|
415
|
+
downgrade(true, ResultViewMode.Compact)
|
|
416
|
+
|
|
417
|
+
const sections = items
|
|
418
|
+
.filter(item => textOf(item) !== '')
|
|
419
|
+
.map(item => Object.freeze({ step: item.entry.label, text: textOf(item) }))
|
|
420
|
+
const omitted = items
|
|
421
|
+
.filter(item => item.mode === ResultViewMode.Omitted && item.dropped && item.entry.full !== '')
|
|
422
|
+
.map(item => item.entry.label)
|
|
423
|
+
const joined = sections.map(section => section.text).join(SEPARATOR)
|
|
424
|
+
const view: CumulativeResults = Object.freeze({
|
|
425
|
+
step: resultLabel(run.ledger, run.runId, step),
|
|
426
|
+
sections: Object.freeze(sections) as CumulativeResults['sections'],
|
|
427
|
+
omitted: Object.freeze(omitted) as string[],
|
|
428
|
+
chars: joined.length,
|
|
429
|
+
digest: prefixHash(`${joined}\u0000${omitted.join('\u0000')}`),
|
|
430
|
+
})
|
|
431
|
+
const visible: readonly VisibleResultEntry[] = Object.freeze(items.map(item => Object.freeze({
|
|
432
|
+
entry: item.entry, mode: item.mode, ...(item.dropped ? { dropped: true } : {}),
|
|
433
|
+
})))
|
|
434
|
+
|
|
435
|
+
say(run, `${step} view · sections=${sections.length} chars=${view.chars}`
|
|
436
|
+
+ ` omitted=${omitted.length} digest=${view.digest}`)
|
|
437
|
+
|
|
438
|
+
const results: StepResults = {
|
|
439
|
+
ledger: run.ledger,
|
|
440
|
+
order: Object.freeze(orderOf(run, step)),
|
|
441
|
+
view,
|
|
442
|
+
entries: () => visible,
|
|
443
|
+
facts: (query = {}) => visible.flatMap(({ entry }) =>
|
|
444
|
+
query.step == null || entry.step === query.step || entry.label === query.step
|
|
445
|
+
? entry.facts
|
|
446
|
+
.filter(fact => (query.kind == null || fact.kind === query.kind)
|
|
447
|
+
&& (query.name == null || fact.name === query.name))
|
|
448
|
+
.map(copyFact)
|
|
449
|
+
: []),
|
|
450
|
+
record: input => {
|
|
451
|
+
const buffer = run.recorded.get(step) ?? { files: new Set<string>(), facts: [] }
|
|
452
|
+
for (const file of input.files ?? []) {
|
|
453
|
+
buffer.files.add(normalizePath(file))
|
|
454
|
+
}
|
|
455
|
+
buffer.facts.push(...(input.facts ?? []).map(copyFact))
|
|
456
|
+
run.recorded.set(step, buffer)
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return Object.freeze(results)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const take = (run: RunLedger<C>, step: string): Recorded => {
|
|
464
|
+
const buffer = run.recorded.get(step) ?? { files: new Set<string>(), facts: [] }
|
|
465
|
+
run.recorded.delete(step)
|
|
466
|
+
|
|
467
|
+
return buffer
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const scopeOf = async (run: RunLedger<C>, step: string, state: Readonly<S>): Promise<string[]> => {
|
|
471
|
+
const files: string[] = []
|
|
472
|
+
for (const name of run.results.steps?.[step]?.extractors ?? []) {
|
|
473
|
+
const extractor = options.extractors?.[name]
|
|
474
|
+
if (extractor == null || typeof extractor === 'function' || extractor.scope == null) {
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
files.push(...await extractor.scope({
|
|
479
|
+
pipeline: run.spec.alias, runId: run.runId, step, state, deps: run.deps,
|
|
480
|
+
}))
|
|
481
|
+
} catch (e) {
|
|
482
|
+
warn(`scope of extractor ${name} failed for ${step}`, e)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return uniqueSorted(files)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const extract = async (
|
|
490
|
+
run: RunLedger<C>, step: string, state: Readonly<S>,
|
|
491
|
+
files: readonly string[], changed: readonly string[],
|
|
492
|
+
): Promise<CumulativeResultFact[]> => {
|
|
493
|
+
const facts: CumulativeResultFact[] = []
|
|
494
|
+
for (const name of run.results.steps?.[step]?.extractors ?? []) {
|
|
495
|
+
const extractor = options.extractors?.[name]
|
|
496
|
+
if (extractor == null) {
|
|
497
|
+
warn(`step ${step} names an extractor nobody registered: ${name}`)
|
|
498
|
+
continue
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
facts.push(...(await extractFn(extractor)({
|
|
502
|
+
pipeline: run.spec.alias, runId: run.runId, step, state, deps: run.deps, files, changed,
|
|
503
|
+
}) ?? []))
|
|
504
|
+
} catch (e) {
|
|
505
|
+
warn(`extractor ${name} failed for ${step}`, e)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return facts
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Facts no earlier entry already owns. "Earlier" is what the step can see, plus the runs composed
|
|
514
|
+
* under the step itself, whose entries are written before the step's own.
|
|
515
|
+
*/
|
|
516
|
+
const unowned = (
|
|
517
|
+
run: RunLedger<C>, step: string, facts: readonly CumulativeResultFact[],
|
|
518
|
+
): CumulativeResultFact[] => {
|
|
519
|
+
const owned = new Set<string>()
|
|
520
|
+
for (const item of visibleTo(run, step)) {
|
|
521
|
+
for (const fact of item.entry.facts) {
|
|
522
|
+
owned.add(factKey(fact))
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (const entry of run.entries.values()) {
|
|
526
|
+
if (entry.runId.startsWith(`${run.runId}/${step}`) && anchorOf(run, entry) === step) {
|
|
527
|
+
for (const fact of entry.facts) {
|
|
528
|
+
owned.add(factKey(fact))
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return facts.filter(fact => !owned.has(factKey(fact)))
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** A step's facts and files, from its durable scope, what it recorded, and its extractors. */
|
|
537
|
+
const collect = async (
|
|
538
|
+
run: RunLedger<C>, step: string, state: Readonly<S>,
|
|
539
|
+
changed: readonly string[], recorded: readonly CumulativeResultFact[],
|
|
540
|
+
): Promise<{ facts: CumulativeResultFact[], files: string[] }> => {
|
|
541
|
+
const files = uniqueSorted([...await scopeOf(run, step, state), ...changed])
|
|
542
|
+
const facts = unowned(run, step, dedupe([...recorded, ...await extract(run, step, state, files, changed)]))
|
|
543
|
+
|
|
544
|
+
return { facts, files: uniqueSorted([...files, ...pathsOf(facts)]) }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const compose = (run: RunLedger<C>, step: string, parts: EntryParts): CumulativeResultEntry => {
|
|
548
|
+
const previous = run.entries.get(keyOf(run.runId, step))
|
|
549
|
+
const label = resultLabel(run.ledger, run.runId, step)
|
|
550
|
+
const source = parts.facts.length === 0 && parts.summary != null
|
|
551
|
+
? CumulativeResultSource.Summarized
|
|
552
|
+
: parts.source
|
|
553
|
+
const facts = sortFacts(parts.facts)
|
|
554
|
+
const rendered = renderResultEntry(
|
|
555
|
+
{ label, source, facts, ...(parts.partial === true ? { partial: true } : {}), ...(parts.summary != null ? { summary: parts.summary } : {}) },
|
|
556
|
+
{
|
|
557
|
+
maxChars: parts.spec?.maxChars ?? run.results.maxEntryChars,
|
|
558
|
+
maxCompactChars: parts.spec?.maxCompactChars ?? run.results.maxCompactChars,
|
|
559
|
+
},
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
return freezeEntry({
|
|
563
|
+
ledger: run.ledger,
|
|
564
|
+
pipeline: run.spec.alias,
|
|
565
|
+
runId: run.runId,
|
|
566
|
+
step,
|
|
567
|
+
label,
|
|
568
|
+
order: parts.order,
|
|
569
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
570
|
+
source,
|
|
571
|
+
...(parts.partial === true ? { partial: true } : {}),
|
|
572
|
+
facts,
|
|
573
|
+
files: parts.files,
|
|
574
|
+
...(parts.summary != null ? { summary: parts.summary } : {}),
|
|
575
|
+
full: rendered.full,
|
|
576
|
+
compact: rendered.compact,
|
|
577
|
+
...(parts.spec?.full != null ? { fullFor: parts.spec.full } : {}),
|
|
578
|
+
...(parts.spec?.omit != null ? { omitFor: parts.spec.omit } : {}),
|
|
579
|
+
...(parts.spec?.window != null ? { window: parts.spec.window } : {}),
|
|
580
|
+
updatedAt: new Date().toISOString(),
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Re-read every earlier entry of this run whose files the step just rewrote — a repair of an
|
|
586
|
+
* earlier step's output would otherwise leave that step's entry describing a file that no longer
|
|
587
|
+
* says it. The refreshed entry keeps what its extractors find now, plus its earlier facts about
|
|
588
|
+
* files the step did not touch.
|
|
589
|
+
*/
|
|
590
|
+
const refreshEarlier = async (
|
|
591
|
+
run: RunLedger<C>, step: string, changed: readonly string[], state: Readonly<S>,
|
|
592
|
+
): Promise<void> => {
|
|
593
|
+
if (changed.length === 0) {
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
const mine = orderOf(run, step)
|
|
597
|
+
const earlier = [...run.entries.values()]
|
|
598
|
+
.filter(entry => entry.runId === run.runId && entry.step !== step
|
|
599
|
+
&& compareResultOrder(entry.order, mine) < 0)
|
|
600
|
+
.sort((a, b) => compareResultOrder(a.order, b.order))
|
|
601
|
+
|
|
602
|
+
for (const entry of earlier) {
|
|
603
|
+
const spec = run.results.steps?.[entry.step]
|
|
604
|
+
if (!hasExtractors(spec) || (spec?.refresh ?? run.results.refresh ?? true) === false) {
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
const overlap = entry.files.filter(file => changed.includes(file))
|
|
608
|
+
if (overlap.length === 0) {
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
const fresh = await extract(run, entry.step, state, entry.files, overlap)
|
|
612
|
+
const freshKeys = new Set(fresh.map(factKey))
|
|
613
|
+
const kept = entry.facts.filter(fact => !freshKeys.has(factKey(fact))
|
|
614
|
+
&& (fact.path == null || !overlap.includes(normalizePath(fact.path))))
|
|
615
|
+
const facts = unowned(run, entry.step, dedupe([...fresh, ...kept]))
|
|
616
|
+
const refreshed = compose(run, entry.step, {
|
|
617
|
+
facts,
|
|
618
|
+
files: uniqueSorted([...entry.files, ...pathsOf(facts)]),
|
|
619
|
+
source: entry.source === CumulativeResultSource.Summarized
|
|
620
|
+
? CumulativeResultSource.Extracted
|
|
621
|
+
: entry.source,
|
|
622
|
+
order: [...entry.order],
|
|
623
|
+
...(spec != null ? { spec } : {}),
|
|
624
|
+
...(entry.partial === true ? { partial: true } : {}),
|
|
625
|
+
...(entry.summary != null ? { summary: entry.summary } : {}),
|
|
626
|
+
})
|
|
627
|
+
await persist(run, refreshed)
|
|
628
|
+
say(run, `${step} rewrote ${overlap.length} file(s) of ${entry.label} · revision=${refreshed.revision}`)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const summaryOf = async (
|
|
633
|
+
run: RunLedger<C>, step: string, ctx: PipelineRunContext<S, C>, state: Readonly<S>,
|
|
634
|
+
facts: readonly CumulativeResultFact[], files: readonly string[],
|
|
635
|
+
): Promise<string | undefined> => {
|
|
636
|
+
const spec = run.results.steps?.[step]?.summary
|
|
637
|
+
if (spec == null || options.summarize == null) {
|
|
638
|
+
return undefined
|
|
639
|
+
}
|
|
640
|
+
const maxChars = spec.maxChars ?? DEFAULT_RESULT_SUMMARY_CHARS
|
|
641
|
+
const label = resultLabel(run.ledger, run.runId, step)
|
|
642
|
+
try {
|
|
643
|
+
const answer = await options.summarize({
|
|
644
|
+
...spec,
|
|
645
|
+
schema: spec.schema ?? DEFAULT_RESULT_SUMMARY_SCHEMA,
|
|
646
|
+
maxChars,
|
|
647
|
+
pipeline: run.spec.alias,
|
|
648
|
+
runId: run.runId,
|
|
649
|
+
step,
|
|
650
|
+
label,
|
|
651
|
+
facts,
|
|
652
|
+
files,
|
|
653
|
+
state,
|
|
654
|
+
deps: run.deps,
|
|
655
|
+
signal: ctx.signal,
|
|
656
|
+
prompt: summaryPrompt(spec, label, facts, files, maxChars),
|
|
657
|
+
})
|
|
658
|
+
const text = renderResultSummary(answer, maxChars)
|
|
659
|
+
|
|
660
|
+
return text !== '' ? text : undefined
|
|
661
|
+
} catch (e) {
|
|
662
|
+
// A summary is the unverified extra. Without it the step's facts still stand.
|
|
663
|
+
warn(`summary of ${step} failed; the entry goes without one`, e)
|
|
664
|
+
return undefined
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
alias,
|
|
670
|
+
...(options.order != null ? { order: options.order } : {}),
|
|
671
|
+
|
|
672
|
+
enter: async event => {
|
|
673
|
+
runs.delete(event.runId)
|
|
674
|
+
const info: ResultsRunInfo<S, C> = {
|
|
675
|
+
pipeline: event.spec.alias,
|
|
676
|
+
spec: event.spec,
|
|
677
|
+
runId: event.runId,
|
|
678
|
+
scope: event.scope,
|
|
679
|
+
...(event.entityId != null ? { entityId: event.entityId } : {}),
|
|
680
|
+
deps: event.deps,
|
|
681
|
+
state: event.state,
|
|
682
|
+
mode: event.mode,
|
|
683
|
+
...(event.parent != null ? { parent: event.parent } : {}),
|
|
684
|
+
}
|
|
685
|
+
const declared = typeof options.spec === 'function' ? await options.spec(info) : options.spec
|
|
686
|
+
if (declared == null || declared === false) {
|
|
687
|
+
return
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const parent = event.parent?.results
|
|
691
|
+
const ledger = options.ledger?.(info) ?? parent?.ledger ?? rootRunOf(event.runId)
|
|
692
|
+
const run: RunLedger<C> = {
|
|
693
|
+
results: declared,
|
|
694
|
+
spec: event.spec,
|
|
695
|
+
runId: event.runId,
|
|
696
|
+
ledger,
|
|
697
|
+
trail: parent != null ? [...parent.order] : [],
|
|
698
|
+
index: new Map(orderPipelineSteps(event.spec).map((step, at) => [step, at + 1])),
|
|
699
|
+
entries: new Map(),
|
|
700
|
+
upstream: parent != null ? parent.entries() : [],
|
|
701
|
+
seeds: [],
|
|
702
|
+
views: new Map(),
|
|
703
|
+
recorded: new Map(),
|
|
704
|
+
deps: event.deps,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (event.mode !== 'continue') {
|
|
708
|
+
try {
|
|
709
|
+
await store.clear(ledger, event.runId)
|
|
710
|
+
} catch (e) {
|
|
711
|
+
warn(`could not clear the earlier entries of ${event.runId}`, e)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
await reload(run)
|
|
715
|
+
|
|
716
|
+
for (const name of Object.keys(options.seeds ?? {}).sort(compare)) {
|
|
717
|
+
try {
|
|
718
|
+
const facts = dedupe(await options.seeds![name]!(info))
|
|
719
|
+
run.seeds.push(compose(run, name, {
|
|
720
|
+
facts, files: uniqueSorted(pathsOf(facts)), source: CumulativeResultSource.Seeded,
|
|
721
|
+
order: [...run.trail, 0],
|
|
722
|
+
}))
|
|
723
|
+
} catch (e) {
|
|
724
|
+
warn(`seed ${name} failed`, e)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
runs.set(event.runId, run)
|
|
728
|
+
|
|
729
|
+
// A continuation's finished steps must have their entries, whether or not the ledger kept
|
|
730
|
+
// them — a store outage, an expiry, a crash between a fact and its `put`. They are read again
|
|
731
|
+
// from the steps' DURABLE inputs; never from what a step once recorded, which is gone.
|
|
732
|
+
let rebuilt = 0
|
|
733
|
+
for (const step of event.inherited) {
|
|
734
|
+
if (run.entries.has(keyOf(run.runId, step)) || !hasExtractors(run.results.steps?.[step])) {
|
|
735
|
+
continue
|
|
736
|
+
}
|
|
737
|
+
const collected = await collect(run, step, event.state, [], [])
|
|
738
|
+
await persist(run, compose(run, step, {
|
|
739
|
+
...collected, source: CumulativeResultSource.Rebuilt, order: orderOf(run, step),
|
|
740
|
+
...(run.results.steps?.[step] != null ? { spec: run.results.steps[step] } : {}),
|
|
741
|
+
}))
|
|
742
|
+
rebuilt += 1
|
|
743
|
+
}
|
|
744
|
+
say(run, `enter ${event.mode} · ledger=${ledger} entries=${run.entries.size} rebuilt=${rebuilt}`)
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
beforeStep: async ({ step, ctx }) => {
|
|
748
|
+
const run = runs.get(ctx.runId)
|
|
749
|
+
if (run == null) {
|
|
750
|
+
return
|
|
751
|
+
}
|
|
752
|
+
const memo = run.views.get(step)
|
|
753
|
+
if (memo != null) {
|
|
754
|
+
return { results: memo }
|
|
755
|
+
}
|
|
756
|
+
// A run composed under an earlier step wrote its entries through its own plugin instance.
|
|
757
|
+
await reload(run)
|
|
758
|
+
const results = cut(run, step)
|
|
759
|
+
run.views.set(step, results)
|
|
760
|
+
|
|
761
|
+
return { results }
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
afterStep: async ({ step, ctx, state }) => {
|
|
765
|
+
const run = runs.get(ctx.runId)
|
|
766
|
+
if (run == null) {
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
const buffer = take(run, step)
|
|
770
|
+
const spec = run.results.steps?.[step]
|
|
771
|
+
if (spec == null && buffer.files.size === 0 && buffer.facts.length === 0
|
|
772
|
+
&& !run.entries.has(keyOf(run.runId, step))) {
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
await reload(run)
|
|
776
|
+
const changed = uniqueSorted(buffer.files)
|
|
777
|
+
await refreshEarlier(run, step, changed, state)
|
|
778
|
+
const collected = await collect(run, step, state, changed, buffer.facts)
|
|
779
|
+
const summary = await summaryOf(run, step, ctx, state, collected.facts, collected.files)
|
|
780
|
+
const entry = compose(run, step, {
|
|
781
|
+
...collected, source: CumulativeResultSource.Extracted, order: orderOf(run, step),
|
|
782
|
+
...(spec != null ? { spec } : {}), ...(summary != null ? { summary } : {}),
|
|
783
|
+
})
|
|
784
|
+
await persist(run, entry)
|
|
785
|
+
say(run, `${step} entry · facts=${entry.facts.length} files=${entry.files.length} revision=${entry.revision}`)
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
passStep: async ({ step, ctx, state, reason }) => {
|
|
789
|
+
const run = runs.get(ctx.runId)
|
|
790
|
+
if (run == null) {
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
const buffer = take(run, step)
|
|
794
|
+
const spec = run.results.steps?.[step]
|
|
795
|
+
await reload(run)
|
|
796
|
+
|
|
797
|
+
if (reason === 'skipped') {
|
|
798
|
+
// A guard that skips is saying the work is already there. What its files support is what
|
|
799
|
+
// the step's entry is; what a real earlier run stored stands as it is.
|
|
800
|
+
if (run.entries.has(keyOf(run.runId, step)) || !hasExtractors(spec)) {
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
const collected = await collect(run, step, state, [], [])
|
|
804
|
+
await persist(run, compose(run, step, {
|
|
805
|
+
...collected, source: CumulativeResultSource.Rebuilt, order: orderOf(run, step),
|
|
806
|
+
...(spec != null ? { spec } : {}),
|
|
807
|
+
}))
|
|
808
|
+
return
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (spec == null && buffer.files.size === 0 && buffer.facts.length === 0) {
|
|
812
|
+
return
|
|
813
|
+
}
|
|
814
|
+
const changed = uniqueSorted(buffer.files)
|
|
815
|
+
await refreshEarlier(run, step, changed, state)
|
|
816
|
+
const collected = await collect(run, step, state, changed, buffer.facts)
|
|
817
|
+
await persist(run, compose(run, step, {
|
|
818
|
+
...collected, source: CumulativeResultSource.Extracted, order: orderOf(run, step),
|
|
819
|
+
partial: true, ...(spec != null ? { spec } : {}),
|
|
820
|
+
}))
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
exit: async ({ runId }) => {
|
|
824
|
+
runs.delete(runId)
|
|
825
|
+
},
|
|
826
|
+
}
|
|
827
|
+
}
|