pi-long-task 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +139 -10
- package/package.json +2 -2
- package/src/coordinator.ts +1132 -45
- package/src/goal_discovery.ts +2 -0
- package/src/goal_loop.ts +76 -0
- package/src/goal_orchestrator.ts +87 -1
- package/src/goal_review.ts +206 -15
- package/src/goal_todo_execution.ts +3 -0
- package/src/goal_todo_generation.ts +96 -3
- package/src/index.ts +20 -5
- package/src/network_failure.ts +574 -0
- package/src/network_recovery.ts +395 -0
- package/src/network_recovery_config.ts +89 -0
- package/src/render.ts +2 -0
- package/src/session_guard.ts +8 -1
- package/src/todo_generator.ts +2 -2
- package/src/types.ts +32 -0
- package/src/worker_config.ts +137 -14
- package/src/worker_reuse_policy.ts +389 -0
- package/src/worker_session.ts +294 -34
package/src/worker_session.ts
CHANGED
|
@@ -8,14 +8,25 @@ import {
|
|
|
8
8
|
parseCompleteTaskResult,
|
|
9
9
|
parseReportedStatus,
|
|
10
10
|
} from "./result_writer.ts";
|
|
11
|
+
import type { NetworkRecoveryConfig } from "./network_recovery_config.ts";
|
|
11
12
|
import type { Task } from "./todo_parser.ts";
|
|
12
13
|
|
|
14
|
+
export interface WorkerNetworkRecoveryContext {
|
|
15
|
+
/** One-based coordinator network retry count; this is not a task attempt. */
|
|
16
|
+
retryCount: number;
|
|
17
|
+
durableEvidencePath: string;
|
|
18
|
+
priorSessionId?: string;
|
|
19
|
+
failure: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export interface WorkerTaskPromptOptions {
|
|
14
23
|
todoPath: string;
|
|
15
24
|
task: Pick<Task, "taskId" | "title" | "section">;
|
|
16
25
|
attempt: number;
|
|
17
26
|
commitRequested: boolean;
|
|
18
27
|
previousAttempts?: string;
|
|
28
|
+
/** Continuity supplied only when a failed transport session is replaced. */
|
|
29
|
+
networkRecoveryContext?: WorkerNetworkRecoveryContext;
|
|
19
30
|
globalInstructions?: string;
|
|
20
31
|
goal?: string;
|
|
21
32
|
maxBashTimeoutSeconds: number;
|
|
@@ -43,6 +54,18 @@ Previous attempts for this same assigned task are below. Use them only as contin
|
|
|
43
54
|
\`\`\`text
|
|
44
55
|
${previousAttempts}
|
|
45
56
|
\`\`\`
|
|
57
|
+
`
|
|
58
|
+
: "";
|
|
59
|
+
|
|
60
|
+
const recovery = options.networkRecoveryContext;
|
|
61
|
+
const recoveryText = recovery
|
|
62
|
+
? `
|
|
63
|
+
Network recovery continuation (network retry ${recovery.retryCount}, still ordinary task attempt ${options.attempt}):
|
|
64
|
+
- The prior worker session ended only after Pi's bounded provider-request retries were exhausted: ${recovery.failure}
|
|
65
|
+
- Durable interruption evidence was recorded in \`${recovery.durableEvidencePath}\`${recovery.priorSessionId ? ` for session \`${recovery.priorSessionId}\`` : ""}.
|
|
66
|
+
- The prior session may already have completed tool calls and changed the working tree. Inspect the durable evidence and current files before acting.
|
|
67
|
+
- Continue this same TODO from its current state. Never blindly replay prior edits, commands, commits, external writes, or other side effects.
|
|
68
|
+
- Report one final TASK_RESULT for the assignment only after verifying what remains.
|
|
46
69
|
`
|
|
47
70
|
: "";
|
|
48
71
|
|
|
@@ -93,7 +116,7 @@ Rules:
|
|
|
93
116
|
- Do not run bash commands with timeout greater than ${options.maxBashTimeoutSeconds.toFixed(0)} seconds. For long full-suite checks, run once with a bounded timeout and report any timeout/failure in TASK_RESULT instead of continuing indefinitely.
|
|
94
117
|
- If TODO-file global instructions restrict scope, obey them strictly. If the task appears to require out-of-scope code changes, stop and report \`status: blocked\` instead of changing those files.
|
|
95
118
|
|
|
96
|
-
${globalText}Assigned task content only:
|
|
119
|
+
${globalText}${recoveryText}Assigned task content only:
|
|
97
120
|
|
|
98
121
|
\`\`\`markdown
|
|
99
122
|
${options.task.section.trimEnd()}
|
|
@@ -116,6 +139,18 @@ Only use \`status: done\` if the assigned task is fully complete and verified as
|
|
|
116
139
|
|
|
117
140
|
export const buildAssignedTaskPrompt = buildTaskPrompt;
|
|
118
141
|
|
|
142
|
+
export function buildReusedAssignmentPrompt(
|
|
143
|
+
options: WorkerTaskPromptOptions,
|
|
144
|
+
previousTask: Pick<Task, "taskId" | "title">,
|
|
145
|
+
): string {
|
|
146
|
+
return `Pi Long Task assignment boundary:
|
|
147
|
+
The prior assignment ${taskLabel(previousTask)} has ended.
|
|
148
|
+
A new, independent assignment has begun: ${taskLabel(options.task)}.
|
|
149
|
+
Treat every instruction and TASK_RESULT below as belonging only to this new assignment. Do not repeat or reuse the prior assignment's TASK_RESULT.
|
|
150
|
+
|
|
151
|
+
${buildTaskPrompt(options)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
119
154
|
export function buildTimeLimitMessage(seconds: number): string {
|
|
120
155
|
return `Pi Long Task notice: this worker session has reached its ${seconds.toFixed(0)}s time budget.
|
|
121
156
|
Stop after the current safe point. Do not start more implementation work.
|
|
@@ -244,6 +279,18 @@ export interface WorkerSessionFactoryResult {
|
|
|
244
279
|
diagnostics?: string[];
|
|
245
280
|
}
|
|
246
281
|
|
|
282
|
+
/** Coordinator-owned session allocation. Disposal is idempotent and owned by disposeWorkerSessionResource(). */
|
|
283
|
+
export interface WorkerSessionResource extends WorkerSessionFactoryResult {
|
|
284
|
+
disposed: boolean;
|
|
285
|
+
/** Last readable cumulative statistics, retained only for task-boundary delta accounting. */
|
|
286
|
+
accountingBaseline?: WorkerSessionStatsSnapshot;
|
|
287
|
+
completedAssignments?: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface ReusedWorkerAssignment {
|
|
291
|
+
previousTask: Pick<Task, "taskId" | "title">;
|
|
292
|
+
}
|
|
293
|
+
|
|
247
294
|
export interface CreateWorkerSessionOptions {
|
|
248
295
|
cwd: string;
|
|
249
296
|
agentDir?: string;
|
|
@@ -265,9 +312,12 @@ export type WorkerSessionFactory = (options: CreateWorkerSessionOptions) => Prom
|
|
|
265
312
|
export interface RunWorkerTaskOptions extends WorkerTaskPromptOptions, CreateWorkerSessionOptions {
|
|
266
313
|
taskTimeoutSeconds?: number;
|
|
267
314
|
gracefulShutdownSeconds?: number;
|
|
315
|
+
/** Normalized coordinator policy for resuming this operation after transient transport failure. */
|
|
316
|
+
networkRecovery?: Readonly<NetworkRecoveryConfig>;
|
|
268
317
|
abortSignal?: AbortSignal;
|
|
269
318
|
sessionFactory?: WorkerSessionFactory;
|
|
270
319
|
onEvent?: (event: CapturedWorkerEvent) => void;
|
|
320
|
+
onSessionDiagnostic?: (diagnostic: WorkerSessionDiagnostic) => void;
|
|
271
321
|
now?: () => Date;
|
|
272
322
|
}
|
|
273
323
|
|
|
@@ -282,6 +332,29 @@ export interface CapturedWorkerEvent {
|
|
|
282
332
|
usageCostKey?: string;
|
|
283
333
|
}
|
|
284
334
|
|
|
335
|
+
export interface WorkerUsageTotals {
|
|
336
|
+
input: number;
|
|
337
|
+
output: number;
|
|
338
|
+
cacheRead: number;
|
|
339
|
+
cacheWrite: number;
|
|
340
|
+
total: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface WorkerSessionStatsSnapshot {
|
|
344
|
+
cost?: number;
|
|
345
|
+
tokens?: WorkerUsageTotals;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export type WorkerSessionLifecycleEvent = "session_started" | "session_reused" | "session_rotated" | "session_retained";
|
|
349
|
+
|
|
350
|
+
export interface WorkerSessionDiagnostic {
|
|
351
|
+
event: WorkerSessionLifecycleEvent;
|
|
352
|
+
reasonCode: string;
|
|
353
|
+
contextUsagePercent?: number;
|
|
354
|
+
contextThresholdPercent?: number;
|
|
355
|
+
previousTaskId?: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
285
358
|
export interface SessionOutcome {
|
|
286
359
|
task: Pick<Task, "taskId" | "title" | "section">;
|
|
287
360
|
attempt: number;
|
|
@@ -297,10 +370,16 @@ export interface SessionOutcome {
|
|
|
297
370
|
events: CapturedWorkerEvent[];
|
|
298
371
|
workerCostTotal: number;
|
|
299
372
|
workerCostSource?: string;
|
|
373
|
+
/** Task/attempt-scoped token deltas when cumulative session statistics are available. */
|
|
374
|
+
workerUsage?: WorkerUsageTotals;
|
|
375
|
+
/** Additive lifecycle evidence; omitted by legacy/custom worker runners. */
|
|
376
|
+
sessionDiagnostics?: WorkerSessionDiagnostic[];
|
|
300
377
|
shutdownRequested: boolean;
|
|
301
378
|
timedOut: boolean;
|
|
302
379
|
aborted: boolean;
|
|
303
380
|
error?: string;
|
|
381
|
+
/** Original provider/transport failure retained for coordinator classification. */
|
|
382
|
+
failure?: unknown;
|
|
304
383
|
}
|
|
305
384
|
|
|
306
385
|
export function buildMissingTaskResultMessage(): string {
|
|
@@ -424,7 +503,29 @@ export async function createIsolatedWorkerSession(
|
|
|
424
503
|
};
|
|
425
504
|
}
|
|
426
505
|
|
|
427
|
-
export async function
|
|
506
|
+
export async function createWorkerSessionResource(
|
|
507
|
+
options: CreateWorkerSessionOptions,
|
|
508
|
+
sessionFactory: WorkerSessionFactory = createIsolatedWorkerSession,
|
|
509
|
+
): Promise<WorkerSessionResource> {
|
|
510
|
+
const result = await sessionFactory(options);
|
|
511
|
+
return { ...result, disposed: false, completedAssignments: 0 };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Dispose an allocated worker session at most once, regardless of competing ownership paths. */
|
|
515
|
+
export async function disposeWorkerSessionResource(resource: WorkerSessionResource): Promise<void> {
|
|
516
|
+
if (resource.disposed) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
resource.disposed = true;
|
|
520
|
+
await Promise.resolve(resource.session.dispose?.());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Execute exactly one assignment in an already-created session without disposing that session. */
|
|
524
|
+
export async function runWorkerTaskAssignment(
|
|
525
|
+
options: RunWorkerTaskOptions,
|
|
526
|
+
resource: WorkerSessionResource,
|
|
527
|
+
reusedAssignment?: ReusedWorkerAssignment,
|
|
528
|
+
): Promise<SessionOutcome> {
|
|
428
529
|
const now = options.now ?? (() => new Date());
|
|
429
530
|
const startedAt = now().toISOString();
|
|
430
531
|
const contextObservations: string[] = [];
|
|
@@ -438,18 +539,24 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
438
539
|
let timedOut = false;
|
|
439
540
|
let aborted = false;
|
|
440
541
|
let error: string | undefined;
|
|
542
|
+
let failure: unknown;
|
|
441
543
|
let finished = false;
|
|
442
544
|
let turnCount = 0;
|
|
443
545
|
let messageUsageCostTotal = 0;
|
|
444
546
|
let hasMessageUsageCost = false;
|
|
445
|
-
let
|
|
547
|
+
let sessionStatsStart: WorkerSessionStatsSnapshot | undefined;
|
|
548
|
+
let sessionStatsEnd: WorkerSessionStatsSnapshot | undefined;
|
|
549
|
+
let accountingBaseline: WorkerSessionStatsSnapshot | undefined;
|
|
550
|
+
const wasFirstAssignment = (resource.completedAssignments ?? 0) === 0;
|
|
446
551
|
let resolvePromptWait: (() => void) | undefined;
|
|
447
552
|
|
|
448
|
-
const prompt =
|
|
553
|
+
const prompt = reusedAssignment
|
|
554
|
+
? buildReusedAssignmentPrompt(options, reusedAssignment.previousTask)
|
|
555
|
+
: buildTaskPrompt(options);
|
|
449
556
|
const taskTimeoutSeconds = options.taskTimeoutSeconds ?? DEFAULT_TASK_TIMEOUT_SECONDS;
|
|
450
557
|
const gracefulShutdownSeconds = options.gracefulShutdownSeconds ?? DEFAULT_GRACEFUL_SHUTDOWN_SECONDS;
|
|
451
|
-
const
|
|
452
|
-
|
|
558
|
+
const session = resource.session;
|
|
559
|
+
const invocationMessageStart = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
453
560
|
let unsubscribe: (() => void) | undefined;
|
|
454
561
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
455
562
|
|
|
@@ -558,6 +665,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
558
665
|
},
|
|
559
666
|
(exc: unknown) => {
|
|
560
667
|
settled = true;
|
|
668
|
+
failure ??= exc;
|
|
561
669
|
error = error ?? errorMessage(exc);
|
|
562
670
|
resolvePromptWait?.();
|
|
563
671
|
resolvePromptWait = undefined;
|
|
@@ -578,14 +686,14 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
578
686
|
throw new Error("worker session aborted before start");
|
|
579
687
|
}
|
|
580
688
|
|
|
581
|
-
|
|
582
|
-
|
|
689
|
+
sessionStatsStart = await workerSessionStatsSnapshot(session);
|
|
690
|
+
accountingBaseline = sessionStatsStart ?? resource.accountingBaseline;
|
|
583
691
|
sessionFile = session.sessionFile;
|
|
584
692
|
sessionId = session.sessionId;
|
|
585
|
-
if (
|
|
586
|
-
contextObservations.push(`model fallback: ${
|
|
693
|
+
if (resource.modelFallbackMessage) {
|
|
694
|
+
contextObservations.push(`model fallback: ${resource.modelFallbackMessage}`);
|
|
587
695
|
}
|
|
588
|
-
for (const diagnostic of
|
|
696
|
+
for (const diagnostic of resource.diagnostics ?? []) {
|
|
589
697
|
contextObservations.push(diagnostic);
|
|
590
698
|
}
|
|
591
699
|
|
|
@@ -684,33 +792,33 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
684
792
|
}
|
|
685
793
|
|
|
686
794
|
await waitForPrompt(prompt);
|
|
687
|
-
assistantText =
|
|
795
|
+
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
688
796
|
|
|
689
797
|
if (!hasCompleteTaskResult(assistantText) && !error && !aborted && !timedOut && !options.abortSignal?.aborted) {
|
|
690
798
|
contextObservations.push(
|
|
691
799
|
"missing TASK_RESULT status after initial prompt, or required fields were incomplete; requested required block once",
|
|
692
800
|
);
|
|
693
801
|
await waitForPrompt(buildMissingTaskResultMessage());
|
|
694
|
-
assistantText =
|
|
802
|
+
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
695
803
|
}
|
|
696
804
|
} catch (exc) {
|
|
805
|
+
failure ??= exc;
|
|
697
806
|
error = error ?? errorMessage(exc);
|
|
698
807
|
} finally {
|
|
699
808
|
finished = true;
|
|
700
809
|
clearTimers();
|
|
701
810
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
702
811
|
unsubscribe?.();
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
compactionEvents.push(`session dispose failed: ${errorMessage(exc)}`);
|
|
712
|
-
}
|
|
812
|
+
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
813
|
+
sessionFile = session.sessionFile ?? sessionFile;
|
|
814
|
+
sessionId = session.sessionId ?? sessionId;
|
|
815
|
+
sessionStatsEnd = await workerSessionStatsSnapshot(session);
|
|
816
|
+
if (sessionStatsEnd) {
|
|
817
|
+
resource.accountingBaseline = sessionStatsEnd;
|
|
818
|
+
} else if (sessionStatsStart) {
|
|
819
|
+
resource.accountingBaseline = sessionStatsStart;
|
|
713
820
|
}
|
|
821
|
+
resource.completedAssignments = (resource.completedAssignments ?? 0) + 1;
|
|
714
822
|
}
|
|
715
823
|
|
|
716
824
|
if ((error || aborted || timedOut) && !hasTaskResult(assistantText)) {
|
|
@@ -719,9 +827,11 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
719
827
|
|
|
720
828
|
const parsedResult = parseCompleteTaskResult(assistantText);
|
|
721
829
|
const reportedStatus = parsedResult?.status ?? parseReportedStatus(assistantText);
|
|
830
|
+
const cancelled = Boolean(options.abortSignal?.aborted);
|
|
831
|
+
const statsDelta = workerSessionStatsDelta(accountingBaseline, sessionStatsEnd, wasFirstAssignment);
|
|
722
832
|
const capturedWorkerCost = selectWorkerCostTotal({
|
|
723
833
|
messageCostTotal: hasMessageUsageCost ? messageUsageCostTotal : undefined,
|
|
724
|
-
statsCostTotal:
|
|
834
|
+
statsCostTotal: statsDelta?.cost,
|
|
725
835
|
});
|
|
726
836
|
return {
|
|
727
837
|
task: options.task,
|
|
@@ -729,7 +839,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
729
839
|
startedAt,
|
|
730
840
|
endedAt: now().toISOString(),
|
|
731
841
|
reportedStatus,
|
|
732
|
-
done: Boolean(parsedResult && isDoneStatus(reportedStatus) && !error && !aborted && !timedOut),
|
|
842
|
+
done: Boolean(parsedResult && isDoneStatus(reportedStatus) && !error && !aborted && !timedOut && !cancelled),
|
|
733
843
|
assistantText,
|
|
734
844
|
sessionFile,
|
|
735
845
|
sessionId,
|
|
@@ -738,10 +848,62 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
738
848
|
events,
|
|
739
849
|
workerCostTotal: capturedWorkerCost.total,
|
|
740
850
|
workerCostSource: capturedWorkerCost.source,
|
|
851
|
+
workerUsage: statsDelta?.tokens,
|
|
741
852
|
shutdownRequested,
|
|
742
853
|
timedOut,
|
|
743
|
-
aborted: aborted ||
|
|
854
|
+
aborted: aborted || cancelled,
|
|
744
855
|
error,
|
|
856
|
+
...(failure === undefined ? {} : { failure }),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Backward-compatible isolated lifecycle: create, execute one assignment, and dispose. */
|
|
861
|
+
export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<SessionOutcome> {
|
|
862
|
+
let resource: WorkerSessionResource | undefined;
|
|
863
|
+
try {
|
|
864
|
+
resource = await createWorkerSessionResource(options, options.sessionFactory ?? createIsolatedWorkerSession);
|
|
865
|
+
return await runWorkerTaskAssignment(options, resource);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (resource) {
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
return buildWorkerSessionCreationFailureOutcome(options, error);
|
|
871
|
+
} finally {
|
|
872
|
+
if (resource) {
|
|
873
|
+
try {
|
|
874
|
+
await disposeWorkerSessionResource(resource);
|
|
875
|
+
} catch {
|
|
876
|
+
// Preserve the historical best-effort worker disposal behavior.
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function buildWorkerSessionCreationFailureOutcome(
|
|
883
|
+
options: RunWorkerTaskOptions,
|
|
884
|
+
error: unknown,
|
|
885
|
+
): SessionOutcome {
|
|
886
|
+
const now = options.now ?? (() => new Date());
|
|
887
|
+
const startedAt = now().toISOString();
|
|
888
|
+
const message = errorMessage(error);
|
|
889
|
+
const assistantText = buildLongTaskFailureTaskResult(message);
|
|
890
|
+
return {
|
|
891
|
+
task: options.task,
|
|
892
|
+
attempt: options.attempt,
|
|
893
|
+
startedAt,
|
|
894
|
+
endedAt: now().toISOString(),
|
|
895
|
+
reportedStatus: parseReportedStatus(assistantText),
|
|
896
|
+
done: false,
|
|
897
|
+
assistantText,
|
|
898
|
+
contextObservations: [],
|
|
899
|
+
compactionEvents: [],
|
|
900
|
+
events: [],
|
|
901
|
+
workerCostTotal: 0,
|
|
902
|
+
shutdownRequested: false,
|
|
903
|
+
timedOut: false,
|
|
904
|
+
aborted: Boolean(options.abortSignal?.aborted),
|
|
905
|
+
error: message,
|
|
906
|
+
failure: error,
|
|
745
907
|
};
|
|
746
908
|
}
|
|
747
909
|
|
|
@@ -1015,18 +1177,92 @@ export function workerUsageCostFromStats(stats: unknown): number | undefined {
|
|
|
1015
1177
|
return usageCostTotal(stats.usage) ?? usageCostTotal(stats);
|
|
1016
1178
|
}
|
|
1017
1179
|
|
|
1018
|
-
async function
|
|
1180
|
+
async function workerSessionStatsSnapshot(session: WorkerSessionLike): Promise<WorkerSessionStatsSnapshot | undefined> {
|
|
1019
1181
|
if (!session.getSessionStats) {
|
|
1020
1182
|
return undefined;
|
|
1021
1183
|
}
|
|
1022
1184
|
|
|
1023
1185
|
try {
|
|
1024
|
-
|
|
1186
|
+
const stats = await session.getSessionStats();
|
|
1187
|
+
const cost = workerUsageCostFromStats(stats);
|
|
1188
|
+
const tokens = workerUsageTokensFromStats(stats);
|
|
1189
|
+
return cost === undefined && !tokens ? undefined : { cost, tokens };
|
|
1025
1190
|
} catch {
|
|
1026
1191
|
return undefined;
|
|
1027
1192
|
}
|
|
1028
1193
|
}
|
|
1029
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* Convert cumulative session counters into one assignment's nonnegative delta.
|
|
1197
|
+
* A lower ending counter means the SDK reset that counter, so the ending value
|
|
1198
|
+
* is the entire post-reset contribution. Without any baseline, cumulative
|
|
1199
|
+
* values are safe only for the resource's first assignment.
|
|
1200
|
+
*/
|
|
1201
|
+
export function workerSessionStatsDelta(
|
|
1202
|
+
baseline: WorkerSessionStatsSnapshot | undefined,
|
|
1203
|
+
ending: WorkerSessionStatsSnapshot | undefined,
|
|
1204
|
+
firstAssignment: boolean,
|
|
1205
|
+
): WorkerSessionStatsSnapshot | undefined {
|
|
1206
|
+
if (!ending) {
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
if (!baseline && !firstAssignment) {
|
|
1210
|
+
return undefined;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const cost = cumulativeCounterDelta(baseline?.cost, ending.cost, firstAssignment);
|
|
1214
|
+
const tokens = ending.tokens
|
|
1215
|
+
? {
|
|
1216
|
+
input: cumulativeCounterDelta(baseline?.tokens?.input, ending.tokens.input, firstAssignment) ?? 0,
|
|
1217
|
+
output: cumulativeCounterDelta(baseline?.tokens?.output, ending.tokens.output, firstAssignment) ?? 0,
|
|
1218
|
+
cacheRead: cumulativeCounterDelta(baseline?.tokens?.cacheRead, ending.tokens.cacheRead, firstAssignment) ?? 0,
|
|
1219
|
+
cacheWrite:
|
|
1220
|
+
cumulativeCounterDelta(baseline?.tokens?.cacheWrite, ending.tokens.cacheWrite, firstAssignment) ?? 0,
|
|
1221
|
+
total: cumulativeCounterDelta(baseline?.tokens?.total, ending.tokens.total, firstAssignment) ?? 0,
|
|
1222
|
+
}
|
|
1223
|
+
: undefined;
|
|
1224
|
+
return cost === undefined && !tokens ? undefined : { cost, tokens };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function workerUsageTokensFromStats(stats: unknown): WorkerUsageTotals | undefined {
|
|
1228
|
+
if (!isRecord(stats)) {
|
|
1229
|
+
return undefined;
|
|
1230
|
+
}
|
|
1231
|
+
const tokens = isRecord(stats.tokens) ? stats.tokens : isRecord(stats.usage) ? stats.usage : undefined;
|
|
1232
|
+
if (!tokens) {
|
|
1233
|
+
return undefined;
|
|
1234
|
+
}
|
|
1235
|
+
const input = finiteNonNegativeNumber(tokens.input);
|
|
1236
|
+
const output = finiteNonNegativeNumber(tokens.output);
|
|
1237
|
+
const cacheRead = finiteNonNegativeNumber(tokens.cacheRead ?? tokens.cache_read);
|
|
1238
|
+
const cacheWrite = finiteNonNegativeNumber(tokens.cacheWrite ?? tokens.cache_write);
|
|
1239
|
+
const total = finiteNonNegativeNumber(tokens.total);
|
|
1240
|
+
if ([input, output, cacheRead, cacheWrite, total].every((value) => value === undefined)) {
|
|
1241
|
+
return undefined;
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
input: input ?? 0,
|
|
1245
|
+
output: output ?? 0,
|
|
1246
|
+
cacheRead: cacheRead ?? 0,
|
|
1247
|
+
cacheWrite: cacheWrite ?? 0,
|
|
1248
|
+
total: total ?? (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function cumulativeCounterDelta(
|
|
1253
|
+
baseline: number | undefined,
|
|
1254
|
+
ending: number | undefined,
|
|
1255
|
+
allowUnbased: boolean,
|
|
1256
|
+
): number | undefined {
|
|
1257
|
+
if (ending === undefined) {
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
1260
|
+
if (baseline === undefined) {
|
|
1261
|
+
return allowUnbased ? ending : undefined;
|
|
1262
|
+
}
|
|
1263
|
+
return ending >= baseline ? ending - baseline : ending;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1030
1266
|
function usageCostTotal(usage: unknown): number | undefined {
|
|
1031
1267
|
if (!isRecord(usage)) {
|
|
1032
1268
|
return undefined;
|
|
@@ -1074,6 +1310,19 @@ function contextUsageFromStats(stats: unknown): unknown {
|
|
|
1074
1310
|
return isRecord(stats) ? stats.contextUsage : undefined;
|
|
1075
1311
|
}
|
|
1076
1312
|
|
|
1313
|
+
export async function workerSessionContextUsagePercent(session: WorkerSessionLike): Promise<number | undefined> {
|
|
1314
|
+
try {
|
|
1315
|
+
const direct = session.getContextUsage?.();
|
|
1316
|
+
if (direct !== undefined) {
|
|
1317
|
+
return contextPercent(direct);
|
|
1318
|
+
}
|
|
1319
|
+
const stats = session.getSessionStats ? await session.getSessionStats() : undefined;
|
|
1320
|
+
return contextPercent(contextUsageFromStats(stats));
|
|
1321
|
+
} catch {
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1077
1326
|
function contextPercent(usage: unknown): number | undefined {
|
|
1078
1327
|
if (!isRecord(usage)) {
|
|
1079
1328
|
return undefined;
|
|
@@ -1128,13 +1377,24 @@ function formatCompactionEndEvent(event: Record<string, unknown>): string {
|
|
|
1128
1377
|
return `compaction_end reason=${reason} aborted=${aborted} error=${String(event.errorMessage ?? "unknown")}`;
|
|
1129
1378
|
}
|
|
1130
1379
|
|
|
1131
|
-
function
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1380
|
+
function latestInvocationAssistantText(
|
|
1381
|
+
session: WorkerSessionLike,
|
|
1382
|
+
fallback: string,
|
|
1383
|
+
messageStart: number,
|
|
1384
|
+
allowDirectFallback: boolean,
|
|
1385
|
+
): string {
|
|
1386
|
+
const invocationMessages = Array.isArray(session.messages) ? session.messages.slice(messageStart) : undefined;
|
|
1387
|
+
const fromInvocation = lastAssistantTextFromMessages(invocationMessages);
|
|
1388
|
+
if (fromInvocation) {
|
|
1389
|
+
return fromInvocation;
|
|
1390
|
+
}
|
|
1391
|
+
if (allowDirectFallback) {
|
|
1392
|
+
const direct = session.getLastAssistantText?.();
|
|
1393
|
+
if (direct) {
|
|
1394
|
+
return direct;
|
|
1395
|
+
}
|
|
1135
1396
|
}
|
|
1136
|
-
|
|
1137
|
-
return fromMessages || fallback;
|
|
1397
|
+
return fallback;
|
|
1138
1398
|
}
|
|
1139
1399
|
|
|
1140
1400
|
function buildLongTaskFailureTaskResult(reason: string): string {
|