@agi-cli/server 0.1.66 → 0.1.68
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 -0
- package/src/routes/ask.ts +6 -6
- package/src/routes/config.ts +196 -159
- package/src/routes/git.ts +160 -47
- package/src/routes/session-messages.ts +114 -95
- package/src/routes/sessions.ts +5 -2
- package/src/runtime/api-error.ts +191 -0
- package/src/runtime/debug-state.ts +124 -0
- package/src/runtime/debug.ts +43 -30
- package/src/runtime/logger.ts +204 -0
package/src/routes/git.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { extname } from 'node:path';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
import { z } from 'zod';
|
|
6
|
-
import { generateText
|
|
7
|
-
import {
|
|
7
|
+
import { generateText } from 'ai';
|
|
8
|
+
import type { ProviderId } from '@agi-cli/sdk';
|
|
9
|
+
import { loadConfig, getAuth } from '@agi-cli/sdk';
|
|
10
|
+
import { resolveModel } from '../runtime/provider.ts';
|
|
11
|
+
import { getProviderSpoofPrompt } from '../runtime/prompt.ts';
|
|
8
12
|
|
|
9
13
|
const execFileAsync = promisify(execFile);
|
|
10
14
|
|
|
@@ -113,10 +117,13 @@ const gitPushSchema = z.object({
|
|
|
113
117
|
// Types
|
|
114
118
|
export interface GitFile {
|
|
115
119
|
path: string;
|
|
120
|
+
absPath: string; // NEW: Absolute filesystem path
|
|
116
121
|
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked';
|
|
117
122
|
staged: boolean;
|
|
118
123
|
insertions?: number;
|
|
119
124
|
deletions?: number;
|
|
125
|
+
oldPath?: string; // For renamed files
|
|
126
|
+
isNew: boolean; // NEW: True for untracked or newly added files
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
interface GitRoot {
|
|
@@ -149,7 +156,25 @@ async function validateAndGetGitRoot(
|
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Check if a file is new/untracked (not in git index)
|
|
161
|
+
*/
|
|
162
|
+
async function checkIfNewFile(gitRoot: string, file: string): Promise<boolean> {
|
|
163
|
+
try {
|
|
164
|
+
// Check if file exists in git index or committed
|
|
165
|
+
await execFileAsync('git', ['ls-files', '--error-unmatch', file], {
|
|
166
|
+
cwd: gitRoot,
|
|
167
|
+
});
|
|
168
|
+
return false; // File exists in git
|
|
169
|
+
} catch {
|
|
170
|
+
return true; // File is new/untracked
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseGitStatus(
|
|
175
|
+
statusOutput: string,
|
|
176
|
+
gitRoot: string,
|
|
177
|
+
): {
|
|
153
178
|
staged: GitFile[];
|
|
154
179
|
unstaged: GitFile[];
|
|
155
180
|
untracked: GitFile[];
|
|
@@ -160,34 +185,50 @@ function parseGitStatus(statusOutput: string): {
|
|
|
160
185
|
const untracked: GitFile[] = [];
|
|
161
186
|
|
|
162
187
|
for (const line of lines) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
// Porcelain v2 format has different line types
|
|
189
|
+
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
190
|
+
// Regular changed entry: "1 XY sub <mH> <mI> <mW> <hH> <hI> <path>"
|
|
191
|
+
// XY is a 2-character field with staged (X) and unstaged (Y) status
|
|
192
|
+
const parts = line.split(' ');
|
|
193
|
+
if (parts.length < 9) continue;
|
|
194
|
+
|
|
195
|
+
const xy = parts[1]; // e.g., ".M", "M.", "MM", "A.", etc.
|
|
196
|
+
const x = xy[0]; // staged status
|
|
197
|
+
const y = xy[1]; // unstaged status
|
|
198
|
+
const path = parts.slice(8).join(' '); // Path can contain spaces
|
|
199
|
+
const absPath = join(gitRoot, path);
|
|
200
|
+
|
|
201
|
+
// Check if file is staged (X is not '.')
|
|
202
|
+
if (x !== '.') {
|
|
203
|
+
staged.push({
|
|
204
|
+
path,
|
|
205
|
+
absPath,
|
|
206
|
+
status: getStatusFromCodeV2(x),
|
|
207
|
+
staged: true,
|
|
208
|
+
isNew: x === 'A',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
184
211
|
|
|
185
|
-
|
|
186
|
-
|
|
212
|
+
// Check if file is unstaged (Y is not '.')
|
|
213
|
+
if (y !== '.') {
|
|
214
|
+
unstaged.push({
|
|
215
|
+
path,
|
|
216
|
+
absPath,
|
|
217
|
+
status: getStatusFromCodeV2(y),
|
|
218
|
+
staged: false,
|
|
219
|
+
isNew: false,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} else if (line.startsWith('? ')) {
|
|
223
|
+
// Untracked file: "? <path>"
|
|
224
|
+
const path = line.slice(2);
|
|
225
|
+
const absPath = join(gitRoot, path);
|
|
187
226
|
untracked.push({
|
|
188
227
|
path,
|
|
228
|
+
absPath,
|
|
189
229
|
status: 'untracked',
|
|
190
230
|
staged: false,
|
|
231
|
+
isNew: true,
|
|
191
232
|
});
|
|
192
233
|
}
|
|
193
234
|
}
|
|
@@ -195,7 +236,22 @@ function parseGitStatus(statusOutput: string): {
|
|
|
195
236
|
return { staged, unstaged, untracked };
|
|
196
237
|
}
|
|
197
238
|
|
|
198
|
-
function
|
|
239
|
+
function _getStatusFromCode(code: string): GitFile['status'] {
|
|
240
|
+
switch (code) {
|
|
241
|
+
case 'M':
|
|
242
|
+
return 'modified';
|
|
243
|
+
case 'A':
|
|
244
|
+
return 'added';
|
|
245
|
+
case 'D':
|
|
246
|
+
return 'deleted';
|
|
247
|
+
case 'R':
|
|
248
|
+
return 'renamed';
|
|
249
|
+
default:
|
|
250
|
+
return 'modified';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getStatusFromCodeV2(code: string): GitFile['status'] {
|
|
199
255
|
switch (code) {
|
|
200
256
|
case 'M':
|
|
201
257
|
return 'modified';
|
|
@@ -205,6 +261,8 @@ function getStatusFromCode(code: string): GitFile['status'] {
|
|
|
205
261
|
return 'deleted';
|
|
206
262
|
case 'R':
|
|
207
263
|
return 'renamed';
|
|
264
|
+
case 'C':
|
|
265
|
+
return 'modified'; // Copied - treat as modified
|
|
208
266
|
default:
|
|
209
267
|
return 'modified';
|
|
210
268
|
}
|
|
@@ -264,11 +322,14 @@ export function registerGitRoutes(app: Hono) {
|
|
|
264
322
|
// Get status
|
|
265
323
|
const { stdout: statusOutput } = await execFileAsync(
|
|
266
324
|
'git',
|
|
267
|
-
['status', '--porcelain=
|
|
325
|
+
['status', '--porcelain=v2'],
|
|
268
326
|
{ cwd: gitRoot },
|
|
269
327
|
);
|
|
270
328
|
|
|
271
|
-
const { staged, unstaged, untracked } = parseGitStatus(
|
|
329
|
+
const { staged, unstaged, untracked } = parseGitStatus(
|
|
330
|
+
statusOutput,
|
|
331
|
+
gitRoot,
|
|
332
|
+
);
|
|
272
333
|
|
|
273
334
|
// Get ahead/behind counts
|
|
274
335
|
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
@@ -286,6 +347,8 @@ export function registerGitRoutes(app: Hono) {
|
|
|
286
347
|
branch,
|
|
287
348
|
ahead,
|
|
288
349
|
behind,
|
|
350
|
+
gitRoot, // NEW: Expose git root path
|
|
351
|
+
workingDir: requestedPath, // NEW: Current working directory
|
|
289
352
|
staged,
|
|
290
353
|
unstaged,
|
|
291
354
|
untracked,
|
|
@@ -324,8 +387,46 @@ export function registerGitRoutes(app: Hono) {
|
|
|
324
387
|
}
|
|
325
388
|
|
|
326
389
|
const { gitRoot } = validation;
|
|
390
|
+
const absPath = join(gitRoot, query.file);
|
|
391
|
+
|
|
392
|
+
// Check if file is new/untracked
|
|
393
|
+
const isNewFile = await checkIfNewFile(gitRoot, query.file);
|
|
394
|
+
|
|
395
|
+
// For new files, read and return full content
|
|
396
|
+
if (isNewFile) {
|
|
397
|
+
try {
|
|
398
|
+
const content = await readFile(absPath, 'utf-8');
|
|
399
|
+
const lineCount = content.split('\n').length;
|
|
400
|
+
const language = inferLanguage(query.file);
|
|
401
|
+
|
|
402
|
+
return c.json({
|
|
403
|
+
status: 'ok',
|
|
404
|
+
data: {
|
|
405
|
+
file: query.file,
|
|
406
|
+
absPath,
|
|
407
|
+
diff: '', // Empty diff for new files
|
|
408
|
+
content, // NEW: Full file content
|
|
409
|
+
isNewFile: true, // NEW: Flag indicating this is a new file
|
|
410
|
+
isBinary: false,
|
|
411
|
+
insertions: lineCount,
|
|
412
|
+
deletions: 0,
|
|
413
|
+
language,
|
|
414
|
+
staged: !!query.staged, // NEW: Whether showing staged or unstaged
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return c.json(
|
|
419
|
+
{
|
|
420
|
+
status: 'error',
|
|
421
|
+
error:
|
|
422
|
+
error instanceof Error ? error.message : 'Failed to read file',
|
|
423
|
+
},
|
|
424
|
+
500,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
327
428
|
|
|
328
|
-
//
|
|
429
|
+
// For existing files, get diff output and stats
|
|
329
430
|
const diffArgs = query.staged
|
|
330
431
|
? ['diff', '--cached', '--', query.file]
|
|
331
432
|
: ['diff', '--', query.file];
|
|
@@ -370,11 +471,14 @@ export function registerGitRoutes(app: Hono) {
|
|
|
370
471
|
status: 'ok',
|
|
371
472
|
data: {
|
|
372
473
|
file: query.file,
|
|
474
|
+
absPath, // NEW: Absolute path
|
|
373
475
|
diff: diffText,
|
|
476
|
+
isNewFile: false, // NEW: Not a new file
|
|
477
|
+
isBinary: binary,
|
|
374
478
|
insertions,
|
|
375
479
|
deletions,
|
|
376
480
|
language,
|
|
377
|
-
|
|
481
|
+
staged: !!query.staged, // NEW: Whether showing staged or unstaged
|
|
378
482
|
},
|
|
379
483
|
});
|
|
380
484
|
} catch (error) {
|
|
@@ -568,25 +672,31 @@ export function registerGitRoutes(app: Hono) {
|
|
|
568
672
|
// Get file list for context
|
|
569
673
|
const { stdout: statusOutput } = await execFileAsync(
|
|
570
674
|
'git',
|
|
571
|
-
['status', '--porcelain=
|
|
675
|
+
['status', '--porcelain=v2'],
|
|
572
676
|
{ cwd: gitRoot },
|
|
573
677
|
);
|
|
574
|
-
const { staged } = parseGitStatus(statusOutput);
|
|
678
|
+
const { staged } = parseGitStatus(statusOutput, gitRoot);
|
|
575
679
|
const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
|
|
576
680
|
|
|
577
681
|
// Load config to get provider settings
|
|
578
682
|
const config = await loadConfig();
|
|
579
683
|
|
|
580
|
-
// Use
|
|
581
|
-
const provider = config.defaults?.provider || 'anthropic';
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
684
|
+
// Use the default provider and model for quick commit message generation
|
|
685
|
+
const provider = (config.defaults?.provider || 'anthropic') as ProviderId;
|
|
686
|
+
const modelId = config.defaults?.model || 'claude-3-5-sonnet-20241022';
|
|
687
|
+
|
|
688
|
+
// Check if we need OAuth spoof prompt (same as runner)
|
|
689
|
+
const auth = await getAuth(provider, config.projectRoot);
|
|
690
|
+
const needsSpoof = auth?.type === 'oauth';
|
|
691
|
+
const spoofPrompt = needsSpoof
|
|
692
|
+
? getProviderSpoofPrompt(provider)
|
|
693
|
+
: undefined;
|
|
694
|
+
|
|
695
|
+
// Resolve model with proper authentication (3-level fallback: OAuth, API key, env var)
|
|
696
|
+
const model = await resolveModel(provider, modelId, config);
|
|
587
697
|
|
|
588
698
|
// Generate commit message using AI
|
|
589
|
-
const
|
|
699
|
+
const userPrompt = `Generate a concise, conventional commit message for these git changes.
|
|
590
700
|
|
|
591
701
|
Staged files:
|
|
592
702
|
${fileList}
|
|
@@ -604,12 +714,15 @@ Guidelines:
|
|
|
604
714
|
|
|
605
715
|
Commit message:`;
|
|
606
716
|
|
|
717
|
+
// Use spoof prompt as system if OAuth, otherwise use normal system prompt
|
|
718
|
+
const systemPrompt = spoofPrompt
|
|
719
|
+
? spoofPrompt
|
|
720
|
+
: 'You are a helpful assistant that generates git commit messages.';
|
|
721
|
+
|
|
607
722
|
const { text } = await generateText({
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
'You are a helpful assistant that generates git commit messages.',
|
|
612
|
-
prompt,
|
|
723
|
+
model,
|
|
724
|
+
system: systemPrompt,
|
|
725
|
+
prompt: userPrompt,
|
|
613
726
|
maxTokens: 200,
|
|
614
727
|
});
|
|
615
728
|
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
ensureProviderEnv,
|
|
10
10
|
} from '@agi-cli/sdk';
|
|
11
11
|
import { dispatchAssistantMessage } from '../runtime/message-service.ts';
|
|
12
|
+
import { logger } from '../runtime/logger.ts';
|
|
13
|
+
import { serializeError } from '../runtime/api-error.ts';
|
|
12
14
|
|
|
13
15
|
type MessagePartRow = typeof messageParts.$inferSelect;
|
|
14
16
|
type SessionRow = typeof sessions.$inferSelect;
|
|
@@ -16,108 +18,125 @@ type SessionRow = typeof sessions.$inferSelect;
|
|
|
16
18
|
export function registerSessionMessagesRoutes(app: Hono) {
|
|
17
19
|
// List messages for a session
|
|
18
20
|
app.get('/v1/sessions/:id/messages', async (c) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
21
|
+
try {
|
|
22
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
23
|
+
const cfg = await loadConfig(projectRoot);
|
|
24
|
+
const db = await getDb(cfg.projectRoot);
|
|
25
|
+
const id = c.req.param('id');
|
|
26
|
+
const rows = await db
|
|
27
|
+
.select()
|
|
28
|
+
.from(messages)
|
|
29
|
+
.where(eq(messages.sessionId, id))
|
|
30
|
+
.orderBy(messages.createdAt);
|
|
31
|
+
const without = c.req.query('without');
|
|
32
|
+
if (without !== 'parts') {
|
|
33
|
+
const ids = rows.map((m) => m.id);
|
|
34
|
+
const parts = ids.length
|
|
35
|
+
? await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(messageParts)
|
|
38
|
+
.where(inArray(messageParts.messageId, ids))
|
|
39
|
+
: [];
|
|
40
|
+
const partsByMsg = new Map<string, MessagePartRow[]>();
|
|
41
|
+
for (const p of parts) {
|
|
42
|
+
const existing = partsByMsg.get(p.messageId);
|
|
43
|
+
if (existing) existing.push(p);
|
|
44
|
+
else partsByMsg.set(p.messageId, [p]);
|
|
45
|
+
}
|
|
46
|
+
const wantParsed = (() => {
|
|
47
|
+
const q = (c.req.query('parsed') || '').toLowerCase();
|
|
48
|
+
return q === '1' || q === 'true' || q === 'yes';
|
|
49
|
+
})();
|
|
50
|
+
function parseContent(raw: string): Record<string, unknown> | string {
|
|
51
|
+
try {
|
|
52
|
+
const v = JSON.parse(String(raw ?? ''));
|
|
53
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
54
|
+
return v as Record<string, unknown>;
|
|
55
|
+
} catch {}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
const enriched = rows.map((m) => {
|
|
59
|
+
const parts = (partsByMsg.get(m.id) ?? []).sort(
|
|
60
|
+
(a, b) => a.index - b.index,
|
|
61
|
+
);
|
|
62
|
+
const mapped = parts.map((p) => {
|
|
63
|
+
const parsed = parseContent(p.content);
|
|
64
|
+
return wantParsed
|
|
65
|
+
? { ...p, content: parsed }
|
|
66
|
+
: { ...p, contentJson: parsed };
|
|
67
|
+
});
|
|
68
|
+
return { ...m, parts: mapped };
|
|
64
69
|
});
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
return c.json(
|
|
70
|
+
return c.json(enriched);
|
|
71
|
+
}
|
|
72
|
+
return c.json(rows);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error('Failed to list session messages', error);
|
|
75
|
+
const errorResponse = serializeError(error);
|
|
76
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
68
77
|
}
|
|
69
|
-
return c.json(rows);
|
|
70
78
|
});
|
|
71
79
|
|
|
72
80
|
// Post a user message and get assistant reply (non-streaming for v0)
|
|
73
81
|
app.post('/v1/sessions/:id/messages', async (c) => {
|
|
74
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
75
|
-
const cfg = await loadConfig(projectRoot);
|
|
76
|
-
const db = await getDb(cfg.projectRoot);
|
|
77
|
-
const sessionId = c.req.param('id');
|
|
78
|
-
const body = await c.req.json().catch(() => ({}));
|
|
79
|
-
// Load session to inherit its provider/model/agent by default
|
|
80
|
-
const sessionRows = await db
|
|
81
|
-
.select()
|
|
82
|
-
.from(sessions)
|
|
83
|
-
.where(eq(sessions.id, sessionId));
|
|
84
|
-
if (!sessionRows.length) return c.json({ error: 'Session not found' }, 404);
|
|
85
|
-
const sess: SessionRow = sessionRows[0];
|
|
86
|
-
const provider = body?.provider ?? sess.provider ?? cfg.defaults.provider;
|
|
87
|
-
const modelName = body?.model ?? sess.model ?? cfg.defaults.model;
|
|
88
|
-
const agent = body?.agent ?? sess.agent ?? cfg.defaults.agent;
|
|
89
|
-
const content = body?.content ?? '';
|
|
90
|
-
|
|
91
|
-
// Validate model capabilities if tools are allowed for this agent
|
|
92
|
-
const wantsToolCalls = true; // agent toolset may be non-empty
|
|
93
82
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
83
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
84
|
+
const cfg = await loadConfig(projectRoot);
|
|
85
|
+
const db = await getDb(cfg.projectRoot);
|
|
86
|
+
const sessionId = c.req.param('id');
|
|
87
|
+
const body = await c.req.json().catch(() => ({}));
|
|
88
|
+
// Load session to inherit its provider/model/agent by default
|
|
89
|
+
const sessionRows = await db
|
|
90
|
+
.select()
|
|
91
|
+
.from(sessions)
|
|
92
|
+
.where(eq(sessions.id, sessionId));
|
|
93
|
+
if (!sessionRows.length) {
|
|
94
|
+
logger.warn('Session not found', { sessionId });
|
|
95
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
96
|
+
}
|
|
97
|
+
const sess: SessionRow = sessionRows[0];
|
|
98
|
+
const provider = body?.provider ?? sess.provider ?? cfg.defaults.provider;
|
|
99
|
+
const modelName = body?.model ?? sess.model ?? cfg.defaults.model;
|
|
100
|
+
const agent = body?.agent ?? sess.agent ?? cfg.defaults.agent;
|
|
101
|
+
const content = body?.content ?? '';
|
|
102
|
+
|
|
103
|
+
// Validate model capabilities if tools are allowed for this agent
|
|
104
|
+
const wantsToolCalls = true; // agent toolset may be non-empty
|
|
105
|
+
try {
|
|
106
|
+
validateProviderModel(provider, modelName, { wantsToolCalls });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.error('Model validation failed', err, { provider, modelName });
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
return c.json({ error: message }, 400);
|
|
111
|
+
}
|
|
112
|
+
// Enforce provider auth: only allow providers/models the user authenticated for
|
|
113
|
+
const authorized = await isProviderAuthorized(cfg, provider);
|
|
114
|
+
if (!authorized) {
|
|
115
|
+
logger.warn('Provider not authorized', { provider });
|
|
116
|
+
return c.json(
|
|
117
|
+
{
|
|
118
|
+
error: `Provider ${provider} is not configured. Run \`agi auth login\` to add credentials.`,
|
|
119
|
+
},
|
|
120
|
+
400,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
await ensureProviderEnv(cfg, provider);
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
const { assistantMessageId } = await dispatchAssistantMessage({
|
|
126
|
+
cfg,
|
|
127
|
+
db,
|
|
128
|
+
session: sess,
|
|
129
|
+
agent,
|
|
130
|
+
provider,
|
|
131
|
+
model: modelName,
|
|
132
|
+
content,
|
|
133
|
+
oneShot: Boolean(body?.oneShot),
|
|
134
|
+
});
|
|
135
|
+
return c.json({ messageId: assistantMessageId }, 202);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.error('Failed to create session message', error);
|
|
138
|
+
const errorResponse = serializeError(error);
|
|
139
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
140
|
+
}
|
|
122
141
|
});
|
|
123
142
|
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { ProviderId } from '@agi-cli/sdk';
|
|
|
7
7
|
import { isProviderId } from '@agi-cli/sdk';
|
|
8
8
|
import { resolveAgentConfig } from '../runtime/agent-registry.ts';
|
|
9
9
|
import { createSession as createSessionRow } from '../runtime/session-manager.ts';
|
|
10
|
+
import { serializeError } from '../runtime/api-error.ts';
|
|
11
|
+
import { logger } from '../runtime/logger.ts';
|
|
10
12
|
|
|
11
13
|
export function registerSessionsRoutes(app: Hono) {
|
|
12
14
|
// List sessions
|
|
@@ -72,8 +74,9 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
72
74
|
});
|
|
73
75
|
return c.json(row, 201);
|
|
74
76
|
} catch (err) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
logger.error('Failed to create session', err);
|
|
78
|
+
const errorResponse = serializeError(err);
|
|
79
|
+
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
77
80
|
}
|
|
78
81
|
});
|
|
79
82
|
|