pi-long-task 0.3.11 → 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 +14 -6
- package/package.json +9 -5
- 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 +163 -44
package/src/worker_session.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
1
3
|
import { coverageGoalAction, coverageGoalVerification, parseCoverageGoal } from "./coverage_goal.ts";
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
hasCompleteTaskResult,
|
|
6
|
+
hasTaskResult,
|
|
7
|
+
isDoneStatus,
|
|
8
|
+
parseCompleteTaskResult,
|
|
9
|
+
parseReportedStatus,
|
|
10
|
+
} from "./result_writer.ts";
|
|
3
11
|
import type { Task } from "./todo_parser.ts";
|
|
4
12
|
|
|
5
13
|
export interface WorkerTaskPromptOptions {
|
|
@@ -243,7 +251,10 @@ export interface CreateWorkerSessionOptions {
|
|
|
243
251
|
model?: unknown;
|
|
244
252
|
modelName?: string;
|
|
245
253
|
thinkingLevel?: string;
|
|
254
|
+
modelRuntime?: unknown;
|
|
255
|
+
/** @deprecated Compatibility injection for Pi SDK versions before 0.80.8. */
|
|
246
256
|
authStorage?: unknown;
|
|
257
|
+
/** @deprecated Compatibility injection for Pi SDK versions before 0.80.8. */
|
|
247
258
|
modelRegistry?: unknown;
|
|
248
259
|
settingsManager?: unknown;
|
|
249
260
|
resourceLoader?: unknown;
|
|
@@ -306,14 +317,72 @@ remaining:
|
|
|
306
317
|
- <remaining item or "none">`;
|
|
307
318
|
}
|
|
308
319
|
|
|
320
|
+
interface WorkerPiSdkModelExports {
|
|
321
|
+
ModelRuntime?: {
|
|
322
|
+
create(options?: { authPath?: string; modelsPath?: string | null }): Promise<unknown>;
|
|
323
|
+
};
|
|
324
|
+
AuthStorage?: {
|
|
325
|
+
create(authPath?: string): unknown;
|
|
326
|
+
};
|
|
327
|
+
ModelRegistry?: {
|
|
328
|
+
create?(authStorage: unknown, modelsPath?: string): unknown;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface WorkerModelContext {
|
|
333
|
+
resolver: unknown;
|
|
334
|
+
sessionOptions: Record<string, unknown>;
|
|
335
|
+
api: "modelRuntime" | "legacy";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function createWorkerModelContext(
|
|
339
|
+
sdk: WorkerPiSdkModelExports,
|
|
340
|
+
options: Pick<CreateWorkerSessionOptions, "modelRuntime" | "authStorage" | "modelRegistry">,
|
|
341
|
+
agentDir: string,
|
|
342
|
+
): Promise<WorkerModelContext> {
|
|
343
|
+
if (options.modelRuntime) {
|
|
344
|
+
return {
|
|
345
|
+
resolver: options.modelRuntime,
|
|
346
|
+
sessionOptions: { modelRuntime: options.modelRuntime },
|
|
347
|
+
api: "modelRuntime",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (sdk.ModelRuntime?.create) {
|
|
352
|
+
const modelRuntime = await sdk.ModelRuntime.create({
|
|
353
|
+
authPath: path.join(agentDir, "auth.json"),
|
|
354
|
+
modelsPath: path.join(agentDir, "models.json"),
|
|
355
|
+
});
|
|
356
|
+
return {
|
|
357
|
+
resolver: modelRuntime,
|
|
358
|
+
sessionOptions: { modelRuntime },
|
|
359
|
+
api: "modelRuntime",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const authStorage = options.authStorage ?? sdk.AuthStorage?.create(path.join(agentDir, "auth.json"));
|
|
364
|
+
const modelRegistry =
|
|
365
|
+
options.modelRegistry ?? sdk.ModelRegistry?.create?.(authStorage, path.join(agentDir, "models.json"));
|
|
366
|
+
if (!authStorage || !modelRegistry) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
"Pi Long Task cannot initialize worker model services: this Pi SDK exposes neither ModelRuntime.create() nor the legacy AuthStorage.create()/ModelRegistry.create() API.",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
resolver: modelRegistry,
|
|
374
|
+
sessionOptions: { authStorage, modelRegistry },
|
|
375
|
+
api: "legacy",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
309
379
|
export async function createIsolatedWorkerSession(
|
|
310
380
|
options: CreateWorkerSessionOptions,
|
|
311
381
|
): Promise<WorkerSessionFactoryResult> {
|
|
312
382
|
const pi = await import("@earendil-works/pi-coding-agent");
|
|
313
383
|
const cwd = options.cwd;
|
|
314
384
|
const agentDir = options.agentDir ?? pi.getAgentDir();
|
|
315
|
-
const
|
|
316
|
-
const modelRegistry = options.modelRegistry ?? pi.ModelRegistry.create(authStorage as never);
|
|
385
|
+
const modelContext = await createWorkerModelContext(pi as unknown as WorkerPiSdkModelExports, options, agentDir);
|
|
317
386
|
const settingsManager = options.settingsManager ?? pi.SettingsManager.create(cwd, agentDir);
|
|
318
387
|
|
|
319
388
|
applyWorkerSettingsDefaults(settingsManager);
|
|
@@ -330,12 +399,12 @@ export async function createIsolatedWorkerSession(
|
|
|
330
399
|
await resourceLoader.reload();
|
|
331
400
|
|
|
332
401
|
const model =
|
|
333
|
-
options.model ??
|
|
402
|
+
options.model ??
|
|
403
|
+
(options.modelName ? await resolveWorkerModel(modelContext.resolver, options.modelName) : undefined);
|
|
334
404
|
const createOptions: Record<string, unknown> = {
|
|
335
405
|
cwd,
|
|
336
406
|
agentDir,
|
|
337
|
-
|
|
338
|
-
modelRegistry,
|
|
407
|
+
...modelContext.sessionOptions,
|
|
339
408
|
settingsManager,
|
|
340
409
|
resourceLoader,
|
|
341
410
|
tools: [...(options.tools ?? DEFAULT_WORKER_TOOLS)],
|
|
@@ -373,6 +442,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
373
442
|
let messageUsageCostTotal = 0;
|
|
374
443
|
let hasMessageUsageCost = false;
|
|
375
444
|
let sessionStatsCostTotal: number | undefined;
|
|
445
|
+
let resolvePromptWait: (() => void) | undefined;
|
|
376
446
|
|
|
377
447
|
const prompt = buildTaskPrompt(options);
|
|
378
448
|
const taskTimeoutSeconds = options.taskTimeoutSeconds ?? DEFAULT_TASK_TIMEOUT_SECONDS;
|
|
@@ -420,38 +490,86 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
420
490
|
timers.add(timer);
|
|
421
491
|
};
|
|
422
492
|
|
|
423
|
-
const requestGracefulTaskResult =
|
|
493
|
+
const requestGracefulTaskResult = (message: string, options: { shutdown?: boolean } = {}) => {
|
|
424
494
|
if (!session || finished || aborted) {
|
|
425
495
|
return;
|
|
426
496
|
}
|
|
427
497
|
if (options.shutdown) {
|
|
428
498
|
shutdownRequested = true;
|
|
429
499
|
}
|
|
430
|
-
|
|
431
|
-
session.abortBash
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
500
|
+
try {
|
|
501
|
+
if (session.isBashRunning && session.abortBash) {
|
|
502
|
+
session.abortBash();
|
|
503
|
+
compactionEvents.push("aborted running bash before graceful shutdown request");
|
|
504
|
+
}
|
|
505
|
+
const request =
|
|
506
|
+
(session.isStreaming || session.isBashRunning) && session.steer
|
|
507
|
+
? session.steer(message)
|
|
508
|
+
: session.followUp
|
|
509
|
+
? session.followUp(message)
|
|
510
|
+
: session.steer
|
|
511
|
+
? session.steer(message)
|
|
512
|
+
: undefined;
|
|
513
|
+
if (!request) {
|
|
514
|
+
compactionEvents.push("graceful TASK_RESULT request skipped: session does not support steer/followUp");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
void request.catch((exc: unknown) => {
|
|
518
|
+
compactionEvents.push(`graceful TASK_RESULT request failed: ${errorMessage(exc)}`);
|
|
519
|
+
});
|
|
520
|
+
} catch (exc) {
|
|
521
|
+
compactionEvents.push(`graceful TASK_RESULT request failed: ${errorMessage(exc)}`);
|
|
438
522
|
}
|
|
439
523
|
};
|
|
440
524
|
|
|
441
|
-
const abortSession =
|
|
525
|
+
const abortSession = (reason: string) => {
|
|
442
526
|
if (!session || finished || aborted) {
|
|
443
527
|
return;
|
|
444
528
|
}
|
|
445
529
|
aborted = true;
|
|
446
530
|
shutdownRequested = true;
|
|
447
531
|
error = error ?? reason;
|
|
448
|
-
|
|
532
|
+
try {
|
|
533
|
+
const abortResult = session.abort?.();
|
|
534
|
+
void Promise.resolve(abortResult).catch((exc: unknown) => {
|
|
535
|
+
compactionEvents.push(`session abort failed: ${errorMessage(exc)}`);
|
|
536
|
+
});
|
|
537
|
+
} catch (exc) {
|
|
538
|
+
compactionEvents.push(`session abort failed: ${errorMessage(exc)}`);
|
|
539
|
+
}
|
|
540
|
+
resolvePromptWait?.();
|
|
541
|
+
resolvePromptWait = undefined;
|
|
449
542
|
};
|
|
450
543
|
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
544
|
+
const waitForPrompt = async (text: string): Promise<void> => {
|
|
545
|
+
if (!session) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
let settled = false;
|
|
549
|
+
const completed = new Promise<void>((resolve) => {
|
|
550
|
+
resolvePromptWait = resolve;
|
|
454
551
|
});
|
|
552
|
+
void session.prompt(text).then(
|
|
553
|
+
() => {
|
|
554
|
+
settled = true;
|
|
555
|
+
resolvePromptWait?.();
|
|
556
|
+
resolvePromptWait = undefined;
|
|
557
|
+
},
|
|
558
|
+
(exc: unknown) => {
|
|
559
|
+
settled = true;
|
|
560
|
+
error = error ?? errorMessage(exc);
|
|
561
|
+
resolvePromptWait?.();
|
|
562
|
+
resolvePromptWait = undefined;
|
|
563
|
+
},
|
|
564
|
+
);
|
|
565
|
+
await completed;
|
|
566
|
+
if (!settled && !aborted) {
|
|
567
|
+
error = error ?? "worker prompt ended without settling";
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const abortListener = () => {
|
|
572
|
+
abortSession("worker session aborted by outer signal");
|
|
455
573
|
};
|
|
456
574
|
|
|
457
575
|
try {
|
|
@@ -549,12 +667,12 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
549
667
|
options.abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
550
668
|
|
|
551
669
|
if (taskTimeoutSeconds > 0) {
|
|
552
|
-
schedule(
|
|
553
|
-
if (finished ||
|
|
670
|
+
schedule(() => {
|
|
671
|
+
if (finished || hasCompleteTaskResult(assistantText)) {
|
|
554
672
|
return;
|
|
555
673
|
}
|
|
556
674
|
timedOut = true;
|
|
557
|
-
|
|
675
|
+
requestGracefulTaskResult(buildTimeLimitMessage(taskTimeoutSeconds), { shutdown: true });
|
|
558
676
|
if (gracefulShutdownSeconds > 0) {
|
|
559
677
|
schedule(
|
|
560
678
|
() => abortSession(`task exceeded ${taskTimeoutSeconds.toFixed(0)}s timeout`),
|
|
@@ -564,12 +682,14 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
564
682
|
}, taskTimeoutSeconds * 1000);
|
|
565
683
|
}
|
|
566
684
|
|
|
567
|
-
await
|
|
685
|
+
await waitForPrompt(prompt);
|
|
568
686
|
assistantText = latestAssistantText(session, assistantText);
|
|
569
687
|
|
|
570
|
-
if (!
|
|
571
|
-
contextObservations.push(
|
|
572
|
-
|
|
688
|
+
if (!hasCompleteTaskResult(assistantText) && !error && !aborted && !timedOut && !options.abortSignal?.aborted) {
|
|
689
|
+
contextObservations.push(
|
|
690
|
+
"missing TASK_RESULT status after initial prompt, or required fields were incomplete; requested required block once",
|
|
691
|
+
);
|
|
692
|
+
await waitForPrompt(buildMissingTaskResultMessage());
|
|
573
693
|
assistantText = latestAssistantText(session, assistantText);
|
|
574
694
|
}
|
|
575
695
|
} catch (exc) {
|
|
@@ -584,7 +704,11 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
584
704
|
sessionFile = session.sessionFile ?? sessionFile;
|
|
585
705
|
sessionId = session.sessionId ?? sessionId;
|
|
586
706
|
sessionStatsCostTotal = await workerUsageCostFromSessionStats(session);
|
|
587
|
-
|
|
707
|
+
try {
|
|
708
|
+
await Promise.resolve(session.dispose?.());
|
|
709
|
+
} catch (exc) {
|
|
710
|
+
compactionEvents.push(`session dispose failed: ${errorMessage(exc)}`);
|
|
711
|
+
}
|
|
588
712
|
}
|
|
589
713
|
}
|
|
590
714
|
|
|
@@ -592,7 +716,8 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
592
716
|
assistantText = buildLongTaskFailureTaskResult(error ?? (timedOut ? "task timed out" : "worker session aborted"));
|
|
593
717
|
}
|
|
594
718
|
|
|
595
|
-
const
|
|
719
|
+
const parsedResult = parseCompleteTaskResult(assistantText);
|
|
720
|
+
const reportedStatus = parsedResult?.status ?? parseReportedStatus(assistantText);
|
|
596
721
|
const capturedWorkerCost = selectWorkerCostTotal({
|
|
597
722
|
messageCostTotal: hasMessageUsageCost ? messageUsageCostTotal : undefined,
|
|
598
723
|
statsCostTotal: sessionStatsCostTotal,
|
|
@@ -603,7 +728,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
603
728
|
startedAt,
|
|
604
729
|
endedAt: now().toISOString(),
|
|
605
730
|
reportedStatus,
|
|
606
|
-
done: isDoneStatus(reportedStatus),
|
|
731
|
+
done: Boolean(parsedResult && isDoneStatus(reportedStatus) && !error && !aborted && !timedOut),
|
|
607
732
|
assistantText,
|
|
608
733
|
sessionFile,
|
|
609
734
|
sessionId,
|
|
@@ -672,24 +797,18 @@ function applyWorkerSettingsDefaults(settingsManager: unknown): void {
|
|
|
672
797
|
}
|
|
673
798
|
}
|
|
674
799
|
|
|
675
|
-
async function resolveWorkerModel(
|
|
800
|
+
async function resolveWorkerModel(modelResolver: unknown, modelName: string): Promise<unknown> {
|
|
676
801
|
const [provider, ...modelIdParts] = modelName.split("/");
|
|
677
802
|
const modelId = modelIdParts.join("/");
|
|
678
|
-
if (provider
|
|
679
|
-
|
|
680
|
-
if (registryModel) {
|
|
681
|
-
return registryModel;
|
|
682
|
-
}
|
|
803
|
+
if (!provider || !modelId || !isRecord(modelResolver)) {
|
|
804
|
+
return undefined;
|
|
683
805
|
}
|
|
684
806
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
} catch {
|
|
692
|
-
// Optional peer resolution can fail in tests that inject a session factory.
|
|
807
|
+
if (typeof modelResolver.getModel === "function") {
|
|
808
|
+
return modelResolver.getModel(provider, modelId);
|
|
809
|
+
}
|
|
810
|
+
if (typeof modelResolver.find === "function") {
|
|
811
|
+
return modelResolver.find(provider, modelId);
|
|
693
812
|
}
|
|
694
813
|
return undefined;
|
|
695
814
|
}
|