pi-long-task 0.3.12 → 0.3.13
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 +19 -0
- package/README.md +13 -7
- package/package.json +6 -2
- package/src/coordinator.ts +68 -20
- package/src/goal_loop.ts +30 -0
- package/src/goal_orchestrator.ts +219 -128
- package/src/goal_review.ts +91 -73
- package/src/goal_state.ts +25 -0
- package/src/goal_todo_execution.ts +25 -8
- package/src/goal_todo_generation.ts +15 -8
- package/src/result_writer.ts +90 -24
- package/src/todo_generator.ts +74 -23
- package/src/todo_parser.ts +35 -11
- package/src/worker_session.ts +87 -25
package/src/goal_orchestrator.ts
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
type GoalDiscoveryEntrypoint,
|
|
9
9
|
type GoalDiscoveryRunner,
|
|
10
10
|
} from "./goal_discovery.ts";
|
|
11
|
-
import type { GoalLoopLimits, GoalLoopStatus } from "./goal_loop.ts";
|
|
11
|
+
import type { GoalIterationStatus, GoalLoopLimits, GoalLoopStatus } from "./goal_loop.ts";
|
|
12
12
|
import {
|
|
13
|
+
cancelGoalLoop,
|
|
13
14
|
createGoalLoopState,
|
|
15
|
+
failGoalLoop,
|
|
14
16
|
goalLoopStopReason,
|
|
15
17
|
startGoalIteration,
|
|
16
18
|
type GoalLoopLimitInput,
|
|
@@ -145,8 +147,8 @@ export async function runGoalLoop(options: RunGoalLoopOptions): Promise<GoalLoop
|
|
|
145
147
|
let goalSpecification: GoalSpecification | undefined = await store.tryLoadGoalSpecification();
|
|
146
148
|
|
|
147
149
|
await store.saveState(state);
|
|
148
|
-
await store.
|
|
149
|
-
await store.appendNewTraceEvents(
|
|
150
|
+
await store.initializeResultIfMissing(state);
|
|
151
|
+
await store.appendNewTraceEvents(await store.durableTraceLength(), state);
|
|
150
152
|
const publish = (phase: GoalLoopProgressPhase, message: string, extra: Partial<GoalLoopProgressUpdate> = {}) => {
|
|
151
153
|
options.onProgress?.({
|
|
152
154
|
message,
|
|
@@ -165,136 +167,201 @@ export async function runGoalLoop(options: RunGoalLoopOptions): Promise<GoalLoop
|
|
|
165
167
|
tracePath: store.paths.tracePath,
|
|
166
168
|
goalSpecPath: store.paths.goalSpecPath,
|
|
167
169
|
discoveryDecision,
|
|
168
|
-
workerCostTotal: accumulatedWorkerCost(
|
|
169
|
-
reviewerCostTotal: accumulatedReviewerCost(
|
|
170
|
-
totalCost: accumulatedWorkerCost(
|
|
170
|
+
workerCostTotal: accumulatedWorkerCost(state),
|
|
171
|
+
reviewerCostTotal: accumulatedReviewerCost(state),
|
|
172
|
+
totalCost: accumulatedWorkerCost(state) + accumulatedReviewerCost(state),
|
|
171
173
|
...extra,
|
|
172
174
|
});
|
|
173
175
|
};
|
|
174
176
|
|
|
175
177
|
publish("goal_start", `Starting goal loop: ${state.goal}`);
|
|
176
178
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
store,
|
|
181
|
-
discoveryDecision,
|
|
182
|
-
existingSpecification: goalSpecification,
|
|
183
|
-
options,
|
|
184
|
-
now,
|
|
185
|
-
publish,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
while (state.status === "running") {
|
|
190
|
-
const stopReason = goalLoopStopReason(state, { now: now(), abortSignal: options.abortSignal });
|
|
191
|
-
if (stopReason) {
|
|
192
|
-
const previousTraceLength = state.trace.length;
|
|
193
|
-
state = startGoalIteration(state, { now: now(), abortSignal: options.abortSignal });
|
|
194
|
-
await persistStateChange(store, previousTraceLength, state);
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const nextIteration = state.currentIteration > 0 ? state.currentIteration : state.iterations.length + 1;
|
|
199
|
-
publish("todo_generation_start", `Goal iteration ${nextIteration}: generating TODO markdown.`, {
|
|
200
|
-
iteration: nextIteration,
|
|
201
|
-
});
|
|
202
|
-
const generation = await runGoalTodoGenerationLongTask({
|
|
203
|
-
state,
|
|
204
|
-
cwd: options.cwd,
|
|
205
|
-
store,
|
|
206
|
-
longTaskRunner: options.todoGenerationRunner,
|
|
207
|
-
abortSignal: options.abortSignal,
|
|
208
|
-
model: options.model,
|
|
209
|
-
modelName: options.modelName,
|
|
210
|
-
thinkingLevel: options.thinkingLevel,
|
|
211
|
-
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
212
|
-
now,
|
|
213
|
-
goalSpecification,
|
|
214
|
-
});
|
|
215
|
-
generationResults.push(generation);
|
|
216
|
-
state = generation.state;
|
|
217
|
-
publish("todo_generated", `Goal iteration ${state.currentIteration}: generated TODO markdown.`, {
|
|
218
|
-
iteration: state.currentIteration,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
publish(
|
|
223
|
-
"todo_execution_start",
|
|
224
|
-
`Goal iteration ${state.currentIteration}: running generated TODO as a long task.`,
|
|
225
|
-
{
|
|
226
|
-
iteration: state.currentIteration,
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
|
-
const execution = await runGoalTodoExecutionLongTask({
|
|
179
|
+
try {
|
|
180
|
+
if (!goalLoopStopReason(state, { now: now(), abortSignal: options.abortSignal })) {
|
|
181
|
+
goalSpecification = await maybeRunGoalDiscovery({
|
|
230
182
|
state,
|
|
231
|
-
cwd: options.cwd,
|
|
232
183
|
store,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
modelName: options.modelName,
|
|
237
|
-
thinkingLevel: options.thinkingLevel,
|
|
238
|
-
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
239
|
-
maxAttemptsPerTask: options.maxAttemptsPerTask,
|
|
240
|
-
commit: options.commit,
|
|
184
|
+
discoveryDecision,
|
|
185
|
+
existingSpecification: goalSpecification,
|
|
186
|
+
options,
|
|
241
187
|
now,
|
|
242
|
-
|
|
243
|
-
publish("todo_execution_start", `Goal iteration ${state.currentIteration}: ${update.message}`, {
|
|
244
|
-
iteration: state.currentIteration,
|
|
245
|
-
workerStatus: update.status,
|
|
246
|
-
childProgress: update,
|
|
247
|
-
});
|
|
248
|
-
options.onWorkerProgress?.(update);
|
|
249
|
-
},
|
|
188
|
+
publish,
|
|
250
189
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
while (state.status === "running") {
|
|
193
|
+
const phaseNow = now();
|
|
194
|
+
const stopReason = goalLoopStopReason(state, { now: phaseNow, abortSignal: options.abortSignal });
|
|
195
|
+
if (stopReason) {
|
|
196
|
+
const previousTraceLength = state.trace.length;
|
|
197
|
+
state = startGoalIteration(state, { now: phaseNow, abortSignal: options.abortSignal });
|
|
198
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const current = state.iterations.find((item) => item.iteration === state.currentIteration);
|
|
203
|
+
if (current && isActiveIterationPhase(current.status) && deadlineExpired(current.deadlineAt, phaseNow)) {
|
|
204
|
+
const previousTraceLength = state.trace.length;
|
|
205
|
+
state = failGoalLoop(state, `Goal iteration ${current.iteration} exceeded its iteration deadline.`, {
|
|
206
|
+
now: phaseNow,
|
|
207
|
+
status: "partial",
|
|
208
|
+
});
|
|
209
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const currentStatus = current?.status;
|
|
214
|
+
if (!current || currentStatus === "reviewed_incomplete") {
|
|
215
|
+
const nextIteration = state.iterations.length + 1;
|
|
216
|
+
publish("todo_generation_start", `Goal iteration ${nextIteration}: generating TODO markdown.`, {
|
|
217
|
+
iteration: nextIteration,
|
|
218
|
+
});
|
|
219
|
+
const generation = await runGoalTodoGenerationLongTask({
|
|
220
|
+
state,
|
|
221
|
+
cwd: options.cwd,
|
|
222
|
+
store,
|
|
223
|
+
longTaskRunner: options.todoGenerationRunner,
|
|
224
|
+
abortSignal: options.abortSignal,
|
|
225
|
+
model: options.model,
|
|
226
|
+
modelName: options.modelName,
|
|
227
|
+
thinkingLevel: options.thinkingLevel,
|
|
228
|
+
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
229
|
+
now,
|
|
230
|
+
goalSpecification,
|
|
231
|
+
});
|
|
232
|
+
generationResults.push(generation);
|
|
233
|
+
state = generation.state;
|
|
234
|
+
publish("todo_generated", `Goal iteration ${state.currentIteration}: generated TODO markdown.`, {
|
|
235
|
+
iteration: state.currentIteration,
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (currentStatus === "pending") {
|
|
241
|
+
publish("todo_generation_start", `Goal iteration ${current.iteration}: resuming TODO generation.`, {
|
|
242
|
+
iteration: current.iteration,
|
|
243
|
+
});
|
|
244
|
+
const generation = await runGoalTodoGenerationLongTask({
|
|
245
|
+
state,
|
|
246
|
+
cwd: options.cwd,
|
|
247
|
+
store,
|
|
248
|
+
longTaskRunner: options.todoGenerationRunner,
|
|
249
|
+
abortSignal: options.abortSignal,
|
|
250
|
+
model: options.model,
|
|
251
|
+
modelName: options.modelName,
|
|
252
|
+
thinkingLevel: options.thinkingLevel,
|
|
253
|
+
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
254
|
+
now,
|
|
255
|
+
goalSpecification,
|
|
256
|
+
});
|
|
257
|
+
generationResults.push(generation);
|
|
258
|
+
state = generation.state;
|
|
259
|
+
publish("todo_generated", `Goal iteration ${state.currentIteration}: generated TODO markdown.`, {
|
|
257
260
|
iteration: state.currentIteration,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (currentStatus === "todo_generated") {
|
|
266
|
+
publish("todo_execution_start", `Goal iteration ${current.iteration}: running generated TODO as a long task.`, {
|
|
267
|
+
iteration: current.iteration,
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const execution = await runGoalTodoExecutionLongTask({
|
|
271
|
+
state,
|
|
272
|
+
cwd: options.cwd,
|
|
273
|
+
store,
|
|
274
|
+
longTaskRunner: options.todoExecutionRunner,
|
|
275
|
+
abortSignal: options.abortSignal,
|
|
276
|
+
model: options.model,
|
|
277
|
+
modelName: options.modelName,
|
|
278
|
+
thinkingLevel: options.thinkingLevel,
|
|
279
|
+
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
280
|
+
maxAttemptsPerTask: options.maxAttemptsPerTask,
|
|
281
|
+
commit: options.commit,
|
|
282
|
+
now,
|
|
283
|
+
onProgress: (update) => {
|
|
284
|
+
publish("todo_execution_start", `Goal iteration ${state.currentIteration}: ${update.message}`, {
|
|
285
|
+
iteration: state.currentIteration,
|
|
286
|
+
workerStatus: update.status,
|
|
287
|
+
childProgress: update,
|
|
288
|
+
});
|
|
289
|
+
options.onWorkerProgress?.(update);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
executionResults.push(execution);
|
|
293
|
+
state = execution.state;
|
|
294
|
+
publish(
|
|
295
|
+
"todo_executed",
|
|
296
|
+
`Goal iteration ${state.currentIteration}: worker finished with ${execution.childResult.status}.`,
|
|
297
|
+
{
|
|
298
|
+
iteration: state.currentIteration,
|
|
299
|
+
workerStatus: execution.childResult.status,
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error instanceof GoalTodoExecutionError && error.state) {
|
|
304
|
+
state = error.state;
|
|
305
|
+
} else {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (currentStatus === "todo_executed" || currentStatus === "failed") {
|
|
313
|
+
publish("review_start", `Goal iteration ${current.iteration}: reviewing goal completion.`, {
|
|
314
|
+
iteration: current.iteration,
|
|
315
|
+
});
|
|
316
|
+
const review = await runGoalReviewSession({
|
|
267
317
|
state,
|
|
318
|
+
cwd: options.cwd,
|
|
319
|
+
store,
|
|
320
|
+
reviewerRunner: options.reviewerRunner,
|
|
321
|
+
abortSignal: options.abortSignal,
|
|
322
|
+
model: options.model,
|
|
323
|
+
modelName: options.modelName,
|
|
324
|
+
thinkingLevel: options.thinkingLevel,
|
|
325
|
+
now,
|
|
326
|
+
goalSpecification,
|
|
327
|
+
timeoutMs: remainingReviewTimeout(state, current.deadlineAt, now()),
|
|
268
328
|
});
|
|
329
|
+
reviewResults.push(review);
|
|
330
|
+
state = review.state;
|
|
331
|
+
publish(
|
|
332
|
+
"reviewed",
|
|
333
|
+
`Goal iteration ${review.iteration.iteration}: reviewer decided ${review.reviewerResult.decision}.`,
|
|
334
|
+
{
|
|
335
|
+
iteration: review.iteration.iteration,
|
|
336
|
+
reviewerDecision: review.reviewerResult.decision,
|
|
337
|
+
remainingWork: review.reviewerResult.remainingWork,
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
continue;
|
|
269
341
|
}
|
|
270
|
-
}
|
|
271
342
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
reviewerDecision: review.reviewerResult.decision,
|
|
295
|
-
remainingWork: review.reviewerResult.remainingWork,
|
|
296
|
-
},
|
|
297
|
-
);
|
|
343
|
+
throw new GoalLoopOrchestratorError(`Cannot resume goal iteration ${current.iteration} from ${currentStatus}.`, {
|
|
344
|
+
state,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const errorState = stateFromError(error);
|
|
349
|
+
if (errorState) {
|
|
350
|
+
state = errorState;
|
|
351
|
+
} else {
|
|
352
|
+
try {
|
|
353
|
+
state = await store.loadState();
|
|
354
|
+
} catch {
|
|
355
|
+
// Retain the latest in-memory state when no newer durable state is available.
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (state.status === "running") {
|
|
359
|
+
const previousTraceLength = state.trace.length;
|
|
360
|
+
state = options.abortSignal?.aborted
|
|
361
|
+
? cancelGoalLoop(state, `Goal loop aborted: ${errorMessage(error)}`, { now: now() })
|
|
362
|
+
: failGoalLoop(state, `Goal loop failed: ${errorMessage(error)}`, { now: now() });
|
|
363
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
364
|
+
}
|
|
298
365
|
}
|
|
299
366
|
|
|
300
367
|
publish("complete", `Goal loop ${state.status}: ${state.completion?.reason ?? "finished"}`);
|
|
@@ -365,18 +432,42 @@ async function persistStateChange(
|
|
|
365
432
|
await store.appendNewTraceEvents(previousTraceLength, state);
|
|
366
433
|
}
|
|
367
434
|
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
435
|
+
function isActiveIterationPhase(status: GoalIterationStatus): boolean {
|
|
436
|
+
return status === "pending" || status === "todo_generated" || status === "todo_executed" || status === "failed";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function deadlineExpired(deadlineAt: string | undefined, now: Date): boolean {
|
|
440
|
+
return Boolean(deadlineAt && now.getTime() >= Date.parse(deadlineAt));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function remainingReviewTimeout(state: GoalLoopState, iterationDeadlineAt: string | undefined, now: Date): number {
|
|
444
|
+
const remaining = [
|
|
445
|
+
state.limits.reviewerTimeoutMs,
|
|
446
|
+
state.deadlineAt ? Date.parse(state.deadlineAt) - now.getTime() : Number.POSITIVE_INFINITY,
|
|
447
|
+
iterationDeadlineAt ? Date.parse(iterationDeadlineAt) - now.getTime() : Number.POSITIVE_INFINITY,
|
|
448
|
+
].filter((value) => Number.isFinite(value));
|
|
449
|
+
return Math.max(1, Math.floor(Math.min(...remaining)));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function stateFromError(error: unknown): GoalLoopState | undefined {
|
|
453
|
+
if (typeof error !== "object" || error === null || !("state" in error)) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
const state = (error as { state?: unknown }).state;
|
|
457
|
+
return state && typeof state === "object" ? (state as GoalLoopState) : undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function accumulatedWorkerCost(state: GoalLoopState): number {
|
|
461
|
+
return sumFinite(
|
|
462
|
+
state.iterations.flatMap((iteration) => [
|
|
463
|
+
iteration.generatedTodo?.generatorWorkerCostTotal,
|
|
464
|
+
iteration.workerResult?.workerCostTotal,
|
|
465
|
+
]),
|
|
466
|
+
);
|
|
376
467
|
}
|
|
377
468
|
|
|
378
|
-
function accumulatedReviewerCost(
|
|
379
|
-
return sumFinite(
|
|
469
|
+
function accumulatedReviewerCost(state: GoalLoopState): number {
|
|
470
|
+
return sumFinite(state.iterations.map((iteration) => iteration.reviewerResult?.reviewerCostTotal));
|
|
380
471
|
}
|
|
381
472
|
|
|
382
473
|
function sumFinite(values: Array<number | undefined>): number {
|
package/src/goal_review.ts
CHANGED
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
recordReviewerResult,
|
|
11
11
|
} from "./goal_loop.ts";
|
|
12
12
|
import { GoalStateStore } from "./goal_state.ts";
|
|
13
|
+
import { runGuardedSessionPrompt } from "./session_guard.ts";
|
|
13
14
|
import { goalSpecificationToMarkdown, type GoalSpecification } from "./goal_spec.ts";
|
|
14
15
|
import {
|
|
15
|
-
assistantTextFromEvent,
|
|
16
16
|
createIsolatedWorkerSession,
|
|
17
17
|
DEFAULT_WORKER_TOOLS,
|
|
18
|
-
lastAssistantTextFromMessages,
|
|
19
18
|
workerUsageCostFromEvent,
|
|
20
19
|
workerUsageCostFromStats,
|
|
20
|
+
workerUsageCostKeyFromEvent,
|
|
21
21
|
type WorkerSessionFactory,
|
|
22
22
|
} from "./worker_session.ts";
|
|
23
23
|
|
|
@@ -36,6 +36,7 @@ export interface GoalReviewOptions {
|
|
|
36
36
|
now?: () => Date;
|
|
37
37
|
sessionFactory?: WorkerSessionFactory;
|
|
38
38
|
goalSpecification?: GoalSpecification;
|
|
39
|
+
timeoutMs?: number;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface GoalReviewResult {
|
|
@@ -121,7 +122,7 @@ export async function runGoalReviewSession(options: GoalReviewOptions): Promise<
|
|
|
121
122
|
prompt: payload,
|
|
122
123
|
cwd: path.resolve(options.cwd ?? process.cwd()),
|
|
123
124
|
abortSignal: options.abortSignal,
|
|
124
|
-
timeoutMs: state.limits.reviewerTimeoutMs,
|
|
125
|
+
timeoutMs: options.timeoutMs ?? state.limits.reviewerTimeoutMs,
|
|
125
126
|
model: options.model,
|
|
126
127
|
modelName: options.modelName,
|
|
127
128
|
thinkingLevel: options.thinkingLevel,
|
|
@@ -149,6 +150,36 @@ export async function runGoalReviewSession(options: GoalReviewOptions): Promise<
|
|
|
149
150
|
const rawReviewerOutput = sessionResult.assistantText;
|
|
150
151
|
await writeFile(rawReviewPath, rawReviewerOutput, "utf8");
|
|
151
152
|
|
|
153
|
+
if (sessionResult.aborted) {
|
|
154
|
+
const reason = sessionResult.error ?? "Goal review was aborted.";
|
|
155
|
+
state = cancelGoalLoop(state, reason, { now: now() });
|
|
156
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
157
|
+
await store.writeIterationSnapshot(currentIteration(state, iteration.iteration));
|
|
158
|
+
throw new GoalReviewError(reason, { state });
|
|
159
|
+
}
|
|
160
|
+
if (sessionResult.timedOut || sessionResult.error) {
|
|
161
|
+
const message = sessionResult.timedOut
|
|
162
|
+
? `Reviewer session timed out: ${sessionResult.error ?? "time budget exceeded"}`
|
|
163
|
+
: `Reviewer session failed: ${sessionResult.error}`;
|
|
164
|
+
const failure = await recordReviewFailure({
|
|
165
|
+
state,
|
|
166
|
+
iteration,
|
|
167
|
+
store,
|
|
168
|
+
previousTraceLength,
|
|
169
|
+
payloadPath,
|
|
170
|
+
rawReviewPath,
|
|
171
|
+
message,
|
|
172
|
+
error: sessionResult.error ?? message,
|
|
173
|
+
rawReviewerOutput,
|
|
174
|
+
sessionResult,
|
|
175
|
+
now,
|
|
176
|
+
});
|
|
177
|
+
throw new GoalReviewError(failure.reviewerResult.summary, {
|
|
178
|
+
state: failure.state,
|
|
179
|
+
reviewerResult: failure.reviewerResult,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
let reviewerResult: GoalReviewerResultState;
|
|
153
184
|
try {
|
|
154
185
|
reviewerResult = {
|
|
@@ -322,33 +353,17 @@ function minimumIterationRemainingWork(state: GoalLoopState): string[] {
|
|
|
322
353
|
|
|
323
354
|
export async function runGoalReviewerSession(options: GoalReviewerRunnerOptions): Promise<GoalReviewerSessionResult> {
|
|
324
355
|
const sessionFactory = options.sessionFactory ?? createIsolatedWorkerSession;
|
|
325
|
-
const events: unknown[] = [];
|
|
326
|
-
let assistantText = "";
|
|
327
|
-
let timedOut = false;
|
|
328
|
-
let aborted = false;
|
|
329
|
-
let error: string | undefined;
|
|
330
356
|
let reviewerCostTotal = 0;
|
|
331
357
|
let session: Awaited<ReturnType<typeof sessionFactory>>["session"] | undefined;
|
|
332
|
-
|
|
333
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
334
|
-
|
|
335
|
-
const abortSession = async (reason: string) => {
|
|
336
|
-
if (!session || aborted) {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
aborted = true;
|
|
340
|
-
error = error ?? reason;
|
|
341
|
-
await session.abort?.();
|
|
342
|
-
};
|
|
343
|
-
const abortListener = () => {
|
|
344
|
-
void abortSession("reviewer session aborted by outer signal").catch((exc: unknown) => {
|
|
345
|
-
error = error ?? errorMessage(exc);
|
|
346
|
-
});
|
|
347
|
-
};
|
|
358
|
+
const costsByMessage = new Map<string, number>();
|
|
348
359
|
|
|
349
360
|
try {
|
|
350
361
|
if (options.abortSignal?.aborted) {
|
|
351
|
-
|
|
362
|
+
return {
|
|
363
|
+
assistantText: "",
|
|
364
|
+
aborted: true,
|
|
365
|
+
error: errorMessage(options.abortSignal.reason ?? "reviewer session aborted before start"),
|
|
366
|
+
};
|
|
352
367
|
}
|
|
353
368
|
const factoryResult = await sessionFactory({
|
|
354
369
|
cwd: options.cwd,
|
|
@@ -358,55 +373,65 @@ export async function runGoalReviewerSession(options: GoalReviewerRunnerOptions)
|
|
|
358
373
|
thinkingLevel: options.thinkingLevel,
|
|
359
374
|
});
|
|
360
375
|
session = factoryResult.session;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
376
|
+
const promptResult = await runGuardedSessionPrompt({
|
|
377
|
+
session,
|
|
378
|
+
prompt: options.prompt,
|
|
379
|
+
abortSignal: options.abortSignal,
|
|
380
|
+
timeoutMs: options.timeoutMs,
|
|
381
|
+
gracefulShutdownMs: 0,
|
|
382
|
+
diagnostics: factoryResult.diagnostics,
|
|
383
|
+
dispose: false,
|
|
384
|
+
onEvent: (event) => {
|
|
385
|
+
const cost = workerUsageCostFromEvent(event);
|
|
386
|
+
if (cost === undefined) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const key = workerUsageCostKeyFromEvent(event);
|
|
390
|
+
if (key) {
|
|
391
|
+
costsByMessage.set(key, cost);
|
|
392
|
+
reviewerCostTotal = [...costsByMessage.values()].reduce((total, item) => total + item, 0);
|
|
393
|
+
} else {
|
|
394
|
+
reviewerCostTotal += cost;
|
|
395
|
+
}
|
|
396
|
+
},
|
|
371
397
|
});
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const stats = session.getSessionStats ? await session.getSessionStats() : undefined;
|
|
401
|
+
const statsCost = workerUsageCostFromStats(stats);
|
|
402
|
+
if (statsCost !== undefined) {
|
|
403
|
+
reviewerCostTotal = statsCost;
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
// Event-based accounting remains available when session stats fail.
|
|
380
407
|
}
|
|
381
|
-
|
|
382
|
-
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
assistantText: promptResult.assistantText,
|
|
411
|
+
reviewerSessionId: promptResult.sessionId,
|
|
412
|
+
reviewerSessionFile: promptResult.sessionFile,
|
|
413
|
+
reviewerCostTotal,
|
|
414
|
+
timedOut: promptResult.timedOut,
|
|
415
|
+
aborted: promptResult.aborted,
|
|
416
|
+
error: promptResult.error,
|
|
417
|
+
};
|
|
383
418
|
} catch (exc) {
|
|
384
|
-
|
|
419
|
+
return {
|
|
420
|
+
assistantText: "",
|
|
421
|
+
reviewerSessionId: session?.sessionId,
|
|
422
|
+
reviewerSessionFile: session?.sessionFile,
|
|
423
|
+
reviewerCostTotal,
|
|
424
|
+
error: errorMessage(exc),
|
|
425
|
+
};
|
|
385
426
|
} finally {
|
|
386
|
-
if (timeout) {
|
|
387
|
-
clearTimeout(timeout);
|
|
388
|
-
}
|
|
389
|
-
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
390
|
-
unsubscribe?.();
|
|
391
427
|
if (session) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
428
|
+
try {
|
|
429
|
+
await Promise.resolve(session.dispose?.());
|
|
430
|
+
} catch {
|
|
431
|
+
// Cleanup is best-effort and must not replace a useful reviewer outcome.
|
|
396
432
|
}
|
|
397
|
-
session.dispose?.();
|
|
398
433
|
}
|
|
399
434
|
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
assistantText,
|
|
403
|
-
reviewerSessionId: session?.sessionId,
|
|
404
|
-
reviewerSessionFile: session?.sessionFile,
|
|
405
|
-
reviewerCostTotal,
|
|
406
|
-
timedOut,
|
|
407
|
-
aborted: aborted || Boolean(options.abortSignal?.aborted),
|
|
408
|
-
error,
|
|
409
|
-
};
|
|
410
435
|
}
|
|
411
436
|
|
|
412
437
|
function currentReviewableIteration(state: GoalLoopState): GoalIterationState {
|
|
@@ -584,13 +609,6 @@ function defaultSummary(decision: GoalReviewerDecision): string {
|
|
|
584
609
|
}
|
|
585
610
|
}
|
|
586
611
|
|
|
587
|
-
function latestAssistantText(
|
|
588
|
-
session: { getLastAssistantText?: () => string | undefined; messages?: unknown[] },
|
|
589
|
-
fallback: string,
|
|
590
|
-
): string {
|
|
591
|
-
return session.getLastAssistantText?.() || lastAssistantTextFromMessages(session.messages) || fallback;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
612
|
function markdownFence(value: string, language: string): string {
|
|
595
613
|
const ticks = longestBacktickRun(value) + 1;
|
|
596
614
|
const fence = "`".repeat(Math.max(3, ticks));
|
package/src/goal_state.ts
CHANGED
|
@@ -97,6 +97,31 @@ export class GoalStateStore {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
async initializeResultIfMissing(state: GoalLoopState): Promise<boolean> {
|
|
101
|
+
try {
|
|
102
|
+
await readFile(this.paths.resultPath, "utf8");
|
|
103
|
+
return false;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (!isNodeErrnoException(error) || error.code !== "ENOENT") {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await this.initializeResult(state);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async durableTraceLength(): Promise<number> {
|
|
114
|
+
try {
|
|
115
|
+
const text = await readFile(this.paths.tracePath, "utf8");
|
|
116
|
+
return text.split(/\r?\n/).filter((line) => line.trim()).length;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (isNodeErrnoException(error) && error.code === "ENOENT") {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
async initializeResult(state: GoalLoopState): Promise<void> {
|
|
101
126
|
validateGoalLoopState(state);
|
|
102
127
|
await this.ensureRunDir();
|