blun-king-cli 9.1.465 → 9.1.466
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { createAgentApiRuntime } from './agent-api-runtime.mjs';
|
|
2
|
+
import usageJournalModule from './agent-api-usage-journal.cjs';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createAgentApiTaskId,
|
|
6
|
+
createAgentApiUsageJournal,
|
|
7
|
+
} = usageJournalModule;
|
|
2
8
|
|
|
3
9
|
const MAIN_AGENT_ID = 'main';
|
|
4
10
|
const PUBLIC_MODEL_ID = 'king';
|
|
@@ -116,6 +122,7 @@ function openAiEnvelope(turn, message, finishReason) {
|
|
|
116
122
|
}],
|
|
117
123
|
blun: {
|
|
118
124
|
mode: 'agent',
|
|
125
|
+
task_id: turn.taskId,
|
|
119
126
|
session_id: turn.sessionId,
|
|
120
127
|
request_id: turn.requestId,
|
|
121
128
|
},
|
|
@@ -149,6 +156,8 @@ function completedResponse(turn, event) {
|
|
|
149
156
|
|
|
150
157
|
function createAgentApiTurnController(options = {}) {
|
|
151
158
|
const runtime = options.runtime ?? createAgentApiRuntime(options.runtimeOptions);
|
|
159
|
+
const usageJournal = options.usageJournal ?? createAgentApiUsageJournal();
|
|
160
|
+
const taskIdFactory = options.taskIdFactory ?? createAgentApiTaskId;
|
|
152
161
|
const publicModel = options.publicModel ?? PUBLIC_MODEL_ID;
|
|
153
162
|
const now = options.now ?? Date.now;
|
|
154
163
|
const activeTurns = new Map();
|
|
@@ -172,7 +181,7 @@ function createAgentApiTurnController(options = {}) {
|
|
|
172
181
|
return turn;
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
async function closeBinding(turn, kind, reason) {
|
|
184
|
+
async function closeBinding(turn, kind, reason, status = 'failed') {
|
|
176
185
|
if (turn.closed) return;
|
|
177
186
|
turn.closed = true;
|
|
178
187
|
activeTurns.delete(turn.sessionId);
|
|
@@ -182,6 +191,12 @@ function createAgentApiTurnController(options = {}) {
|
|
|
182
191
|
if (kind === 'finish') {
|
|
183
192
|
try {
|
|
184
193
|
await turn.binding.finish();
|
|
194
|
+
usageJournal.finishTask({
|
|
195
|
+
taskId: turn.taskId,
|
|
196
|
+
requestId: turn.requestId,
|
|
197
|
+
sessionId: turn.sessionId,
|
|
198
|
+
status,
|
|
199
|
+
});
|
|
185
200
|
return;
|
|
186
201
|
} catch (error) {
|
|
187
202
|
try {
|
|
@@ -189,10 +204,38 @@ function createAgentApiTurnController(options = {}) {
|
|
|
189
204
|
} catch {
|
|
190
205
|
// Preserve the original finish error.
|
|
191
206
|
}
|
|
207
|
+
try {
|
|
208
|
+
usageJournal.finishTask({
|
|
209
|
+
taskId: turn.taskId,
|
|
210
|
+
requestId: turn.requestId,
|
|
211
|
+
sessionId: turn.sessionId,
|
|
212
|
+
status: 'failed',
|
|
213
|
+
});
|
|
214
|
+
} catch {
|
|
215
|
+
// Preserve the original finish error.
|
|
216
|
+
}
|
|
192
217
|
throw error;
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
|
-
|
|
220
|
+
let abortError;
|
|
221
|
+
try {
|
|
222
|
+
await turn.binding.abort(reason ?? 'Agent API turn aborted');
|
|
223
|
+
} catch (error) {
|
|
224
|
+
abortError = error;
|
|
225
|
+
}
|
|
226
|
+
let journalError;
|
|
227
|
+
try {
|
|
228
|
+
usageJournal.finishTask({
|
|
229
|
+
taskId: turn.taskId,
|
|
230
|
+
requestId: turn.requestId,
|
|
231
|
+
sessionId: turn.sessionId,
|
|
232
|
+
status,
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
journalError = error;
|
|
236
|
+
}
|
|
237
|
+
if (abortError !== undefined) throw abortError;
|
|
238
|
+
if (journalError !== undefined) throw journalError;
|
|
196
239
|
}
|
|
197
240
|
|
|
198
241
|
function settleTerminal(turn, event) {
|
|
@@ -223,6 +266,23 @@ function createAgentApiTurnController(options = {}) {
|
|
|
223
266
|
}
|
|
224
267
|
if (turn.turnId === undefined || event.turnId !== turn.turnId) return;
|
|
225
268
|
switch (event.type) {
|
|
269
|
+
case 'turn.step.completed':
|
|
270
|
+
try {
|
|
271
|
+
usageJournal.recordModelCall({
|
|
272
|
+
taskId: turn.taskId,
|
|
273
|
+
requestId: turn.requestId,
|
|
274
|
+
sessionId: turn.sessionId,
|
|
275
|
+
callId: event.stepId ?? `${turn.turnId}:${event.step}`,
|
|
276
|
+
usage: event.usage ?? null,
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
settleTerminal(turn, {
|
|
280
|
+
type: 'turn.error',
|
|
281
|
+
reason: 'failed',
|
|
282
|
+
error,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
226
286
|
case 'turn.step.retrying':
|
|
227
287
|
turn.assistantText = '';
|
|
228
288
|
return;
|
|
@@ -268,7 +328,7 @@ function createAgentApiTurnController(options = {}) {
|
|
|
268
328
|
}
|
|
269
329
|
|
|
270
330
|
const response = completedResponse(turn, outcome.event);
|
|
271
|
-
await closeBinding(turn, 'finish');
|
|
331
|
+
await closeBinding(turn, 'finish', undefined, 'completed');
|
|
272
332
|
return response;
|
|
273
333
|
}
|
|
274
334
|
|
|
@@ -293,6 +353,7 @@ function createAgentApiTurnController(options = {}) {
|
|
|
293
353
|
requestId,
|
|
294
354
|
tools: input.tools ?? [],
|
|
295
355
|
});
|
|
356
|
+
const taskId = requireId(taskIdFactory(sessionId, requestId), 'taskId');
|
|
296
357
|
const turn = {
|
|
297
358
|
assistantText: '',
|
|
298
359
|
binding,
|
|
@@ -302,11 +363,22 @@ function createAgentApiTurnController(options = {}) {
|
|
|
302
363
|
requestId,
|
|
303
364
|
session: input.session,
|
|
304
365
|
sessionId,
|
|
366
|
+
taskId,
|
|
305
367
|
terminal: createDeferred(),
|
|
306
368
|
terminalSettled: false,
|
|
307
369
|
turnId: undefined,
|
|
308
370
|
unsubscribe: undefined,
|
|
309
371
|
};
|
|
372
|
+
try {
|
|
373
|
+
usageJournal.startTask({ taskId, requestId, sessionId });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
try {
|
|
376
|
+
await binding.abort('Agent API usage task could not be started');
|
|
377
|
+
} catch {
|
|
378
|
+
// Preserve the journal error.
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
310
382
|
activeTurns.set(sessionId, turn);
|
|
311
383
|
turn.unsubscribe = installEventListener(turn);
|
|
312
384
|
|
|
@@ -362,7 +434,7 @@ function createAgentApiTurnController(options = {}) {
|
|
|
362
434
|
: 'Agent API turn cancelled';
|
|
363
435
|
const outcomes = await Promise.allSettled([
|
|
364
436
|
turn.session.cancel(),
|
|
365
|
-
closeBinding(turn, 'abort', reason),
|
|
437
|
+
closeBinding(turn, 'abort', reason, 'cancelled'),
|
|
366
438
|
]);
|
|
367
439
|
const failure = outcomes.find((outcome) => outcome.status === 'rejected');
|
|
368
440
|
if (failure !== undefined) throw failure.reason;
|
|
@@ -373,6 +445,7 @@ function createAgentApiTurnController(options = {}) {
|
|
|
373
445
|
activeTurnCount: () => activeTurns.size,
|
|
374
446
|
cancelTurn,
|
|
375
447
|
continueTurn,
|
|
448
|
+
getUsageTask: (taskId) => usageJournal.getTask(taskId),
|
|
376
449
|
startTurn,
|
|
377
450
|
});
|
|
378
451
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const PUBLIC_MODEL_ID = 'king';
|
|
8
|
+
const JOURNAL_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
class AgentApiUsageJournalError extends Error {
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'AgentApiUsageJournalError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function journalError(code, message) {
|
|
19
|
+
return new AgentApiUsageJournalError(code, message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireId(value, label) {
|
|
23
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
24
|
+
throw journalError('AGENT_API_USAGE_INVALID_SCOPE', `${label} must be a non-empty string`);
|
|
25
|
+
}
|
|
26
|
+
return value.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tokenCount(value) {
|
|
30
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
31
|
+
throw journalError(
|
|
32
|
+
'AGENT_API_USAGE_INVALID_VALUE',
|
|
33
|
+
'usage token counters must be finite non-negative numbers',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return Math.trunc(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeRawUsage(usage) {
|
|
40
|
+
if (usage === undefined || usage === null) return null;
|
|
41
|
+
if (typeof usage !== 'object' || Array.isArray(usage)) {
|
|
42
|
+
throw journalError('AGENT_API_USAGE_INVALID_VALUE', 'usage must be an object or null');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Object.hasOwn(usage, 'prompt_tokens') || Object.hasOwn(usage, 'completion_tokens')) {
|
|
46
|
+
const promptTokens = tokenCount(usage.prompt_tokens ?? 0);
|
|
47
|
+
const completionTokens = tokenCount(usage.completion_tokens ?? 0);
|
|
48
|
+
const cachedTokens = Math.min(
|
|
49
|
+
promptTokens,
|
|
50
|
+
tokenCount(usage.prompt_tokens_details?.cached_tokens ?? 0),
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
prompt_tokens: promptTokens,
|
|
54
|
+
completion_tokens: completionTokens,
|
|
55
|
+
prompt_tokens_details: { cached_tokens: cachedTokens },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const inputOther = tokenCount(usage.inputOther ?? 0);
|
|
60
|
+
const inputCacheRead = tokenCount(usage.inputCacheRead ?? 0);
|
|
61
|
+
const inputCacheCreation = tokenCount(usage.inputCacheCreation ?? 0);
|
|
62
|
+
const output = tokenCount(usage.output ?? 0);
|
|
63
|
+
return {
|
|
64
|
+
prompt_tokens: inputOther + inputCacheRead + inputCacheCreation,
|
|
65
|
+
completion_tokens: output,
|
|
66
|
+
prompt_tokens_details: { cached_tokens: inputCacheRead },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createAgentApiTaskId(sessionId, requestId) {
|
|
71
|
+
const session = requireId(sessionId, 'sessionId');
|
|
72
|
+
const request = requireId(requestId, 'requestId');
|
|
73
|
+
const digest = crypto.createHash('sha256').update(`${session}\0${request}`).digest('hex');
|
|
74
|
+
return `task-${digest.slice(0, 24)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clone(value) {
|
|
78
|
+
return structuredClone(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeTimestamp(value, label) {
|
|
82
|
+
const timestamp = typeof value === 'string' ? value.trim() : '';
|
|
83
|
+
if (timestamp.length === 0 || Number.isNaN(Date.parse(timestamp))) {
|
|
84
|
+
throw journalError('AGENT_API_USAGE_INVALID_TIME', `${label} must be an ISO timestamp`);
|
|
85
|
+
}
|
|
86
|
+
return timestamp;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createAgentApiUsageJournal(options = {}) {
|
|
90
|
+
const filePath = options.filePath === undefined
|
|
91
|
+
? null
|
|
92
|
+
: path.resolve(String(options.filePath));
|
|
93
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
94
|
+
const tasks = new Map();
|
|
95
|
+
|
|
96
|
+
function taskForScope(input) {
|
|
97
|
+
const taskId = requireId(input.taskId, 'taskId');
|
|
98
|
+
const requestId = requireId(input.requestId, 'requestId');
|
|
99
|
+
const sessionId = requireId(input.sessionId, 'sessionId');
|
|
100
|
+
const task = tasks.get(taskId);
|
|
101
|
+
if (task === undefined) {
|
|
102
|
+
throw journalError('AGENT_API_USAGE_TASK_NOT_FOUND', 'usage task does not exist');
|
|
103
|
+
}
|
|
104
|
+
if (task.request_id !== requestId || task.session_id !== sessionId) {
|
|
105
|
+
throw journalError(
|
|
106
|
+
'AGENT_API_USAGE_SCOPE_MISMATCH',
|
|
107
|
+
'usage event does not belong to this task',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return task;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function appendEvent(event) {
|
|
114
|
+
if (filePath === null) return;
|
|
115
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
116
|
+
fs.appendFileSync(filePath, `${JSON.stringify(event)}\n`, {
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
mode: 0o600,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function replay(event) {
|
|
123
|
+
if (event === null || typeof event !== 'object' || event.version !== JOURNAL_VERSION) {
|
|
124
|
+
throw journalError('AGENT_API_USAGE_JOURNAL_CORRUPT', 'invalid usage journal event');
|
|
125
|
+
}
|
|
126
|
+
if (event.type === 'task.started') {
|
|
127
|
+
if (tasks.has(event.task_id)) {
|
|
128
|
+
throw journalError('AGENT_API_USAGE_JOURNAL_CORRUPT', 'duplicate task start');
|
|
129
|
+
}
|
|
130
|
+
tasks.set(event.task_id, {
|
|
131
|
+
version: JOURNAL_VERSION,
|
|
132
|
+
task_id: event.task_id,
|
|
133
|
+
request_id: event.request_id,
|
|
134
|
+
session_id: event.session_id,
|
|
135
|
+
model: PUBLIC_MODEL_ID,
|
|
136
|
+
status: 'active',
|
|
137
|
+
started_at: event.at,
|
|
138
|
+
finished_at: null,
|
|
139
|
+
calls: [],
|
|
140
|
+
callIds: new Set(),
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const task = tasks.get(event.task_id);
|
|
145
|
+
if (task === undefined) {
|
|
146
|
+
throw journalError('AGENT_API_USAGE_JOURNAL_CORRUPT', 'event references unknown task');
|
|
147
|
+
}
|
|
148
|
+
if (event.type === 'model.usage') {
|
|
149
|
+
if (task.callIds.has(event.call_id)) return;
|
|
150
|
+
task.callIds.add(event.call_id);
|
|
151
|
+
task.calls.push({
|
|
152
|
+
call_id: event.call_id,
|
|
153
|
+
sequence: event.sequence,
|
|
154
|
+
recorded_at: event.at,
|
|
155
|
+
usage: event.usage,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (event.type === 'task.finished') {
|
|
160
|
+
task.status = event.status;
|
|
161
|
+
task.finished_at = event.at;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
throw journalError('AGENT_API_USAGE_JOURNAL_CORRUPT', 'unknown usage journal event');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function load() {
|
|
168
|
+
if (filePath === null || !fs.existsSync(filePath)) return;
|
|
169
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
170
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
171
|
+
if (line.length === 0) continue;
|
|
172
|
+
let event;
|
|
173
|
+
try {
|
|
174
|
+
event = JSON.parse(line);
|
|
175
|
+
} catch {
|
|
176
|
+
throw journalError('AGENT_API_USAGE_JOURNAL_CORRUPT', 'invalid JSONL record');
|
|
177
|
+
}
|
|
178
|
+
replay(event);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function startTask(input) {
|
|
183
|
+
const taskId = requireId(input.taskId, 'taskId');
|
|
184
|
+
const requestId = requireId(input.requestId, 'requestId');
|
|
185
|
+
const sessionId = requireId(input.sessionId, 'sessionId');
|
|
186
|
+
if (tasks.has(taskId)) {
|
|
187
|
+
throw journalError('AGENT_API_USAGE_TASK_EXISTS', 'usage task already exists');
|
|
188
|
+
}
|
|
189
|
+
const event = {
|
|
190
|
+
version: JOURNAL_VERSION,
|
|
191
|
+
type: 'task.started',
|
|
192
|
+
task_id: taskId,
|
|
193
|
+
request_id: requestId,
|
|
194
|
+
session_id: sessionId,
|
|
195
|
+
at: normalizeTimestamp(input.at ?? now(), 'task start time'),
|
|
196
|
+
};
|
|
197
|
+
appendEvent(event);
|
|
198
|
+
replay(event);
|
|
199
|
+
return taskId;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function recordModelCall(input) {
|
|
203
|
+
const task = taskForScope(input);
|
|
204
|
+
if (task.status !== 'active') {
|
|
205
|
+
throw journalError('AGENT_API_USAGE_TASK_CLOSED', 'usage task is already closed');
|
|
206
|
+
}
|
|
207
|
+
const callId = requireId(input.callId, 'callId');
|
|
208
|
+
if (task.callIds.has(callId)) return false;
|
|
209
|
+
const event = {
|
|
210
|
+
version: JOURNAL_VERSION,
|
|
211
|
+
type: 'model.usage',
|
|
212
|
+
task_id: task.task_id,
|
|
213
|
+
call_id: callId,
|
|
214
|
+
sequence: task.calls.length + 1,
|
|
215
|
+
usage: normalizeRawUsage(input.usage),
|
|
216
|
+
at: normalizeTimestamp(input.at ?? now(), 'model call time'),
|
|
217
|
+
};
|
|
218
|
+
appendEvent(event);
|
|
219
|
+
replay(event);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function finishTask(input) {
|
|
224
|
+
const task = taskForScope(input);
|
|
225
|
+
const status = requireId(input.status, 'status');
|
|
226
|
+
if (!['completed', 'cancelled', 'failed'].includes(status)) {
|
|
227
|
+
throw journalError('AGENT_API_USAGE_INVALID_STATUS', 'invalid usage task status');
|
|
228
|
+
}
|
|
229
|
+
if (task.status !== 'active') return false;
|
|
230
|
+
const event = {
|
|
231
|
+
version: JOURNAL_VERSION,
|
|
232
|
+
type: 'task.finished',
|
|
233
|
+
task_id: task.task_id,
|
|
234
|
+
status,
|
|
235
|
+
at: normalizeTimestamp(input.at ?? now(), 'task finish time'),
|
|
236
|
+
};
|
|
237
|
+
appendEvent(event);
|
|
238
|
+
replay(event);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getTask(taskId) {
|
|
243
|
+
const task = tasks.get(requireId(taskId, 'taskId'));
|
|
244
|
+
if (task === undefined) return null;
|
|
245
|
+
const { callIds: _callIds, ...snapshot } = task;
|
|
246
|
+
return clone(snapshot);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
load();
|
|
250
|
+
return Object.freeze({ finishTask, getTask, recordModelCall, startTask });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
AgentApiUsageJournalError,
|
|
255
|
+
JOURNAL_VERSION,
|
|
256
|
+
createAgentApiTaskId,
|
|
257
|
+
createAgentApiUsageJournal,
|
|
258
|
+
normalizeRawUsage,
|
|
259
|
+
};
|