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
- await turn.binding.abort(reason ?? 'Agent API turn aborted');
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.465",
3
+ "version": "9.1.466",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {