@agi-cli/server 0.1.67 → 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.
@@ -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
- const projectRoot = c.req.query('project') || process.cwd();
20
- const cfg = await loadConfig(projectRoot);
21
- const db = await getDb(cfg.projectRoot);
22
- const id = c.req.param('id');
23
- const rows = await db
24
- .select()
25
- .from(messages)
26
- .where(eq(messages.sessionId, id))
27
- .orderBy(messages.createdAt);
28
- const without = c.req.query('without');
29
- if (without !== 'parts') {
30
- const ids = rows.map((m) => m.id);
31
- const parts = ids.length
32
- ? await db
33
- .select()
34
- .from(messageParts)
35
- .where(inArray(messageParts.messageId, ids))
36
- : [];
37
- const partsByMsg = new Map<string, MessagePartRow[]>();
38
- for (const p of parts) {
39
- const existing = partsByMsg.get(p.messageId);
40
- if (existing) existing.push(p);
41
- else partsByMsg.set(p.messageId, [p]);
42
- }
43
- const wantParsed = (() => {
44
- const q = (c.req.query('parsed') || '').toLowerCase();
45
- return q === '1' || q === 'true' || q === 'yes';
46
- })();
47
- function parseContent(raw: string): Record<string, unknown> | string {
48
- try {
49
- const v = JSON.parse(String(raw ?? ''));
50
- if (v && typeof v === 'object' && !Array.isArray(v))
51
- return v as Record<string, unknown>;
52
- } catch {}
53
- return raw;
54
- }
55
- const enriched = rows.map((m) => {
56
- const parts = (partsByMsg.get(m.id) ?? []).sort(
57
- (a, b) => a.index - b.index,
58
- );
59
- const mapped = parts.map((p) => {
60
- const parsed = parseContent(p.content);
61
- return wantParsed
62
- ? { ...p, content: parsed }
63
- : { ...p, contentJson: parsed };
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 { ...m, parts: mapped };
66
- });
67
- return c.json(enriched);
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
- validateProviderModel(provider, modelName, { wantsToolCalls });
95
- } catch (err) {
96
- const message = err instanceof Error ? err.message : String(err);
97
- return c.json({ error: message }, 400);
98
- }
99
- // Enforce provider auth: only allow providers/models the user authenticated for
100
- const authorized = await isProviderAuthorized(cfg, provider);
101
- if (!authorized) {
102
- return c.json(
103
- {
104
- error: `Provider ${provider} is not configured. Run \`agi auth login\` to add credentials.`,
105
- },
106
- 400,
107
- );
108
- }
109
- await ensureProviderEnv(cfg, provider);
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
- const { assistantMessageId } = await dispatchAssistantMessage({
112
- cfg,
113
- db,
114
- session: sess,
115
- agent,
116
- provider,
117
- model: modelName,
118
- content,
119
- oneShot: Boolean(body?.oneShot),
120
- });
121
- return c.json({ messageId: assistantMessageId }, 202);
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
  }
@@ -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
- const message = err instanceof Error ? err.message : String(err);
76
- return c.json({ error: message }, 400);
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
 
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Unified API error handling
3
+ *
4
+ * Provides consistent error serialization and response formatting
5
+ * across all API endpoints.
6
+ */
7
+
8
+ import { isDebugEnabled } from './debug-state';
9
+ import { toErrorPayload } from './error-handling';
10
+
11
+ /**
12
+ * Standard API error response format
13
+ */
14
+ export type APIErrorResponse = {
15
+ error: {
16
+ message: string;
17
+ type: string;
18
+ code?: string;
19
+ status?: number;
20
+ details?: Record<string, unknown>;
21
+ stack?: string;
22
+ };
23
+ };
24
+
25
+ /**
26
+ * Custom API Error class
27
+ */
28
+ export class APIError extends Error {
29
+ public readonly code?: string;
30
+ public readonly status: number;
31
+ public readonly type: string;
32
+ public readonly details?: Record<string, unknown>;
33
+
34
+ constructor(
35
+ message: string,
36
+ options?: {
37
+ code?: string;
38
+ status?: number;
39
+ type?: string;
40
+ details?: Record<string, unknown>;
41
+ cause?: unknown;
42
+ },
43
+ ) {
44
+ super(message);
45
+ this.name = 'APIError';
46
+ this.code = options?.code;
47
+ this.status = options?.status ?? 500;
48
+ this.type = options?.type ?? 'api_error';
49
+ this.details = options?.details;
50
+
51
+ if (options?.cause) {
52
+ this.cause = options.cause;
53
+ }
54
+
55
+ // Maintain proper stack trace
56
+ if (Error.captureStackTrace) {
57
+ Error.captureStackTrace(this, APIError);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Serialize any error into a consistent API error response
64
+ *
65
+ * @param err - The error to serialize
66
+ * @returns A properly formatted API error response
67
+ */
68
+ export function serializeError(err: unknown): APIErrorResponse {
69
+ // Use existing error payload logic
70
+ const payload = toErrorPayload(err);
71
+
72
+ // Determine HTTP status code
73
+ let status = 500;
74
+ if (err && typeof err === 'object') {
75
+ const errObj = err as Record<string, unknown>;
76
+ if (typeof errObj.status === 'number') {
77
+ status = errObj.status;
78
+ } else if (typeof errObj.statusCode === 'number') {
79
+ status = errObj.statusCode;
80
+ } else if (
81
+ errObj.details &&
82
+ typeof errObj.details === 'object' &&
83
+ typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
84
+ ) {
85
+ status = (errObj.details as Record<string, unknown>).statusCode as number;
86
+ }
87
+ }
88
+
89
+ // Handle APIError instances
90
+ if (err instanceof APIError) {
91
+ status = err.status;
92
+ }
93
+
94
+ // Extract code if available
95
+ let code: string | undefined;
96
+ if (err && typeof err === 'object') {
97
+ const errObj = err as Record<string, unknown>;
98
+ if (typeof errObj.code === 'string') {
99
+ code = errObj.code;
100
+ }
101
+ }
102
+
103
+ if (err instanceof APIError && err.code) {
104
+ code = err.code;
105
+ }
106
+
107
+ // Build response
108
+ const response: APIErrorResponse = {
109
+ error: {
110
+ message: payload.message || 'An error occurred',
111
+ type: payload.type || 'unknown',
112
+ status,
113
+ ...(code ? { code } : {}),
114
+ ...(payload.details ? { details: payload.details } : {}),
115
+ },
116
+ };
117
+
118
+ // Include stack trace in debug mode
119
+ if (isDebugEnabled() && err instanceof Error && err.stack) {
120
+ response.error.stack = err.stack;
121
+ }
122
+
123
+ return response;
124
+ }
125
+
126
+ /**
127
+ * Create an error response with proper HTTP status code
128
+ *
129
+ * @param err - The error to convert
130
+ * @returns Tuple of [APIErrorResponse, HTTP status code]
131
+ */
132
+ export function createErrorResponse(err: unknown): [APIErrorResponse, number] {
133
+ const response = serializeError(err);
134
+ return [response, response.error.status ?? 500];
135
+ }
136
+
137
+ /**
138
+ * Normalize error to ensure it's an Error instance
139
+ *
140
+ * @param err - The error to normalize
141
+ * @returns An Error instance
142
+ */
143
+ export function normalizeError(err: unknown): Error {
144
+ if (err instanceof Error) {
145
+ return err;
146
+ }
147
+
148
+ if (typeof err === 'string') {
149
+ return new Error(err);
150
+ }
151
+
152
+ if (err && typeof err === 'object') {
153
+ const errObj = err as Record<string, unknown>;
154
+ if (typeof errObj.message === 'string') {
155
+ return new Error(errObj.message);
156
+ }
157
+ }
158
+
159
+ return new Error('An unknown error occurred');
160
+ }
161
+
162
+ /**
163
+ * Extract error message from any error type
164
+ *
165
+ * @param err - The error to extract message from
166
+ * @returns The error message string
167
+ */
168
+ export function getErrorMessage(err: unknown): string {
169
+ if (typeof err === 'string') {
170
+ return err;
171
+ }
172
+
173
+ if (err instanceof Error) {
174
+ return err.message;
175
+ }
176
+
177
+ if (err && typeof err === 'object') {
178
+ const errObj = err as Record<string, unknown>;
179
+ if (typeof errObj.message === 'string') {
180
+ return errObj.message;
181
+ }
182
+ if (typeof errObj.error === 'string') {
183
+ return errObj.error;
184
+ }
185
+ }
186
+
187
+ return 'An unknown error occurred';
188
+ }
189
+
190
+ // Legacy compatibility - AskServiceError alias
191
+ export { APIError as AskServiceError };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Runtime debug state management
3
+ *
4
+ * Centralizes debug flag state that can be set either via:
5
+ * - Environment variables (AGI_DEBUG, DEBUG_AGI)
6
+ * - Runtime configuration (CLI --debug flag)
7
+ */
8
+
9
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
10
+
11
+ type DebugState = {
12
+ enabled: boolean;
13
+ traceEnabled: boolean;
14
+ runtimeOverride: boolean | null;
15
+ runtimeTraceOverride: boolean | null;
16
+ };
17
+
18
+ // Global state
19
+ const state: DebugState = {
20
+ enabled: false,
21
+ traceEnabled: false,
22
+ runtimeOverride: null,
23
+ runtimeTraceOverride: null,
24
+ };
25
+
26
+ /**
27
+ * Check if environment variables indicate debug mode
28
+ */
29
+ function checkEnvDebug(): boolean {
30
+ const sources = [process.env.AGI_DEBUG, process.env.DEBUG_AGI];
31
+ for (const value of sources) {
32
+ if (!value) continue;
33
+ const trimmed = value.trim().toLowerCase();
34
+ if (TRUTHY.has(trimmed) || trimmed === 'all') {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Check if environment variables indicate trace mode
43
+ */
44
+ function checkEnvTrace(): boolean {
45
+ const sources = [process.env.AGI_TRACE, process.env.TRACE_AGI];
46
+ for (const value of sources) {
47
+ if (!value) continue;
48
+ const trimmed = value.trim().toLowerCase();
49
+ if (TRUTHY.has(trimmed)) {
50
+ return true;
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Initialize debug state from environment
58
+ */
59
+ function initialize() {
60
+ if (state.runtimeOverride === null) {
61
+ state.enabled = checkEnvDebug();
62
+ }
63
+ if (state.runtimeTraceOverride === null) {
64
+ state.traceEnabled = checkEnvTrace();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Check if debug mode is enabled
70
+ * Considers both runtime override and environment variables
71
+ */
72
+ export function isDebugEnabled(): boolean {
73
+ initialize();
74
+ return state.enabled;
75
+ }
76
+
77
+ /**
78
+ * Check if trace mode is enabled (shows stack traces)
79
+ * Trace mode requires debug mode to be enabled
80
+ */
81
+ export function isTraceEnabled(): boolean {
82
+ initialize();
83
+ return state.enabled && state.traceEnabled;
84
+ }
85
+
86
+ /**
87
+ * Enable or disable debug mode at runtime
88
+ * Overrides environment variable settings
89
+ *
90
+ * @param enabled - true to enable debug mode, false to disable
91
+ */
92
+ export function setDebugEnabled(enabled: boolean): void {
93
+ state.enabled = enabled;
94
+ state.runtimeOverride = enabled;
95
+ }
96
+
97
+ /**
98
+ * Enable or disable trace mode at runtime
99
+ * Trace mode shows full stack traces in error logs
100
+ *
101
+ * @param enabled - true to enable trace mode, false to disable
102
+ */
103
+ export function setTraceEnabled(enabled: boolean): void {
104
+ state.traceEnabled = enabled;
105
+ state.runtimeTraceOverride = enabled;
106
+ }
107
+
108
+ /**
109
+ * Reset debug state to environment defaults
110
+ */
111
+ export function resetDebugState(): void {
112
+ state.runtimeOverride = null;
113
+ state.runtimeTraceOverride = null;
114
+ state.enabled = checkEnvDebug();
115
+ state.traceEnabled = checkEnvTrace();
116
+ }
117
+
118
+ /**
119
+ * Get current debug state (for testing/diagnostics)
120
+ */
121
+ export function getDebugState(): Readonly<DebugState> {
122
+ initialize();
123
+ return { ...state };
124
+ }
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Legacy debug utilities - now integrated with new logger
3
+ *
4
+ * This file maintains backward compatibility while using the new
5
+ * centralized debug-state and logger modules.
6
+ */
7
+
8
+ import { isDebugEnabled as isDebugEnabledNew } from './debug-state';
9
+ import { time as timeNew, debug as debugNew } from './logger';
10
+
1
11
  const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
2
12
 
3
13
  const SYNONYMS: Record<string, string> = {
@@ -60,45 +70,48 @@ function getDebugConfig(): DebugConfig {
60
70
  return cachedConfig;
61
71
  }
62
72
 
73
+ /**
74
+ * Check if debug mode is enabled for a specific flag
75
+ * Now uses the centralized debug state
76
+ *
77
+ * @deprecated Use isDebugEnabled from debug-state.ts instead
78
+ */
63
79
  export function isDebugEnabled(flag?: string): boolean {
80
+ // Use new centralized debug state for general debug
81
+ if (!flag || flag === 'log') {
82
+ return isDebugEnabledNew();
83
+ }
84
+
85
+ // For specific flags like 'timing', check both new state and legacy env vars
86
+ if (flag === 'timing') {
87
+ // If new debug state is enabled OR timing flag is set
88
+ if (isDebugEnabledNew()) return true;
89
+ }
90
+
91
+ // Legacy flag checking
64
92
  const config = getDebugConfig();
65
93
  if (config.flags.has('all')) return true;
66
94
  if (flag) return config.flags.has(flag);
67
95
  return config.flags.has('log');
68
96
  }
69
97
 
98
+ /**
99
+ * Log debug message
100
+ * Now uses the centralized logger
101
+ *
102
+ * @deprecated Use logger.debug from logger.ts instead
103
+ */
70
104
  export function debugLog(...args: unknown[]) {
71
105
  if (!isDebugEnabled('log')) return;
72
- try {
73
- console.log('[debug]', ...args);
74
- } catch {}
75
- }
76
-
77
- function nowMs(): number {
78
- const perf = (globalThis as { performance?: { now?: () => number } })
79
- .performance;
80
- if (perf && typeof perf.now === 'function') return perf.now();
81
- return Date.now();
106
+ debugNew(args.map((arg) => String(arg)).join(' '));
82
107
  }
83
108
 
84
- type Timer = { end(meta?: Record<string, unknown>): void };
85
-
86
- export function time(label: string): Timer {
87
- if (!isDebugEnabled('timing')) {
88
- return { end() {} };
89
- }
90
- const start = nowMs();
91
- let finished = false;
92
- return {
93
- end(meta?: Record<string, unknown>) {
94
- if (finished) return;
95
- finished = true;
96
- const duration = nowMs() - start;
97
- try {
98
- const line = `[timing] ${label} ${duration.toFixed(1)}ms`;
99
- if (meta && Object.keys(meta).length) console.log(line, meta);
100
- else console.log(line);
101
- } catch {}
102
- },
103
- };
109
+ /**
110
+ * Create a timer for performance measurement
111
+ * Integrated with centralized logger
112
+ */
113
+ export function time(label: string): {
114
+ end(meta?: Record<string, unknown>): void;
115
+ } {
116
+ return timeNew(label);
104
117
  }