blun-king-cli 9.1.464 → 9.1.465

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.
@@ -0,0 +1,384 @@
1
+ import { createAgentApiRuntime } from './agent-api-runtime.mjs';
2
+
3
+ const MAIN_AGENT_ID = 'main';
4
+ const PUBLIC_MODEL_ID = 'king';
5
+
6
+ class AgentApiTurnControllerError extends Error {
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = 'AgentApiTurnControllerError';
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ function controllerError(code, message) {
15
+ return new AgentApiTurnControllerError(code, message);
16
+ }
17
+
18
+ function requireId(value, label) {
19
+ if (typeof value !== 'string' || value.trim().length === 0) {
20
+ throw controllerError('AGENT_API_INVALID_SCOPE', `${label} must be a non-empty string`);
21
+ }
22
+ return value.trim();
23
+ }
24
+
25
+ function validateSession(session) {
26
+ if (session === null || typeof session !== 'object') {
27
+ throw controllerError('AGENT_API_INVALID_SESSION', 'session must be an SDK session');
28
+ }
29
+ for (const method of ['onEvent', 'cancel']) {
30
+ if (typeof session[method] !== 'function') {
31
+ throw controllerError(
32
+ 'AGENT_API_INVALID_SESSION',
33
+ `session.${method} must be a function`,
34
+ );
35
+ }
36
+ }
37
+ if (typeof session.promptAccepted !== 'function' && typeof session.prompt !== 'function') {
38
+ throw controllerError(
39
+ 'AGENT_API_INVALID_SESSION',
40
+ 'session.promptAccepted or session.prompt must be a function',
41
+ );
42
+ }
43
+ return requireId(session.id, 'session.id');
44
+ }
45
+
46
+ function createDeferred() {
47
+ let resolve;
48
+ let reject;
49
+ const promise = new Promise((resolvePromise, rejectPromise) => {
50
+ resolve = resolvePromise;
51
+ reject = rejectPromise;
52
+ });
53
+ return { promise, reject, resolve };
54
+ }
55
+
56
+ function isMainAgentEvent(event) {
57
+ return event.agentId === undefined || event.agentId === MAIN_AGENT_ID;
58
+ }
59
+
60
+ function normalizeToolResults(value) {
61
+ if (!Array.isArray(value) || value.length === 0) {
62
+ throw controllerError(
63
+ 'AGENT_API_INVALID_TOOL_RESULTS',
64
+ 'toolResults must be a non-empty array',
65
+ );
66
+ }
67
+ return value.map((result, index) => {
68
+ if (result === null || typeof result !== 'object' || Array.isArray(result)) {
69
+ throw controllerError(
70
+ 'AGENT_API_INVALID_TOOL_RESULT',
71
+ `toolResults[${index}] must be an object`,
72
+ );
73
+ }
74
+ const toolCallId = requireId(result.tool_call_id, `toolResults[${index}].tool_call_id`);
75
+ if (typeof result.output !== 'string') {
76
+ throw controllerError(
77
+ 'AGENT_API_INVALID_TOOL_RESULT',
78
+ `toolResults[${index}].output must be a string`,
79
+ );
80
+ }
81
+ if (result.is_error !== undefined && typeof result.is_error !== 'boolean') {
82
+ throw controllerError(
83
+ 'AGENT_API_INVALID_TOOL_RESULT',
84
+ `toolResults[${index}].is_error must be a boolean`,
85
+ );
86
+ }
87
+ return {
88
+ toolCallId,
89
+ output: result.output,
90
+ isError: result.is_error === true,
91
+ };
92
+ });
93
+ }
94
+
95
+ function serializeToolArgs(args) {
96
+ try {
97
+ return JSON.stringify(args ?? {});
98
+ } catch {
99
+ throw controllerError(
100
+ 'AGENT_API_INVALID_TOOL_ARGS',
101
+ 'client tool arguments must be JSON-serializable',
102
+ );
103
+ }
104
+ }
105
+
106
+ function openAiEnvelope(turn, message, finishReason) {
107
+ return {
108
+ id: `agent-${turn.requestId}`,
109
+ object: 'chat.completion',
110
+ created: turn.created,
111
+ model: turn.publicModel,
112
+ choices: [{
113
+ index: 0,
114
+ message,
115
+ finish_reason: finishReason,
116
+ }],
117
+ blun: {
118
+ mode: 'agent',
119
+ session_id: turn.sessionId,
120
+ request_id: turn.requestId,
121
+ },
122
+ };
123
+ }
124
+
125
+ function toolCallResponse(turn, event) {
126
+ const content = turn.assistantText.length > 0 ? turn.assistantText : null;
127
+ turn.assistantText = '';
128
+ return openAiEnvelope(turn, {
129
+ role: 'assistant',
130
+ content,
131
+ tool_calls: [{
132
+ id: event.toolCallId,
133
+ type: 'function',
134
+ function: {
135
+ name: event.toolName,
136
+ arguments: serializeToolArgs(event.args),
137
+ },
138
+ }],
139
+ }, 'tool_calls');
140
+ }
141
+
142
+ function completedResponse(turn, event) {
143
+ const reason = event.reason === 'completed' ? 'stop' : event.reason;
144
+ return openAiEnvelope(turn, {
145
+ role: 'assistant',
146
+ content: turn.assistantText,
147
+ }, reason);
148
+ }
149
+
150
+ function createAgentApiTurnController(options = {}) {
151
+ const runtime = options.runtime ?? createAgentApiRuntime(options.runtimeOptions);
152
+ const publicModel = options.publicModel ?? PUBLIC_MODEL_ID;
153
+ const now = options.now ?? Date.now;
154
+ const activeTurns = new Map();
155
+
156
+ function turnForScope(input) {
157
+ const sessionId = requireId(input.sessionId, 'sessionId');
158
+ const requestId = requireId(input.requestId, 'requestId');
159
+ const turn = activeTurns.get(sessionId);
160
+ if (turn === undefined) {
161
+ throw controllerError(
162
+ 'AGENT_API_TURN_NOT_ACTIVE',
163
+ 'no active Agent API turn for session',
164
+ );
165
+ }
166
+ if (turn.requestId !== requestId) {
167
+ throw controllerError(
168
+ 'AGENT_API_TURN_SCOPE_MISMATCH',
169
+ 'request does not own the active Agent API turn',
170
+ );
171
+ }
172
+ return turn;
173
+ }
174
+
175
+ async function closeBinding(turn, kind, reason) {
176
+ if (turn.closed) return;
177
+ turn.closed = true;
178
+ activeTurns.delete(turn.sessionId);
179
+ turn.unsubscribe?.();
180
+ turn.unsubscribe = undefined;
181
+
182
+ if (kind === 'finish') {
183
+ try {
184
+ await turn.binding.finish();
185
+ return;
186
+ } catch (error) {
187
+ try {
188
+ await turn.binding.abort(reason ?? 'Agent API turn cleanup failed');
189
+ } catch {
190
+ // Preserve the original finish error.
191
+ }
192
+ throw error;
193
+ }
194
+ }
195
+ await turn.binding.abort(reason ?? 'Agent API turn aborted');
196
+ }
197
+
198
+ function settleTerminal(turn, event) {
199
+ if (turn.terminalSettled) return;
200
+ turn.terminalSettled = true;
201
+ turn.terminal.resolve(event);
202
+ }
203
+
204
+ function installEventListener(turn) {
205
+ return turn.session.onEvent((event) => {
206
+ if (!isMainAgentEvent(event)) return;
207
+ if (event.type === 'turn.started') {
208
+ if (turn.turnId === undefined) turn.turnId = event.turnId;
209
+ return;
210
+ }
211
+ if (event.type === 'error') {
212
+ if (turn.turnId !== undefined && event.turnId !== undefined
213
+ && event.turnId !== turn.turnId) return;
214
+ settleTerminal(turn, {
215
+ type: 'turn.error',
216
+ reason: 'failed',
217
+ error: controllerError(
218
+ event.code ?? 'AGENT_API_TURN_FAILED',
219
+ event.message ?? 'Agent API turn failed',
220
+ ),
221
+ });
222
+ return;
223
+ }
224
+ if (turn.turnId === undefined || event.turnId !== turn.turnId) return;
225
+ switch (event.type) {
226
+ case 'turn.step.retrying':
227
+ turn.assistantText = '';
228
+ return;
229
+ case 'assistant.delta':
230
+ turn.assistantText += event.delta;
231
+ return;
232
+ case 'turn.ended':
233
+ settleTerminal(turn, event);
234
+ return;
235
+ default:
236
+ }
237
+ });
238
+ }
239
+
240
+ async function waitForOutput(turn) {
241
+ if (turn.closed) {
242
+ throw controllerError('AGENT_API_TURN_NOT_ACTIVE', 'Agent API turn is closed');
243
+ }
244
+
245
+ const tool = turn.binding.nextToolCall().then(
246
+ (event) => ({ kind: 'tool', event }),
247
+ (error) => ({ kind: 'tool_error', error }),
248
+ );
249
+ const terminal = turn.terminal.promise.then((event) => ({ kind: 'terminal', event }));
250
+ const outcome = await Promise.race([tool, terminal]);
251
+
252
+ if (outcome.kind === 'tool') return toolCallResponse(turn, outcome.event);
253
+ if (outcome.kind === 'tool_error') {
254
+ if (turn.closed) {
255
+ throw controllerError('AGENT_API_TURN_NOT_ACTIVE', 'Agent API turn is closed');
256
+ }
257
+ await closeBinding(turn, 'abort', 'Agent API tool bridge failed');
258
+ throw outcome.error;
259
+ }
260
+
261
+ if (outcome.event.type === 'turn.error') {
262
+ await closeBinding(turn, 'abort', 'Agent API turn failed');
263
+ throw outcome.event.error;
264
+ }
265
+ if (outcome.event.reason === 'failed') {
266
+ await closeBinding(turn, 'abort', 'Agent API turn failed');
267
+ throw controllerError('AGENT_API_TURN_FAILED', 'Agent API turn failed');
268
+ }
269
+
270
+ const response = completedResponse(turn, outcome.event);
271
+ await closeBinding(turn, 'finish');
272
+ return response;
273
+ }
274
+
275
+ async function startTurn(input) {
276
+ if (input === null || typeof input !== 'object') {
277
+ throw controllerError('AGENT_API_INVALID_REQUEST', 'turn input must be an object');
278
+ }
279
+ const sessionId = validateSession(input.session);
280
+ const requestId = requireId(input.requestId, 'requestId');
281
+ if (activeTurns.has(sessionId)) {
282
+ throw controllerError(
283
+ 'AGENT_API_SESSION_BUSY',
284
+ 'session already has an active Agent API turn',
285
+ );
286
+ }
287
+ if (input.prompt === undefined || input.prompt === null) {
288
+ throw controllerError('AGENT_API_INVALID_PROMPT', 'prompt is required');
289
+ }
290
+
291
+ const binding = await runtime.bindClientTools({
292
+ session: input.session,
293
+ requestId,
294
+ tools: input.tools ?? [],
295
+ });
296
+ const turn = {
297
+ assistantText: '',
298
+ binding,
299
+ closed: false,
300
+ created: Math.floor(Number(now()) / 1000),
301
+ publicModel,
302
+ requestId,
303
+ session: input.session,
304
+ sessionId,
305
+ terminal: createDeferred(),
306
+ terminalSettled: false,
307
+ turnId: undefined,
308
+ unsubscribe: undefined,
309
+ };
310
+ activeTurns.set(sessionId, turn);
311
+ turn.unsubscribe = installEventListener(turn);
312
+
313
+ try {
314
+ if (typeof input.session.promptAccepted === 'function') {
315
+ const accepted = await input.session.promptAccepted(input.prompt);
316
+ if (accepted?.accepted === false || accepted?.buffered === true) {
317
+ await closeBinding(turn, 'abort', 'Agent API session is busy');
318
+ throw controllerError(
319
+ 'AGENT_API_SESSION_BUSY',
320
+ 'Agent API prompt was not accepted as a new active turn',
321
+ );
322
+ }
323
+ if (Number.isSafeInteger(accepted?.turnId)) {
324
+ if (turn.turnId !== undefined && turn.turnId !== accepted.turnId) {
325
+ await closeBinding(turn, 'abort', 'Agent API turn identity mismatch');
326
+ throw controllerError(
327
+ 'AGENT_API_TURN_ID_MISMATCH',
328
+ 'accepted turn ID does not match the emitted turn',
329
+ );
330
+ }
331
+ turn.turnId = accepted.turnId;
332
+ }
333
+ } else {
334
+ await input.session.prompt(input.prompt);
335
+ }
336
+ } catch (error) {
337
+ if (!turn.closed) await closeBinding(turn, 'abort', 'Agent API prompt failed');
338
+ throw error;
339
+ }
340
+
341
+ return waitForOutput(turn);
342
+ }
343
+
344
+ async function continueTurn(input) {
345
+ if (input === null || typeof input !== 'object') {
346
+ throw controllerError('AGENT_API_INVALID_REQUEST', 'continuation input must be an object');
347
+ }
348
+ const turn = turnForScope(input);
349
+ for (const result of normalizeToolResults(input.toolResults)) {
350
+ turn.binding.submitToolResult(result);
351
+ }
352
+ return waitForOutput(turn);
353
+ }
354
+
355
+ async function cancelTurn(input) {
356
+ if (input === null || typeof input !== 'object') {
357
+ throw controllerError('AGENT_API_INVALID_REQUEST', 'cancel input must be an object');
358
+ }
359
+ const turn = turnForScope(input);
360
+ const reason = typeof input.reason === 'string' && input.reason.trim().length > 0
361
+ ? input.reason.trim()
362
+ : 'Agent API turn cancelled';
363
+ const outcomes = await Promise.allSettled([
364
+ turn.session.cancel(),
365
+ closeBinding(turn, 'abort', reason),
366
+ ]);
367
+ const failure = outcomes.find((outcome) => outcome.status === 'rejected');
368
+ if (failure !== undefined) throw failure.reason;
369
+ return true;
370
+ }
371
+
372
+ return Object.freeze({
373
+ activeTurnCount: () => activeTurns.size,
374
+ cancelTurn,
375
+ continueTurn,
376
+ startTurn,
377
+ });
378
+ }
379
+
380
+ export {
381
+ AgentApiTurnControllerError,
382
+ PUBLIC_MODEL_ID,
383
+ createAgentApiTurnController,
384
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.464",
3
+ "version": "9.1.465",
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": {