@agi-cli/server 0.1.119 → 0.1.121
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/index.ts +9 -5
- package/src/openapi/paths/git.ts +4 -0
- package/src/routes/ask.ts +13 -14
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +1 -1
- package/src/routes/config/cwd.ts +1 -1
- package/src/routes/config/main.ts +1 -1
- package/src/routes/config/models.ts +32 -4
- package/src/routes/config/providers.ts +1 -1
- package/src/routes/config/utils.ts +14 -1
- package/src/routes/files.ts +1 -1
- package/src/routes/git/commit.ts +23 -6
- package/src/routes/git/schemas.ts +1 -0
- package/src/routes/session-files.ts +1 -1
- package/src/routes/session-messages.ts +2 -2
- package/src/routes/sessions.ts +8 -6
- package/src/runtime/agent/registry.ts +333 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +265 -0
- package/src/runtime/agent/runner.ts +356 -0
- package/src/runtime/agent-registry.ts +6 -333
- package/src/runtime/{ask-service.ts → ask/service.ts} +5 -5
- package/src/runtime/{debug.ts → debug/index.ts} +1 -1
- package/src/runtime/{api-error.ts → errors/api-error.ts} +2 -2
- package/src/runtime/message/compaction-auto.ts +137 -0
- package/src/runtime/message/compaction-context.ts +64 -0
- package/src/runtime/message/compaction-detect.ts +19 -0
- package/src/runtime/message/compaction-limits.ts +58 -0
- package/src/runtime/message/compaction-mark.ts +115 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +23 -0
- package/src/runtime/{history-builder.ts → message/history-builder.ts} +2 -2
- package/src/runtime/{message-service.ts → message/service.ts} +8 -14
- package/src/runtime/{history → message}/tool-history-tracker.ts +1 -1
- package/src/runtime/{prompt.ts → prompt/builder.ts} +1 -1
- package/src/runtime/{provider.ts → provider/anthropic.ts} +4 -219
- package/src/runtime/provider/google.ts +12 -0
- package/src/runtime/provider/index.ts +44 -0
- package/src/runtime/provider/openai.ts +26 -0
- package/src/runtime/provider/opencode.ts +61 -0
- package/src/runtime/provider/openrouter.ts +11 -0
- package/src/runtime/provider/solforge.ts +22 -0
- package/src/runtime/provider/zai.ts +53 -0
- package/src/runtime/session/branch.ts +277 -0
- package/src/runtime/{db-operations.ts → session/db-operations.ts} +1 -1
- package/src/runtime/{session-manager.ts → session/manager.ts} +1 -1
- package/src/runtime/{session-queue.ts → session/queue.ts} +2 -2
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +200 -0
- package/src/runtime/stream/finish-handler.ts +123 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +17 -0
- package/src/runtime/{tool-context.ts → tools/context.ts} +1 -1
- package/src/runtime/{tool-context-setup.ts → tools/setup.ts} +3 -3
- package/src/runtime/{token-utils.ts → utils/token.ts} +2 -2
- package/src/tools/adapter.ts +4 -4
- package/src/runtime/compaction.ts +0 -536
- package/src/runtime/runner.ts +0 -654
- package/src/runtime/stream-handlers.ts +0 -508
- /package/src/runtime/{cache-optimizer.ts → context/cache-optimizer.ts} +0 -0
- /package/src/runtime/{environment.ts → context/environment.ts} +0 -0
- /package/src/runtime/{context-optimizer.ts → context/optimizer.ts} +0 -0
- /package/src/runtime/{debug-state.ts → debug/state.ts} +0 -0
- /package/src/runtime/{error-handling.ts → errors/handling.ts} +0 -0
- /package/src/runtime/{history-truncator.ts → message/history-truncator.ts} +0 -0
- /package/src/runtime/{provider-selection.ts → provider/selection.ts} +0 -0
- /package/src/runtime/{tool-mapping.ts → tools/mapping.ts} +0 -0
- /package/src/runtime/{cwd.ts → utils/cwd.ts} +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { eq, asc } from 'drizzle-orm';
|
|
2
|
+
import type { DB } from '@agi-cli/database';
|
|
3
|
+
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
4
|
+
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import type { ProviderId } from '@agi-cli/sdk';
|
|
6
|
+
|
|
7
|
+
type SessionRow = typeof sessions.$inferSelect;
|
|
8
|
+
|
|
9
|
+
export type CreateBranchInput = {
|
|
10
|
+
db: DB;
|
|
11
|
+
parentSessionId: string;
|
|
12
|
+
fromMessageId: string;
|
|
13
|
+
provider?: ProviderId;
|
|
14
|
+
model?: string;
|
|
15
|
+
agent?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
projectPath: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BranchResult = {
|
|
21
|
+
session: SessionRow;
|
|
22
|
+
parentSessionId: string;
|
|
23
|
+
branchPointMessageId: string;
|
|
24
|
+
copiedMessages: number;
|
|
25
|
+
copiedParts: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function createBranch({
|
|
29
|
+
db,
|
|
30
|
+
parentSessionId,
|
|
31
|
+
fromMessageId,
|
|
32
|
+
provider,
|
|
33
|
+
model,
|
|
34
|
+
agent,
|
|
35
|
+
title,
|
|
36
|
+
projectPath,
|
|
37
|
+
}: CreateBranchInput): Promise<BranchResult> {
|
|
38
|
+
const parentRows = await db
|
|
39
|
+
.select()
|
|
40
|
+
.from(sessions)
|
|
41
|
+
.where(eq(sessions.id, parentSessionId));
|
|
42
|
+
|
|
43
|
+
if (!parentRows.length) {
|
|
44
|
+
throw new Error('Parent session not found');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parent = parentRows[0];
|
|
48
|
+
|
|
49
|
+
if (parent.projectPath !== projectPath) {
|
|
50
|
+
throw new Error('Parent session not found in this project');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const branchPointRows = await db
|
|
54
|
+
.select()
|
|
55
|
+
.from(messages)
|
|
56
|
+
.where(eq(messages.id, fromMessageId));
|
|
57
|
+
|
|
58
|
+
if (!branchPointRows.length) {
|
|
59
|
+
throw new Error('Branch point message not found');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const branchPoint = branchPointRows[0];
|
|
63
|
+
|
|
64
|
+
if (branchPoint.sessionId !== parentSessionId) {
|
|
65
|
+
throw new Error('Branch point message does not belong to parent session');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const allMessages = await db
|
|
69
|
+
.select()
|
|
70
|
+
.from(messages)
|
|
71
|
+
.where(eq(messages.sessionId, parentSessionId))
|
|
72
|
+
.orderBy(asc(messages.createdAt));
|
|
73
|
+
|
|
74
|
+
const branchPointIndex = allMessages.findIndex((m) => m.id === fromMessageId);
|
|
75
|
+
if (branchPointIndex === -1) {
|
|
76
|
+
throw new Error('Branch point message not found in session');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const messagesToCopy = allMessages.slice(0, branchPointIndex + 1);
|
|
80
|
+
|
|
81
|
+
const newSessionId = crypto.randomUUID();
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
|
|
84
|
+
const newSession: typeof sessions.$inferInsert = {
|
|
85
|
+
id: newSessionId,
|
|
86
|
+
title: title || `Branch of ${parent.title || 'Untitled'}`,
|
|
87
|
+
agent: agent || parent.agent,
|
|
88
|
+
provider: provider || parent.provider,
|
|
89
|
+
model: model || parent.model,
|
|
90
|
+
projectPath: parent.projectPath,
|
|
91
|
+
createdAt: now,
|
|
92
|
+
lastActiveAt: now,
|
|
93
|
+
parentSessionId,
|
|
94
|
+
branchPointMessageId: fromMessageId,
|
|
95
|
+
sessionType: 'branch',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await db.insert(sessions).values(newSession);
|
|
99
|
+
|
|
100
|
+
const messageIdMap = new Map<string, string>();
|
|
101
|
+
let copiedParts = 0;
|
|
102
|
+
|
|
103
|
+
for (const msg of messagesToCopy) {
|
|
104
|
+
const newMessageId = crypto.randomUUID();
|
|
105
|
+
messageIdMap.set(msg.id, newMessageId);
|
|
106
|
+
|
|
107
|
+
const newMessage: typeof messages.$inferInsert = {
|
|
108
|
+
id: newMessageId,
|
|
109
|
+
sessionId: newSessionId,
|
|
110
|
+
role: msg.role,
|
|
111
|
+
status: msg.status,
|
|
112
|
+
agent: msg.agent,
|
|
113
|
+
provider: msg.provider,
|
|
114
|
+
model: msg.model,
|
|
115
|
+
createdAt: msg.createdAt,
|
|
116
|
+
completedAt: msg.completedAt,
|
|
117
|
+
latencyMs: msg.latencyMs,
|
|
118
|
+
promptTokens: msg.promptTokens,
|
|
119
|
+
completionTokens: msg.completionTokens,
|
|
120
|
+
totalTokens: msg.totalTokens,
|
|
121
|
+
cachedInputTokens: msg.cachedInputTokens,
|
|
122
|
+
reasoningTokens: msg.reasoningTokens,
|
|
123
|
+
error: msg.error,
|
|
124
|
+
errorType: msg.errorType,
|
|
125
|
+
errorDetails: msg.errorDetails,
|
|
126
|
+
isAborted: msg.isAborted,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await db.insert(messages).values(newMessage);
|
|
130
|
+
|
|
131
|
+
const parts = await db
|
|
132
|
+
.select()
|
|
133
|
+
.from(messageParts)
|
|
134
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
135
|
+
.orderBy(asc(messageParts.index));
|
|
136
|
+
|
|
137
|
+
for (const part of parts) {
|
|
138
|
+
const newPart: typeof messageParts.$inferInsert = {
|
|
139
|
+
id: crypto.randomUUID(),
|
|
140
|
+
messageId: newMessageId,
|
|
141
|
+
index: part.index,
|
|
142
|
+
stepIndex: part.stepIndex,
|
|
143
|
+
type: part.type,
|
|
144
|
+
content: part.content,
|
|
145
|
+
agent: part.agent,
|
|
146
|
+
provider: part.provider,
|
|
147
|
+
model: part.model,
|
|
148
|
+
startedAt: part.startedAt,
|
|
149
|
+
completedAt: part.completedAt,
|
|
150
|
+
compactedAt: part.compactedAt,
|
|
151
|
+
toolName: part.toolName,
|
|
152
|
+
toolCallId: part.toolCallId,
|
|
153
|
+
toolDurationMs: part.toolDurationMs,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await db.insert(messageParts).values(newPart);
|
|
157
|
+
copiedParts++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result: SessionRow = {
|
|
162
|
+
...newSession,
|
|
163
|
+
totalInputTokens: null,
|
|
164
|
+
totalOutputTokens: null,
|
|
165
|
+
totalCachedTokens: null,
|
|
166
|
+
totalReasoningTokens: null,
|
|
167
|
+
totalToolTimeMs: null,
|
|
168
|
+
toolCountsJson: null,
|
|
169
|
+
contextSummary: null,
|
|
170
|
+
lastCompactedAt: null,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
publish({
|
|
174
|
+
type: 'session.created',
|
|
175
|
+
sessionId: newSessionId,
|
|
176
|
+
payload: result,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
session: result,
|
|
181
|
+
parentSessionId,
|
|
182
|
+
branchPointMessageId: fromMessageId,
|
|
183
|
+
copiedMessages: messagesToCopy.length,
|
|
184
|
+
copiedParts,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ListBranchesResult = Array<{
|
|
189
|
+
session: SessionRow;
|
|
190
|
+
branchPointMessageId: string | null;
|
|
191
|
+
branchPointPreview: string | null;
|
|
192
|
+
createdAt: number;
|
|
193
|
+
}>;
|
|
194
|
+
|
|
195
|
+
export async function listBranches(
|
|
196
|
+
db: DB,
|
|
197
|
+
sessionId: string,
|
|
198
|
+
projectPath: string,
|
|
199
|
+
): Promise<ListBranchesResult> {
|
|
200
|
+
const branches = await db
|
|
201
|
+
.select()
|
|
202
|
+
.from(sessions)
|
|
203
|
+
.where(eq(sessions.parentSessionId, sessionId))
|
|
204
|
+
.orderBy(asc(sessions.createdAt));
|
|
205
|
+
|
|
206
|
+
const results: ListBranchesResult = [];
|
|
207
|
+
|
|
208
|
+
for (const branch of branches) {
|
|
209
|
+
if (branch.projectPath !== projectPath) continue;
|
|
210
|
+
|
|
211
|
+
let preview: string | null = null;
|
|
212
|
+
|
|
213
|
+
if (branch.branchPointMessageId) {
|
|
214
|
+
const msgRows = await db
|
|
215
|
+
.select()
|
|
216
|
+
.from(messages)
|
|
217
|
+
.where(eq(messages.id, branch.branchPointMessageId));
|
|
218
|
+
|
|
219
|
+
if (msgRows.length > 0) {
|
|
220
|
+
const parts = await db
|
|
221
|
+
.select()
|
|
222
|
+
.from(messageParts)
|
|
223
|
+
.where(eq(messageParts.messageId, branch.branchPointMessageId))
|
|
224
|
+
.orderBy(asc(messageParts.index));
|
|
225
|
+
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (part.type === 'text') {
|
|
228
|
+
try {
|
|
229
|
+
const content = JSON.parse(part.content || '{}');
|
|
230
|
+
if (content.text) {
|
|
231
|
+
preview = content.text.slice(0, 100);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
results.push({
|
|
241
|
+
session: branch,
|
|
242
|
+
branchPointMessageId: branch.branchPointMessageId,
|
|
243
|
+
branchPointPreview: preview,
|
|
244
|
+
createdAt: branch.createdAt,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function getParentSession(
|
|
252
|
+
db: DB,
|
|
253
|
+
sessionId: string,
|
|
254
|
+
projectPath: string,
|
|
255
|
+
): Promise<SessionRow | null> {
|
|
256
|
+
const sessionRows = await db
|
|
257
|
+
.select()
|
|
258
|
+
.from(sessions)
|
|
259
|
+
.where(eq(sessions.id, sessionId));
|
|
260
|
+
|
|
261
|
+
if (!sessionRows.length) return null;
|
|
262
|
+
|
|
263
|
+
const session = sessionRows[0];
|
|
264
|
+
if (!session.parentSessionId) return null;
|
|
265
|
+
|
|
266
|
+
const parentRows = await db
|
|
267
|
+
.select()
|
|
268
|
+
.from(sessions)
|
|
269
|
+
.where(eq(sessions.id, session.parentSessionId));
|
|
270
|
+
|
|
271
|
+
if (!parentRows.length) return null;
|
|
272
|
+
|
|
273
|
+
const parent = parentRows[0];
|
|
274
|
+
if (parent.projectPath !== projectPath) return null;
|
|
275
|
+
|
|
276
|
+
return parent;
|
|
277
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { getDb } from '@agi-cli/database';
|
|
2
2
|
import { messages, messageParts, sessions } from '@agi-cli/database/schema';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
|
-
import type { RunOpts } from './
|
|
4
|
+
import type { RunOpts } from './queue.ts';
|
|
5
5
|
|
|
6
6
|
export type UsageData = {
|
|
7
7
|
inputTokens?: number;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
6
|
+
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
7
|
+
import type { AbortEvent } from './types.ts';
|
|
8
|
+
|
|
9
|
+
export function createAbortHandler(
|
|
10
|
+
opts: RunOpts,
|
|
11
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
12
|
+
getStepIndex: () => number,
|
|
13
|
+
sharedCtx: ToolAdapterContext,
|
|
14
|
+
) {
|
|
15
|
+
return async ({ steps }: AbortEvent) => {
|
|
16
|
+
const stepIndex = getStepIndex();
|
|
17
|
+
|
|
18
|
+
const abortPartId = crypto.randomUUID();
|
|
19
|
+
await db.insert(messageParts).values({
|
|
20
|
+
id: abortPartId,
|
|
21
|
+
messageId: opts.assistantMessageId,
|
|
22
|
+
index: await sharedCtx.nextIndex(),
|
|
23
|
+
stepIndex,
|
|
24
|
+
type: 'error',
|
|
25
|
+
content: JSON.stringify({
|
|
26
|
+
message: 'Generation stopped by user',
|
|
27
|
+
type: 'abort',
|
|
28
|
+
isAborted: true,
|
|
29
|
+
stepsCompleted: steps.length,
|
|
30
|
+
}),
|
|
31
|
+
agent: opts.agent,
|
|
32
|
+
provider: opts.provider,
|
|
33
|
+
model: opts.model,
|
|
34
|
+
startedAt: Date.now(),
|
|
35
|
+
completedAt: Date.now(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await db
|
|
39
|
+
.update(messages)
|
|
40
|
+
.set({
|
|
41
|
+
status: 'error',
|
|
42
|
+
error: 'Generation stopped by user',
|
|
43
|
+
errorType: 'abort',
|
|
44
|
+
errorDetails: JSON.stringify({
|
|
45
|
+
stepsCompleted: steps.length,
|
|
46
|
+
abortedAt: Date.now(),
|
|
47
|
+
}),
|
|
48
|
+
isAborted: true,
|
|
49
|
+
})
|
|
50
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
51
|
+
|
|
52
|
+
publish({
|
|
53
|
+
type: 'error',
|
|
54
|
+
sessionId: opts.sessionId,
|
|
55
|
+
payload: {
|
|
56
|
+
messageId: opts.assistantMessageId,
|
|
57
|
+
partId: abortPartId,
|
|
58
|
+
error: 'Generation stopped by user',
|
|
59
|
+
errorType: 'abort',
|
|
60
|
+
isAborted: true,
|
|
61
|
+
stepsCompleted: steps.length,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { APICallError } from 'ai';
|
|
5
|
+
import { publish } from '../../events/bus.ts';
|
|
6
|
+
import { toErrorPayload } from '../errors/handling.ts';
|
|
7
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
8
|
+
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
9
|
+
import { pruneSession, performAutoCompaction } from '../message/compaction.ts';
|
|
10
|
+
import { debugLog } from '../debug/index.ts';
|
|
11
|
+
import { enqueueAssistantRun } from '../session/queue.ts';
|
|
12
|
+
|
|
13
|
+
export function createErrorHandler(
|
|
14
|
+
opts: RunOpts,
|
|
15
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
16
|
+
getStepIndex: () => number,
|
|
17
|
+
sharedCtx: ToolAdapterContext,
|
|
18
|
+
retryCallback?: (sessionId: string) => Promise<void>,
|
|
19
|
+
) {
|
|
20
|
+
return async (err: unknown) => {
|
|
21
|
+
const errorPayload = toErrorPayload(err);
|
|
22
|
+
const isApiError = APICallError.isInstance(err);
|
|
23
|
+
const stepIndex = getStepIndex();
|
|
24
|
+
|
|
25
|
+
const errObj = err as Record<string, unknown>;
|
|
26
|
+
const nestedError = (errObj?.error as Record<string, unknown>)?.error as
|
|
27
|
+
| Record<string, unknown>
|
|
28
|
+
| undefined;
|
|
29
|
+
const errorCode =
|
|
30
|
+
(errObj?.code as string) ?? (nestedError?.code as string) ?? '';
|
|
31
|
+
const errorType =
|
|
32
|
+
(errObj?.apiErrorType as string) ?? (nestedError?.type as string) ?? '';
|
|
33
|
+
const fullErrorStr = JSON.stringify(err).toLowerCase();
|
|
34
|
+
|
|
35
|
+
const isPromptTooLong =
|
|
36
|
+
fullErrorStr.includes('prompt is too long') ||
|
|
37
|
+
fullErrorStr.includes('maximum context length') ||
|
|
38
|
+
fullErrorStr.includes('too many tokens') ||
|
|
39
|
+
fullErrorStr.includes('context_length_exceeded') ||
|
|
40
|
+
fullErrorStr.includes('request too large') ||
|
|
41
|
+
fullErrorStr.includes('exceeds the model') ||
|
|
42
|
+
fullErrorStr.includes('context window') ||
|
|
43
|
+
fullErrorStr.includes('input is too long') ||
|
|
44
|
+
errorCode === 'context_length_exceeded' ||
|
|
45
|
+
errorType === 'invalid_request_error';
|
|
46
|
+
|
|
47
|
+
debugLog(
|
|
48
|
+
`[stream-handlers] isPromptTooLong: ${isPromptTooLong}, errorCode: ${errorCode}, errorType: ${errorType}`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (isPromptTooLong && !opts.isCompactCommand) {
|
|
52
|
+
debugLog(
|
|
53
|
+
'[stream-handlers] Prompt too long detected, auto-compacting...',
|
|
54
|
+
);
|
|
55
|
+
let compactionSucceeded = false;
|
|
56
|
+
try {
|
|
57
|
+
const publishWrapper = (event: {
|
|
58
|
+
type: string;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
payload: Record<string, unknown>;
|
|
61
|
+
}) => {
|
|
62
|
+
publish(event as Parameters<typeof publish>[0]);
|
|
63
|
+
};
|
|
64
|
+
const compactResult = await performAutoCompaction(
|
|
65
|
+
db,
|
|
66
|
+
opts.sessionId,
|
|
67
|
+
opts.assistantMessageId,
|
|
68
|
+
publishWrapper,
|
|
69
|
+
opts.provider,
|
|
70
|
+
opts.model,
|
|
71
|
+
);
|
|
72
|
+
if (compactResult.success) {
|
|
73
|
+
debugLog(
|
|
74
|
+
`[stream-handlers] Auto-compaction succeeded: ${compactResult.summary?.slice(0, 100)}...`,
|
|
75
|
+
);
|
|
76
|
+
compactionSucceeded = true;
|
|
77
|
+
} else {
|
|
78
|
+
debugLog(
|
|
79
|
+
`[stream-handlers] Auto-compaction failed: ${compactResult.error}, falling back to prune`,
|
|
80
|
+
);
|
|
81
|
+
const pruneResult = await pruneSession(db, opts.sessionId);
|
|
82
|
+
debugLog(
|
|
83
|
+
`[stream-handlers] Fallback pruned ${pruneResult.pruned} parts, saved ~${pruneResult.saved} tokens`,
|
|
84
|
+
);
|
|
85
|
+
compactionSucceeded = pruneResult.pruned > 0;
|
|
86
|
+
}
|
|
87
|
+
} catch (compactErr) {
|
|
88
|
+
debugLog(
|
|
89
|
+
`[stream-handlers] Auto-compact error: ${compactErr instanceof Error ? compactErr.message : String(compactErr)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (compactionSucceeded) {
|
|
94
|
+
await db
|
|
95
|
+
.update(messages)
|
|
96
|
+
.set({
|
|
97
|
+
status: 'completed',
|
|
98
|
+
})
|
|
99
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
100
|
+
|
|
101
|
+
publish({
|
|
102
|
+
type: 'message.completed',
|
|
103
|
+
sessionId: opts.sessionId,
|
|
104
|
+
payload: {
|
|
105
|
+
id: opts.assistantMessageId,
|
|
106
|
+
autoCompacted: true,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (retryCallback) {
|
|
111
|
+
debugLog('[stream-handlers] Triggering retry after compaction...');
|
|
112
|
+
const newAssistantMessageId = crypto.randomUUID();
|
|
113
|
+
await db.insert(messages).values({
|
|
114
|
+
id: newAssistantMessageId,
|
|
115
|
+
sessionId: opts.sessionId,
|
|
116
|
+
role: 'assistant',
|
|
117
|
+
status: 'pending',
|
|
118
|
+
agent: opts.agent,
|
|
119
|
+
provider: opts.provider,
|
|
120
|
+
model: opts.model,
|
|
121
|
+
createdAt: Date.now(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
publish({
|
|
125
|
+
type: 'message.created',
|
|
126
|
+
sessionId: opts.sessionId,
|
|
127
|
+
payload: { id: newAssistantMessageId, role: 'assistant' },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
enqueueAssistantRun(
|
|
131
|
+
{
|
|
132
|
+
...opts,
|
|
133
|
+
assistantMessageId: newAssistantMessageId,
|
|
134
|
+
},
|
|
135
|
+
retryCallback,
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
debugLog(
|
|
139
|
+
'[stream-handlers] No retryCallback provided, cannot auto-retry',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const errorPartId = crypto.randomUUID();
|
|
148
|
+
const displayMessage =
|
|
149
|
+
isPromptTooLong && !opts.isCompactCommand
|
|
150
|
+
? `${errorPayload.message}. Context auto-compacted - please retry your message.`
|
|
151
|
+
: errorPayload.message;
|
|
152
|
+
await db.insert(messageParts).values({
|
|
153
|
+
id: errorPartId,
|
|
154
|
+
messageId: opts.assistantMessageId,
|
|
155
|
+
index: await sharedCtx.nextIndex(),
|
|
156
|
+
stepIndex,
|
|
157
|
+
type: 'error',
|
|
158
|
+
content: JSON.stringify({
|
|
159
|
+
message: displayMessage,
|
|
160
|
+
type: errorPayload.type,
|
|
161
|
+
details: errorPayload.details,
|
|
162
|
+
isAborted: false,
|
|
163
|
+
}),
|
|
164
|
+
agent: opts.agent,
|
|
165
|
+
provider: opts.provider,
|
|
166
|
+
model: opts.model,
|
|
167
|
+
startedAt: Date.now(),
|
|
168
|
+
completedAt: Date.now(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await db
|
|
172
|
+
.update(messages)
|
|
173
|
+
.set({
|
|
174
|
+
status: 'error',
|
|
175
|
+
error: displayMessage,
|
|
176
|
+
errorType: errorPayload.type,
|
|
177
|
+
errorDetails: JSON.stringify({
|
|
178
|
+
...errorPayload.details,
|
|
179
|
+
isApiError,
|
|
180
|
+
autoCompacted: isPromptTooLong && !opts.isCompactCommand,
|
|
181
|
+
}),
|
|
182
|
+
isAborted: false,
|
|
183
|
+
})
|
|
184
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
185
|
+
|
|
186
|
+
publish({
|
|
187
|
+
type: 'error',
|
|
188
|
+
sessionId: opts.sessionId,
|
|
189
|
+
payload: {
|
|
190
|
+
messageId: opts.assistantMessageId,
|
|
191
|
+
partId: errorPartId,
|
|
192
|
+
error: displayMessage,
|
|
193
|
+
errorType: errorPayload.type,
|
|
194
|
+
details: errorPayload.details,
|
|
195
|
+
isAborted: false,
|
|
196
|
+
autoCompacted: isPromptTooLong && !opts.isCompactCommand,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { getDb } from '@agi-cli/database';
|
|
2
|
+
import { messages, messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import { estimateModelCostUsd } from '@agi-cli/sdk';
|
|
6
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
7
|
+
import {
|
|
8
|
+
pruneSession,
|
|
9
|
+
isOverflow,
|
|
10
|
+
getModelLimits,
|
|
11
|
+
type TokenUsage,
|
|
12
|
+
markSessionCompacted,
|
|
13
|
+
} from '../message/compaction.ts';
|
|
14
|
+
import { debugLog } from '../debug/index.ts';
|
|
15
|
+
import type { FinishEvent } from './types.ts';
|
|
16
|
+
|
|
17
|
+
export function createFinishHandler(
|
|
18
|
+
opts: RunOpts,
|
|
19
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
20
|
+
completeAssistantMessageFn: (
|
|
21
|
+
fin: FinishEvent,
|
|
22
|
+
opts: RunOpts,
|
|
23
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
24
|
+
) => Promise<void>,
|
|
25
|
+
) {
|
|
26
|
+
return async (fin: FinishEvent) => {
|
|
27
|
+
try {
|
|
28
|
+
await completeAssistantMessageFn(fin, opts, db);
|
|
29
|
+
} catch {}
|
|
30
|
+
|
|
31
|
+
if (opts.isCompactCommand && fin.finishReason !== 'error') {
|
|
32
|
+
const assistantParts = await db
|
|
33
|
+
.select()
|
|
34
|
+
.from(messageParts)
|
|
35
|
+
.where(eq(messageParts.messageId, opts.assistantMessageId));
|
|
36
|
+
const hasTextContent = assistantParts.some(
|
|
37
|
+
(p) => p.type === 'text' && p.content && p.content !== '{"text":""}',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!hasTextContent) {
|
|
41
|
+
debugLog(
|
|
42
|
+
'[stream-handlers] /compact finished but no summary generated, skipping compaction marking',
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
try {
|
|
46
|
+
debugLog(
|
|
47
|
+
`[stream-handlers] /compact complete, marking session compacted`,
|
|
48
|
+
);
|
|
49
|
+
const result = await markSessionCompacted(
|
|
50
|
+
db,
|
|
51
|
+
opts.sessionId,
|
|
52
|
+
opts.assistantMessageId,
|
|
53
|
+
);
|
|
54
|
+
debugLog(
|
|
55
|
+
`[stream-handlers] Compacted ${result.compacted} parts, saved ~${result.saved} tokens`,
|
|
56
|
+
);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
debugLog(
|
|
59
|
+
`[stream-handlers] Compaction failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sessRows = await db
|
|
66
|
+
.select()
|
|
67
|
+
.from(messages)
|
|
68
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
69
|
+
|
|
70
|
+
const usage = sessRows[0]
|
|
71
|
+
? {
|
|
72
|
+
inputTokens: Number(sessRows[0].promptTokens ?? 0),
|
|
73
|
+
outputTokens: Number(sessRows[0].completionTokens ?? 0),
|
|
74
|
+
totalTokens: Number(sessRows[0].totalTokens ?? 0),
|
|
75
|
+
cachedInputTokens: Number(sessRows[0].cachedInputTokens ?? 0),
|
|
76
|
+
}
|
|
77
|
+
: fin.usage;
|
|
78
|
+
|
|
79
|
+
const costUsd = usage
|
|
80
|
+
? estimateModelCostUsd(opts.provider, opts.model, usage)
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
if (usage) {
|
|
84
|
+
try {
|
|
85
|
+
const limits = getModelLimits(opts.provider, opts.model);
|
|
86
|
+
if (limits) {
|
|
87
|
+
const tokenUsage: TokenUsage = {
|
|
88
|
+
input: usage.inputTokens ?? 0,
|
|
89
|
+
output: usage.outputTokens ?? 0,
|
|
90
|
+
cacheRead:
|
|
91
|
+
(usage as { cachedInputTokens?: number }).cachedInputTokens ?? 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (isOverflow(tokenUsage, limits)) {
|
|
95
|
+
debugLog(
|
|
96
|
+
`[stream-handlers] Context overflow detected, triggering prune for session ${opts.sessionId}`,
|
|
97
|
+
);
|
|
98
|
+
pruneSession(db, opts.sessionId).catch((err) => {
|
|
99
|
+
debugLog(
|
|
100
|
+
`[stream-handlers] Prune failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
debugLog(
|
|
107
|
+
`[stream-handlers] Overflow check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
publish({
|
|
113
|
+
type: 'message.completed',
|
|
114
|
+
sessionId: opts.sessionId,
|
|
115
|
+
payload: {
|
|
116
|
+
id: opts.assistantMessageId,
|
|
117
|
+
usage,
|
|
118
|
+
costUsd,
|
|
119
|
+
finishReason: fin.finishReason,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createStepFinishHandler } from './step-finish.ts';
|
|
2
|
+
export { createErrorHandler } from './error-handler.ts';
|
|
3
|
+
export { createAbortHandler } from './abort-handler.ts';
|
|
4
|
+
export { createFinishHandler } from './finish-handler.ts';
|
|
5
|
+
export type { StepFinishEvent, FinishEvent, AbortEvent } from './types.ts';
|