pi-long-task 0.3.17 → 0.5.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 +25 -0
- package/README.md +74 -8
- package/package.json +2 -2
- package/src/coordinator.ts +1102 -50
- package/src/index.ts +60 -18
- package/src/plan_revision.ts +471 -0
- package/src/plan_revision_generation.ts +376 -0
- package/src/plan_store.ts +281 -0
- package/src/render.ts +2 -0
- package/src/steering.ts +234 -0
- package/src/todo_parser.ts +46 -7
- package/src/worker_config.ts +63 -14
- package/src/worker_reuse_policy.ts +389 -0
- package/src/worker_session.ts +261 -33
package/src/worker_session.ts
CHANGED
|
@@ -116,6 +116,18 @@ Only use \`status: done\` if the assigned task is fully complete and verified as
|
|
|
116
116
|
|
|
117
117
|
export const buildAssignedTaskPrompt = buildTaskPrompt;
|
|
118
118
|
|
|
119
|
+
export function buildReusedAssignmentPrompt(
|
|
120
|
+
options: WorkerTaskPromptOptions,
|
|
121
|
+
previousTask: Pick<Task, "taskId" | "title">,
|
|
122
|
+
): string {
|
|
123
|
+
return `Pi Long Task assignment boundary:
|
|
124
|
+
The prior assignment ${taskLabel(previousTask)} has ended.
|
|
125
|
+
A new, independent assignment has begun: ${taskLabel(options.task)}.
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
${buildTaskPrompt(options)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
export function buildTimeLimitMessage(seconds: number): string {
|
|
120
132
|
return `Pi Long Task notice: this worker session has reached its ${seconds.toFixed(0)}s time budget.
|
|
121
133
|
Stop after the current safe point. Do not start more implementation work.
|
|
@@ -244,6 +256,18 @@ export interface WorkerSessionFactoryResult {
|
|
|
244
256
|
diagnostics?: string[];
|
|
245
257
|
}
|
|
246
258
|
|
|
259
|
+
/** Coordinator-owned session allocation. Disposal is idempotent and owned by disposeWorkerSessionResource(). */
|
|
260
|
+
export interface WorkerSessionResource extends WorkerSessionFactoryResult {
|
|
261
|
+
disposed: boolean;
|
|
262
|
+
/** Last readable cumulative statistics, retained only for task-boundary delta accounting. */
|
|
263
|
+
accountingBaseline?: WorkerSessionStatsSnapshot;
|
|
264
|
+
completedAssignments?: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface ReusedWorkerAssignment {
|
|
268
|
+
previousTask: Pick<Task, "taskId" | "title">;
|
|
269
|
+
}
|
|
270
|
+
|
|
247
271
|
export interface CreateWorkerSessionOptions {
|
|
248
272
|
cwd: string;
|
|
249
273
|
agentDir?: string;
|
|
@@ -268,6 +292,7 @@ export interface RunWorkerTaskOptions extends WorkerTaskPromptOptions, CreateWor
|
|
|
268
292
|
abortSignal?: AbortSignal;
|
|
269
293
|
sessionFactory?: WorkerSessionFactory;
|
|
270
294
|
onEvent?: (event: CapturedWorkerEvent) => void;
|
|
295
|
+
onSessionDiagnostic?: (diagnostic: WorkerSessionDiagnostic) => void;
|
|
271
296
|
now?: () => Date;
|
|
272
297
|
}
|
|
273
298
|
|
|
@@ -282,6 +307,29 @@ export interface CapturedWorkerEvent {
|
|
|
282
307
|
usageCostKey?: string;
|
|
283
308
|
}
|
|
284
309
|
|
|
310
|
+
export interface WorkerUsageTotals {
|
|
311
|
+
input: number;
|
|
312
|
+
output: number;
|
|
313
|
+
cacheRead: number;
|
|
314
|
+
cacheWrite: number;
|
|
315
|
+
total: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export interface WorkerSessionStatsSnapshot {
|
|
319
|
+
cost?: number;
|
|
320
|
+
tokens?: WorkerUsageTotals;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export type WorkerSessionLifecycleEvent = "session_started" | "session_reused" | "session_rotated" | "session_retained";
|
|
324
|
+
|
|
325
|
+
export interface WorkerSessionDiagnostic {
|
|
326
|
+
event: WorkerSessionLifecycleEvent;
|
|
327
|
+
reasonCode: string;
|
|
328
|
+
contextUsagePercent?: number;
|
|
329
|
+
contextThresholdPercent?: number;
|
|
330
|
+
previousTaskId?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
285
333
|
export interface SessionOutcome {
|
|
286
334
|
task: Pick<Task, "taskId" | "title" | "section">;
|
|
287
335
|
attempt: number;
|
|
@@ -297,6 +345,10 @@ export interface SessionOutcome {
|
|
|
297
345
|
events: CapturedWorkerEvent[];
|
|
298
346
|
workerCostTotal: number;
|
|
299
347
|
workerCostSource?: string;
|
|
348
|
+
/** Task/attempt-scoped token deltas when cumulative session statistics are available. */
|
|
349
|
+
workerUsage?: WorkerUsageTotals;
|
|
350
|
+
/** Additive lifecycle evidence; omitted by legacy/custom worker runners. */
|
|
351
|
+
sessionDiagnostics?: WorkerSessionDiagnostic[];
|
|
300
352
|
shutdownRequested: boolean;
|
|
301
353
|
timedOut: boolean;
|
|
302
354
|
aborted: boolean;
|
|
@@ -424,7 +476,29 @@ export async function createIsolatedWorkerSession(
|
|
|
424
476
|
};
|
|
425
477
|
}
|
|
426
478
|
|
|
427
|
-
export async function
|
|
479
|
+
export async function createWorkerSessionResource(
|
|
480
|
+
options: CreateWorkerSessionOptions,
|
|
481
|
+
sessionFactory: WorkerSessionFactory = createIsolatedWorkerSession,
|
|
482
|
+
): Promise<WorkerSessionResource> {
|
|
483
|
+
const result = await sessionFactory(options);
|
|
484
|
+
return { ...result, disposed: false, completedAssignments: 0 };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Dispose an allocated worker session at most once, regardless of competing ownership paths. */
|
|
488
|
+
export async function disposeWorkerSessionResource(resource: WorkerSessionResource): Promise<void> {
|
|
489
|
+
if (resource.disposed) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
resource.disposed = true;
|
|
493
|
+
await Promise.resolve(resource.session.dispose?.());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Execute exactly one assignment in an already-created session without disposing that session. */
|
|
497
|
+
export async function runWorkerTaskAssignment(
|
|
498
|
+
options: RunWorkerTaskOptions,
|
|
499
|
+
resource: WorkerSessionResource,
|
|
500
|
+
reusedAssignment?: ReusedWorkerAssignment,
|
|
501
|
+
): Promise<SessionOutcome> {
|
|
428
502
|
const now = options.now ?? (() => new Date());
|
|
429
503
|
const startedAt = now().toISOString();
|
|
430
504
|
const contextObservations: string[] = [];
|
|
@@ -442,14 +516,19 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
442
516
|
let turnCount = 0;
|
|
443
517
|
let messageUsageCostTotal = 0;
|
|
444
518
|
let hasMessageUsageCost = false;
|
|
445
|
-
let
|
|
519
|
+
let sessionStatsStart: WorkerSessionStatsSnapshot | undefined;
|
|
520
|
+
let sessionStatsEnd: WorkerSessionStatsSnapshot | undefined;
|
|
521
|
+
let accountingBaseline: WorkerSessionStatsSnapshot | undefined;
|
|
522
|
+
const wasFirstAssignment = (resource.completedAssignments ?? 0) === 0;
|
|
446
523
|
let resolvePromptWait: (() => void) | undefined;
|
|
447
524
|
|
|
448
|
-
const prompt =
|
|
525
|
+
const prompt = reusedAssignment
|
|
526
|
+
? buildReusedAssignmentPrompt(options, reusedAssignment.previousTask)
|
|
527
|
+
: buildTaskPrompt(options);
|
|
449
528
|
const taskTimeoutSeconds = options.taskTimeoutSeconds ?? DEFAULT_TASK_TIMEOUT_SECONDS;
|
|
450
529
|
const gracefulShutdownSeconds = options.gracefulShutdownSeconds ?? DEFAULT_GRACEFUL_SHUTDOWN_SECONDS;
|
|
451
|
-
const
|
|
452
|
-
|
|
530
|
+
const session = resource.session;
|
|
531
|
+
const invocationMessageStart = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
453
532
|
let unsubscribe: (() => void) | undefined;
|
|
454
533
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
455
534
|
|
|
@@ -578,14 +657,14 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
578
657
|
throw new Error("worker session aborted before start");
|
|
579
658
|
}
|
|
580
659
|
|
|
581
|
-
|
|
582
|
-
|
|
660
|
+
sessionStatsStart = await workerSessionStatsSnapshot(session);
|
|
661
|
+
accountingBaseline = sessionStatsStart ?? resource.accountingBaseline;
|
|
583
662
|
sessionFile = session.sessionFile;
|
|
584
663
|
sessionId = session.sessionId;
|
|
585
|
-
if (
|
|
586
|
-
contextObservations.push(`model fallback: ${
|
|
664
|
+
if (resource.modelFallbackMessage) {
|
|
665
|
+
contextObservations.push(`model fallback: ${resource.modelFallbackMessage}`);
|
|
587
666
|
}
|
|
588
|
-
for (const diagnostic of
|
|
667
|
+
for (const diagnostic of resource.diagnostics ?? []) {
|
|
589
668
|
contextObservations.push(diagnostic);
|
|
590
669
|
}
|
|
591
670
|
|
|
@@ -684,14 +763,14 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
684
763
|
}
|
|
685
764
|
|
|
686
765
|
await waitForPrompt(prompt);
|
|
687
|
-
assistantText =
|
|
766
|
+
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
688
767
|
|
|
689
768
|
if (!hasCompleteTaskResult(assistantText) && !error && !aborted && !timedOut && !options.abortSignal?.aborted) {
|
|
690
769
|
contextObservations.push(
|
|
691
770
|
"missing TASK_RESULT status after initial prompt, or required fields were incomplete; requested required block once",
|
|
692
771
|
);
|
|
693
772
|
await waitForPrompt(buildMissingTaskResultMessage());
|
|
694
|
-
assistantText =
|
|
773
|
+
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
695
774
|
}
|
|
696
775
|
} catch (exc) {
|
|
697
776
|
error = error ?? errorMessage(exc);
|
|
@@ -700,17 +779,16 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
700
779
|
clearTimers();
|
|
701
780
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
702
781
|
unsubscribe?.();
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
compactionEvents.push(`session dispose failed: ${errorMessage(exc)}`);
|
|
712
|
-
}
|
|
782
|
+
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
783
|
+
sessionFile = session.sessionFile ?? sessionFile;
|
|
784
|
+
sessionId = session.sessionId ?? sessionId;
|
|
785
|
+
sessionStatsEnd = await workerSessionStatsSnapshot(session);
|
|
786
|
+
if (sessionStatsEnd) {
|
|
787
|
+
resource.accountingBaseline = sessionStatsEnd;
|
|
788
|
+
} else if (sessionStatsStart) {
|
|
789
|
+
resource.accountingBaseline = sessionStatsStart;
|
|
713
790
|
}
|
|
791
|
+
resource.completedAssignments = (resource.completedAssignments ?? 0) + 1;
|
|
714
792
|
}
|
|
715
793
|
|
|
716
794
|
if ((error || aborted || timedOut) && !hasTaskResult(assistantText)) {
|
|
@@ -719,9 +797,11 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
719
797
|
|
|
720
798
|
const parsedResult = parseCompleteTaskResult(assistantText);
|
|
721
799
|
const reportedStatus = parsedResult?.status ?? parseReportedStatus(assistantText);
|
|
800
|
+
const cancelled = Boolean(options.abortSignal?.aborted);
|
|
801
|
+
const statsDelta = workerSessionStatsDelta(accountingBaseline, sessionStatsEnd, wasFirstAssignment);
|
|
722
802
|
const capturedWorkerCost = selectWorkerCostTotal({
|
|
723
803
|
messageCostTotal: hasMessageUsageCost ? messageUsageCostTotal : undefined,
|
|
724
|
-
statsCostTotal:
|
|
804
|
+
statsCostTotal: statsDelta?.cost,
|
|
725
805
|
});
|
|
726
806
|
return {
|
|
727
807
|
task: options.task,
|
|
@@ -729,7 +809,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
729
809
|
startedAt,
|
|
730
810
|
endedAt: now().toISOString(),
|
|
731
811
|
reportedStatus,
|
|
732
|
-
done: Boolean(parsedResult && isDoneStatus(reportedStatus) && !error && !aborted && !timedOut),
|
|
812
|
+
done: Boolean(parsedResult && isDoneStatus(reportedStatus) && !error && !aborted && !timedOut && !cancelled),
|
|
733
813
|
assistantText,
|
|
734
814
|
sessionFile,
|
|
735
815
|
sessionId,
|
|
@@ -738,13 +818,63 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
738
818
|
events,
|
|
739
819
|
workerCostTotal: capturedWorkerCost.total,
|
|
740
820
|
workerCostSource: capturedWorkerCost.source,
|
|
821
|
+
workerUsage: statsDelta?.tokens,
|
|
741
822
|
shutdownRequested,
|
|
742
823
|
timedOut,
|
|
743
|
-
aborted: aborted ||
|
|
824
|
+
aborted: aborted || cancelled,
|
|
744
825
|
error,
|
|
745
826
|
};
|
|
746
827
|
}
|
|
747
828
|
|
|
829
|
+
/** Backward-compatible isolated lifecycle: create, execute one assignment, and dispose. */
|
|
830
|
+
export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<SessionOutcome> {
|
|
831
|
+
let resource: WorkerSessionResource | undefined;
|
|
832
|
+
try {
|
|
833
|
+
resource = await createWorkerSessionResource(options, options.sessionFactory ?? createIsolatedWorkerSession);
|
|
834
|
+
return await runWorkerTaskAssignment(options, resource);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
if (resource) {
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
return buildWorkerSessionCreationFailureOutcome(options, error);
|
|
840
|
+
} finally {
|
|
841
|
+
if (resource) {
|
|
842
|
+
try {
|
|
843
|
+
await disposeWorkerSessionResource(resource);
|
|
844
|
+
} catch {
|
|
845
|
+
// Preserve the historical best-effort worker disposal behavior.
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function buildWorkerSessionCreationFailureOutcome(
|
|
852
|
+
options: RunWorkerTaskOptions,
|
|
853
|
+
error: unknown,
|
|
854
|
+
): SessionOutcome {
|
|
855
|
+
const now = options.now ?? (() => new Date());
|
|
856
|
+
const startedAt = now().toISOString();
|
|
857
|
+
const message = errorMessage(error);
|
|
858
|
+
const assistantText = buildLongTaskFailureTaskResult(message);
|
|
859
|
+
return {
|
|
860
|
+
task: options.task,
|
|
861
|
+
attempt: options.attempt,
|
|
862
|
+
startedAt,
|
|
863
|
+
endedAt: now().toISOString(),
|
|
864
|
+
reportedStatus: parseReportedStatus(assistantText),
|
|
865
|
+
done: false,
|
|
866
|
+
assistantText,
|
|
867
|
+
contextObservations: [],
|
|
868
|
+
compactionEvents: [],
|
|
869
|
+
events: [],
|
|
870
|
+
workerCostTotal: 0,
|
|
871
|
+
shutdownRequested: false,
|
|
872
|
+
timedOut: false,
|
|
873
|
+
aborted: Boolean(options.abortSignal?.aborted),
|
|
874
|
+
error: message,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
748
878
|
function textFromContentPart(item: unknown): string {
|
|
749
879
|
if (!isRecord(item)) {
|
|
750
880
|
return "";
|
|
@@ -1015,18 +1145,92 @@ export function workerUsageCostFromStats(stats: unknown): number | undefined {
|
|
|
1015
1145
|
return usageCostTotal(stats.usage) ?? usageCostTotal(stats);
|
|
1016
1146
|
}
|
|
1017
1147
|
|
|
1018
|
-
async function
|
|
1148
|
+
async function workerSessionStatsSnapshot(session: WorkerSessionLike): Promise<WorkerSessionStatsSnapshot | undefined> {
|
|
1019
1149
|
if (!session.getSessionStats) {
|
|
1020
1150
|
return undefined;
|
|
1021
1151
|
}
|
|
1022
1152
|
|
|
1023
1153
|
try {
|
|
1024
|
-
|
|
1154
|
+
const stats = await session.getSessionStats();
|
|
1155
|
+
const cost = workerUsageCostFromStats(stats);
|
|
1156
|
+
const tokens = workerUsageTokensFromStats(stats);
|
|
1157
|
+
return cost === undefined && !tokens ? undefined : { cost, tokens };
|
|
1025
1158
|
} catch {
|
|
1026
1159
|
return undefined;
|
|
1027
1160
|
}
|
|
1028
1161
|
}
|
|
1029
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* Convert cumulative session counters into one assignment's nonnegative delta.
|
|
1165
|
+
* A lower ending counter means the SDK reset that counter, so the ending value
|
|
1166
|
+
* is the entire post-reset contribution. Without any baseline, cumulative
|
|
1167
|
+
* values are safe only for the resource's first assignment.
|
|
1168
|
+
*/
|
|
1169
|
+
export function workerSessionStatsDelta(
|
|
1170
|
+
baseline: WorkerSessionStatsSnapshot | undefined,
|
|
1171
|
+
ending: WorkerSessionStatsSnapshot | undefined,
|
|
1172
|
+
firstAssignment: boolean,
|
|
1173
|
+
): WorkerSessionStatsSnapshot | undefined {
|
|
1174
|
+
if (!ending) {
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
if (!baseline && !firstAssignment) {
|
|
1178
|
+
return undefined;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const cost = cumulativeCounterDelta(baseline?.cost, ending.cost, firstAssignment);
|
|
1182
|
+
const tokens = ending.tokens
|
|
1183
|
+
? {
|
|
1184
|
+
input: cumulativeCounterDelta(baseline?.tokens?.input, ending.tokens.input, firstAssignment) ?? 0,
|
|
1185
|
+
output: cumulativeCounterDelta(baseline?.tokens?.output, ending.tokens.output, firstAssignment) ?? 0,
|
|
1186
|
+
cacheRead: cumulativeCounterDelta(baseline?.tokens?.cacheRead, ending.tokens.cacheRead, firstAssignment) ?? 0,
|
|
1187
|
+
cacheWrite:
|
|
1188
|
+
cumulativeCounterDelta(baseline?.tokens?.cacheWrite, ending.tokens.cacheWrite, firstAssignment) ?? 0,
|
|
1189
|
+
total: cumulativeCounterDelta(baseline?.tokens?.total, ending.tokens.total, firstAssignment) ?? 0,
|
|
1190
|
+
}
|
|
1191
|
+
: undefined;
|
|
1192
|
+
return cost === undefined && !tokens ? undefined : { cost, tokens };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function workerUsageTokensFromStats(stats: unknown): WorkerUsageTotals | undefined {
|
|
1196
|
+
if (!isRecord(stats)) {
|
|
1197
|
+
return undefined;
|
|
1198
|
+
}
|
|
1199
|
+
const tokens = isRecord(stats.tokens) ? stats.tokens : isRecord(stats.usage) ? stats.usage : undefined;
|
|
1200
|
+
if (!tokens) {
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
const input = finiteNonNegativeNumber(tokens.input);
|
|
1204
|
+
const output = finiteNonNegativeNumber(tokens.output);
|
|
1205
|
+
const cacheRead = finiteNonNegativeNumber(tokens.cacheRead ?? tokens.cache_read);
|
|
1206
|
+
const cacheWrite = finiteNonNegativeNumber(tokens.cacheWrite ?? tokens.cache_write);
|
|
1207
|
+
const total = finiteNonNegativeNumber(tokens.total);
|
|
1208
|
+
if ([input, output, cacheRead, cacheWrite, total].every((value) => value === undefined)) {
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
input: input ?? 0,
|
|
1213
|
+
output: output ?? 0,
|
|
1214
|
+
cacheRead: cacheRead ?? 0,
|
|
1215
|
+
cacheWrite: cacheWrite ?? 0,
|
|
1216
|
+
total: total ?? (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function cumulativeCounterDelta(
|
|
1221
|
+
baseline: number | undefined,
|
|
1222
|
+
ending: number | undefined,
|
|
1223
|
+
allowUnbased: boolean,
|
|
1224
|
+
): number | undefined {
|
|
1225
|
+
if (ending === undefined) {
|
|
1226
|
+
return undefined;
|
|
1227
|
+
}
|
|
1228
|
+
if (baseline === undefined) {
|
|
1229
|
+
return allowUnbased ? ending : undefined;
|
|
1230
|
+
}
|
|
1231
|
+
return ending >= baseline ? ending - baseline : ending;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1030
1234
|
function usageCostTotal(usage: unknown): number | undefined {
|
|
1031
1235
|
if (!isRecord(usage)) {
|
|
1032
1236
|
return undefined;
|
|
@@ -1074,6 +1278,19 @@ function contextUsageFromStats(stats: unknown): unknown {
|
|
|
1074
1278
|
return isRecord(stats) ? stats.contextUsage : undefined;
|
|
1075
1279
|
}
|
|
1076
1280
|
|
|
1281
|
+
export async function workerSessionContextUsagePercent(session: WorkerSessionLike): Promise<number | undefined> {
|
|
1282
|
+
try {
|
|
1283
|
+
const direct = session.getContextUsage?.();
|
|
1284
|
+
if (direct !== undefined) {
|
|
1285
|
+
return contextPercent(direct);
|
|
1286
|
+
}
|
|
1287
|
+
const stats = session.getSessionStats ? await session.getSessionStats() : undefined;
|
|
1288
|
+
return contextPercent(contextUsageFromStats(stats));
|
|
1289
|
+
} catch {
|
|
1290
|
+
return undefined;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1077
1294
|
function contextPercent(usage: unknown): number | undefined {
|
|
1078
1295
|
if (!isRecord(usage)) {
|
|
1079
1296
|
return undefined;
|
|
@@ -1128,13 +1345,24 @@ function formatCompactionEndEvent(event: Record<string, unknown>): string {
|
|
|
1128
1345
|
return `compaction_end reason=${reason} aborted=${aborted} error=${String(event.errorMessage ?? "unknown")}`;
|
|
1129
1346
|
}
|
|
1130
1347
|
|
|
1131
|
-
function
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1348
|
+
function latestInvocationAssistantText(
|
|
1349
|
+
session: WorkerSessionLike,
|
|
1350
|
+
fallback: string,
|
|
1351
|
+
messageStart: number,
|
|
1352
|
+
allowDirectFallback: boolean,
|
|
1353
|
+
): string {
|
|
1354
|
+
const invocationMessages = Array.isArray(session.messages) ? session.messages.slice(messageStart) : undefined;
|
|
1355
|
+
const fromInvocation = lastAssistantTextFromMessages(invocationMessages);
|
|
1356
|
+
if (fromInvocation) {
|
|
1357
|
+
return fromInvocation;
|
|
1358
|
+
}
|
|
1359
|
+
if (allowDirectFallback) {
|
|
1360
|
+
const direct = session.getLastAssistantText?.();
|
|
1361
|
+
if (direct) {
|
|
1362
|
+
return direct;
|
|
1363
|
+
}
|
|
1135
1364
|
}
|
|
1136
|
-
|
|
1137
|
-
return fromMessages || fallback;
|
|
1365
|
+
return fallback;
|
|
1138
1366
|
}
|
|
1139
1367
|
|
|
1140
1368
|
function buildLongTaskFailureTaskResult(reason: string): string {
|