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.
@@ -1,5 +1,13 @@
1
+ import path from "node:path";
2
+
1
3
  import { coverageGoalAction, coverageGoalVerification, parseCoverageGoal } from "./coverage_goal.ts";
2
- import { hasTaskResult, hasTaskResultStatus, isDoneStatus, parseReportedStatus } from "./result_writer.ts";
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 authStorage = options.authStorage ?? pi.AuthStorage.create();
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 ?? (options.modelName ? await resolveWorkerModel(modelRegistry, options.modelName) : undefined);
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
- authStorage,
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 = async (message: string, options: { shutdown?: boolean } = {}) => {
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
- if (session.isBashRunning && session.abortBash) {
431
- session.abortBash();
432
- compactionEvents.push("aborted running bash before graceful shutdown request");
433
- }
434
- if ((session.isStreaming || session.isBashRunning) && session.steer) {
435
- await session.steer(message);
436
- } else {
437
- await session.prompt(message);
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 = async (reason: string) => {
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
- await session.abort?.();
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 abortListener = () => {
452
- void abortSession("worker session aborted by outer signal").catch((exc: unknown) => {
453
- compactionEvents.push(`abort by outer signal failed: ${errorMessage(exc)}`);
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(async () => {
553
- if (finished || hasTaskResultStatus(assistantText)) {
670
+ schedule(() => {
671
+ if (finished || hasCompleteTaskResult(assistantText)) {
554
672
  return;
555
673
  }
556
674
  timedOut = true;
557
- await requestGracefulTaskResult(buildTimeLimitMessage(taskTimeoutSeconds), { shutdown: true });
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 session.prompt(prompt);
685
+ await waitForPrompt(prompt);
568
686
  assistantText = latestAssistantText(session, assistantText);
569
687
 
570
- if (!hasTaskResultStatus(assistantText) && !aborted && !options.abortSignal?.aborted) {
571
- contextObservations.push("missing TASK_RESULT status after initial prompt; requested required block once");
572
- await requestGracefulTaskResult(buildMissingTaskResultMessage());
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
- session.dispose?.();
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 reportedStatus = parseReportedStatus(assistantText);
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(modelRegistry: unknown, modelName: string): Promise<unknown> {
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 && modelId && isRecord(modelRegistry) && typeof modelRegistry.find === "function") {
679
- const registryModel = modelRegistry.find(provider, modelId);
680
- if (registryModel) {
681
- return registryModel;
682
- }
803
+ if (!provider || !modelId || !isRecord(modelResolver)) {
804
+ return undefined;
683
805
  }
684
806
 
685
- try {
686
- const ai = await import("@earendil-works/pi-ai");
687
- if (typeof ai.getModel === "function" && provider && modelId) {
688
- const getModel = ai.getModel as (providerName: string, modelId: string) => unknown;
689
- return getModel(provider, modelId);
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
  }