@agi-cli/server 0.1.124 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/server",
3
- "version": "0.1.124",
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.124",
33
- "@agi-cli/database": "0.1.124",
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
+ }
@@ -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(eq(sessions.projectPath, cfg.projectRoot))
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: `${prompt}\n\nIMPORTANT: Generate a comprehensive summary. This will replace the detailed conversation history.`,
109
+ system: systemPrompt,
81
110
  messages: [
82
111
  {
83
112
  role: 'user',
84
- content: `Please summarize this conversation:\n\n<conversation-to-summarize>\n${context}\n</conversation-to-summarize>`,
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,