pi-long-task 0.2.0 → 0.3.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/package.json +1 -1
- package/src/coordinator.ts +129 -4
- package/src/index.ts +63 -0
- package/src/render.ts +32 -5
- package/src/types.ts +1 -0
- package/src/worker_session.ts +142 -2
package/package.json
CHANGED
package/src/coordinator.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface CoordinatorProgressUpdate {
|
|
|
80
80
|
workerEventType?: string;
|
|
81
81
|
isError?: boolean;
|
|
82
82
|
totalTasks?: number;
|
|
83
|
+
workerCostTotal: number;
|
|
83
84
|
currentTask?: CoordinatorProgressTask;
|
|
84
85
|
subtasks?: CoordinatorProgressSubtask[];
|
|
85
86
|
taskProgress?: TaskProgressModel;
|
|
@@ -145,10 +146,18 @@ export interface CoordinatorResult {
|
|
|
145
146
|
commits: CoordinatorCommitSummary[];
|
|
146
147
|
attempts: TaskAttemptSummary[];
|
|
147
148
|
taskProgress: TaskProgressModel;
|
|
149
|
+
workerCostTotal: number;
|
|
148
150
|
commit: boolean;
|
|
149
151
|
error?: string;
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
interface WorkerCostState {
|
|
155
|
+
total: number;
|
|
156
|
+
finalizedByWorker: Map<string, number>;
|
|
157
|
+
liveByWorker: Map<string, number>;
|
|
158
|
+
liveByMessage: Map<string, number>;
|
|
159
|
+
}
|
|
160
|
+
|
|
152
161
|
interface RuntimeOptions {
|
|
153
162
|
cwd: string;
|
|
154
163
|
runId: string;
|
|
@@ -167,6 +176,7 @@ interface RuntimeOptions {
|
|
|
167
176
|
todoSessionFactory?: WorkerSessionFactory;
|
|
168
177
|
now: () => Date;
|
|
169
178
|
onProgress?: CoordinatorProgressHandler;
|
|
179
|
+
workerCostState: WorkerCostState;
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
export async function runCoordinator(options: RunCoordinatorOptions): Promise<CoordinatorResult> {
|
|
@@ -237,6 +247,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
237
247
|
onEvent: (event) => emitWorkerEventProgress(runtime, tasksBeforeAttempt, nextTask, attempts, attempt, event),
|
|
238
248
|
});
|
|
239
249
|
outcomes.push(outcome);
|
|
250
|
+
finalizeWorkerCost(runtime.workerCostState, outcome);
|
|
240
251
|
|
|
241
252
|
if (outcome.done) {
|
|
242
253
|
todoMarkdown = markTaskDone(todoMarkdown, nextTask.taskId);
|
|
@@ -346,6 +357,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
346
357
|
commits,
|
|
347
358
|
attempts,
|
|
348
359
|
taskProgress,
|
|
360
|
+
workerCostTotal: runtime.workerCostState.total,
|
|
349
361
|
commit: options.commit,
|
|
350
362
|
error: failure,
|
|
351
363
|
};
|
|
@@ -385,6 +397,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
385
397
|
commits,
|
|
386
398
|
attempts,
|
|
387
399
|
taskProgress: buildTaskProgressModel({ tasks: [], attempts }),
|
|
400
|
+
workerCostTotal: runtime.workerCostState.total,
|
|
388
401
|
commit: options.commit,
|
|
389
402
|
error: message,
|
|
390
403
|
};
|
|
@@ -467,23 +480,101 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
467
480
|
todoSessionFactory: options.todoSessionFactory,
|
|
468
481
|
now: options.now ?? (() => new Date()),
|
|
469
482
|
onProgress: options.onProgress,
|
|
483
|
+
workerCostState: createWorkerCostState(),
|
|
470
484
|
};
|
|
471
485
|
}
|
|
472
486
|
|
|
473
487
|
function emitProgress(
|
|
474
488
|
runtime: RuntimeOptions,
|
|
475
489
|
message: string,
|
|
476
|
-
update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath">,
|
|
490
|
+
update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath" | "workerCostTotal">,
|
|
477
491
|
): void {
|
|
478
492
|
runtime.onProgress?.({
|
|
479
493
|
message,
|
|
480
494
|
runId: runtime.runId,
|
|
481
495
|
todoPath: runtime.todoPath,
|
|
482
496
|
resultPath: runtime.taskResultPath,
|
|
497
|
+
workerCostTotal: runtime.workerCostState.total,
|
|
483
498
|
...update,
|
|
484
499
|
});
|
|
485
500
|
}
|
|
486
501
|
|
|
502
|
+
function createWorkerCostState(): WorkerCostState {
|
|
503
|
+
return {
|
|
504
|
+
total: 0,
|
|
505
|
+
finalizedByWorker: new Map(),
|
|
506
|
+
liveByWorker: new Map(),
|
|
507
|
+
liveByMessage: new Map(),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function recordLiveWorkerCost(
|
|
512
|
+
state: WorkerCostState,
|
|
513
|
+
worker: string,
|
|
514
|
+
event: { usageCostTotal?: number; usageCostKey?: string },
|
|
515
|
+
): boolean {
|
|
516
|
+
if (state.finalizedByWorker.has(worker) || event.usageCostTotal === undefined || !event.usageCostKey) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const cost = finiteNonNegativeNumber(event.usageCostTotal);
|
|
521
|
+
if (cost === undefined) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const messageKey = `${worker}:${event.usageCostKey}`;
|
|
526
|
+
if (state.liveByMessage.get(messageKey) === cost) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
state.liveByMessage.set(messageKey, cost);
|
|
531
|
+
recomputeLiveWorkerCost(state, worker);
|
|
532
|
+
recomputeWorkerCostTotal(state);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function finalizeWorkerCost(
|
|
537
|
+
state: WorkerCostState,
|
|
538
|
+
outcome: Pick<SessionOutcome, "task" | "attempt" | "workerCostTotal">,
|
|
539
|
+
): void {
|
|
540
|
+
const worker = workerKey(outcome.task.taskId, outcome.attempt);
|
|
541
|
+
state.finalizedByWorker.set(worker, finiteNonNegativeNumber(outcome.workerCostTotal) ?? 0);
|
|
542
|
+
state.liveByWorker.delete(worker);
|
|
543
|
+
for (const messageKey of state.liveByMessage.keys()) {
|
|
544
|
+
if (messageKey.startsWith(`${worker}:`)) {
|
|
545
|
+
state.liveByMessage.delete(messageKey);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
recomputeWorkerCostTotal(state);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function recomputeLiveWorkerCost(state: WorkerCostState, worker: string): void {
|
|
552
|
+
let total = 0;
|
|
553
|
+
for (const [messageKey, cost] of state.liveByMessage) {
|
|
554
|
+
if (messageKey.startsWith(`${worker}:`)) {
|
|
555
|
+
total += cost;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
state.liveByWorker.set(worker, total);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function recomputeWorkerCostTotal(state: WorkerCostState): void {
|
|
562
|
+
let total = 0;
|
|
563
|
+
for (const cost of state.finalizedByWorker.values()) {
|
|
564
|
+
total += cost;
|
|
565
|
+
}
|
|
566
|
+
for (const [worker, cost] of state.liveByWorker) {
|
|
567
|
+
if (!state.finalizedByWorker.has(worker)) {
|
|
568
|
+
total += cost;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
state.total = total;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function workerKey(taskId: string, attempt: number): string {
|
|
575
|
+
return `${taskId}:${attempt}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
487
578
|
function currentTaskProgress(
|
|
488
579
|
task: Pick<Task, "taskId" | "title" | "statusItems">,
|
|
489
580
|
status: CoordinatorProgressItemStatus,
|
|
@@ -521,13 +612,33 @@ function emitWorkerEventProgress(
|
|
|
521
612
|
task: Pick<Task, "taskId" | "title" | "statusItems">,
|
|
522
613
|
attempts: readonly TaskAttemptSummary[],
|
|
523
614
|
attempt: number,
|
|
524
|
-
event: { type: string; toolName?: string; isError?: boolean },
|
|
615
|
+
event: { type: string; toolName?: string; isError?: boolean; usageCostTotal?: number; usageCostKey?: string },
|
|
525
616
|
): void {
|
|
617
|
+
if (event.usageCostTotal !== undefined) {
|
|
618
|
+
const changed = recordLiveWorkerCost(runtime.workerCostState, workerKey(task.taskId, attempt), event);
|
|
619
|
+
if (changed) {
|
|
620
|
+
emitProgress(
|
|
621
|
+
runtime,
|
|
622
|
+
`TODO ${task.taskId}: worker cost updated to ${formatCost(runtime.workerCostState.total)}.`,
|
|
623
|
+
{
|
|
624
|
+
phase: "worker_tool",
|
|
625
|
+
taskId: task.taskId,
|
|
626
|
+
title: task.title,
|
|
627
|
+
attempt,
|
|
628
|
+
status: "in_progress",
|
|
629
|
+
workerEventType: event.type,
|
|
630
|
+
...currentTaskProgress(task, "in_progress"),
|
|
631
|
+
taskProgress: buildTaskProgressModel({ tasks, attempts, currentTaskId: task.taskId }),
|
|
632
|
+
},
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
526
637
|
if (!event.toolName || (event.type !== "tool_execution_start" && event.type !== "tool_execution_end")) {
|
|
527
638
|
return;
|
|
528
639
|
}
|
|
529
640
|
const action = event.type === "tool_execution_start" ? "started" : event.isError ? "failed" : "finished";
|
|
530
|
-
const update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath"> = {
|
|
641
|
+
const update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath" | "workerCostTotal"> = {
|
|
531
642
|
phase: "worker_tool",
|
|
532
643
|
taskId: task.taskId,
|
|
533
644
|
title: task.title,
|
|
@@ -568,7 +679,7 @@ function emitTaskOutcomeProgress(
|
|
|
568
679
|
: outcome.reportedStatus === "blocked"
|
|
569
680
|
? "task_blocked"
|
|
570
681
|
: "task_failed";
|
|
571
|
-
const update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath"> = {
|
|
682
|
+
const update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath" | "workerCostTotal"> = {
|
|
572
683
|
phase,
|
|
573
684
|
taskId: task.taskId,
|
|
574
685
|
title: task.title,
|
|
@@ -759,6 +870,20 @@ function positiveMilliseconds(value: number | undefined, fallback: number): numb
|
|
|
759
870
|
return fallback;
|
|
760
871
|
}
|
|
761
872
|
|
|
873
|
+
function finiteNonNegativeNumber(value: unknown): number | undefined {
|
|
874
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function formatCost(value: number): string {
|
|
878
|
+
if (value === 0) {
|
|
879
|
+
return "$0";
|
|
880
|
+
}
|
|
881
|
+
if (value < 0.01) {
|
|
882
|
+
return `$${value.toFixed(4)}`;
|
|
883
|
+
}
|
|
884
|
+
return `$${value.toFixed(2)}`;
|
|
885
|
+
}
|
|
886
|
+
|
|
762
887
|
function errorMessage(error: unknown): string {
|
|
763
888
|
return error instanceof Error ? error.message : String(error);
|
|
764
889
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
3
|
|
|
3
4
|
import { runCoordinator, type CoordinatorProgressUpdate, type CoordinatorResult } from "./coordinator.ts";
|
|
4
5
|
import { longTaskInputTransform } from "./input_router.ts";
|
|
5
6
|
import { renderLongTaskToolCall, renderLongTaskToolResult } from "./render.ts";
|
|
6
7
|
import { PiLongTaskParams } from "./types.ts";
|
|
7
8
|
|
|
9
|
+
export function createWorkerCostAccumulator() {
|
|
10
|
+
let pendingWorkerCostTotal = 0;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
add(cost: number): void {
|
|
14
|
+
const value = finiteNonNegativeNumber(cost);
|
|
15
|
+
if (value && value > 0) {
|
|
16
|
+
pendingWorkerCostTotal += value;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
applyToAssistantMessage(message: AssistantMessage): AssistantMessage | undefined {
|
|
20
|
+
if (pendingWorkerCostTotal <= 0) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const replacement = addWorkerCostToAssistantMessage(message, pendingWorkerCostTotal);
|
|
25
|
+
if (replacement) {
|
|
26
|
+
pendingWorkerCostTotal = 0;
|
|
27
|
+
}
|
|
28
|
+
return replacement;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addWorkerCostToAssistantMessage(
|
|
34
|
+
message: AssistantMessage,
|
|
35
|
+
workerCostTotal: number,
|
|
36
|
+
): AssistantMessage | undefined {
|
|
37
|
+
const workerCost = finiteNonNegativeNumber(workerCostTotal);
|
|
38
|
+
if (!workerCost || workerCost <= 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...message,
|
|
44
|
+
usage: {
|
|
45
|
+
...message.usage,
|
|
46
|
+
cost: {
|
|
47
|
+
...message.usage.cost,
|
|
48
|
+
total: message.usage.cost.total + workerCost,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function finiteNonNegativeNumber(value: unknown): number | undefined {
|
|
55
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
8
58
|
function toolDetails(result: CoordinatorResult) {
|
|
9
59
|
return {
|
|
10
60
|
runId: result.runId,
|
|
@@ -19,11 +69,23 @@ function toolDetails(result: CoordinatorResult) {
|
|
|
19
69
|
blockedTasks: result.blockedTasks,
|
|
20
70
|
remainingTasks: result.remainingTasks,
|
|
21
71
|
taskProgress: result.taskProgress,
|
|
72
|
+
workerCostTotal: result.workerCostTotal,
|
|
22
73
|
summary: result.summary,
|
|
23
74
|
};
|
|
24
75
|
}
|
|
25
76
|
|
|
26
77
|
export default function registerPiLongTaskExtension(pi: ExtensionAPI) {
|
|
78
|
+
const workerCostAccumulator = createWorkerCostAccumulator();
|
|
79
|
+
|
|
80
|
+
pi.on("message_end", (event) => {
|
|
81
|
+
if (event.message.role !== "assistant") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const message = workerCostAccumulator.applyToAssistantMessage(event.message);
|
|
86
|
+
return message ? { message } : undefined;
|
|
87
|
+
});
|
|
88
|
+
|
|
27
89
|
pi.on("input", (event) => {
|
|
28
90
|
if (event.source === "extension") {
|
|
29
91
|
return { action: "continue" as const };
|
|
@@ -64,6 +126,7 @@ export default function registerPiLongTaskExtension(pi: ExtensionAPI) {
|
|
|
64
126
|
abortSignal: signal,
|
|
65
127
|
onProgress: publishProgress,
|
|
66
128
|
});
|
|
129
|
+
workerCostAccumulator.add(result.workerCostTotal);
|
|
67
130
|
|
|
68
131
|
return {
|
|
69
132
|
content: [
|
package/src/render.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface CoordinatorResultForRendering {
|
|
|
17
17
|
commits?: CoordinatorCommitSummary[];
|
|
18
18
|
remainingTasks?: CoordinatorRemainingTask[];
|
|
19
19
|
taskProgress?: TaskProgressModel;
|
|
20
|
+
workerCostTotal?: number;
|
|
20
21
|
error?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -45,11 +46,13 @@ const SIDEBAR_GAP = 2;
|
|
|
45
46
|
class LongTaskSidebarShell implements Component {
|
|
46
47
|
private readonly mainText: string;
|
|
47
48
|
private readonly taskProgress: TaskProgressModel;
|
|
49
|
+
private readonly workerCostTotal: number | undefined;
|
|
48
50
|
private readonly theme: Theme;
|
|
49
51
|
|
|
50
|
-
constructor(mainText: string, taskProgress: TaskProgressModel, theme: Theme) {
|
|
52
|
+
constructor(mainText: string, taskProgress: TaskProgressModel, theme: Theme, workerCostTotal?: number) {
|
|
51
53
|
this.mainText = mainText;
|
|
52
54
|
this.taskProgress = taskProgress;
|
|
55
|
+
this.workerCostTotal = workerCostTotal;
|
|
53
56
|
this.theme = theme;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -92,7 +95,7 @@ class LongTaskSidebarShell implements Component {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
const innerWidth = Math.max(0, width - 2);
|
|
95
|
-
const rows = sidebarRows(this.taskProgress, this.theme);
|
|
98
|
+
const rows = sidebarRows(this.taskProgress, this.theme, this.workerCostTotal);
|
|
96
99
|
return [
|
|
97
100
|
sidebarBorder("Long Task", width, this.theme),
|
|
98
101
|
...rows.map((row) => sidebarRow(row, innerWidth, this.theme)),
|
|
@@ -113,6 +116,10 @@ export function formatCoordinatorResultMessage(result: CoordinatorResultForRende
|
|
|
113
116
|
`TODO file: ${result.todoPath}`,
|
|
114
117
|
];
|
|
115
118
|
|
|
119
|
+
if (result.workerCostTotal) {
|
|
120
|
+
lines.push(`Worker spend: ${formatCost(result.workerCostTotal)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
const commitLines = commits
|
|
117
124
|
.filter((commit) => commit.hash || commit.error)
|
|
118
125
|
.map((commit) => {
|
|
@@ -154,8 +161,9 @@ export function renderLongTaskToolResult(
|
|
|
154
161
|
const details = recordOrUndefined(result.details);
|
|
155
162
|
if (options.isPartial) {
|
|
156
163
|
const taskProgress = taskProgressModel(details?.taskProgress);
|
|
164
|
+
const workerCostTotal = numberValue(details?.workerCostTotal);
|
|
157
165
|
const main = renderLongTaskProgress(details, contentText(result), theme);
|
|
158
|
-
return taskProgress ? new LongTaskSidebarShell(main, taskProgress, theme) : new Text(main, 0, 0);
|
|
166
|
+
return taskProgress ? new LongTaskSidebarShell(main, taskProgress, theme, workerCostTotal) : new Text(main, 0, 0);
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
const finalDetails = longTaskDetails(details);
|
|
@@ -165,7 +173,7 @@ export function renderLongTaskToolResult(
|
|
|
165
173
|
|
|
166
174
|
const main = renderLongTaskSummary(finalDetails, options.expanded, theme);
|
|
167
175
|
return finalDetails.taskProgress
|
|
168
|
-
? new LongTaskSidebarShell(main, finalDetails.taskProgress, theme)
|
|
176
|
+
? new LongTaskSidebarShell(main, finalDetails.taskProgress, theme, finalDetails.workerCostTotal)
|
|
169
177
|
: new Text(main, 0, 0);
|
|
170
178
|
}
|
|
171
179
|
|
|
@@ -208,6 +216,7 @@ function renderLongTaskSummary(details: CoordinatorToolRenderDetails, expanded:
|
|
|
208
216
|
details.blockedTasks ? theme.fg("warning", `${details.blockedTasks} blocked`) : undefined,
|
|
209
217
|
remainingCount ? theme.fg("muted", `${remainingCount} remaining`) : undefined,
|
|
210
218
|
commitCount ? theme.fg("success", `${commitCount} commit${commitCount === 1 ? "" : "s"}`) : undefined,
|
|
219
|
+
details.workerCostTotal ? theme.fg("muted", `worker ${formatCost(details.workerCostTotal)}`) : undefined,
|
|
211
220
|
].filter(Boolean);
|
|
212
221
|
|
|
213
222
|
if (!expanded) {
|
|
@@ -252,9 +261,12 @@ function padLine(line: string, width: number): string {
|
|
|
252
261
|
return truncateToWidth(line, width, "…", true);
|
|
253
262
|
}
|
|
254
263
|
|
|
255
|
-
function sidebarRows(taskProgress: TaskProgressModel, theme: Theme): string[] {
|
|
264
|
+
function sidebarRows(taskProgress: TaskProgressModel, theme: Theme, workerCostTotal?: number): string[] {
|
|
256
265
|
const summary = normalizedTaskProgressSummary(taskProgress);
|
|
257
266
|
const rows = [theme.fg("toolTitle", theme.bold("Task sidebar")), theme.fg("dim", "Centered timeline")];
|
|
267
|
+
if (workerCostTotal) {
|
|
268
|
+
rows.push(theme.fg("muted", `Worker spend: ${formatCost(workerCostTotal)}`));
|
|
269
|
+
}
|
|
258
270
|
if (summary.totalTasks === 0) {
|
|
259
271
|
rows.push("", theme.fg("muted", "Waiting for TODO plan..."));
|
|
260
272
|
return rows;
|
|
@@ -503,6 +515,7 @@ function longTaskDetails(details: Record<string, unknown> | undefined): Coordina
|
|
|
503
515
|
commits: commitSummaries(details.commits),
|
|
504
516
|
remainingTasks: remainingTaskSummaries(details.remainingTasks),
|
|
505
517
|
taskProgress: taskProgressModel(details.taskProgress),
|
|
518
|
+
workerCostTotal: nonNegativeNumberValue(details.workerCostTotal),
|
|
506
519
|
error: stringValue(details.error),
|
|
507
520
|
};
|
|
508
521
|
}
|
|
@@ -592,6 +605,20 @@ function numberValue(value: unknown): number | undefined {
|
|
|
592
605
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
593
606
|
}
|
|
594
607
|
|
|
608
|
+
function nonNegativeNumberValue(value: unknown): number | undefined {
|
|
609
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function formatCost(value: number): string {
|
|
613
|
+
if (value === 0) {
|
|
614
|
+
return "$0";
|
|
615
|
+
}
|
|
616
|
+
if (value < 0.01) {
|
|
617
|
+
return `$${value.toFixed(4)}`;
|
|
618
|
+
}
|
|
619
|
+
return `$${value.toFixed(2)}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
595
622
|
function recordOrUndefined(value: unknown): Record<string, unknown> | undefined {
|
|
596
623
|
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
597
624
|
}
|
package/src/types.ts
CHANGED
package/src/worker_session.ts
CHANGED
|
@@ -201,7 +201,7 @@ export interface WorkerSessionLike {
|
|
|
201
201
|
compact?(customInstructions?: string): Promise<unknown>;
|
|
202
202
|
dispose?(): void;
|
|
203
203
|
getLastAssistantText?(): string | undefined;
|
|
204
|
-
getSessionStats?(): unknown
|
|
204
|
+
getSessionStats?(): unknown | Promise<unknown>;
|
|
205
205
|
getContextUsage?(): unknown;
|
|
206
206
|
sessionFile?: string;
|
|
207
207
|
sessionId?: string;
|
|
@@ -246,6 +246,8 @@ export interface CapturedWorkerEvent {
|
|
|
246
246
|
toolName?: string;
|
|
247
247
|
isError?: boolean;
|
|
248
248
|
note?: string;
|
|
249
|
+
usageCostTotal?: number;
|
|
250
|
+
usageCostKey?: string;
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
export interface SessionOutcome {
|
|
@@ -261,6 +263,8 @@ export interface SessionOutcome {
|
|
|
261
263
|
contextObservations: string[];
|
|
262
264
|
compactionEvents: string[];
|
|
263
265
|
events: CapturedWorkerEvent[];
|
|
266
|
+
workerCostTotal: number;
|
|
267
|
+
workerCostSource?: string;
|
|
264
268
|
shutdownRequested: boolean;
|
|
265
269
|
timedOut: boolean;
|
|
266
270
|
aborted: boolean;
|
|
@@ -345,6 +349,9 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
345
349
|
let error: string | undefined;
|
|
346
350
|
let finished = false;
|
|
347
351
|
let turnCount = 0;
|
|
352
|
+
let messageUsageCostTotal = 0;
|
|
353
|
+
let hasMessageUsageCost = false;
|
|
354
|
+
let sessionStatsCostTotal: number | undefined;
|
|
348
355
|
|
|
349
356
|
const prompt = buildTaskPrompt(options);
|
|
350
357
|
const taskTimeoutSeconds = options.taskTimeoutSeconds ?? DEFAULT_TASK_TIMEOUT_SECONDS;
|
|
@@ -366,6 +373,22 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
366
373
|
timers.clear();
|
|
367
374
|
};
|
|
368
375
|
|
|
376
|
+
const messageUsageCostsByKey = new Map<string, number>();
|
|
377
|
+
const recordWorkerUsageCost = (cost: number | undefined, key: string | undefined) => {
|
|
378
|
+
if (cost === undefined) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
hasMessageUsageCost = true;
|
|
382
|
+
if (!key) {
|
|
383
|
+
messageUsageCostTotal += cost;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const previousCost = messageUsageCostsByKey.get(key) ?? 0;
|
|
388
|
+
messageUsageCostsByKey.set(key, cost);
|
|
389
|
+
messageUsageCostTotal += cost - previousCost;
|
|
390
|
+
};
|
|
391
|
+
|
|
369
392
|
const schedule = (fn: () => void | Promise<void>, ms: number) => {
|
|
370
393
|
const timer = setTimeout(() => {
|
|
371
394
|
timers.delete(timer);
|
|
@@ -458,6 +481,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
458
481
|
if (messageText) {
|
|
459
482
|
assistantText = messageText;
|
|
460
483
|
}
|
|
484
|
+
recordWorkerUsageCost(workerUsageCostFromEvent(event), workerUsageCostKeyFromEvent(event));
|
|
461
485
|
break;
|
|
462
486
|
}
|
|
463
487
|
case "turn_end": {
|
|
@@ -538,6 +562,7 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
538
562
|
assistantText = latestAssistantText(session, assistantText);
|
|
539
563
|
sessionFile = session.sessionFile ?? sessionFile;
|
|
540
564
|
sessionId = session.sessionId ?? sessionId;
|
|
565
|
+
sessionStatsCostTotal = await workerUsageCostFromSessionStats(session);
|
|
541
566
|
session.dispose?.();
|
|
542
567
|
}
|
|
543
568
|
}
|
|
@@ -547,6 +572,10 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
547
572
|
}
|
|
548
573
|
|
|
549
574
|
const reportedStatus = parseReportedStatus(assistantText);
|
|
575
|
+
const capturedWorkerCost = selectWorkerCostTotal({
|
|
576
|
+
messageCostTotal: hasMessageUsageCost ? messageUsageCostTotal : undefined,
|
|
577
|
+
statsCostTotal: sessionStatsCostTotal,
|
|
578
|
+
});
|
|
550
579
|
return {
|
|
551
580
|
task: options.task,
|
|
552
581
|
attempt: options.attempt,
|
|
@@ -560,6 +589,8 @@ export async function runWorkerTask(options: RunWorkerTaskOptions): Promise<Sess
|
|
|
560
589
|
contextObservations,
|
|
561
590
|
compactionEvents,
|
|
562
591
|
events,
|
|
592
|
+
workerCostTotal: capturedWorkerCost.total,
|
|
593
|
+
workerCostSource: capturedWorkerCost.source,
|
|
563
594
|
shutdownRequested,
|
|
564
595
|
timedOut,
|
|
565
596
|
aborted: aborted || Boolean(options.abortSignal?.aborted),
|
|
@@ -723,9 +754,15 @@ function summarizeWorkerEvent(event: unknown): CapturedWorkerEvent | undefined {
|
|
|
723
754
|
};
|
|
724
755
|
}
|
|
725
756
|
|
|
757
|
+
if (event.type === "message_end") {
|
|
758
|
+
const usageCostTotal = workerUsageCostFromEvent(event);
|
|
759
|
+
return usageCostTotal === undefined
|
|
760
|
+
? { type: event.type }
|
|
761
|
+
: { type: event.type, usageCostTotal, usageCostKey: workerUsageCostKeyFromEvent(event) };
|
|
762
|
+
}
|
|
763
|
+
|
|
726
764
|
if (
|
|
727
765
|
event.type === "turn_end" ||
|
|
728
|
-
event.type === "message_end" ||
|
|
729
766
|
event.type === "compaction_start" ||
|
|
730
767
|
event.type === "compaction_end" ||
|
|
731
768
|
event.type === "agent_end" ||
|
|
@@ -738,6 +775,109 @@ function summarizeWorkerEvent(event: unknown): CapturedWorkerEvent | undefined {
|
|
|
738
775
|
return undefined;
|
|
739
776
|
}
|
|
740
777
|
|
|
778
|
+
export function workerUsageCostFromEvent(event: unknown): number | undefined {
|
|
779
|
+
if (!isRecord(event)) {
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (const candidate of [event.assistantMessage, event.message, event]) {
|
|
784
|
+
const cost = workerUsageCostFromAssistantMessage(candidate);
|
|
785
|
+
if (cost !== undefined) {
|
|
786
|
+
return cost;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function workerUsageCostFromAssistantMessage(message: unknown): number | undefined {
|
|
793
|
+
if (!isRecord(message)) {
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
return usageCostTotal(message.usage);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function workerUsageCostKeyFromEvent(event: unknown): string | undefined {
|
|
800
|
+
if (!isRecord(event)) {
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
for (const candidate of [event.assistantMessage, event.message, event]) {
|
|
805
|
+
const key = workerUsageCostKeyFromAssistantMessage(candidate);
|
|
806
|
+
if (key) {
|
|
807
|
+
return key;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return undefined;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function workerUsageCostKeyFromAssistantMessage(message: unknown): string | undefined {
|
|
814
|
+
if (!isRecord(message)) {
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
for (const keyName of ["id", "messageId", "uuid"] as const) {
|
|
819
|
+
const value = message[keyName];
|
|
820
|
+
if (typeof value === "string" && value) {
|
|
821
|
+
return `${keyName}:${value}`;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function workerUsageCostFromStats(stats: unknown): number | undefined {
|
|
828
|
+
if (!isRecord(stats)) {
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const directCost = finiteNonNegativeNumber(stats.cost);
|
|
833
|
+
if (directCost !== undefined) {
|
|
834
|
+
return directCost;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return usageCostTotal(stats.usage) ?? usageCostTotal(stats);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function workerUsageCostFromSessionStats(session: WorkerSessionLike): Promise<number | undefined> {
|
|
841
|
+
if (!session.getSessionStats) {
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
return workerUsageCostFromStats(await session.getSessionStats());
|
|
847
|
+
} catch {
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function usageCostTotal(usage: unknown): number | undefined {
|
|
853
|
+
if (!isRecord(usage)) {
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const cost = usage.cost;
|
|
858
|
+
if (isRecord(cost)) {
|
|
859
|
+
return finiteNonNegativeNumber(cost.total);
|
|
860
|
+
}
|
|
861
|
+
return finiteNonNegativeNumber(cost);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function selectWorkerCostTotal(options: { messageCostTotal: number | undefined; statsCostTotal: number | undefined }): {
|
|
865
|
+
total: number;
|
|
866
|
+
source?: string;
|
|
867
|
+
} {
|
|
868
|
+
if (options.statsCostTotal !== undefined) {
|
|
869
|
+
return { total: options.statsCostTotal, source: "session_stats" };
|
|
870
|
+
}
|
|
871
|
+
if (options.messageCostTotal !== undefined) {
|
|
872
|
+
return { total: options.messageCostTotal, source: "message_end" };
|
|
873
|
+
}
|
|
874
|
+
return { total: 0 };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function finiteNonNegativeNumber(value: unknown): number | undefined {
|
|
878
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
879
|
+
}
|
|
880
|
+
|
|
741
881
|
function captureContextUsage(
|
|
742
882
|
session: WorkerSessionLike | undefined,
|
|
743
883
|
turnCount: number,
|