@agi-cli/server 0.1.86 → 0.1.87
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 +3 -3
- package/src/events/types.ts +1 -0
- package/src/runtime/db-operations.ts +7 -3
- package/src/runtime/history-builder.ts +9 -0
- package/src/runtime/message-service.ts +15 -26
- package/src/runtime/runner.ts +202 -79
- package/src/runtime/session-queue.ts +0 -1
- package/src/runtime/stream-handlers.ts +13 -25
- package/src/runtime/tool-context-setup.ts +6 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"description": "HTTP API server for AGI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agi-cli/sdk": "0.1.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
32
|
+
"@agi-cli/sdk": "0.1.87",
|
|
33
|
+
"@agi-cli/database": "0.1.87",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/events/types.ts
CHANGED
|
@@ -28,7 +28,7 @@ export async function updateSessionTokensIncremental(
|
|
|
28
28
|
opts: RunOpts,
|
|
29
29
|
db: Awaited<ReturnType<typeof getDb>>,
|
|
30
30
|
) {
|
|
31
|
-
if (!usage) return;
|
|
31
|
+
if (!usage || !db) return;
|
|
32
32
|
|
|
33
33
|
// Read session totals
|
|
34
34
|
const sessRows = await db
|
|
@@ -106,7 +106,7 @@ export async function updateSessionTokens(
|
|
|
106
106
|
opts: RunOpts,
|
|
107
107
|
db: Awaited<ReturnType<typeof getDb>>,
|
|
108
108
|
) {
|
|
109
|
-
if (!fin.usage) return;
|
|
109
|
+
if (!fin.usage || !db) return;
|
|
110
110
|
|
|
111
111
|
const sessRows = await db
|
|
112
112
|
.select()
|
|
@@ -140,7 +140,7 @@ export async function updateMessageTokensIncremental(
|
|
|
140
140
|
opts: RunOpts,
|
|
141
141
|
db: Awaited<ReturnType<typeof getDb>>,
|
|
142
142
|
) {
|
|
143
|
-
if (!usage) return;
|
|
143
|
+
if (!usage || !db) return;
|
|
144
144
|
|
|
145
145
|
const msgRows = await db
|
|
146
146
|
.select()
|
|
@@ -204,6 +204,8 @@ export async function completeAssistantMessage(
|
|
|
204
204
|
opts: RunOpts,
|
|
205
205
|
db: Awaited<ReturnType<typeof getDb>>,
|
|
206
206
|
) {
|
|
207
|
+
if (!db) return;
|
|
208
|
+
|
|
207
209
|
// Only mark as complete - tokens are already tracked incrementally
|
|
208
210
|
await db
|
|
209
211
|
.update(messages)
|
|
@@ -221,6 +223,8 @@ export async function cleanupEmptyTextParts(
|
|
|
221
223
|
opts: RunOpts,
|
|
222
224
|
db: Awaited<ReturnType<typeof getDb>>,
|
|
223
225
|
) {
|
|
226
|
+
if (!db) return;
|
|
227
|
+
|
|
224
228
|
const parts = await db
|
|
225
229
|
.select()
|
|
226
230
|
.from(messageParts)
|
|
@@ -21,6 +21,13 @@ export async function buildHistoryMessages(
|
|
|
21
21
|
const ui: UIMessage[] = [];
|
|
22
22
|
|
|
23
23
|
for (const m of rows) {
|
|
24
|
+
if (m.role === 'assistant' && m.status !== 'complete') {
|
|
25
|
+
debugLog(
|
|
26
|
+
`[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status}`,
|
|
27
|
+
);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
const parts = await db
|
|
25
32
|
.select()
|
|
26
33
|
.from(messageParts)
|
|
@@ -54,6 +61,8 @@ export async function buildHistoryMessages(
|
|
|
54
61
|
}> = [];
|
|
55
62
|
|
|
56
63
|
for (const p of parts) {
|
|
64
|
+
if (p.type === 'reasoning') continue;
|
|
65
|
+
|
|
57
66
|
if (p.type === 'text') {
|
|
58
67
|
try {
|
|
59
68
|
const obj = JSON.parse(p.content ?? '{}');
|
|
@@ -4,7 +4,8 @@ import type { AGIConfig } from '@agi-cli/sdk';
|
|
|
4
4
|
import type { DB } from '@agi-cli/database';
|
|
5
5
|
import { messages, messageParts, sessions } from '@agi-cli/database/schema';
|
|
6
6
|
import { publish } from '../events/bus.ts';
|
|
7
|
-
import { enqueueAssistantRun } from './
|
|
7
|
+
import { enqueueAssistantRun } from './session-queue.ts';
|
|
8
|
+
import { runSessionLoop } from './runner.ts';
|
|
8
9
|
import { resolveModel } from './provider.ts';
|
|
9
10
|
import type { ProviderId } from '@agi-cli/sdk';
|
|
10
11
|
import { debugLog } from './debug.ts';
|
|
@@ -86,20 +87,6 @@ export async function dispatchAssistantMessage(
|
|
|
86
87
|
model,
|
|
87
88
|
createdAt: Date.now(),
|
|
88
89
|
});
|
|
89
|
-
const assistantPartId = crypto.randomUUID();
|
|
90
|
-
const startTs = Date.now();
|
|
91
|
-
await db.insert(messageParts).values({
|
|
92
|
-
id: assistantPartId,
|
|
93
|
-
messageId: assistantMessageId,
|
|
94
|
-
index: 0,
|
|
95
|
-
stepIndex: 0,
|
|
96
|
-
type: 'text',
|
|
97
|
-
content: JSON.stringify({ text: '' }),
|
|
98
|
-
agent,
|
|
99
|
-
provider,
|
|
100
|
-
model,
|
|
101
|
-
startedAt: startTs,
|
|
102
|
-
});
|
|
103
90
|
publish({
|
|
104
91
|
type: 'message.created',
|
|
105
92
|
sessionId,
|
|
@@ -111,17 +98,19 @@ export async function dispatchAssistantMessage(
|
|
|
111
98
|
`[MESSAGE_SERVICE] Enqueuing assistant run with userContext: ${userContext ? `${userContext.substring(0, 50)}...` : 'NONE'}`,
|
|
112
99
|
);
|
|
113
100
|
|
|
114
|
-
enqueueAssistantRun(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
101
|
+
enqueueAssistantRun(
|
|
102
|
+
{
|
|
103
|
+
sessionId,
|
|
104
|
+
assistantMessageId,
|
|
105
|
+
agent,
|
|
106
|
+
provider,
|
|
107
|
+
model,
|
|
108
|
+
projectRoot: cfg.projectRoot,
|
|
109
|
+
oneShot: Boolean(oneShot),
|
|
110
|
+
userContext,
|
|
111
|
+
},
|
|
112
|
+
runSessionLoop,
|
|
113
|
+
);
|
|
125
114
|
|
|
126
115
|
void touchSessionLastActive({ db, sessionId });
|
|
127
116
|
|
package/src/runtime/runner.ts
CHANGED
|
@@ -15,9 +15,6 @@ import { toErrorPayload } from './error-handling.ts';
|
|
|
15
15
|
import { getMaxOutputTokens } from './token-utils.ts';
|
|
16
16
|
import {
|
|
17
17
|
type RunOpts,
|
|
18
|
-
enqueueAssistantRun as enqueueRun,
|
|
19
|
-
abortSession as abortSessionQueue,
|
|
20
|
-
getRunnerState,
|
|
21
18
|
setRunning,
|
|
22
19
|
dequeueJob,
|
|
23
20
|
cleanupSession,
|
|
@@ -36,32 +33,19 @@ import {
|
|
|
36
33
|
createFinishHandler,
|
|
37
34
|
} from './stream-handlers.ts';
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
export function enqueueAssistantRun(opts: Omit<RunOpts, 'abortSignal'>) {
|
|
43
|
-
enqueueRun(opts, processQueue);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Aborts an active session.
|
|
48
|
-
*/
|
|
49
|
-
export function abortSession(sessionId: string) {
|
|
50
|
-
abortSessionQueue(sessionId);
|
|
51
|
-
}
|
|
36
|
+
export { enqueueAssistantRun, abortSession } from './session-queue.ts';
|
|
37
|
+
export { getRunnerState } from './session-queue.ts';
|
|
52
38
|
|
|
53
39
|
/**
|
|
54
|
-
*
|
|
40
|
+
* Main loop that processes the queue for a given session.
|
|
55
41
|
*/
|
|
56
|
-
async function
|
|
57
|
-
const state = getRunnerState(sessionId);
|
|
58
|
-
if (!state) return;
|
|
59
|
-
if (state.running) return;
|
|
42
|
+
export async function runSessionLoop(sessionId: string) {
|
|
60
43
|
setRunning(sessionId, true);
|
|
61
44
|
|
|
62
|
-
while (
|
|
63
|
-
const job = dequeueJob(sessionId);
|
|
45
|
+
while (true) {
|
|
46
|
+
const job = await dequeueJob(sessionId);
|
|
64
47
|
if (!job) break;
|
|
48
|
+
|
|
65
49
|
try {
|
|
66
50
|
await runAssistant(job);
|
|
67
51
|
} catch (_err) {
|
|
@@ -170,12 +154,9 @@ async function runAssistant(opts: RunOpts) {
|
|
|
170
154
|
const toolsTimer = time('runner:discoverTools');
|
|
171
155
|
const allTools = await discoverProjectTools(cfg.projectRoot);
|
|
172
156
|
toolsTimer.end({ count: allTools.length });
|
|
173
|
-
const allowedNames = new Set([
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'progress_update',
|
|
177
|
-
]);
|
|
178
|
-
const gated = allTools.filter((t) => allowedNames.has(t.name));
|
|
157
|
+
const allowedNames = new Set([...(agentCfg.tools || []), 'finish']);
|
|
158
|
+
const gated = allTools.filter((tool) => allowedNames.has(tool.name));
|
|
159
|
+
debugLog(`[tools] ${gated.length} allowed tools`);
|
|
179
160
|
|
|
180
161
|
// FIX: For OAuth, ALWAYS prepend the system message because it's never in history
|
|
181
162
|
// For API key mode, only add on first message (when additionalSystemMessages is empty)
|
|
@@ -184,6 +165,8 @@ async function runAssistant(opts: RunOpts) {
|
|
|
184
165
|
...history,
|
|
185
166
|
];
|
|
186
167
|
|
|
168
|
+
debugLog(`[RUNNER] About to create model with provider: ${opts.provider}`);
|
|
169
|
+
debugLog(`[RUNNER] About to create model ID: ${opts.model}`);
|
|
187
170
|
debugLog(
|
|
188
171
|
`[RUNNER] messagesWithSystemInstructions length: ${messagesWithSystemInstructions.length}`,
|
|
189
172
|
);
|
|
@@ -199,22 +182,21 @@ async function runAssistant(opts: RunOpts) {
|
|
|
199
182
|
);
|
|
200
183
|
}
|
|
201
184
|
|
|
185
|
+
const model = await resolveModel(opts.provider, opts.model, cfg);
|
|
186
|
+
debugLog(
|
|
187
|
+
`[RUNNER] Model created: ${JSON.stringify({ id: model.modelId, provider: model.provider })}`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const maxOutputTokens = getMaxOutputTokens(opts.provider, opts.model);
|
|
191
|
+
debugLog(`[RUNNER] maxOutputTokens for ${opts.model}: ${maxOutputTokens}`);
|
|
192
|
+
|
|
193
|
+
// Setup tool context
|
|
202
194
|
const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
|
|
203
195
|
opts,
|
|
204
196
|
db,
|
|
205
197
|
);
|
|
206
198
|
const toolset = adaptTools(gated, sharedCtx, opts.provider);
|
|
207
199
|
|
|
208
|
-
const modelTimer = time('runner:resolveModel');
|
|
209
|
-
const model = await resolveModel(opts.provider, opts.model, cfg);
|
|
210
|
-
modelTimer.end();
|
|
211
|
-
|
|
212
|
-
const maxOutputTokens = getMaxOutputTokens(opts.provider, opts.model);
|
|
213
|
-
|
|
214
|
-
let currentPartId = opts.assistantPartId;
|
|
215
|
-
let accumulated = '';
|
|
216
|
-
let stepIndex = 0;
|
|
217
|
-
|
|
218
200
|
let _finishObserved = false;
|
|
219
201
|
const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
|
|
220
202
|
if (evt.type !== 'tool.result') return;
|
|
@@ -231,7 +213,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
231
213
|
// State management helpers
|
|
232
214
|
const getCurrentPartId = () => currentPartId;
|
|
233
215
|
const getStepIndex = () => stepIndex;
|
|
234
|
-
const updateCurrentPartId = (id: string) => {
|
|
216
|
+
const updateCurrentPartId = (id: string | null) => {
|
|
235
217
|
currentPartId = id;
|
|
236
218
|
};
|
|
237
219
|
const updateAccumulated = (text: string) => {
|
|
@@ -242,16 +224,29 @@ async function runAssistant(opts: RunOpts) {
|
|
|
242
224
|
return stepIndex;
|
|
243
225
|
};
|
|
244
226
|
|
|
227
|
+
type ReasoningState = {
|
|
228
|
+
partId: string;
|
|
229
|
+
text: string;
|
|
230
|
+
providerMetadata?: unknown;
|
|
231
|
+
};
|
|
232
|
+
const reasoningStates = new Map<string, ReasoningState>();
|
|
233
|
+
const serializeReasoningContent = (state: ReasoningState) =>
|
|
234
|
+
JSON.stringify(
|
|
235
|
+
state.providerMetadata != null
|
|
236
|
+
? { text: state.text, providerMetadata: state.providerMetadata }
|
|
237
|
+
: { text: state.text },
|
|
238
|
+
);
|
|
239
|
+
|
|
245
240
|
// Create stream handlers
|
|
246
241
|
const onStepFinish = createStepFinishHandler(
|
|
247
242
|
opts,
|
|
248
243
|
db,
|
|
249
|
-
getCurrentPartId,
|
|
250
244
|
getStepIndex,
|
|
251
|
-
|
|
245
|
+
incrementStepIndex,
|
|
246
|
+
getCurrentPartId,
|
|
252
247
|
updateCurrentPartId,
|
|
253
248
|
updateAccumulated,
|
|
254
|
-
|
|
249
|
+
sharedCtx,
|
|
255
250
|
updateSessionTokensIncremental,
|
|
256
251
|
updateMessageTokensIncremental,
|
|
257
252
|
);
|
|
@@ -306,6 +301,11 @@ async function runAssistant(opts: RunOpts) {
|
|
|
306
301
|
`[RUNNER] cachedSystem (spoof): ${typeof cachedSystem === 'string' ? cachedSystem.substring(0, 100) : JSON.stringify(cachedSystem).substring(0, 100)}`,
|
|
307
302
|
);
|
|
308
303
|
|
|
304
|
+
// Part tracking - will be created on first text-delta
|
|
305
|
+
let currentPartId: string | null = null;
|
|
306
|
+
let accumulated = '';
|
|
307
|
+
let stepIndex = 0;
|
|
308
|
+
|
|
309
309
|
try {
|
|
310
310
|
// @ts-expect-error this is fine 🔥
|
|
311
311
|
const result = streamText({
|
|
@@ -322,50 +322,173 @@ async function runAssistant(opts: RunOpts) {
|
|
|
322
322
|
onFinish,
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
-
for await (const
|
|
326
|
-
if (!
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
325
|
+
for await (const part of result.fullStream) {
|
|
326
|
+
if (!part) continue;
|
|
327
|
+
if (part.type === 'text-delta') {
|
|
328
|
+
const delta = part.text;
|
|
329
|
+
if (!delta) continue;
|
|
330
|
+
if (!firstDeltaSeen) {
|
|
331
|
+
firstDeltaSeen = true;
|
|
332
|
+
streamStartTimer.end();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Create text part on first delta
|
|
336
|
+
if (!currentPartId) {
|
|
337
|
+
currentPartId = crypto.randomUUID();
|
|
338
|
+
sharedCtx.assistantPartId = currentPartId;
|
|
339
|
+
await db.insert(messageParts).values({
|
|
340
|
+
id: currentPartId,
|
|
341
|
+
messageId: opts.assistantMessageId,
|
|
342
|
+
index: sharedCtx.nextIndex(),
|
|
343
|
+
stepIndex: null,
|
|
344
|
+
type: 'text',
|
|
345
|
+
content: JSON.stringify({ text: '' }),
|
|
346
|
+
agent: opts.agent,
|
|
347
|
+
provider: opts.provider,
|
|
348
|
+
model: opts.model,
|
|
349
|
+
startedAt: Date.now(),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
accumulated += delta;
|
|
354
|
+
publish({
|
|
355
|
+
type: 'message.part.delta',
|
|
356
|
+
sessionId: opts.sessionId,
|
|
357
|
+
payload: {
|
|
358
|
+
messageId: opts.assistantMessageId,
|
|
359
|
+
partId: currentPartId,
|
|
360
|
+
stepIndex,
|
|
361
|
+
delta,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
await db
|
|
365
|
+
.update(messageParts)
|
|
366
|
+
.set({ content: JSON.stringify({ text: accumulated }) })
|
|
367
|
+
.where(eq(messageParts.id, currentPartId));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (part.type === 'reasoning-start') {
|
|
372
|
+
const reasoningId = part.id;
|
|
373
|
+
if (!reasoningId) continue;
|
|
374
|
+
const reasoningPartId = crypto.randomUUID();
|
|
375
|
+
const state: ReasoningState = {
|
|
376
|
+
partId: reasoningPartId,
|
|
377
|
+
text: '',
|
|
378
|
+
providerMetadata: part.providerMetadata,
|
|
379
|
+
};
|
|
380
|
+
reasoningStates.set(reasoningId, state);
|
|
381
|
+
try {
|
|
382
|
+
await db.insert(messageParts).values({
|
|
383
|
+
id: reasoningPartId,
|
|
384
|
+
messageId: opts.assistantMessageId,
|
|
385
|
+
index: sharedCtx.nextIndex(),
|
|
386
|
+
stepIndex: getStepIndex(),
|
|
387
|
+
type: 'reasoning',
|
|
388
|
+
content: serializeReasoningContent(state),
|
|
389
|
+
agent: opts.agent,
|
|
390
|
+
provider: opts.provider,
|
|
391
|
+
model: opts.model,
|
|
392
|
+
startedAt: Date.now(),
|
|
393
|
+
});
|
|
394
|
+
} catch {}
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (part.type === 'reasoning-delta') {
|
|
399
|
+
const state = reasoningStates.get(part.id);
|
|
400
|
+
if (!state) continue;
|
|
401
|
+
state.text += part.text;
|
|
402
|
+
if (part.providerMetadata != null) {
|
|
403
|
+
state.providerMetadata = part.providerMetadata;
|
|
404
|
+
}
|
|
405
|
+
publish({
|
|
406
|
+
type: 'reasoning.delta',
|
|
407
|
+
sessionId: opts.sessionId,
|
|
408
|
+
payload: {
|
|
409
|
+
messageId: opts.assistantMessageId,
|
|
410
|
+
partId: state.partId,
|
|
411
|
+
stepIndex: getStepIndex(),
|
|
412
|
+
delta: part.text,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
try {
|
|
416
|
+
await db
|
|
417
|
+
.update(messageParts)
|
|
418
|
+
.set({ content: serializeReasoningContent(state) })
|
|
419
|
+
.where(eq(messageParts.id, state.partId));
|
|
420
|
+
} catch {}
|
|
421
|
+
continue;
|
|
330
422
|
}
|
|
331
|
-
|
|
423
|
+
|
|
424
|
+
if (part.type === 'reasoning-end') {
|
|
425
|
+
const state = reasoningStates.get(part.id);
|
|
426
|
+
if (!state) continue;
|
|
427
|
+
try {
|
|
428
|
+
await db
|
|
429
|
+
.update(messageParts)
|
|
430
|
+
.set({ completedAt: Date.now() })
|
|
431
|
+
.where(eq(messageParts.id, state.partId));
|
|
432
|
+
} catch {}
|
|
433
|
+
reasoningStates.delete(part.id);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Emit finish-step at the end if there were no tool calls and no finish
|
|
438
|
+
const fs = firstToolSeen();
|
|
439
|
+
if (!fs && !_finishObserved) {
|
|
332
440
|
publish({
|
|
333
|
-
type: '
|
|
441
|
+
type: 'finish-step',
|
|
334
442
|
sessionId: opts.sessionId,
|
|
335
|
-
payload: {
|
|
336
|
-
messageId: opts.assistantMessageId,
|
|
337
|
-
partId: currentPartId,
|
|
338
|
-
stepIndex,
|
|
339
|
-
delta,
|
|
340
|
-
},
|
|
443
|
+
payload: { reason: 'no-tool-calls' },
|
|
341
444
|
});
|
|
342
|
-
await db
|
|
343
|
-
.update(messageParts)
|
|
344
|
-
.set({ content: JSON.stringify({ text: accumulated }) })
|
|
345
|
-
.where(eq(messageParts.id, currentPartId));
|
|
346
445
|
}
|
|
347
|
-
} catch (error) {
|
|
348
|
-
const errorPayload = toErrorPayload(error);
|
|
349
|
-
await db
|
|
350
|
-
.update(messageParts)
|
|
351
|
-
.set({
|
|
352
|
-
content: JSON.stringify({ error: errorPayload }),
|
|
353
|
-
})
|
|
354
|
-
.where(eq(messageParts.id, currentPartId));
|
|
355
446
|
|
|
447
|
+
unsubscribeFinish();
|
|
448
|
+
|
|
449
|
+
await cleanupEmptyTextParts(opts, db);
|
|
450
|
+
|
|
451
|
+
firstToolTimer.end({ seen: firstToolSeen() });
|
|
452
|
+
|
|
453
|
+
debugLog(
|
|
454
|
+
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}`,
|
|
455
|
+
);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
unsubscribeFinish();
|
|
458
|
+
const payload = toErrorPayload(err);
|
|
459
|
+
debugLog(`[RUNNER] Error during stream: ${payload.message}`);
|
|
460
|
+
debugLog(
|
|
461
|
+
`[RUNNER] Error stack: ${err instanceof Error ? err.stack : 'no stack'}`,
|
|
462
|
+
);
|
|
463
|
+
debugLog(
|
|
464
|
+
`[RUNNER] db is: ${typeof db}, db.select is: ${typeof db?.select}`,
|
|
465
|
+
);
|
|
356
466
|
publish({
|
|
357
|
-
type: '
|
|
467
|
+
type: 'error',
|
|
358
468
|
sessionId: opts.sessionId,
|
|
359
|
-
payload
|
|
360
|
-
messageId: opts.assistantMessageId,
|
|
361
|
-
partId: currentPartId,
|
|
362
|
-
error: errorPayload,
|
|
363
|
-
},
|
|
469
|
+
payload,
|
|
364
470
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
471
|
+
try {
|
|
472
|
+
await updateSessionTokensIncremental(
|
|
473
|
+
{
|
|
474
|
+
inputTokens: 0,
|
|
475
|
+
outputTokens: 0,
|
|
476
|
+
},
|
|
477
|
+
undefined,
|
|
478
|
+
opts,
|
|
479
|
+
db,
|
|
480
|
+
);
|
|
481
|
+
await updateMessageTokensIncremental(
|
|
482
|
+
{
|
|
483
|
+
inputTokens: 0,
|
|
484
|
+
outputTokens: 0,
|
|
485
|
+
},
|
|
486
|
+
undefined,
|
|
487
|
+
opts,
|
|
488
|
+
db,
|
|
489
|
+
);
|
|
490
|
+
await completeAssistantMessage({}, opts, db);
|
|
491
|
+
} catch {}
|
|
492
|
+
throw err;
|
|
370
493
|
}
|
|
371
494
|
}
|
|
@@ -31,12 +31,12 @@ type AbortEvent = {
|
|
|
31
31
|
export function createStepFinishHandler(
|
|
32
32
|
opts: RunOpts,
|
|
33
33
|
db: Awaited<ReturnType<typeof getDb>>,
|
|
34
|
-
getCurrentPartId: () => string,
|
|
35
34
|
getStepIndex: () => number,
|
|
36
|
-
sharedCtx: ToolAdapterContext,
|
|
37
|
-
updateCurrentPartId: (id: string) => void,
|
|
38
|
-
updateAccumulated: (text: string) => void,
|
|
39
35
|
incrementStepIndex: () => number,
|
|
36
|
+
getCurrentPartId: () => string | null,
|
|
37
|
+
updateCurrentPartId: (id: string | null) => void,
|
|
38
|
+
updateAccumulated: (text: string) => void,
|
|
39
|
+
sharedCtx: ToolAdapterContext,
|
|
40
40
|
updateSessionTokensIncrementalFn: (
|
|
41
41
|
usage: UsageData,
|
|
42
42
|
providerMetadata: ProviderMetadata | undefined,
|
|
@@ -56,10 +56,12 @@ export function createStepFinishHandler(
|
|
|
56
56
|
const stepIndex = getStepIndex();
|
|
57
57
|
|
|
58
58
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
if (currentPartId) {
|
|
60
|
+
await db
|
|
61
|
+
.update(messageParts)
|
|
62
|
+
.set({ completedAt: finishedAt })
|
|
63
|
+
.where(eq(messageParts.id, currentPartId));
|
|
64
|
+
}
|
|
63
65
|
} catch {}
|
|
64
66
|
|
|
65
67
|
// Update token counts incrementally after each step
|
|
@@ -104,25 +106,11 @@ export function createStepFinishHandler(
|
|
|
104
106
|
} catch {}
|
|
105
107
|
|
|
106
108
|
try {
|
|
109
|
+
// Increment step index but defer creating the new text part
|
|
110
|
+
// until we actually get a text-delta (so reasoning blocks can complete first)
|
|
107
111
|
const newStepIndex = incrementStepIndex();
|
|
108
|
-
const newPartId = crypto.randomUUID();
|
|
109
|
-
const index = await sharedCtx.nextIndex();
|
|
110
|
-
const nowTs = Date.now();
|
|
111
|
-
await db.insert(messageParts).values({
|
|
112
|
-
id: newPartId,
|
|
113
|
-
messageId: opts.assistantMessageId,
|
|
114
|
-
index,
|
|
115
|
-
stepIndex: newStepIndex,
|
|
116
|
-
type: 'text',
|
|
117
|
-
content: JSON.stringify({ text: '' }),
|
|
118
|
-
agent: opts.agent,
|
|
119
|
-
provider: opts.provider,
|
|
120
|
-
model: opts.model,
|
|
121
|
-
startedAt: nowTs,
|
|
122
|
-
});
|
|
123
|
-
updateCurrentPartId(newPartId);
|
|
124
|
-
sharedCtx.assistantPartId = newPartId;
|
|
125
112
|
sharedCtx.stepIndex = newStepIndex;
|
|
113
|
+
updateCurrentPartId(null); // Signal that next text-delta should create new part
|
|
126
114
|
updateAccumulated('');
|
|
127
115
|
} catch {}
|
|
128
116
|
};
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { getDb } from '@agi-cli/database';
|
|
2
|
-
import { messageParts } from '@agi-cli/database/schema';
|
|
3
|
-
import { eq } from 'drizzle-orm';
|
|
4
2
|
import { time } from './debug.ts';
|
|
5
3
|
import type { ToolAdapterContext } from '../tools/adapter.ts';
|
|
6
4
|
import type { RunOpts } from './session-queue.ts';
|
|
@@ -18,12 +16,16 @@ export async function setupToolContext(
|
|
|
18
16
|
const firstToolTimer = time('runner:first-tool-call');
|
|
19
17
|
let firstToolSeen = false;
|
|
20
18
|
|
|
19
|
+
// Simple counter starting at 0 - first event gets 0, second gets 1, etc.
|
|
20
|
+
let currentIndex = 0;
|
|
21
|
+
const nextIndex = () => currentIndex++;
|
|
22
|
+
|
|
21
23
|
const sharedCtx: RunnerToolContext = {
|
|
22
|
-
nextIndex
|
|
24
|
+
nextIndex,
|
|
23
25
|
stepIndex: 0,
|
|
24
26
|
sessionId: opts.sessionId,
|
|
25
27
|
messageId: opts.assistantMessageId,
|
|
26
|
-
assistantPartId:
|
|
28
|
+
assistantPartId: '', // Will be set by runner when first text part is created
|
|
27
29
|
db,
|
|
28
30
|
agent: opts.agent,
|
|
29
31
|
provider: opts.provider,
|
|
@@ -36,23 +38,5 @@ export async function setupToolContext(
|
|
|
36
38
|
},
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
let counter = 0;
|
|
40
|
-
try {
|
|
41
|
-
const existing = await db
|
|
42
|
-
.select()
|
|
43
|
-
.from(messageParts)
|
|
44
|
-
.where(eq(messageParts.messageId, opts.assistantMessageId));
|
|
45
|
-
if (existing.length) {
|
|
46
|
-
const indexes = existing.map((p) => Number(p.index ?? 0));
|
|
47
|
-
const maxIndex = Math.max(...indexes);
|
|
48
|
-
if (Number.isFinite(maxIndex)) counter = maxIndex;
|
|
49
|
-
}
|
|
50
|
-
} catch {}
|
|
51
|
-
|
|
52
|
-
sharedCtx.nextIndex = () => {
|
|
53
|
-
counter += 1;
|
|
54
|
-
return counter;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
41
|
return { sharedCtx, firstToolTimer, firstToolSeen: () => firstToolSeen };
|
|
58
42
|
}
|