@agi-cli/server 0.1.73 → 0.1.75
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/routes/session-messages.ts +21 -0
- package/src/runtime/api-error.ts +8 -7
- package/src/runtime/message-service.ts +107 -154
- package/src/runtime/prompt.ts +11 -0
- package/src/runtime/runner.ts +85 -17
- package/src/runtime/session-queue.ts +10 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
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.75",
|
|
33
|
+
"@agi-cli/database": "0.1.75",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
|
@@ -85,6 +85,17 @@ export function registerSessionMessagesRoutes(app: Hono) {
|
|
|
85
85
|
const db = await getDb(cfg.projectRoot);
|
|
86
86
|
const sessionId = c.req.param('id');
|
|
87
87
|
const body = await c.req.json().catch(() => ({}));
|
|
88
|
+
|
|
89
|
+
// DEBUG: Log received body
|
|
90
|
+
logger.info('[API] Received message request', {
|
|
91
|
+
sessionId,
|
|
92
|
+
hasContent: !!body?.content,
|
|
93
|
+
hasUserContext: !!body?.userContext,
|
|
94
|
+
userContext: body?.userContext
|
|
95
|
+
? `${String(body.userContext).substring(0, 50)}...`
|
|
96
|
+
: 'NONE',
|
|
97
|
+
});
|
|
98
|
+
|
|
88
99
|
// Load session to inherit its provider/model/agent by default
|
|
89
100
|
const sessionRows = await db
|
|
90
101
|
.select()
|
|
@@ -99,6 +110,15 @@ export function registerSessionMessagesRoutes(app: Hono) {
|
|
|
99
110
|
const modelName = body?.model ?? sess.model ?? cfg.defaults.model;
|
|
100
111
|
const agent = body?.agent ?? sess.agent ?? cfg.defaults.agent;
|
|
101
112
|
const content = body?.content ?? '';
|
|
113
|
+
const userContext = body?.userContext;
|
|
114
|
+
|
|
115
|
+
// DEBUG: Log extracted userContext
|
|
116
|
+
logger.info('[API] Extracted userContext', {
|
|
117
|
+
userContext: userContext
|
|
118
|
+
? `${String(userContext).substring(0, 50)}...`
|
|
119
|
+
: 'NONE',
|
|
120
|
+
typeOf: typeof userContext,
|
|
121
|
+
});
|
|
102
122
|
|
|
103
123
|
// Validate model capabilities if tools are allowed for this agent
|
|
104
124
|
const wantsToolCalls = true; // agent toolset may be non-empty
|
|
@@ -131,6 +151,7 @@ export function registerSessionMessagesRoutes(app: Hono) {
|
|
|
131
151
|
model: modelName,
|
|
132
152
|
content,
|
|
133
153
|
oneShot: Boolean(body?.oneShot),
|
|
154
|
+
userContext,
|
|
134
155
|
});
|
|
135
156
|
return c.json({ messageId: assistantMessageId }, 202);
|
|
136
157
|
} catch (error) {
|
package/src/runtime/api-error.ts
CHANGED
|
@@ -70,8 +70,14 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
70
70
|
const payload = toErrorPayload(err);
|
|
71
71
|
|
|
72
72
|
// Determine HTTP status code
|
|
73
|
-
|
|
74
|
-
if
|
|
73
|
+
// Default to 400 for generic errors (client errors)
|
|
74
|
+
// Only use 500 if explicitly set or for APIError instances without a status
|
|
75
|
+
let status = 400;
|
|
76
|
+
|
|
77
|
+
// Handle APIError instances first
|
|
78
|
+
if (err instanceof APIError) {
|
|
79
|
+
status = err.status;
|
|
80
|
+
} else if (err && typeof err === 'object') {
|
|
75
81
|
const errObj = err as Record<string, unknown>;
|
|
76
82
|
if (typeof errObj.status === 'number') {
|
|
77
83
|
status = errObj.status;
|
|
@@ -86,11 +92,6 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
// Handle APIError instances
|
|
90
|
-
if (err instanceof APIError) {
|
|
91
|
-
status = err.status;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
95
|
// Extract code if available
|
|
95
96
|
let code: string | undefined;
|
|
96
97
|
if (err && typeof err === 'object') {
|
|
@@ -20,13 +20,29 @@ type DispatchOptions = {
|
|
|
20
20
|
model: string;
|
|
21
21
|
content: string;
|
|
22
22
|
oneShot?: boolean;
|
|
23
|
+
userContext?: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export async function dispatchAssistantMessage(
|
|
26
27
|
options: DispatchOptions,
|
|
27
28
|
): Promise<{ assistantMessageId: string }> {
|
|
28
|
-
const {
|
|
29
|
-
|
|
29
|
+
const {
|
|
30
|
+
cfg,
|
|
31
|
+
db,
|
|
32
|
+
session,
|
|
33
|
+
agent,
|
|
34
|
+
provider,
|
|
35
|
+
model,
|
|
36
|
+
content,
|
|
37
|
+
oneShot,
|
|
38
|
+
userContext,
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
// DEBUG: Log userContext in dispatch
|
|
42
|
+
debugLog(
|
|
43
|
+
`[MESSAGE_SERVICE] dispatchAssistantMessage called with userContext: ${userContext ? userContext.substring(0, 50) + '...' : 'NONE'}`,
|
|
44
|
+
);
|
|
45
|
+
|
|
30
46
|
const sessionId = session.id;
|
|
31
47
|
const now = Date.now();
|
|
32
48
|
const userMessageId = crypto.randomUUID();
|
|
@@ -90,6 +106,11 @@ export async function dispatchAssistantMessage(
|
|
|
90
106
|
payload: { id: assistantMessageId, role: 'assistant' },
|
|
91
107
|
});
|
|
92
108
|
|
|
109
|
+
// DEBUG: Log before enqueue
|
|
110
|
+
debugLog(
|
|
111
|
+
`[MESSAGE_SERVICE] Enqueuing assistant run with userContext: ${userContext ? userContext.substring(0, 50) + '...' : 'NONE'}`,
|
|
112
|
+
);
|
|
113
|
+
|
|
93
114
|
enqueueAssistantRun({
|
|
94
115
|
sessionId,
|
|
95
116
|
assistantMessageId,
|
|
@@ -99,6 +120,7 @@ export async function dispatchAssistantMessage(
|
|
|
99
120
|
model,
|
|
100
121
|
projectRoot: cfg.projectRoot,
|
|
101
122
|
oneShot: Boolean(oneShot),
|
|
123
|
+
userContext,
|
|
102
124
|
});
|
|
103
125
|
|
|
104
126
|
void touchSessionLastActive({ db, sessionId });
|
|
@@ -117,21 +139,44 @@ function scheduleSessionTitle(args: {
|
|
|
117
139
|
db: DB;
|
|
118
140
|
sessionId: string;
|
|
119
141
|
content: unknown;
|
|
120
|
-
})
|
|
121
|
-
const { sessionId } = args;
|
|
122
|
-
|
|
123
|
-
titleInFlight.
|
|
124
|
-
|
|
142
|
+
}) {
|
|
143
|
+
const { cfg, db, sessionId, content } = args;
|
|
144
|
+
|
|
145
|
+
if (titleInFlight.has(sessionId) || titlePending.has(sessionId)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const processNext = () => {
|
|
150
|
+
if (titleQueue.length === 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (titleActiveCount >= TITLE_CONCURRENCY_LIMIT) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const next = titleQueue.shift();
|
|
157
|
+
if (!next) return;
|
|
158
|
+
titleActiveCount++;
|
|
159
|
+
next();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const task = async () => {
|
|
163
|
+
titleInFlight.add(sessionId);
|
|
164
|
+
titlePending.delete(sessionId);
|
|
125
165
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Swallow title generation errors; they are non-blocking.
|
|
166
|
+
await generateSessionTitle({ cfg, db, sessionId, content });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
debugLog('[TITLE_GEN] Title generation error:');
|
|
169
|
+
debugLog(err);
|
|
131
170
|
} finally {
|
|
132
171
|
titleInFlight.delete(sessionId);
|
|
172
|
+
titleActiveCount--;
|
|
173
|
+
processNext();
|
|
133
174
|
}
|
|
134
|
-
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
titlePending.add(sessionId);
|
|
178
|
+
titleQueue.push(task);
|
|
179
|
+
processNext();
|
|
135
180
|
}
|
|
136
181
|
|
|
137
182
|
function enqueueSessionTitle(args: {
|
|
@@ -140,72 +185,54 @@ function enqueueSessionTitle(args: {
|
|
|
140
185
|
sessionId: string;
|
|
141
186
|
content: unknown;
|
|
142
187
|
}) {
|
|
143
|
-
|
|
144
|
-
if (titlePending.has(sessionId) || titleInFlight.has(sessionId)) return;
|
|
145
|
-
titlePending.add(sessionId);
|
|
146
|
-
Promise.resolve()
|
|
147
|
-
.then(() => {
|
|
148
|
-
titlePending.delete(sessionId);
|
|
149
|
-
scheduleSessionTitle(args);
|
|
150
|
-
})
|
|
151
|
-
.catch(() => {
|
|
152
|
-
titlePending.delete(sessionId);
|
|
153
|
-
});
|
|
188
|
+
scheduleSessionTitle(args);
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
async function
|
|
191
|
+
async function generateSessionTitle(args: {
|
|
157
192
|
cfg: AGIConfig;
|
|
158
193
|
db: DB;
|
|
159
194
|
sessionId: string;
|
|
160
195
|
content: unknown;
|
|
161
|
-
}) {
|
|
196
|
+
}): Promise<void> {
|
|
162
197
|
const { cfg, db, sessionId, content } = args;
|
|
198
|
+
|
|
163
199
|
try {
|
|
164
|
-
const
|
|
200
|
+
const existingSession = await db
|
|
165
201
|
.select()
|
|
166
202
|
.from(sessions)
|
|
167
203
|
.where(eq(sessions.id, sessionId));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.set({ title: heuristic })
|
|
179
|
-
.where(eq(sessions.id, sessionId));
|
|
180
|
-
publish({
|
|
181
|
-
type: 'session.updated',
|
|
182
|
-
sessionId,
|
|
183
|
-
payload: { title: heuristic },
|
|
184
|
-
});
|
|
185
|
-
}
|
|
204
|
+
|
|
205
|
+
if (!existingSession.length) {
|
|
206
|
+
debugLog('[TITLE_GEN] Session not found, aborting');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sess = existingSession[0];
|
|
211
|
+
if (sess.title && sess.title !== 'New Session') {
|
|
212
|
+
debugLog('[TITLE_GEN] Session already has a title, skipping');
|
|
213
|
+
return;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const modelId = current.model ?? cfg.defaults.model;
|
|
216
|
+
const provider = sess.provider ?? cfg.defaults.provider;
|
|
217
|
+
const modelName = sess.model ?? cfg.defaults.model;
|
|
191
218
|
|
|
192
|
-
debugLog('[TITLE_GEN]
|
|
193
|
-
debugLog(`[TITLE_GEN] Provider: ${
|
|
219
|
+
debugLog('[TITLE_GEN] Generating title for session');
|
|
220
|
+
debugLog(`[TITLE_GEN] Provider: ${provider}, Model: ${modelName}`);
|
|
221
|
+
|
|
222
|
+
const model = await resolveModel(provider, modelName, cfg);
|
|
194
223
|
|
|
195
|
-
// Check if we need OAuth spoof prompt (same logic as runner)
|
|
196
224
|
const { getAuth } = await import('@agi-cli/sdk');
|
|
197
225
|
const { getProviderSpoofPrompt } = await import('./prompt.ts');
|
|
198
|
-
const auth = await getAuth(
|
|
226
|
+
const auth = await getAuth(provider, cfg.projectRoot);
|
|
199
227
|
const needsSpoof = auth?.type === 'oauth';
|
|
200
228
|
const spoofPrompt = needsSpoof
|
|
201
|
-
? getProviderSpoofPrompt(
|
|
229
|
+
? getProviderSpoofPrompt(provider)
|
|
202
230
|
: undefined;
|
|
203
231
|
|
|
204
|
-
debugLog(
|
|
205
|
-
|
|
206
|
-
|
|
232
|
+
debugLog(
|
|
233
|
+
`[TITLE_GEN] needsSpoof: ${needsSpoof}, spoofPrompt: ${spoofPrompt || 'NONE'}`,
|
|
234
|
+
);
|
|
207
235
|
|
|
208
|
-
const model = await resolveModel(providerId, modelId, cfg);
|
|
209
236
|
const promptText = String(content ?? '').slice(0, 2000);
|
|
210
237
|
|
|
211
238
|
const titlePrompt = [
|
|
@@ -270,123 +297,49 @@ async function updateSessionTitle(args: {
|
|
|
270
297
|
const sanitized = sanitizeTitle(modelTitle);
|
|
271
298
|
debugLog(`[TITLE_GEN] After sanitization: "${sanitized}"`);
|
|
272
299
|
|
|
273
|
-
if (!sanitized) {
|
|
274
|
-
debugLog('[TITLE_GEN]
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
modelTitle = sanitized;
|
|
279
|
-
|
|
280
|
-
const check = await db
|
|
281
|
-
.select()
|
|
282
|
-
.from(sessions)
|
|
283
|
-
.where(eq(sessions.id, sessionId));
|
|
284
|
-
if (!check.length) return;
|
|
285
|
-
const currentTitle = String(check[0].title ?? '').trim();
|
|
286
|
-
if (currentTitle && currentTitle !== heuristic) {
|
|
287
|
-
debugLog(
|
|
288
|
-
`[TITLE_GEN] Session already has different title: "${currentTitle}", skipping`,
|
|
289
|
-
);
|
|
300
|
+
if (!sanitized || sanitized === 'New Session') {
|
|
301
|
+
debugLog('[TITLE_GEN] Sanitized title is empty or default, aborting');
|
|
290
302
|
return;
|
|
291
303
|
}
|
|
292
304
|
|
|
293
|
-
debugLog(`[TITLE_GEN] Setting final title: "${modelTitle}"`);
|
|
294
305
|
await db
|
|
295
306
|
.update(sessions)
|
|
296
|
-
.set({ title:
|
|
307
|
+
.set({ title: sanitized, updatedAt: Date.now() })
|
|
297
308
|
.where(eq(sessions.id, sessionId));
|
|
309
|
+
|
|
310
|
+
debugLog(`[TITLE_GEN] Setting final title: "${sanitized}"`);
|
|
311
|
+
|
|
298
312
|
publish({
|
|
299
313
|
type: 'session.updated',
|
|
300
314
|
sessionId,
|
|
301
|
-
payload: { title:
|
|
315
|
+
payload: { id: sessionId, title: sanitized },
|
|
302
316
|
});
|
|
303
317
|
} catch (err) {
|
|
304
|
-
debugLog('[TITLE_GEN]
|
|
318
|
+
debugLog('[TITLE_GEN] Error in generateSessionTitle:');
|
|
305
319
|
debugLog(err);
|
|
306
320
|
}
|
|
307
321
|
}
|
|
308
322
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function sessionHasTitle(db: DB, sessionId: string): Promise<boolean> {
|
|
319
|
-
try {
|
|
320
|
-
const rows = await db
|
|
321
|
-
.select({ title: sessions.title })
|
|
322
|
-
.from(sessions)
|
|
323
|
-
.where(eq(sessions.id, sessionId))
|
|
324
|
-
.limit(1);
|
|
325
|
-
if (!rows.length) return false;
|
|
326
|
-
const title = rows[0]?.title;
|
|
327
|
-
return typeof title === 'string' && title.trim().length > 0;
|
|
328
|
-
} catch {
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function acquireTitleSlot(): Promise<void> {
|
|
334
|
-
if (titleActiveCount < TITLE_CONCURRENCY_LIMIT) {
|
|
335
|
-
titleActiveCount += 1;
|
|
336
|
-
return Promise.resolve();
|
|
337
|
-
}
|
|
338
|
-
return new Promise((resolve) => {
|
|
339
|
-
titleQueue.push(() => {
|
|
340
|
-
titleActiveCount += 1;
|
|
341
|
-
resolve();
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function releaseTitleSlot(): void {
|
|
347
|
-
if (titleActiveCount > 0) titleActiveCount -= 1;
|
|
348
|
-
const next = titleQueue.shift();
|
|
349
|
-
if (next) {
|
|
350
|
-
next();
|
|
351
|
-
}
|
|
323
|
+
function sanitizeTitle(raw: string): string {
|
|
324
|
+
let s = raw.trim();
|
|
325
|
+
s = s.replace(/^["']|["']$/g, '');
|
|
326
|
+
s = s.replace(/[.!?]+$/, '');
|
|
327
|
+
s = s.replace(/\s+/g, ' ');
|
|
328
|
+
if (s.length > 80) s = s.slice(0, 80).trim();
|
|
329
|
+
return s;
|
|
352
330
|
}
|
|
353
331
|
|
|
354
|
-
async function touchSessionLastActive(args: {
|
|
332
|
+
async function touchSessionLastActive(args: {
|
|
333
|
+
db: DB;
|
|
334
|
+
sessionId: string;
|
|
335
|
+
}): Promise<void> {
|
|
355
336
|
const { db, sessionId } = args;
|
|
356
337
|
try {
|
|
357
338
|
await db
|
|
358
339
|
.update(sessions)
|
|
359
|
-
.set({
|
|
340
|
+
.set({ updatedAt: Date.now() })
|
|
360
341
|
.where(eq(sessions.id, sessionId));
|
|
361
|
-
} catch {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
function deriveTitle(text: string): string {
|
|
365
|
-
const cleaned = String(text || '')
|
|
366
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
367
|
-
.replace(/`[^`]*`/g, ' ')
|
|
368
|
-
.replace(/\s+/g, ' ')
|
|
369
|
-
.trim();
|
|
370
|
-
if (!cleaned) return '';
|
|
371
|
-
const endIdx = (() => {
|
|
372
|
-
const punct = ['? ', '. ', '! ']
|
|
373
|
-
.map((p) => cleaned.indexOf(p))
|
|
374
|
-
.filter((i) => i > 0);
|
|
375
|
-
const idx = Math.min(...(punct.length ? punct : [cleaned.length]));
|
|
376
|
-
return Math.min(idx + 1, cleaned.length);
|
|
377
|
-
})();
|
|
378
|
-
const first = cleaned.slice(0, endIdx).trim();
|
|
379
|
-
const maxLen = 64;
|
|
380
|
-
const base = first.length > 8 ? first : cleaned;
|
|
381
|
-
const truncated =
|
|
382
|
-
base.length > maxLen ? `${base.slice(0, maxLen - 1).trimEnd()}…` : base;
|
|
383
|
-
return truncated;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function sanitizeTitle(s: string): string {
|
|
387
|
-
let t = s.trim();
|
|
388
|
-
t = t.replace(/^['"""''()[\]]+|['"""''()[\]]+$/g, '').trim();
|
|
389
|
-
t = t.replace(/[\s\-_:–—]+$/g, '').trim();
|
|
390
|
-
if (t.length > 64) t = `${t.slice(0, 63).trimEnd()}…`;
|
|
391
|
-
return t;
|
|
342
|
+
} catch (err) {
|
|
343
|
+
debugLog('[touchSessionLastActive] Error:', err);
|
|
344
|
+
}
|
|
392
345
|
}
|
package/src/runtime/prompt.ts
CHANGED
|
@@ -20,6 +20,7 @@ export async function composeSystemPrompt(options: {
|
|
|
20
20
|
spoofPrompt?: string;
|
|
21
21
|
includeEnvironment?: boolean;
|
|
22
22
|
includeProjectTree?: boolean;
|
|
23
|
+
userContext?: string;
|
|
23
24
|
}): Promise<string> {
|
|
24
25
|
if (options.spoofPrompt) {
|
|
25
26
|
return options.spoofPrompt.trim();
|
|
@@ -61,6 +62,16 @@ export async function composeSystemPrompt(options: {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// Add user-provided context if present
|
|
66
|
+
if (options.userContext?.trim()) {
|
|
67
|
+
const userContextBlock = [
|
|
68
|
+
'<user-provided-state-context>',
|
|
69
|
+
options.userContext.trim(),
|
|
70
|
+
'</user-provided-state-context>',
|
|
71
|
+
].join('\n');
|
|
72
|
+
parts.push(userContextBlock);
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
const composed = parts.filter(Boolean).join('\n\n').trim();
|
|
65
76
|
if (composed) return composed;
|
|
66
77
|
|
package/src/runtime/runner.ts
CHANGED
|
@@ -122,7 +122,18 @@ async function runAssistant(opts: RunOpts) {
|
|
|
122
122
|
const history = await buildHistoryMessages(db, opts.sessionId);
|
|
123
123
|
historyTimer.end({ messages: history.length });
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// FIX: For OAuth, we need to check if this is the first ASSISTANT message
|
|
126
|
+
// The user message is already in history by this point, so history.length will be > 0
|
|
127
|
+
// We need to add additionalSystemMessages on the first assistant turn
|
|
128
|
+
const isFirstMessage = !history.some((m) => m.role === 'assistant');
|
|
129
|
+
|
|
130
|
+
debugLog(`[RUNNER] isFirstMessage: ${isFirstMessage}`);
|
|
131
|
+
debugLog(`[RUNNER] userContext provided: ${opts.userContext ? 'YES' : 'NO'}`);
|
|
132
|
+
if (opts.userContext) {
|
|
133
|
+
debugLog(
|
|
134
|
+
`[RUNNER] userContext value: ${opts.userContext.substring(0, 100)}${opts.userContext.length > 100 ? '...' : ''}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
126
137
|
|
|
127
138
|
const systemTimer = time('runner:composeSystemPrompt');
|
|
128
139
|
const { getAuth } = await import('@agi-cli/sdk');
|
|
@@ -133,10 +144,14 @@ async function runAssistant(opts: RunOpts) {
|
|
|
133
144
|
? getProviderSpoofPrompt(opts.provider)
|
|
134
145
|
: undefined;
|
|
135
146
|
|
|
147
|
+
debugLog(`[RUNNER] needsSpoof (OAuth): ${needsSpoof}`);
|
|
148
|
+
debugLog(`[RUNNER] spoofPrompt: ${spoofPrompt || 'NONE'}`);
|
|
149
|
+
|
|
136
150
|
let system: string;
|
|
137
151
|
let additionalSystemMessages: Array<{ role: 'system'; content: string }> = [];
|
|
138
152
|
|
|
139
153
|
if (spoofPrompt) {
|
|
154
|
+
// OAuth mode: short spoof in system field, full instructions in messages array
|
|
140
155
|
system = spoofPrompt;
|
|
141
156
|
const fullPrompt = await composeSystemPrompt({
|
|
142
157
|
provider: opts.provider,
|
|
@@ -146,9 +161,27 @@ async function runAssistant(opts: RunOpts) {
|
|
|
146
161
|
oneShot: opts.oneShot,
|
|
147
162
|
spoofPrompt: undefined,
|
|
148
163
|
includeProjectTree: isFirstMessage,
|
|
164
|
+
userContext: opts.userContext,
|
|
149
165
|
});
|
|
166
|
+
|
|
167
|
+
// FIX: Always add the system message for OAuth because:
|
|
168
|
+
// 1. System messages are NOT stored in the database
|
|
169
|
+
// 2. buildHistoryMessages only returns user/assistant messages
|
|
170
|
+
// 3. We need the full instructions on every turn
|
|
150
171
|
additionalSystemMessages = [{ role: 'system', content: fullPrompt }];
|
|
172
|
+
|
|
173
|
+
debugLog('[RUNNER] OAuth mode: additionalSystemMessages created');
|
|
174
|
+
debugLog(`[RUNNER] fullPrompt length: ${fullPrompt.length}`);
|
|
175
|
+
debugLog(
|
|
176
|
+
`[RUNNER] fullPrompt contains userContext: ${fullPrompt.includes('<user-provided-state-context>') ? 'YES' : 'NO'}`,
|
|
177
|
+
);
|
|
178
|
+
if (opts.userContext && fullPrompt.includes(opts.userContext)) {
|
|
179
|
+
debugLog('[RUNNER] ✅ userContext IS in fullPrompt');
|
|
180
|
+
} else if (opts.userContext) {
|
|
181
|
+
debugLog('[RUNNER] ❌ userContext NOT in fullPrompt!');
|
|
182
|
+
}
|
|
151
183
|
} else {
|
|
184
|
+
// API key mode: full instructions in system field
|
|
152
185
|
system = await composeSystemPrompt({
|
|
153
186
|
provider: opts.provider,
|
|
154
187
|
model: opts.model,
|
|
@@ -157,6 +190,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
157
190
|
oneShot: opts.oneShot,
|
|
158
191
|
spoofPrompt: undefined,
|
|
159
192
|
includeProjectTree: isFirstMessage,
|
|
193
|
+
userContext: opts.userContext,
|
|
160
194
|
});
|
|
161
195
|
}
|
|
162
196
|
systemTimer.end();
|
|
@@ -172,11 +206,29 @@ async function runAssistant(opts: RunOpts) {
|
|
|
172
206
|
'progress_update',
|
|
173
207
|
]);
|
|
174
208
|
const gated = allTools.filter((t) => allowedNames.has(t.name));
|
|
209
|
+
|
|
210
|
+
// FIX: For OAuth, ALWAYS prepend the system message because it's never in history
|
|
211
|
+
// For API key mode, only add on first message (when additionalSystemMessages is empty)
|
|
175
212
|
const messagesWithSystemInstructions = [
|
|
176
|
-
...
|
|
213
|
+
...additionalSystemMessages, // Always add for OAuth, empty for API key mode
|
|
177
214
|
...history,
|
|
178
215
|
];
|
|
179
216
|
|
|
217
|
+
debugLog(
|
|
218
|
+
`[RUNNER] messagesWithSystemInstructions length: ${messagesWithSystemInstructions.length}`,
|
|
219
|
+
);
|
|
220
|
+
debugLog(
|
|
221
|
+
`[RUNNER] additionalSystemMessages length: ${additionalSystemMessages.length}`,
|
|
222
|
+
);
|
|
223
|
+
if (additionalSystemMessages.length > 0) {
|
|
224
|
+
debugLog(
|
|
225
|
+
'[RUNNER] ✅ additionalSystemMessages ADDED to messagesWithSystemInstructions',
|
|
226
|
+
);
|
|
227
|
+
debugLog(
|
|
228
|
+
`[RUNNER] This happens on EVERY turn for OAuth (system messages not stored in DB)`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
180
232
|
const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
|
|
181
233
|
opts,
|
|
182
234
|
db,
|
|
@@ -257,9 +309,24 @@ async function runAssistant(opts: RunOpts) {
|
|
|
257
309
|
maxToolResults: 30,
|
|
258
310
|
});
|
|
259
311
|
|
|
312
|
+
debugLog(
|
|
313
|
+
`[RUNNER] After optimizeContext: ${contextOptimized.length} messages`,
|
|
314
|
+
);
|
|
315
|
+
|
|
260
316
|
// 2. Truncate history
|
|
261
317
|
const truncatedMessages = truncateHistory(contextOptimized, 20);
|
|
262
318
|
|
|
319
|
+
debugLog(
|
|
320
|
+
`[RUNNER] After truncateHistory: ${truncatedMessages.length} messages`,
|
|
321
|
+
);
|
|
322
|
+
if (truncatedMessages.length > 0 && truncatedMessages[0].role === 'system') {
|
|
323
|
+
debugLog('[RUNNER] ✅ First message is system message');
|
|
324
|
+
} else if (truncatedMessages.length > 0) {
|
|
325
|
+
debugLog(
|
|
326
|
+
`[RUNNER] ⚠️ First message is NOT system (it's ${truncatedMessages[0].role})`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
263
330
|
// 3. Add cache control
|
|
264
331
|
const { system: cachedSystem, messages: optimizedMessages } = addCacheControl(
|
|
265
332
|
opts.provider,
|
|
@@ -267,6 +334,13 @@ async function runAssistant(opts: RunOpts) {
|
|
|
267
334
|
truncatedMessages,
|
|
268
335
|
);
|
|
269
336
|
|
|
337
|
+
debugLog(
|
|
338
|
+
`[RUNNER] Final optimizedMessages: ${optimizedMessages.length} messages`,
|
|
339
|
+
);
|
|
340
|
+
debugLog(
|
|
341
|
+
`[RUNNER] cachedSystem (spoof): ${typeof cachedSystem === 'string' ? cachedSystem.substring(0, 100) : JSON.stringify(cachedSystem).substring(0, 100)}`,
|
|
342
|
+
);
|
|
343
|
+
|
|
270
344
|
try {
|
|
271
345
|
// @ts-expect-error this is fine 🔥
|
|
272
346
|
const result = streamText({
|
|
@@ -310,29 +384,23 @@ async function runAssistant(opts: RunOpts) {
|
|
|
310
384
|
await db
|
|
311
385
|
.update(messageParts)
|
|
312
386
|
.set({
|
|
313
|
-
content: JSON.stringify({
|
|
314
|
-
text: accumulated,
|
|
315
|
-
error: errorPayload.message,
|
|
316
|
-
}),
|
|
387
|
+
content: JSON.stringify({ error: errorPayload }),
|
|
317
388
|
})
|
|
318
|
-
.where(eq(messageParts.
|
|
389
|
+
.where(eq(messageParts.id, currentPartId));
|
|
390
|
+
|
|
319
391
|
publish({
|
|
320
|
-
type: 'error',
|
|
392
|
+
type: 'message.error',
|
|
321
393
|
sessionId: opts.sessionId,
|
|
322
394
|
payload: {
|
|
323
395
|
messageId: opts.assistantMessageId,
|
|
324
|
-
|
|
325
|
-
|
|
396
|
+
partId: currentPartId,
|
|
397
|
+
error: errorPayload,
|
|
326
398
|
},
|
|
327
399
|
});
|
|
328
400
|
throw error;
|
|
329
401
|
} finally {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
} catch {}
|
|
334
|
-
try {
|
|
335
|
-
await cleanupEmptyTextParts(opts, db);
|
|
336
|
-
} catch {}
|
|
402
|
+
unsubscribeFinish();
|
|
403
|
+
await cleanupEmptyTextParts(db, opts.assistantMessageId);
|
|
404
|
+
firstToolTimer.end({ seen: firstToolSeen });
|
|
337
405
|
}
|
|
338
406
|
}
|
|
@@ -9,6 +9,7 @@ export type RunOpts = {
|
|
|
9
9
|
model: string;
|
|
10
10
|
projectRoot: string;
|
|
11
11
|
oneShot?: boolean;
|
|
12
|
+
userContext?: string;
|
|
12
13
|
abortSignal?: AbortSignal;
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -39,7 +40,8 @@ export function enqueueAssistantRun(
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
*
|
|
43
|
+
* Signals the abort controller for a session.
|
|
44
|
+
* This will trigger the abortSignal in the streamText call.
|
|
43
45
|
*/
|
|
44
46
|
export function abortSession(sessionId: string) {
|
|
45
47
|
const controller = sessionAbortControllers.get(sessionId);
|
|
@@ -49,34 +51,25 @@ export function abortSession(sessionId: string) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/**
|
|
53
|
-
* Gets the current state of a session's queue.
|
|
54
|
-
*/
|
|
55
54
|
export function getRunnerState(sessionId: string): RunnerState | undefined {
|
|
56
55
|
return runners.get(sessionId);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
/**
|
|
60
|
-
* Marks a session queue as running.
|
|
61
|
-
*/
|
|
62
58
|
export function setRunning(sessionId: string, running: boolean) {
|
|
63
59
|
const state = runners.get(sessionId);
|
|
64
|
-
if (state)
|
|
65
|
-
state.running = running;
|
|
66
|
-
}
|
|
60
|
+
if (state) state.running = running;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
/**
|
|
70
|
-
* Dequeues the next job from a session's queue.
|
|
71
|
-
*/
|
|
72
63
|
export function dequeueJob(sessionId: string): RunOpts | undefined {
|
|
73
64
|
const state = runners.get(sessionId);
|
|
74
65
|
return state?.queue.shift();
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
/**
|
|
78
|
-
* Cleanup abort controller for a session (called when queue is done).
|
|
79
|
-
*/
|
|
80
68
|
export function cleanupSession(sessionId: string) {
|
|
81
|
-
|
|
69
|
+
const state = runners.get(sessionId);
|
|
70
|
+
if (state && state.queue.length === 0 && !state.running) {
|
|
71
|
+
runners.delete(sessionId);
|
|
72
|
+
// Clean up any lingering abort controller
|
|
73
|
+
sessionAbortControllers.delete(sessionId);
|
|
74
|
+
}
|
|
82
75
|
}
|