@yeaft/webchat-agent 1.0.151 → 1.0.152
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/yeaft/llm/anthropic.js +17 -5
- package/yeaft/llm/openai-responses.js +4 -2
- package/yeaft/llm/usage-accounting.js +35 -14
- package/yeaft/work-center/controller.js +6 -0
- package/yeaft/work-center/projection.js +35 -7
- package/yeaft/work-center/runner.js +42 -29
- package/yeaft/work-center/store.js +76 -10
- package/yeaft/work-center/watcher.js +8 -3
package/package.json
CHANGED
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -240,7 +240,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
240
240
|
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', effortContext?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
241
241
|
* @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
|
|
242
242
|
*/
|
|
243
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange }) {
|
|
243
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange, onRequestStart }) {
|
|
244
244
|
if (signal?.aborted) throw new LLMAbortError();
|
|
245
245
|
|
|
246
246
|
const body = {
|
|
@@ -272,6 +272,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
272
272
|
|
|
273
273
|
let response;
|
|
274
274
|
try {
|
|
275
|
+
onRequestStart?.();
|
|
275
276
|
response = await fetch(url, {
|
|
276
277
|
method: 'POST',
|
|
277
278
|
headers,
|
|
@@ -328,6 +329,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
328
329
|
const responseStatus = response.status;
|
|
329
330
|
let sawStop = false;
|
|
330
331
|
let sawMessageStart = false;
|
|
332
|
+
let cumulativeOutputTokens = 0;
|
|
331
333
|
|
|
332
334
|
try {
|
|
333
335
|
while (true) {
|
|
@@ -454,12 +456,17 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
454
456
|
stopReason: this.#mapStopReason(stopReason),
|
|
455
457
|
};
|
|
456
458
|
}
|
|
457
|
-
//
|
|
459
|
+
// Anthropic message_delta usage is cumulative across the response.
|
|
460
|
+
// Expose only the newly consumed output tokens so shared accounting
|
|
461
|
+
// can safely add events from message_start and multiple deltas.
|
|
458
462
|
if (event.usage) {
|
|
463
|
+
const nextOutputTokens = Math.max(0, Number(event.usage.output_tokens) || 0);
|
|
464
|
+
const outputTokens = Math.max(0, nextOutputTokens - cumulativeOutputTokens);
|
|
465
|
+
cumulativeOutputTokens = Math.max(cumulativeOutputTokens, nextOutputTokens);
|
|
459
466
|
yield {
|
|
460
467
|
type: 'usage',
|
|
461
468
|
inputTokens: 0, // Only in message_start
|
|
462
|
-
outputTokens
|
|
469
|
+
outputTokens,
|
|
463
470
|
};
|
|
464
471
|
}
|
|
465
472
|
} else if (type === 'message_stop') {
|
|
@@ -468,10 +475,14 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
468
475
|
sawMessageStart = true;
|
|
469
476
|
// Usage from message_start
|
|
470
477
|
if (event.message?.usage) {
|
|
478
|
+
cumulativeOutputTokens = Math.max(
|
|
479
|
+
cumulativeOutputTokens,
|
|
480
|
+
Math.max(0, Number(event.message.usage.output_tokens) || 0),
|
|
481
|
+
);
|
|
471
482
|
yield {
|
|
472
483
|
type: 'usage',
|
|
473
484
|
inputTokens: event.message.usage.input_tokens || 0,
|
|
474
|
-
outputTokens:
|
|
485
|
+
outputTokens: cumulativeOutputTokens,
|
|
475
486
|
cacheReadTokens: event.message.usage.cache_read_input_tokens || 0,
|
|
476
487
|
cacheWriteTokens: event.message.usage.cache_creation_input_tokens || 0,
|
|
477
488
|
};
|
|
@@ -518,7 +529,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
518
529
|
* models silently drop the param. max_tokens auto-widens to budget+1024
|
|
519
530
|
* when needed.
|
|
520
531
|
*/
|
|
521
|
-
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, effortContext, signal }) {
|
|
532
|
+
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, effortContext, signal, onRequestStart }) {
|
|
522
533
|
if (signal?.aborted) throw new LLMAbortError();
|
|
523
534
|
|
|
524
535
|
const body = {
|
|
@@ -536,6 +547,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
536
547
|
|
|
537
548
|
let response;
|
|
538
549
|
try {
|
|
550
|
+
onRequestStart?.();
|
|
539
551
|
response = await fetch(`${this.#baseUrl}/v1/messages`, {
|
|
540
552
|
method: 'POST',
|
|
541
553
|
headers: this.#headers(),
|
|
@@ -257,7 +257,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
257
257
|
* `api-key` headers are auto-redacted (see `redactRawRequest` in
|
|
258
258
|
* `adapter.js`); request-body fields are caller-controlled.
|
|
259
259
|
*/
|
|
260
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange }) {
|
|
260
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange, onRequestStart }) {
|
|
261
261
|
if (signal?.aborted) throw new LLMAbortError();
|
|
262
262
|
|
|
263
263
|
const body = {
|
|
@@ -297,6 +297,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
297
297
|
|
|
298
298
|
let response;
|
|
299
299
|
try {
|
|
300
|
+
onRequestStart?.();
|
|
300
301
|
response = await fetch(url, {
|
|
301
302
|
method: 'POST',
|
|
302
303
|
headers,
|
|
@@ -516,7 +517,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
516
517
|
* expose them, mirror the stream() instrumentation. Parity with
|
|
517
518
|
* anthropic.js's `call()`.
|
|
518
519
|
*/
|
|
519
|
-
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal }) {
|
|
520
|
+
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal, onRequestStart }) {
|
|
520
521
|
if (signal?.aborted) throw new LLMAbortError();
|
|
521
522
|
|
|
522
523
|
const body = {
|
|
@@ -540,6 +541,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
540
541
|
|
|
541
542
|
let response;
|
|
542
543
|
try {
|
|
544
|
+
onRequestStart?.();
|
|
543
545
|
response = await fetch(`${this.#baseUrl}/responses`, {
|
|
544
546
|
method: 'POST',
|
|
545
547
|
headers: {
|
|
@@ -43,14 +43,10 @@ function addUsage(total, usage) {
|
|
|
43
43
|
total.totalTokens += normalized.totalTokens;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function hasUsage(usage) {
|
|
47
|
-
return Object.values(usage).some(value => value > 0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
/**
|
|
51
47
|
* Wrap the shared adapter at the provider-call boundary. Every stream request
|
|
52
|
-
* reports once after its event stream finishes
|
|
53
|
-
*
|
|
48
|
+
* reports once after its event stream finishes or aborts, and every
|
|
49
|
+
* non-streaming side call reports once whether it succeeds or fails.
|
|
54
50
|
*
|
|
55
51
|
* Parent VP engines, sub-agent engines, Dream, compact, reflection, AMS, and
|
|
56
52
|
* classifiers all reuse this adapter, so none need their own accounting hook.
|
|
@@ -58,15 +54,35 @@ function hasUsage(usage) {
|
|
|
58
54
|
export class UsageAccountingAdapter extends LLMAdapter {
|
|
59
55
|
#adapter;
|
|
60
56
|
#onUsage;
|
|
57
|
+
#onRequest;
|
|
61
58
|
|
|
62
|
-
constructor(adapter, onUsage) {
|
|
59
|
+
constructor(adapter, onUsage, onRequest = null) {
|
|
63
60
|
super(adapter?.config || {});
|
|
64
61
|
this.#adapter = adapter;
|
|
65
62
|
this.#onUsage = onUsage;
|
|
63
|
+
this.#onRequest = onRequest;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#requestParams(params) {
|
|
67
|
+
if (typeof this.#onRequest !== 'function') return params;
|
|
68
|
+
const upstream = params?.onRequestStart;
|
|
69
|
+
return {
|
|
70
|
+
...params,
|
|
71
|
+
onRequestStart: () => {
|
|
72
|
+
try {
|
|
73
|
+
upstream?.();
|
|
74
|
+
} finally {
|
|
75
|
+
try {
|
|
76
|
+
this.#onRequest();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.warn(`[llm-usage] request callback failed: ${error?.message || error}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
#report(usage) {
|
|
69
|
-
if (!hasUsage(usage)) return;
|
|
70
86
|
try {
|
|
71
87
|
this.#onUsage(usage);
|
|
72
88
|
} catch (error) {
|
|
@@ -77,7 +93,7 @@ export class UsageAccountingAdapter extends LLMAdapter {
|
|
|
77
93
|
async *stream(params) {
|
|
78
94
|
const total = normalizeTokenUsage();
|
|
79
95
|
try {
|
|
80
|
-
for await (const event of this.#adapter.stream(params)) {
|
|
96
|
+
for await (const event of this.#adapter.stream(this.#requestParams(params))) {
|
|
81
97
|
if (event?.type === 'usage') addUsage(total, event);
|
|
82
98
|
yield event;
|
|
83
99
|
}
|
|
@@ -87,9 +103,14 @@ export class UsageAccountingAdapter extends LLMAdapter {
|
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
async call(params) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
let usage = normalizeTokenUsage();
|
|
107
|
+
try {
|
|
108
|
+
const result = await this.#adapter.call(this.#requestParams(params));
|
|
109
|
+
usage = normalizeTokenUsage(result?.usage || {});
|
|
110
|
+
return result;
|
|
111
|
+
} finally {
|
|
112
|
+
this.#report(usage);
|
|
113
|
+
}
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
refreshProviders(providers) {
|
|
@@ -105,8 +126,8 @@ export class UsageAccountingAdapter extends LLMAdapter {
|
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
|
|
108
|
-
export function withUsageAccounting(adapter, onUsage) {
|
|
129
|
+
export function withUsageAccounting(adapter, onUsage, onRequest = null) {
|
|
109
130
|
return typeof onUsage === 'function'
|
|
110
|
-
? new UsageAccountingAdapter(adapter, onUsage)
|
|
131
|
+
? new UsageAccountingAdapter(adapter, onUsage, onRequest)
|
|
111
132
|
: adapter;
|
|
112
133
|
}
|
|
@@ -82,6 +82,12 @@ function normalizeTerminalResult(result, action) {
|
|
|
82
82
|
: null,
|
|
83
83
|
loopCount: Math.max(0, Number(result.loopCount) || 0),
|
|
84
84
|
toolCount: Math.max(0, Number(result.toolCount) || 0),
|
|
85
|
+
llmRequestCount: Math.max(0, Number(result.llmRequestCount) || 0),
|
|
86
|
+
inputTokens: Math.max(0, Number(result.inputTokens) || 0),
|
|
87
|
+
outputTokens: Math.max(0, Number(result.outputTokens) || 0),
|
|
88
|
+
cacheReadTokens: Math.max(0, Number(result.cacheReadTokens) || 0),
|
|
89
|
+
cacheWriteTokens: Math.max(0, Number(result.cacheWriteTokens) || 0),
|
|
90
|
+
totalTokens: Math.max(0, Number(result.totalTokens) || 0),
|
|
85
91
|
acceptanceChecks: Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : [],
|
|
86
92
|
checkpoint: result.checkpoint && typeof result.checkpoint === 'object'
|
|
87
93
|
? result.checkpoint : null,
|
|
@@ -7,22 +7,45 @@ function count(value) {
|
|
|
7
7
|
return Math.max(0, Number(value) || 0);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function emptyExecutionStats() {
|
|
11
|
+
return {
|
|
12
|
+
llmRequestCount: 0,
|
|
13
|
+
loopCount: 0,
|
|
14
|
+
toolCount: 0,
|
|
15
|
+
inputTokens: 0,
|
|
16
|
+
outputTokens: 0,
|
|
17
|
+
cacheReadTokens: 0,
|
|
18
|
+
cacheWriteTokens: 0,
|
|
19
|
+
totalTokens: 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function executionStats(value) {
|
|
24
|
+
const stats = emptyExecutionStats();
|
|
25
|
+
for (const key of Object.keys(stats)) stats[key] = count(value?.[key]);
|
|
26
|
+
return stats;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sumExecutionStats(values) {
|
|
30
|
+
return values.reduce((total, value) => {
|
|
31
|
+
const next = executionStats(value);
|
|
32
|
+
for (const key of Object.keys(total)) total[key] += next[key];
|
|
33
|
+
return total;
|
|
34
|
+
}, emptyExecutionStats());
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
function actionExecution(action, runs) {
|
|
11
38
|
const matchingRuns = Array.isArray(runs)
|
|
12
39
|
? runs.filter(run => run?.actionId === action?.id)
|
|
13
40
|
: [];
|
|
14
41
|
if (matchingRuns.length === 0) {
|
|
15
42
|
return {
|
|
16
|
-
|
|
17
|
-
toolCount: count(action?.toolCount),
|
|
43
|
+
...executionStats(action),
|
|
18
44
|
response: '',
|
|
19
45
|
progressRevision: 0,
|
|
20
46
|
};
|
|
21
47
|
}
|
|
22
|
-
const stats = matchingRuns
|
|
23
|
-
loopCount: total.loopCount + count(run.loopCount),
|
|
24
|
-
toolCount: total.toolCount + count(run.toolCount),
|
|
25
|
-
}), { loopCount: 0, toolCount: 0 });
|
|
48
|
+
const stats = sumExecutionStats(matchingRuns);
|
|
26
49
|
const latest = [...matchingRuns].sort((left, right) => (
|
|
27
50
|
count(right.progressRevision) - count(left.progressRevision)
|
|
28
51
|
|| count(right.startedAt) - count(left.startedAt)
|
|
@@ -65,6 +88,7 @@ function projectAction(action, runs) {
|
|
|
65
88
|
assignmentPolicy: projectAssignmentPolicy(action.assignmentPolicy),
|
|
66
89
|
requiredRole: action.requiredRole || '',
|
|
67
90
|
status: action.status,
|
|
91
|
+
executionStats: executionStats(execution),
|
|
68
92
|
loopCount: execution.loopCount,
|
|
69
93
|
toolCount: execution.toolCount,
|
|
70
94
|
response: execution.response,
|
|
@@ -79,6 +103,7 @@ function projectActionStats(detail) {
|
|
|
79
103
|
return {
|
|
80
104
|
id: projected.id,
|
|
81
105
|
status: projected.status,
|
|
106
|
+
executionStats: projected.executionStats,
|
|
82
107
|
loopCount: projected.loopCount,
|
|
83
108
|
toolCount: projected.toolCount,
|
|
84
109
|
response: projected.response,
|
|
@@ -97,7 +122,7 @@ function waitingReason(detail) {
|
|
|
97
122
|
|
|
98
123
|
/**
|
|
99
124
|
* Authenticated browser detail DTO. Raw execution records stay Agent-local;
|
|
100
|
-
* the browser receives only
|
|
125
|
+
* the browser receives only aggregate execution stats plus the explicit user-facing response.
|
|
101
126
|
*/
|
|
102
127
|
export function projectWorkItemDetail(detail) {
|
|
103
128
|
if (!detail) return null;
|
|
@@ -112,6 +137,7 @@ export function projectWorkItemDetail(detail) {
|
|
|
112
137
|
planningMode: detail.workflowSnapshot?.planningMode || 'static',
|
|
113
138
|
status: detail.status,
|
|
114
139
|
currentActionId: detail.currentActionId || null,
|
|
140
|
+
executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
|
|
115
141
|
reuseMemory: detail.reuseMemory !== false,
|
|
116
142
|
waitingReason: waitingReason(detail),
|
|
117
143
|
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|
|
@@ -142,6 +168,7 @@ export function projectWorkItemSummary(detail) {
|
|
|
142
168
|
status: detail.status,
|
|
143
169
|
currentActionId: detail.currentActionId || null,
|
|
144
170
|
currentAction: null,
|
|
171
|
+
executionStats: executionStats(detail.executionStats),
|
|
145
172
|
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|
|
146
173
|
linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
|
|
147
174
|
attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
|
|
@@ -160,6 +187,7 @@ export function projectWorkItemSummary(detail) {
|
|
|
160
187
|
planningMode: detail.workflowSnapshot?.planningMode || 'static',
|
|
161
188
|
status: detail.status,
|
|
162
189
|
currentActionId: detail.currentActionId || null,
|
|
190
|
+
executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
|
|
163
191
|
currentAction: projectedAction ? {
|
|
164
192
|
id: projectedAction.id,
|
|
165
193
|
type: projectedAction.type,
|
|
@@ -12,6 +12,7 @@ import { formatPickedForInjection } from '../sessions/pre-flow.js';
|
|
|
12
12
|
import { existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { buildWorkItemAttachmentContext } from './attachments.js';
|
|
15
|
+
import { withUsageAccounting } from '../llm/usage-accounting.js';
|
|
15
16
|
import {
|
|
16
17
|
appendCheckpointToolEvent,
|
|
17
18
|
renderActionResumeBlock,
|
|
@@ -499,34 +500,24 @@ export class WorkItemRunner {
|
|
|
499
500
|
);
|
|
500
501
|
if (!snapshotsWritten) throw new Error('Work Center Run lost its lease before execution');
|
|
501
502
|
|
|
502
|
-
const engine = new Engine({
|
|
503
|
-
adapter: runtime.adapter,
|
|
504
|
-
trace: new NullTrace(),
|
|
505
|
-
config,
|
|
506
|
-
conversationStore: null,
|
|
507
|
-
memoryIndex: null,
|
|
508
|
-
amsRegistry: null,
|
|
509
|
-
toolRegistry,
|
|
510
|
-
skillManager: null,
|
|
511
|
-
mcpManager: null,
|
|
512
|
-
// WorkItem execution has its own durable Run/Event record. Do not let
|
|
513
|
-
// the Engine create Session exec logs, archives, or shared tool stats.
|
|
514
|
-
yeaftDir: null,
|
|
515
|
-
toolStats: null,
|
|
516
|
-
taskManager: null,
|
|
517
|
-
vpId: vp.id,
|
|
518
|
-
});
|
|
519
|
-
|
|
520
503
|
let text = '';
|
|
521
504
|
let loopCount = 0;
|
|
522
505
|
let toolCount = 0;
|
|
523
506
|
let checkpoint = null;
|
|
524
507
|
const toolInputs = new Map();
|
|
508
|
+
const usageStats = {
|
|
509
|
+
llmRequestCount: 0,
|
|
510
|
+
inputTokens: 0,
|
|
511
|
+
outputTokens: 0,
|
|
512
|
+
cacheReadTokens: 0,
|
|
513
|
+
cacheWriteTokens: 0,
|
|
514
|
+
totalTokens: 0,
|
|
515
|
+
};
|
|
525
516
|
let lastProgressAt = 0;
|
|
517
|
+
const executionStats = () => ({ loopCount, toolCount, ...usageStats });
|
|
526
518
|
const currentProgress = () => ({
|
|
527
519
|
response: publicWorkItemResponse(text),
|
|
528
|
-
|
|
529
|
-
toolCount,
|
|
520
|
+
...executionStats(),
|
|
530
521
|
checkpoint,
|
|
531
522
|
});
|
|
532
523
|
const reportProgress = (force = false) => {
|
|
@@ -537,6 +528,34 @@ export class WorkItemRunner {
|
|
|
537
528
|
return onProgress(currentProgress());
|
|
538
529
|
};
|
|
539
530
|
if (typeof registerProgressReader === 'function') registerProgressReader(currentProgress);
|
|
531
|
+
const adapter = withUsageAccounting(runtime.adapter, usage => {
|
|
532
|
+
usageStats.inputTokens += usage.inputTokens;
|
|
533
|
+
usageStats.outputTokens += usage.outputTokens;
|
|
534
|
+
usageStats.cacheReadTokens += usage.cacheReadTokens;
|
|
535
|
+
usageStats.cacheWriteTokens += usage.cacheWriteTokens;
|
|
536
|
+
usageStats.totalTokens += usage.totalTokens;
|
|
537
|
+
reportProgress(true);
|
|
538
|
+
}, () => {
|
|
539
|
+
usageStats.llmRequestCount += 1;
|
|
540
|
+
reportProgress(true);
|
|
541
|
+
});
|
|
542
|
+
const engine = new Engine({
|
|
543
|
+
adapter,
|
|
544
|
+
trace: new NullTrace(),
|
|
545
|
+
config,
|
|
546
|
+
conversationStore: null,
|
|
547
|
+
memoryIndex: null,
|
|
548
|
+
amsRegistry: null,
|
|
549
|
+
toolRegistry,
|
|
550
|
+
skillManager: null,
|
|
551
|
+
mcpManager: null,
|
|
552
|
+
// WorkItem execution has its own durable Run/Event record. Do not let
|
|
553
|
+
// the Engine create Session exec logs, archives, or shared tool stats.
|
|
554
|
+
yeaftDir: null,
|
|
555
|
+
toolStats: null,
|
|
556
|
+
taskManager: null,
|
|
557
|
+
vpId: vp.id,
|
|
558
|
+
});
|
|
540
559
|
try {
|
|
541
560
|
const prompt = `${executionAction.instruction}${resumeBlock}${attachmentContext.promptBlock}${memoryBlock}${completionContract(executionAction, workItem)}`;
|
|
542
561
|
const promptParts = attachmentContext.promptParts.length > 0
|
|
@@ -567,15 +586,10 @@ export class WorkItemRunner {
|
|
|
567
586
|
});
|
|
568
587
|
}
|
|
569
588
|
if (event?.type === 'text_delta' && typeof event.text === 'string') text += event.text;
|
|
570
|
-
reportProgress();
|
|
589
|
+
reportProgress(event?.type === 'loop');
|
|
571
590
|
}
|
|
572
591
|
} catch (error) {
|
|
573
|
-
error.workItemExecutionStats =
|
|
574
|
-
response: publicWorkItemResponse(text),
|
|
575
|
-
loopCount,
|
|
576
|
-
toolCount,
|
|
577
|
-
checkpoint,
|
|
578
|
-
};
|
|
592
|
+
error.workItemExecutionStats = currentProgress();
|
|
579
593
|
throw error;
|
|
580
594
|
} finally {
|
|
581
595
|
try { engine.abort?.('work_item_run_finished'); } catch {}
|
|
@@ -585,8 +599,7 @@ export class WorkItemRunner {
|
|
|
585
599
|
return {
|
|
586
600
|
...parseStructuredResult(text, executionAction.type),
|
|
587
601
|
response,
|
|
588
|
-
|
|
589
|
-
toolCount,
|
|
602
|
+
...executionStats(),
|
|
590
603
|
checkpoint,
|
|
591
604
|
};
|
|
592
605
|
}
|
|
@@ -48,6 +48,16 @@ function mapWorkItem(row) {
|
|
|
48
48
|
origin: parseJson(row.origin, null),
|
|
49
49
|
linkedSessionIds: parseJson(row.linked_session_ids, []),
|
|
50
50
|
attachments: parseJson(row.attachments, []),
|
|
51
|
+
executionStats: {
|
|
52
|
+
llmRequestCount: Math.max(0, Number(row.usage_llm_request_count) || 0),
|
|
53
|
+
loopCount: Math.max(0, Number(row.usage_loop_count) || 0),
|
|
54
|
+
toolCount: Math.max(0, Number(row.usage_tool_count) || 0),
|
|
55
|
+
inputTokens: Math.max(0, Number(row.usage_input_tokens) || 0),
|
|
56
|
+
outputTokens: Math.max(0, Number(row.usage_output_tokens) || 0),
|
|
57
|
+
cacheReadTokens: Math.max(0, Number(row.usage_cache_read_tokens) || 0),
|
|
58
|
+
cacheWriteTokens: Math.max(0, Number(row.usage_cache_write_tokens) || 0),
|
|
59
|
+
totalTokens: Math.max(0, Number(row.usage_total_tokens) || 0),
|
|
60
|
+
},
|
|
51
61
|
createdAt: row.created_at,
|
|
52
62
|
updatedAt: row.updated_at,
|
|
53
63
|
};
|
|
@@ -102,6 +112,12 @@ function mapRun(row) {
|
|
|
102
112
|
contractPatch: parseJson(row.contract_patch, null),
|
|
103
113
|
loopCount: Math.max(0, Number(row.loop_count) || 0),
|
|
104
114
|
toolCount: Math.max(0, Number(row.tool_count) || 0),
|
|
115
|
+
llmRequestCount: Math.max(0, Number(row.llm_request_count) || 0),
|
|
116
|
+
inputTokens: Math.max(0, Number(row.input_tokens) || 0),
|
|
117
|
+
outputTokens: Math.max(0, Number(row.output_tokens) || 0),
|
|
118
|
+
cacheReadTokens: Math.max(0, Number(row.cache_read_tokens) || 0),
|
|
119
|
+
cacheWriteTokens: Math.max(0, Number(row.cache_write_tokens) || 0),
|
|
120
|
+
totalTokens: Math.max(0, Number(row.total_tokens) || 0),
|
|
105
121
|
progressRevision: Math.max(0, Number(row.progress_revision) || 0),
|
|
106
122
|
checkpoint: normalizeActionCheckpoint(parseJson(row.checkpoint, null)),
|
|
107
123
|
};
|
|
@@ -220,6 +236,12 @@ export class WorkItemStore {
|
|
|
220
236
|
response TEXT NOT NULL DEFAULT '',
|
|
221
237
|
loop_count INTEGER NOT NULL DEFAULT 0,
|
|
222
238
|
tool_count INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
llm_request_count INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
241
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
242
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
243
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
223
245
|
progress_revision INTEGER NOT NULL DEFAULT 0,
|
|
224
246
|
checkpoint TEXT
|
|
225
247
|
);
|
|
@@ -274,6 +296,18 @@ export class WorkItemStore {
|
|
|
274
296
|
if (!hasColumn(this.db, 'runs', 'tool_count')) {
|
|
275
297
|
this.db.exec('ALTER TABLE runs ADD COLUMN tool_count INTEGER NOT NULL DEFAULT 0');
|
|
276
298
|
}
|
|
299
|
+
for (const column of [
|
|
300
|
+
'llm_request_count',
|
|
301
|
+
'input_tokens',
|
|
302
|
+
'output_tokens',
|
|
303
|
+
'cache_read_tokens',
|
|
304
|
+
'cache_write_tokens',
|
|
305
|
+
'total_tokens',
|
|
306
|
+
]) {
|
|
307
|
+
if (!hasColumn(this.db, 'runs', column)) {
|
|
308
|
+
this.db.exec(`ALTER TABLE runs ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
277
311
|
if (!hasColumn(this.db, 'runs', 'response')) {
|
|
278
312
|
this.db.exec("ALTER TABLE runs ADD COLUMN response TEXT NOT NULL DEFAULT ''");
|
|
279
313
|
}
|
|
@@ -437,22 +471,32 @@ export class WorkItemStore {
|
|
|
437
471
|
const where = [];
|
|
438
472
|
const values = [];
|
|
439
473
|
if (typeof filters.status === 'string' && filters.status) {
|
|
440
|
-
where.push('status = ?');
|
|
474
|
+
where.push('w.status = ?');
|
|
441
475
|
values.push(filters.status);
|
|
442
476
|
}
|
|
443
477
|
if (typeof filters.sessionId === 'string' && filters.sessionId.trim()) {
|
|
444
478
|
const sessionId = filters.sessionId.trim();
|
|
445
|
-
where.push('(instr(origin, ?) > 0 OR instr(linked_session_ids, ?) > 0)');
|
|
479
|
+
where.push('(instr(w.origin, ?) > 0 OR instr(w.linked_session_ids, ?) > 0)');
|
|
446
480
|
values.push(`\"sessionId\":${JSON.stringify(sessionId)}`, JSON.stringify(sessionId));
|
|
447
481
|
}
|
|
448
482
|
if (typeof filters.search === 'string' && filters.search.trim()) {
|
|
449
|
-
where.push('(title LIKE ? OR goal LIKE ?)');
|
|
483
|
+
where.push('(w.title LIKE ? OR w.goal LIKE ?)');
|
|
450
484
|
const query = `%${filters.search.trim()}%`;
|
|
451
485
|
values.push(query, query);
|
|
452
486
|
}
|
|
453
487
|
const limit = Math.min(Math.max(Number(filters.limit) || 100, 1), 500);
|
|
454
|
-
const sql = `SELECT
|
|
455
|
-
|
|
488
|
+
const sql = `SELECT w.*,
|
|
489
|
+
COALESCE(SUM(r.llm_request_count), 0) AS usage_llm_request_count,
|
|
490
|
+
COALESCE(SUM(r.loop_count), 0) AS usage_loop_count,
|
|
491
|
+
COALESCE(SUM(r.tool_count), 0) AS usage_tool_count,
|
|
492
|
+
COALESCE(SUM(r.input_tokens), 0) AS usage_input_tokens,
|
|
493
|
+
COALESCE(SUM(r.output_tokens), 0) AS usage_output_tokens,
|
|
494
|
+
COALESCE(SUM(r.cache_read_tokens), 0) AS usage_cache_read_tokens,
|
|
495
|
+
COALESCE(SUM(r.cache_write_tokens), 0) AS usage_cache_write_tokens,
|
|
496
|
+
COALESCE(SUM(r.total_tokens), 0) AS usage_total_tokens
|
|
497
|
+
FROM work_items w LEFT JOIN runs r ON r.work_item_id = w.id
|
|
498
|
+
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
499
|
+
GROUP BY w.id ORDER BY w.updated_at DESC LIMIT ?`;
|
|
456
500
|
return this.db.prepare(sql).all(...values, limit).map(mapWorkItem);
|
|
457
501
|
}
|
|
458
502
|
|
|
@@ -762,7 +806,8 @@ export class WorkItemStore {
|
|
|
762
806
|
const hasFinalProgress = finalProgress && typeof finalProgress === 'object';
|
|
763
807
|
const runChanged = hasFinalProgress
|
|
764
808
|
? this.db.prepare(`UPDATE runs SET status = 'interrupted', ended_at = ?, error = ?,
|
|
765
|
-
response = ?, loop_count = ?, tool_count = ?,
|
|
809
|
+
response = ?, loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?,
|
|
810
|
+
output_tokens = ?, cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?, checkpoint = ?,
|
|
766
811
|
progress_revision = progress_revision + 1
|
|
767
812
|
WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'`).run(
|
|
768
813
|
now,
|
|
@@ -770,6 +815,12 @@ export class WorkItemStore {
|
|
|
770
815
|
normalizeRunResponse(finalProgress.response),
|
|
771
816
|
Math.max(0, Number(finalProgress.loopCount) || 0),
|
|
772
817
|
Math.max(0, Number(finalProgress.toolCount) || 0),
|
|
818
|
+
Math.max(0, Number(finalProgress.llmRequestCount) || 0),
|
|
819
|
+
Math.max(0, Number(finalProgress.inputTokens) || 0),
|
|
820
|
+
Math.max(0, Number(finalProgress.outputTokens) || 0),
|
|
821
|
+
Math.max(0, Number(finalProgress.cacheReadTokens) || 0),
|
|
822
|
+
Math.max(0, Number(finalProgress.cacheWriteTokens) || 0),
|
|
823
|
+
Math.max(0, Number(finalProgress.totalTokens) || 0),
|
|
773
824
|
stringify(normalizeActionCheckpoint(finalProgress.checkpoint)),
|
|
774
825
|
runId,
|
|
775
826
|
ownerBootId,
|
|
@@ -835,12 +886,19 @@ export class WorkItemStore {
|
|
|
835
886
|
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
836
887
|
if (!active) return null;
|
|
837
888
|
const checkpoint = normalizeActionCheckpoint(progress.checkpoint);
|
|
838
|
-
const result = this.db.prepare(`UPDATE runs SET response = ?, loop_count = ?, tool_count = ?,
|
|
839
|
-
|
|
840
|
-
|
|
889
|
+
const result = this.db.prepare(`UPDATE runs SET response = ?, loop_count = ?, tool_count = ?,
|
|
890
|
+
llm_request_count = ?, input_tokens = ?, output_tokens = ?, cache_read_tokens = ?,
|
|
891
|
+
cache_write_tokens = ?, total_tokens = ?, checkpoint = ?, progress_revision = progress_revision + 1
|
|
892
|
+
WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'`).run(
|
|
841
893
|
normalizeRunResponse(progress.response),
|
|
842
894
|
Math.max(0, Number(progress.loopCount) || 0),
|
|
843
895
|
Math.max(0, Number(progress.toolCount) || 0),
|
|
896
|
+
Math.max(0, Number(progress.llmRequestCount) || 0),
|
|
897
|
+
Math.max(0, Number(progress.inputTokens) || 0),
|
|
898
|
+
Math.max(0, Number(progress.outputTokens) || 0),
|
|
899
|
+
Math.max(0, Number(progress.cacheReadTokens) || 0),
|
|
900
|
+
Math.max(0, Number(progress.cacheWriteTokens) || 0),
|
|
901
|
+
Math.max(0, Number(progress.totalTokens) || 0),
|
|
844
902
|
stringify(checkpoint),
|
|
845
903
|
runId,
|
|
846
904
|
ownerBootId,
|
|
@@ -886,7 +944,9 @@ export class WorkItemStore {
|
|
|
886
944
|
const now = this.now();
|
|
887
945
|
this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, response = ?, summary = ?, evidence = ?,
|
|
888
946
|
waiting_reason = ?, error = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
889
|
-
loop_count = ?, tool_count = ?,
|
|
947
|
+
loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?, output_tokens = ?,
|
|
948
|
+
cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?,
|
|
949
|
+
progress_revision = progress_revision + 1 WHERE id = ?`).run(
|
|
890
950
|
result.outcome,
|
|
891
951
|
now,
|
|
892
952
|
normalizeRunResponse(result.response),
|
|
@@ -899,6 +959,12 @@ export class WorkItemStore {
|
|
|
899
959
|
stringify(normalizeActionCheckpoint(result.checkpoint)),
|
|
900
960
|
Math.max(0, Number(result.loopCount) || 0),
|
|
901
961
|
Math.max(0, Number(result.toolCount) || 0),
|
|
962
|
+
Math.max(0, Number(result.llmRequestCount) || 0),
|
|
963
|
+
Math.max(0, Number(result.inputTokens) || 0),
|
|
964
|
+
Math.max(0, Number(result.outputTokens) || 0),
|
|
965
|
+
Math.max(0, Number(result.cacheReadTokens) || 0),
|
|
966
|
+
Math.max(0, Number(result.cacheWriteTokens) || 0),
|
|
967
|
+
Math.max(0, Number(result.totalTokens) || 0),
|
|
902
968
|
runId,
|
|
903
969
|
);
|
|
904
970
|
this.onTransitionStep?.('after_run_update');
|
|
@@ -36,6 +36,8 @@ export class WorkItemWatcher {
|
|
|
36
36
|
if (this.timer) clearInterval(this.timer);
|
|
37
37
|
this.timer = null;
|
|
38
38
|
const active = Array.from(this.activeRuns.values());
|
|
39
|
+
for (const entry of active) entry.abortController.abort('watcher_stopped');
|
|
40
|
+
await Promise.allSettled(active.map(entry => entry.promise));
|
|
39
41
|
const failures = [];
|
|
40
42
|
for (const entry of active) {
|
|
41
43
|
try {
|
|
@@ -50,11 +52,8 @@ export class WorkItemWatcher {
|
|
|
50
52
|
} catch (error) {
|
|
51
53
|
entry.interrupted = false;
|
|
52
54
|
failures.push(error);
|
|
53
|
-
} finally {
|
|
54
|
-
entry.abortController.abort('watcher_stopped');
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
await Promise.allSettled(active.map(entry => entry.promise));
|
|
58
57
|
if (failures.length > 0) {
|
|
59
58
|
throw new AggregateError(failures, 'Could not persist one or more Work Center interruptions');
|
|
60
59
|
}
|
|
@@ -140,6 +139,12 @@ export class WorkItemWatcher {
|
|
|
140
139
|
error: err?.message || String(err),
|
|
141
140
|
loopCount: err?.workItemExecutionStats?.loopCount || 0,
|
|
142
141
|
toolCount: err?.workItemExecutionStats?.toolCount || 0,
|
|
142
|
+
llmRequestCount: err?.workItemExecutionStats?.llmRequestCount || 0,
|
|
143
|
+
inputTokens: err?.workItemExecutionStats?.inputTokens || 0,
|
|
144
|
+
outputTokens: err?.workItemExecutionStats?.outputTokens || 0,
|
|
145
|
+
cacheReadTokens: err?.workItemExecutionStats?.cacheReadTokens || 0,
|
|
146
|
+
cacheWriteTokens: err?.workItemExecutionStats?.cacheWriteTokens || 0,
|
|
147
|
+
totalTokens: err?.workItemExecutionStats?.totalTokens || 0,
|
|
143
148
|
checkpoint: err?.workItemExecutionStats?.checkpoint || null,
|
|
144
149
|
};
|
|
145
150
|
}
|