@agi-cli/server 0.1.123 → 0.1.125
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 +4 -0
- package/src/routes/research.ts +423 -0
- package/src/routes/sessions.ts +33 -2
- package/src/runtime/agent/registry.ts +18 -0
- package/src/runtime/agent/runner-setup.ts +16 -0
- package/src/runtime/message/compaction-auto.ts +32 -3
- package/src/tools/database/get-parent-session.ts +178 -0
- package/src/tools/database/get-session-context.ts +156 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +124 -0
- package/src/tools/database/search-history.ts +135 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.125",
|
|
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.125",
|
|
33
|
+
"@agi-cli/database": "0.1.125",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { registerGitRoutes } from './routes/git/index.ts';
|
|
|
15
15
|
import { registerTerminalsRoutes } from './routes/terminals.ts';
|
|
16
16
|
import { registerSessionFilesRoutes } from './routes/session-files.ts';
|
|
17
17
|
import { registerBranchRoutes } from './routes/branch.ts';
|
|
18
|
+
import { registerResearchRoutes } from './routes/research.ts';
|
|
18
19
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
19
20
|
|
|
20
21
|
const globalTerminalManager = new TerminalManager();
|
|
@@ -66,6 +67,7 @@ function initApp() {
|
|
|
66
67
|
registerTerminalsRoutes(app, globalTerminalManager);
|
|
67
68
|
registerSessionFilesRoutes(app);
|
|
68
69
|
registerBranchRoutes(app);
|
|
70
|
+
registerResearchRoutes(app);
|
|
69
71
|
|
|
70
72
|
return app;
|
|
71
73
|
}
|
|
@@ -133,6 +135,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
133
135
|
registerTerminalsRoutes(honoApp, globalTerminalManager);
|
|
134
136
|
registerSessionFilesRoutes(honoApp);
|
|
135
137
|
registerBranchRoutes(honoApp);
|
|
138
|
+
registerResearchRoutes(honoApp);
|
|
136
139
|
|
|
137
140
|
return honoApp;
|
|
138
141
|
}
|
|
@@ -228,6 +231,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
228
231
|
registerTerminalsRoutes(honoApp, globalTerminalManager);
|
|
229
232
|
registerSessionFilesRoutes(honoApp);
|
|
230
233
|
registerBranchRoutes(honoApp);
|
|
234
|
+
registerResearchRoutes(honoApp);
|
|
231
235
|
|
|
232
236
|
return honoApp;
|
|
233
237
|
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { loadConfig } from '@agi-cli/sdk';
|
|
3
|
+
import { getDb } from '@agi-cli/database';
|
|
4
|
+
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
5
|
+
import { desc, eq, and, asc, count } from 'drizzle-orm';
|
|
6
|
+
import type { ProviderId } from '@agi-cli/sdk';
|
|
7
|
+
import { isProviderId, catalog } from '@agi-cli/sdk';
|
|
8
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
9
|
+
import { logger } from '@agi-cli/sdk';
|
|
10
|
+
import { publish } from '../events/bus.ts';
|
|
11
|
+
|
|
12
|
+
export function registerResearchRoutes(app: Hono) {
|
|
13
|
+
app.get('/v1/sessions/:parentId/research', async (c) => {
|
|
14
|
+
const parentId = c.req.param('parentId');
|
|
15
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
16
|
+
const cfg = await loadConfig(projectRoot);
|
|
17
|
+
const db = await getDb(cfg.projectRoot);
|
|
18
|
+
|
|
19
|
+
const parentRows = await db
|
|
20
|
+
.select()
|
|
21
|
+
.from(sessions)
|
|
22
|
+
.where(eq(sessions.id, parentId))
|
|
23
|
+
.limit(1);
|
|
24
|
+
|
|
25
|
+
if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
|
|
26
|
+
return c.json({ error: 'Parent session not found' }, 404);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const researchRows = await db
|
|
30
|
+
.select({
|
|
31
|
+
id: sessions.id,
|
|
32
|
+
title: sessions.title,
|
|
33
|
+
createdAt: sessions.createdAt,
|
|
34
|
+
lastActiveAt: sessions.lastActiveAt,
|
|
35
|
+
provider: sessions.provider,
|
|
36
|
+
model: sessions.model,
|
|
37
|
+
})
|
|
38
|
+
.from(sessions)
|
|
39
|
+
.where(
|
|
40
|
+
and(
|
|
41
|
+
eq(sessions.parentSessionId, parentId),
|
|
42
|
+
eq(sessions.sessionType, 'research'),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
.orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt));
|
|
46
|
+
|
|
47
|
+
const sessionsWithCounts = await Promise.all(
|
|
48
|
+
researchRows.map(async (row) => {
|
|
49
|
+
const msgCount = await db
|
|
50
|
+
.select({ count: count() })
|
|
51
|
+
.from(messages)
|
|
52
|
+
.where(eq(messages.sessionId, row.id));
|
|
53
|
+
return {
|
|
54
|
+
...row,
|
|
55
|
+
messageCount: msgCount[0]?.count ?? 0,
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return c.json({ sessions: sessionsWithCounts });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
app.post('/v1/sessions/:parentId/research', async (c) => {
|
|
64
|
+
const parentId = c.req.param('parentId');
|
|
65
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
66
|
+
const cfg = await loadConfig(projectRoot);
|
|
67
|
+
const db = await getDb(cfg.projectRoot);
|
|
68
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
69
|
+
string,
|
|
70
|
+
unknown
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
const parentRows = await db
|
|
74
|
+
.select()
|
|
75
|
+
.from(sessions)
|
|
76
|
+
.where(eq(sessions.id, parentId))
|
|
77
|
+
.limit(1);
|
|
78
|
+
|
|
79
|
+
if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
|
|
80
|
+
return c.json({ error: 'Parent session not found' }, 404);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parent = parentRows[0];
|
|
84
|
+
|
|
85
|
+
const providerCandidate =
|
|
86
|
+
typeof body.provider === 'string' ? body.provider : undefined;
|
|
87
|
+
const provider: ProviderId = (() => {
|
|
88
|
+
if (providerCandidate && isProviderId(providerCandidate))
|
|
89
|
+
return providerCandidate;
|
|
90
|
+
return parent.provider as ProviderId;
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
const modelCandidate =
|
|
94
|
+
typeof body.model === 'string' ? body.model.trim() : undefined;
|
|
95
|
+
const model = modelCandidate?.length ? modelCandidate : parent.model;
|
|
96
|
+
|
|
97
|
+
const id = crypto.randomUUID();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const title = typeof body.title === 'string' ? body.title : null;
|
|
100
|
+
|
|
101
|
+
const row = {
|
|
102
|
+
id,
|
|
103
|
+
title,
|
|
104
|
+
agent: 'research',
|
|
105
|
+
provider,
|
|
106
|
+
model,
|
|
107
|
+
projectPath: cfg.projectRoot,
|
|
108
|
+
createdAt: now,
|
|
109
|
+
lastActiveAt: now,
|
|
110
|
+
parentSessionId: parentId,
|
|
111
|
+
sessionType: 'research',
|
|
112
|
+
totalInputTokens: null,
|
|
113
|
+
totalOutputTokens: null,
|
|
114
|
+
totalCachedTokens: null,
|
|
115
|
+
totalReasoningTokens: null,
|
|
116
|
+
totalToolTimeMs: null,
|
|
117
|
+
toolCountsJson: null,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await db.insert(sessions).values(row);
|
|
122
|
+
publish({ type: 'session.created', sessionId: id, payload: row });
|
|
123
|
+
return c.json({ session: row, parentSessionId: parentId }, 201);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.error('Failed to create research session', err);
|
|
126
|
+
const errorResponse = serializeError(err);
|
|
127
|
+
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
app.delete('/v1/research/:researchId', async (c) => {
|
|
132
|
+
const researchId = c.req.param('researchId');
|
|
133
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
134
|
+
const cfg = await loadConfig(projectRoot);
|
|
135
|
+
const db = await getDb(cfg.projectRoot);
|
|
136
|
+
|
|
137
|
+
const rows = await db
|
|
138
|
+
.select()
|
|
139
|
+
.from(sessions)
|
|
140
|
+
.where(eq(sessions.id, researchId))
|
|
141
|
+
.limit(1);
|
|
142
|
+
|
|
143
|
+
if (!rows.length) {
|
|
144
|
+
return c.json({ error: 'Research session not found' }, 404);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const session = rows[0];
|
|
148
|
+
if (session.projectPath !== cfg.projectRoot) {
|
|
149
|
+
return c.json(
|
|
150
|
+
{ error: 'Research session not found in this project' },
|
|
151
|
+
404,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (session.sessionType !== 'research') {
|
|
156
|
+
return c.json({ error: 'Session is not a research session' }, 400);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await db.delete(sessions).where(eq(sessions.id, researchId));
|
|
160
|
+
publish({
|
|
161
|
+
type: 'session.deleted',
|
|
162
|
+
sessionId: researchId,
|
|
163
|
+
payload: { id: researchId },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return c.json({ success: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
app.post('/v1/sessions/:parentId/inject', async (c) => {
|
|
170
|
+
const parentId = c.req.param('parentId');
|
|
171
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
172
|
+
const cfg = await loadConfig(projectRoot);
|
|
173
|
+
const db = await getDb(cfg.projectRoot);
|
|
174
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
175
|
+
string,
|
|
176
|
+
unknown
|
|
177
|
+
>;
|
|
178
|
+
|
|
179
|
+
const researchSessionId =
|
|
180
|
+
typeof body.researchSessionId === 'string' ? body.researchSessionId : '';
|
|
181
|
+
const label =
|
|
182
|
+
typeof body.label === 'string' ? body.label : 'Research context';
|
|
183
|
+
|
|
184
|
+
if (!researchSessionId) {
|
|
185
|
+
return c.json({ error: 'researchSessionId is required' }, 400);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const [parentRows, researchRows] = await Promise.all([
|
|
189
|
+
db.select().from(sessions).where(eq(sessions.id, parentId)).limit(1),
|
|
190
|
+
db
|
|
191
|
+
.select()
|
|
192
|
+
.from(sessions)
|
|
193
|
+
.where(eq(sessions.id, researchSessionId))
|
|
194
|
+
.limit(1),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
|
|
198
|
+
return c.json({ error: 'Parent session not found' }, 404);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!researchRows.length || researchRows[0].sessionType !== 'research') {
|
|
202
|
+
return c.json({ error: 'Research session not found' }, 404);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const researchSession = researchRows[0];
|
|
206
|
+
|
|
207
|
+
const researchMessages = await db
|
|
208
|
+
.select({
|
|
209
|
+
id: messages.id,
|
|
210
|
+
role: messages.role,
|
|
211
|
+
createdAt: messages.createdAt,
|
|
212
|
+
})
|
|
213
|
+
.from(messages)
|
|
214
|
+
.where(eq(messages.sessionId, researchSessionId))
|
|
215
|
+
.orderBy(asc(messages.createdAt));
|
|
216
|
+
|
|
217
|
+
let contextContent = '';
|
|
218
|
+
for (const msg of researchMessages) {
|
|
219
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
220
|
+
const parts = await db
|
|
221
|
+
.select({ type: messageParts.type, content: messageParts.content })
|
|
222
|
+
.from(messageParts)
|
|
223
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
224
|
+
.orderBy(asc(messageParts.index));
|
|
225
|
+
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (part.type === 'text' && part.content) {
|
|
228
|
+
contextContent += `[${msg.role}]: ${part.content}\n\n`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const injectedContext = `<research-context from="${researchSessionId}" label="${label}" injected-at="${new Date().toISOString()}">\n${contextContent}</research-context>`;
|
|
235
|
+
|
|
236
|
+
const msgId = crypto.randomUUID();
|
|
237
|
+
const partId = crypto.randomUUID();
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
|
|
240
|
+
await db.insert(messages).values({
|
|
241
|
+
id: msgId,
|
|
242
|
+
sessionId: parentId,
|
|
243
|
+
role: 'system',
|
|
244
|
+
status: 'complete',
|
|
245
|
+
agent: parentRows[0].agent,
|
|
246
|
+
provider: parentRows[0].provider,
|
|
247
|
+
model: parentRows[0].model,
|
|
248
|
+
createdAt: now,
|
|
249
|
+
completedAt: now,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await db.insert(messageParts).values({
|
|
253
|
+
id: partId,
|
|
254
|
+
messageId: msgId,
|
|
255
|
+
index: 0,
|
|
256
|
+
type: 'text',
|
|
257
|
+
content: injectedContext,
|
|
258
|
+
agent: parentRows[0].agent,
|
|
259
|
+
provider: parentRows[0].provider,
|
|
260
|
+
model: parentRows[0].model,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await db
|
|
264
|
+
.update(sessions)
|
|
265
|
+
.set({ lastActiveAt: now })
|
|
266
|
+
.where(eq(sessions.id, parentId));
|
|
267
|
+
|
|
268
|
+
publish({
|
|
269
|
+
type: 'message.created',
|
|
270
|
+
sessionId: parentId,
|
|
271
|
+
payload: {
|
|
272
|
+
id: msgId,
|
|
273
|
+
role: 'system',
|
|
274
|
+
content: injectedContext,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return c.json({
|
|
279
|
+
injectedMessageId: msgId,
|
|
280
|
+
tokenEstimate: Math.ceil(injectedContext.length / 4),
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
app.post('/v1/research/:researchId/export', async (c) => {
|
|
285
|
+
const researchId = c.req.param('researchId');
|
|
286
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
287
|
+
const cfg = await loadConfig(projectRoot);
|
|
288
|
+
const db = await getDb(cfg.projectRoot);
|
|
289
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
290
|
+
string,
|
|
291
|
+
unknown
|
|
292
|
+
>;
|
|
293
|
+
|
|
294
|
+
const researchRows = await db
|
|
295
|
+
.select()
|
|
296
|
+
.from(sessions)
|
|
297
|
+
.where(eq(sessions.id, researchId))
|
|
298
|
+
.limit(1);
|
|
299
|
+
|
|
300
|
+
if (!researchRows.length || researchRows[0].sessionType !== 'research') {
|
|
301
|
+
return c.json({ error: 'Research session not found' }, 404);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const researchSession = researchRows[0];
|
|
305
|
+
|
|
306
|
+
if (researchSession.projectPath !== cfg.projectRoot) {
|
|
307
|
+
return c.json({ error: 'Research session not in this project' }, 404);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const providerCandidate =
|
|
311
|
+
typeof body.provider === 'string' ? body.provider : undefined;
|
|
312
|
+
const provider: ProviderId = (() => {
|
|
313
|
+
if (providerCandidate && isProviderId(providerCandidate))
|
|
314
|
+
return providerCandidate;
|
|
315
|
+
return cfg.defaults.provider;
|
|
316
|
+
})();
|
|
317
|
+
|
|
318
|
+
const modelCandidate =
|
|
319
|
+
typeof body.model === 'string' ? body.model.trim() : undefined;
|
|
320
|
+
const model = modelCandidate?.length ? modelCandidate : cfg.defaults.model;
|
|
321
|
+
|
|
322
|
+
const agentCandidate =
|
|
323
|
+
typeof body.agent === 'string' ? body.agent.trim() : undefined;
|
|
324
|
+
const agent = agentCandidate?.length ? agentCandidate : cfg.defaults.agent;
|
|
325
|
+
|
|
326
|
+
const researchMessages = await db
|
|
327
|
+
.select({
|
|
328
|
+
id: messages.id,
|
|
329
|
+
role: messages.role,
|
|
330
|
+
createdAt: messages.createdAt,
|
|
331
|
+
})
|
|
332
|
+
.from(messages)
|
|
333
|
+
.where(eq(messages.sessionId, researchId))
|
|
334
|
+
.orderBy(asc(messages.createdAt));
|
|
335
|
+
|
|
336
|
+
let contextContent = '';
|
|
337
|
+
for (const msg of researchMessages) {
|
|
338
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
339
|
+
const parts = await db
|
|
340
|
+
.select({ type: messageParts.type, content: messageParts.content })
|
|
341
|
+
.from(messageParts)
|
|
342
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
343
|
+
.orderBy(asc(messageParts.index));
|
|
344
|
+
|
|
345
|
+
for (const part of parts) {
|
|
346
|
+
if (part.type === 'text' && part.content) {
|
|
347
|
+
contextContent += `[${msg.role}]: ${part.content}\n\n`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const injectedContext = `<research-context from="${researchId}" exported-at="${new Date().toISOString()}">\n${contextContent}</research-context>`;
|
|
354
|
+
|
|
355
|
+
const newSessionId = crypto.randomUUID();
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
|
|
358
|
+
await db.insert(sessions).values({
|
|
359
|
+
id: newSessionId,
|
|
360
|
+
title: researchSession.title ? `From: ${researchSession.title}` : null,
|
|
361
|
+
agent,
|
|
362
|
+
provider,
|
|
363
|
+
model,
|
|
364
|
+
projectPath: cfg.projectRoot,
|
|
365
|
+
createdAt: now,
|
|
366
|
+
lastActiveAt: now,
|
|
367
|
+
parentSessionId: null,
|
|
368
|
+
sessionType: 'main',
|
|
369
|
+
totalInputTokens: null,
|
|
370
|
+
totalOutputTokens: null,
|
|
371
|
+
totalCachedTokens: null,
|
|
372
|
+
totalReasoningTokens: null,
|
|
373
|
+
totalToolTimeMs: null,
|
|
374
|
+
toolCountsJson: null,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const msgId = crypto.randomUUID();
|
|
378
|
+
const partId = crypto.randomUUID();
|
|
379
|
+
|
|
380
|
+
await db.insert(messages).values({
|
|
381
|
+
id: msgId,
|
|
382
|
+
sessionId: newSessionId,
|
|
383
|
+
role: 'system',
|
|
384
|
+
status: 'complete',
|
|
385
|
+
agent,
|
|
386
|
+
provider,
|
|
387
|
+
model,
|
|
388
|
+
createdAt: now,
|
|
389
|
+
completedAt: now,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await db.insert(messageParts).values({
|
|
393
|
+
id: partId,
|
|
394
|
+
messageId: msgId,
|
|
395
|
+
index: 0,
|
|
396
|
+
type: 'text',
|
|
397
|
+
content: injectedContext,
|
|
398
|
+
agent,
|
|
399
|
+
provider,
|
|
400
|
+
model,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
publish({
|
|
404
|
+
type: 'session.created',
|
|
405
|
+
sessionId: newSessionId,
|
|
406
|
+
payload: { id: newSessionId },
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const newSession = await db
|
|
410
|
+
.select()
|
|
411
|
+
.from(sessions)
|
|
412
|
+
.where(eq(sessions.id, newSessionId))
|
|
413
|
+
.limit(1);
|
|
414
|
+
|
|
415
|
+
return c.json(
|
|
416
|
+
{
|
|
417
|
+
newSession: newSession[0],
|
|
418
|
+
injectedContext,
|
|
419
|
+
},
|
|
420
|
+
201,
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Hono } from 'hono';
|
|
|
2
2
|
import { loadConfig } from '@agi-cli/sdk';
|
|
3
3
|
import { getDb } from '@agi-cli/database';
|
|
4
4
|
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
5
|
-
import { desc, eq, and, inArray } from 'drizzle-orm';
|
|
5
|
+
import { desc, eq, and, or, isNull, ne, inArray } from 'drizzle-orm';
|
|
6
6
|
import type { ProviderId } from '@agi-cli/sdk';
|
|
7
7
|
import { isProviderId, catalog } from '@agi-cli/sdk';
|
|
8
8
|
import { resolveAgentConfig } from '../runtime/agent/registry.ts';
|
|
@@ -20,7 +20,12 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
20
20
|
const rows = await db
|
|
21
21
|
.select()
|
|
22
22
|
.from(sessions)
|
|
23
|
-
.where(
|
|
23
|
+
.where(
|
|
24
|
+
and(
|
|
25
|
+
eq(sessions.projectPath, cfg.projectRoot),
|
|
26
|
+
or(eq(sessions.sessionType, 'main'), isNull(sessions.sessionType)),
|
|
27
|
+
),
|
|
28
|
+
)
|
|
24
29
|
.orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt));
|
|
25
30
|
const normalized = rows.map((r) => {
|
|
26
31
|
let counts: Record<string, unknown> | undefined;
|
|
@@ -298,6 +303,32 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
298
303
|
});
|
|
299
304
|
}
|
|
300
305
|
|
|
306
|
+
// If not queued or running, try to delete directly from database
|
|
307
|
+
// This handles system messages (like injected research context)
|
|
308
|
+
try {
|
|
309
|
+
const existingMsg = await db
|
|
310
|
+
.select()
|
|
311
|
+
.from(messages)
|
|
312
|
+
.where(
|
|
313
|
+
and(eq(messages.id, messageId), eq(messages.sessionId, sessionId)),
|
|
314
|
+
)
|
|
315
|
+
.limit(1);
|
|
316
|
+
|
|
317
|
+
if (existingMsg.length > 0) {
|
|
318
|
+
// Delete message parts first (foreign key constraint)
|
|
319
|
+
await db
|
|
320
|
+
.delete(messageParts)
|
|
321
|
+
.where(eq(messageParts.messageId, messageId));
|
|
322
|
+
// Delete message
|
|
323
|
+
await db.delete(messages).where(eq(messages.id, messageId));
|
|
324
|
+
|
|
325
|
+
return c.json({ success: true, removed: true, wasStored: true });
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
logger.error('Failed to delete message from DB', err);
|
|
329
|
+
return c.json({ success: false, error: 'Failed to delete message' }, 500);
|
|
330
|
+
}
|
|
331
|
+
|
|
301
332
|
return c.json({ success: false, removed: false }, 404);
|
|
302
333
|
});
|
|
303
334
|
}
|
|
@@ -15,6 +15,9 @@ import AGENT_PLAN from '@agi-cli/sdk/prompts/agents/plan.txt' with {
|
|
|
15
15
|
import AGENT_GENERAL from '@agi-cli/sdk/prompts/agents/general.txt' with {
|
|
16
16
|
type: 'text',
|
|
17
17
|
};
|
|
18
|
+
import AGENT_RESEARCH from '@agi-cli/sdk/prompts/agents/research.txt' with {
|
|
19
|
+
type: 'text',
|
|
20
|
+
};
|
|
18
21
|
|
|
19
22
|
export type AgentConfig = {
|
|
20
23
|
name: string;
|
|
@@ -140,6 +143,20 @@ const defaultToolExtras: Record<string, string[]> = {
|
|
|
140
143
|
],
|
|
141
144
|
git: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
142
145
|
commit: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
146
|
+
research: [
|
|
147
|
+
'read',
|
|
148
|
+
'ls',
|
|
149
|
+
'tree',
|
|
150
|
+
'ripgrep',
|
|
151
|
+
'websearch',
|
|
152
|
+
'update_todos',
|
|
153
|
+
'query_sessions',
|
|
154
|
+
'query_messages',
|
|
155
|
+
'get_session_context',
|
|
156
|
+
'search_history',
|
|
157
|
+
'get_parent_session',
|
|
158
|
+
'present_action',
|
|
159
|
+
],
|
|
143
160
|
};
|
|
144
161
|
|
|
145
162
|
export function defaultToolsForAgent(name: string): string[] {
|
|
@@ -288,6 +305,7 @@ export async function resolveAgentConfig(
|
|
|
288
305
|
if (n === 'build') return AGENT_BUILD;
|
|
289
306
|
if (n === 'plan') return AGENT_PLAN;
|
|
290
307
|
if (n === 'general') return AGENT_GENERAL;
|
|
308
|
+
if (n === 'research') return AGENT_RESEARCH;
|
|
291
309
|
return undefined;
|
|
292
310
|
};
|
|
293
311
|
const candidate = byName(name)?.trim();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../prompt/builder.ts';
|
|
11
11
|
import { discoverProjectTools } from '@agi-cli/sdk';
|
|
12
12
|
import { adaptTools } from '../../tools/adapter.ts';
|
|
13
|
+
import { buildDatabaseTools } from '../../tools/database/index.ts';
|
|
13
14
|
import { debugLog, time } from '../debug/index.ts';
|
|
14
15
|
import { buildHistoryMessages } from '../message/history-builder.ts';
|
|
15
16
|
import { getMaxOutputTokens } from '../utils/token.ts';
|
|
@@ -170,6 +171,21 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
170
171
|
|
|
171
172
|
const toolsTimer = time('runner:discoverTools');
|
|
172
173
|
const allTools = await discoverProjectTools(cfg.projectRoot);
|
|
174
|
+
|
|
175
|
+
if (opts.agent === 'research') {
|
|
176
|
+
// Get parent session ID for research sessions
|
|
177
|
+
const currentSession = sessionRows[0];
|
|
178
|
+
const parentSessionId = currentSession?.parentSessionId ?? null;
|
|
179
|
+
|
|
180
|
+
const dbTools = buildDatabaseTools(cfg.projectRoot, parentSessionId);
|
|
181
|
+
for (const dt of dbTools) {
|
|
182
|
+
allTools.push(dt);
|
|
183
|
+
}
|
|
184
|
+
debugLog(
|
|
185
|
+
`[tools] Added ${dbTools.length} database tools for research agent (parent: ${parentSessionId ?? 'none'})`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
toolsTimer.end({ count: allTools.length });
|
|
174
190
|
const allowedNames = new Set([...(agentCfg.tools || []), 'finish']);
|
|
175
191
|
const gated = allTools.filter((tool) => allowedNames.has(tool.name));
|
|
@@ -3,6 +3,8 @@ import { messageParts } from '@agi-cli/database/schema';
|
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
4
|
import { streamText } from 'ai';
|
|
5
5
|
import { resolveModel } from '../provider/index.ts';
|
|
6
|
+
import { getAuth } from '@agi-cli/sdk';
|
|
7
|
+
import { getProviderSpoofPrompt } from '../prompt/builder.ts';
|
|
6
8
|
import { loadConfig } from '@agi-cli/sdk';
|
|
7
9
|
import { debugLog } from '../debug/index.ts';
|
|
8
10
|
import { getModelLimits } from './compaction-limits.ts';
|
|
@@ -52,12 +54,40 @@ export async function performAutoCompaction(
|
|
|
52
54
|
debugLog(
|
|
53
55
|
`[compaction] Using session model ${provider}/${modelId} for auto-compaction`,
|
|
54
56
|
);
|
|
57
|
+
|
|
58
|
+
const auth = await getAuth(
|
|
59
|
+
provider as
|
|
60
|
+
| 'anthropic'
|
|
61
|
+
| 'openai'
|
|
62
|
+
| 'google'
|
|
63
|
+
| 'openrouter'
|
|
64
|
+
| 'opencode'
|
|
65
|
+
| 'solforge'
|
|
66
|
+
| 'zai'
|
|
67
|
+
| 'zai-coding',
|
|
68
|
+
cfg.projectRoot,
|
|
69
|
+
);
|
|
70
|
+
const needsSpoof = auth?.type === 'oauth';
|
|
71
|
+
const spoofPrompt = needsSpoof
|
|
72
|
+
? getProviderSpoofPrompt(provider as 'anthropic' | 'openai')
|
|
73
|
+
: undefined;
|
|
74
|
+
|
|
75
|
+
debugLog(
|
|
76
|
+
`[compaction] OAuth mode: ${needsSpoof}, spoof: ${spoofPrompt ? 'yes' : 'no'}`,
|
|
77
|
+
);
|
|
78
|
+
|
|
55
79
|
const model = await resolveModel(
|
|
56
80
|
provider as Parameters<typeof resolveModel>[0],
|
|
57
81
|
modelId,
|
|
58
82
|
cfg,
|
|
59
83
|
);
|
|
60
84
|
|
|
85
|
+
const compactionPrompt = getCompactionSystemPrompt();
|
|
86
|
+
const systemPrompt = spoofPrompt ? spoofPrompt : compactionPrompt;
|
|
87
|
+
const userInstructions = spoofPrompt
|
|
88
|
+
? `${compactionPrompt}\n\nIMPORTANT: Generate a comprehensive summary. This will replace the detailed conversation history.`
|
|
89
|
+
: 'IMPORTANT: Generate a comprehensive summary. This will replace the detailed conversation history.';
|
|
90
|
+
|
|
61
91
|
const compactPartId = crypto.randomUUID();
|
|
62
92
|
const now = Date.now();
|
|
63
93
|
|
|
@@ -74,14 +104,13 @@ export async function performAutoCompaction(
|
|
|
74
104
|
startedAt: now,
|
|
75
105
|
});
|
|
76
106
|
|
|
77
|
-
const prompt = getCompactionSystemPrompt();
|
|
78
107
|
const result = streamText({
|
|
79
108
|
model,
|
|
80
|
-
system:
|
|
109
|
+
system: systemPrompt,
|
|
81
110
|
messages: [
|
|
82
111
|
{
|
|
83
112
|
role: 'user',
|
|
84
|
-
content:
|
|
113
|
+
content: `${userInstructions}\n\nPlease summarize this conversation:\n\n<conversation-to-summarize>\n${context}\n</conversation-to-summarize>`,
|
|
85
114
|
},
|
|
86
115
|
],
|
|
87
116
|
maxOutputTokens: 2000,
|