@agi-cli/server 0.1.93 → 0.1.95
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 +1 -1
- package/src/routes/ask.ts +1 -1
- package/src/routes/config/agents.ts +1 -1
- package/src/routes/config/cwd.ts +1 -1
- package/src/routes/config/main.ts +1 -1
- package/src/routes/config/models.ts +1 -1
- package/src/routes/config/providers.ts +1 -1
- package/src/routes/config/utils.ts +1 -1
- package/src/routes/files.ts +1 -1
- package/src/routes/session-messages.ts +1 -1
- package/src/routes/sessions.ts +1 -1
- package/src/routes/terminals.ts +17 -15
- package/src/runtime/debug-state.ts +16 -0
- package/src/runtime/debug.ts +1 -1
- package/src/runtime/tool-context-setup.ts +1 -0
- package/src/runtime/tool-context.ts +8 -0
- package/src/tools/adapter.ts +326 -237
- package/src/runtime/logger.ts +0 -206
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.95",
|
|
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.95",
|
|
33
|
+
"@agi-cli/database": "0.1.95",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
package/src/routes/ask.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
import { handleAskRequest } from '../runtime/ask-service.ts';
|
|
8
8
|
import type { EmbeddedAppConfig } from '../index.ts';
|
|
9
9
|
import { serializeError } from '../runtime/api-error.ts';
|
|
10
|
-
import { logger } from '
|
|
10
|
+
import { logger } from '@agi-cli/sdk';
|
|
11
11
|
|
|
12
12
|
export function registerAskRoutes(app: Hono) {
|
|
13
13
|
app.post('/v1/ask', async (c) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { loadConfig } from '@agi-cli/sdk';
|
|
3
3
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
4
|
-
import { logger } from '
|
|
4
|
+
import { logger } from '@agi-cli/sdk';
|
|
5
5
|
import { serializeError } from '../../runtime/api-error.ts';
|
|
6
6
|
import { discoverAllAgents, getDefault } from './utils.ts';
|
|
7
7
|
|
package/src/routes/config/cwd.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
|
-
import { logger } from '
|
|
3
|
+
import { logger } from '@agi-cli/sdk';
|
|
4
4
|
import { serializeError } from '../../runtime/api-error.ts';
|
|
5
5
|
|
|
6
6
|
export function registerCwdRoute(app: Hono) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { loadConfig } from '@agi-cli/sdk';
|
|
3
3
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
4
|
-
import { logger } from '
|
|
4
|
+
import { logger } from '@agi-cli/sdk';
|
|
5
5
|
import { serializeError } from '../../runtime/api-error.ts';
|
|
6
6
|
import {
|
|
7
7
|
discoverAllAgents,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { loadConfig, catalog, type ProviderId } from '@agi-cli/sdk';
|
|
3
3
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
4
|
-
import { logger } from '
|
|
4
|
+
import { logger } from '@agi-cli/sdk';
|
|
5
5
|
import { serializeError } from '../../runtime/api-error.ts';
|
|
6
6
|
import {
|
|
7
7
|
isProviderAuthorizedHybrid,
|
|
@@ -2,7 +2,7 @@ import type { Hono } from 'hono';
|
|
|
2
2
|
import { loadConfig } from '@agi-cli/sdk';
|
|
3
3
|
import type { ProviderId } from '@agi-cli/sdk';
|
|
4
4
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
5
|
-
import { logger } from '
|
|
5
|
+
import { logger } from '@agi-cli/sdk';
|
|
6
6
|
import { serializeError } from '../../runtime/api-error.ts';
|
|
7
7
|
import { getAuthorizedProviders, getDefault } from './utils.ts';
|
|
8
8
|
|
|
@@ -8,7 +8,7 @@ import { readdir } from 'node:fs/promises';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
10
10
|
import type { AGIConfig } from '@agi-cli/sdk';
|
|
11
|
-
import { logger } from '
|
|
11
|
+
import { logger } from '@agi-cli/sdk';
|
|
12
12
|
import { loadAgentsConfig } from '../../runtime/agent-registry.ts';
|
|
13
13
|
|
|
14
14
|
export async function isProviderAuthorizedHybrid(
|
package/src/routes/files.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { join, relative } from 'node:path';
|
|
|
4
4
|
import { exec } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { serializeError } from '../runtime/api-error.ts';
|
|
7
|
-
import { logger } from '
|
|
7
|
+
import { logger } from '@agi-cli/sdk';
|
|
8
8
|
|
|
9
9
|
const execAsync = promisify(exec);
|
|
10
10
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ensureProviderEnv,
|
|
10
10
|
} from '@agi-cli/sdk';
|
|
11
11
|
import { dispatchAssistantMessage } from '../runtime/message-service.ts';
|
|
12
|
-
import { logger } from '
|
|
12
|
+
import { logger } from '@agi-cli/sdk';
|
|
13
13
|
import { serializeError } from '../runtime/api-error.ts';
|
|
14
14
|
|
|
15
15
|
type MessagePartRow = typeof messageParts.$inferSelect;
|
package/src/routes/sessions.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { isProviderId, catalog } 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
10
|
import { serializeError } from '../runtime/api-error.ts';
|
|
11
|
-
import { logger } from '
|
|
11
|
+
import { logger } from '@agi-cli/sdk';
|
|
12
12
|
|
|
13
13
|
export function registerSessionsRoutes(app: Hono) {
|
|
14
14
|
// List sessions
|
package/src/routes/terminals.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { streamSSE } from 'hono/streaming';
|
|
3
3
|
import type { TerminalManager } from '@agi-cli/sdk';
|
|
4
|
+
import { logger } from '@agi-cli/sdk';
|
|
4
5
|
|
|
5
6
|
export function registerTerminalsRoutes(
|
|
6
7
|
app: Hono,
|
|
@@ -16,9 +17,9 @@ export function registerTerminalsRoutes(
|
|
|
16
17
|
|
|
17
18
|
app.post('/v1/terminals', async (c) => {
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
+
logger.debug('POST /v1/terminals called');
|
|
20
21
|
const body = await c.req.json();
|
|
21
|
-
|
|
22
|
+
logger.debug('Creating terminal request received', body);
|
|
22
23
|
const { command, args, purpose, cwd, title } = body;
|
|
23
24
|
|
|
24
25
|
if (!command || !purpose) {
|
|
@@ -31,7 +32,7 @@ export function registerTerminalsRoutes(
|
|
|
31
32
|
}
|
|
32
33
|
const resolvedCwd = cwd || process.cwd();
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
logger.debug('Creating terminal', {
|
|
35
36
|
command: resolvedCommand,
|
|
36
37
|
args,
|
|
37
38
|
purpose,
|
|
@@ -47,7 +48,7 @@ export function registerTerminalsRoutes(
|
|
|
47
48
|
title,
|
|
48
49
|
});
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
logger.debug('Terminal created successfully', { id: terminal.id });
|
|
51
52
|
|
|
52
53
|
return c.json({
|
|
53
54
|
terminalId: terminal.id,
|
|
@@ -56,11 +57,7 @@ export function registerTerminalsRoutes(
|
|
|
56
57
|
command: terminal.command,
|
|
57
58
|
});
|
|
58
59
|
} catch (error) {
|
|
59
|
-
|
|
60
|
-
console.error(
|
|
61
|
-
'[API] Error stack:',
|
|
62
|
-
error instanceof Error ? error.stack : 'No stack',
|
|
63
|
-
);
|
|
60
|
+
logger.error('Error creating terminal', error);
|
|
64
61
|
const message = error instanceof Error ? error.message : String(error);
|
|
65
62
|
return c.json({ error: message }, 500);
|
|
66
63
|
}
|
|
@@ -79,21 +76,24 @@ export function registerTerminalsRoutes(
|
|
|
79
76
|
|
|
80
77
|
app.get('/v1/terminals/:id/output', async (c) => {
|
|
81
78
|
const id = c.req.param('id');
|
|
82
|
-
|
|
79
|
+
logger.debug('SSE client connecting to terminal', { id });
|
|
83
80
|
const terminal = terminalManager.get(id);
|
|
84
81
|
|
|
85
82
|
if (!terminal) {
|
|
86
|
-
|
|
83
|
+
logger.debug('SSE terminal not found', { id });
|
|
87
84
|
return c.json({ error: 'Terminal not found' }, 404);
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
return streamSSE(c, async (stream) => {
|
|
91
|
-
|
|
88
|
+
logger.debug('SSE stream started for terminal', { id });
|
|
92
89
|
// Send historical buffer first (unless skipHistory is set)
|
|
93
90
|
const skipHistory = c.req.query('skipHistory') === 'true';
|
|
94
91
|
if (!skipHistory) {
|
|
95
92
|
const history = terminal.read();
|
|
96
|
-
|
|
93
|
+
logger.debug('SSE sending terminal history', {
|
|
94
|
+
id,
|
|
95
|
+
lines: history.length,
|
|
96
|
+
});
|
|
97
97
|
for (const line of history) {
|
|
98
98
|
await stream.write(
|
|
99
99
|
`data: ${JSON.stringify({ type: 'data', line })}\n\n`,
|
|
@@ -105,7 +105,7 @@ export function registerTerminalsRoutes(
|
|
|
105
105
|
try {
|
|
106
106
|
await stream.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
107
107
|
} catch (error) {
|
|
108
|
-
|
|
108
|
+
logger.error('SSE error writing event', error, { id });
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
111
|
|
|
@@ -141,7 +141,9 @@ export function registerTerminalsRoutes(
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function onAbort() {
|
|
144
|
-
|
|
144
|
+
logger.debug('SSE client disconnected from terminal', {
|
|
145
|
+
id: terminal.id,
|
|
146
|
+
});
|
|
145
147
|
stream.close();
|
|
146
148
|
finish();
|
|
147
149
|
}
|
|
@@ -23,6 +23,18 @@ const state: DebugState = {
|
|
|
23
23
|
runtimeTraceOverride: null,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
type GlobalDebugFlags = {
|
|
27
|
+
__AGI_DEBUG_ENABLED__?: boolean;
|
|
28
|
+
__AGI_TRACE_ENABLED__?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const globalFlags = globalThis as GlobalDebugFlags;
|
|
32
|
+
|
|
33
|
+
function syncGlobalFlags() {
|
|
34
|
+
globalFlags.__AGI_DEBUG_ENABLED__ = state.enabled;
|
|
35
|
+
globalFlags.__AGI_TRACE_ENABLED__ = state.traceEnabled;
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
/**
|
|
27
39
|
* Check if environment variables indicate debug mode
|
|
28
40
|
*/
|
|
@@ -63,6 +75,7 @@ function initialize() {
|
|
|
63
75
|
if (state.runtimeTraceOverride === null) {
|
|
64
76
|
state.traceEnabled = checkEnvTrace();
|
|
65
77
|
}
|
|
78
|
+
syncGlobalFlags();
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/**
|
|
@@ -92,6 +105,7 @@ export function isTraceEnabled(): boolean {
|
|
|
92
105
|
export function setDebugEnabled(enabled: boolean): void {
|
|
93
106
|
state.enabled = enabled;
|
|
94
107
|
state.runtimeOverride = enabled;
|
|
108
|
+
syncGlobalFlags();
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
/**
|
|
@@ -103,6 +117,7 @@ export function setDebugEnabled(enabled: boolean): void {
|
|
|
103
117
|
export function setTraceEnabled(enabled: boolean): void {
|
|
104
118
|
state.traceEnabled = enabled;
|
|
105
119
|
state.runtimeTraceOverride = enabled;
|
|
120
|
+
syncGlobalFlags();
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
/**
|
|
@@ -113,6 +128,7 @@ export function resetDebugState(): void {
|
|
|
113
128
|
state.runtimeTraceOverride = null;
|
|
114
129
|
state.enabled = checkEnvDebug();
|
|
115
130
|
state.traceEnabled = checkEnvTrace();
|
|
131
|
+
syncGlobalFlags();
|
|
116
132
|
}
|
|
117
133
|
|
|
118
134
|
/**
|
package/src/runtime/debug.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { isDebugEnabled as isDebugEnabledNew } from './debug-state';
|
|
9
|
-
import { time as timeNew, debug as debugNew } from '
|
|
9
|
+
import { time as timeNew, debug as debugNew } from '@agi-cli/sdk';
|
|
10
10
|
|
|
11
11
|
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
12
12
|
|
|
@@ -3,6 +3,11 @@ import type { DB } from '@agi-cli/database';
|
|
|
3
3
|
import { messageParts } from '@agi-cli/database/schema';
|
|
4
4
|
import { publish } from '../events/bus.ts';
|
|
5
5
|
|
|
6
|
+
export type StepExecutionState = {
|
|
7
|
+
chain: Promise<void>;
|
|
8
|
+
failed: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
6
11
|
export type ToolAdapterContext = {
|
|
7
12
|
sessionId: string;
|
|
8
13
|
messageId: string;
|
|
@@ -15,6 +20,9 @@ export type ToolAdapterContext = {
|
|
|
15
20
|
nextIndex: () => number | Promise<number>;
|
|
16
21
|
stepIndex?: number;
|
|
17
22
|
onFirstToolCall?: () => void;
|
|
23
|
+
stepExecution?: {
|
|
24
|
+
states: Map<number, StepExecutionState>;
|
|
25
|
+
};
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
export function extractFinishText(input: unknown): string | undefined {
|
package/src/tools/adapter.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { eq } from 'drizzle-orm';
|
|
|
4
4
|
import { publish } from '../events/bus.ts';
|
|
5
5
|
import type { DiscoveredTool } from '@agi-cli/sdk';
|
|
6
6
|
import { getCwd, setCwd, joinRelative } from '../runtime/cwd.ts';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
ToolAdapterContext,
|
|
9
|
+
StepExecutionState,
|
|
10
|
+
} from '../runtime/tool-context.ts';
|
|
8
11
|
import { isToolError } from '@agi-cli/sdk/tools/error';
|
|
9
12
|
|
|
10
13
|
export type { ToolAdapterContext } from '../runtime/tool-context.ts';
|
|
@@ -49,6 +52,11 @@ export function adaptTools(
|
|
|
49
52
|
const pendingCalls = new Map<string, PendingCallMeta[]>();
|
|
50
53
|
let firstToolCallReported = false;
|
|
51
54
|
|
|
55
|
+
if (!ctx.stepExecution) {
|
|
56
|
+
ctx.stepExecution = { states: new Map<number, StepExecutionState>() };
|
|
57
|
+
}
|
|
58
|
+
const stepStates = ctx.stepExecution.states;
|
|
59
|
+
|
|
52
60
|
// Anthropic allows max 4 cache_control blocks
|
|
53
61
|
// Cache only the most frequently used tools: read, write, bash
|
|
54
62
|
const cacheableTools = new Set(['read', 'write', 'bash', 'edit']);
|
|
@@ -57,6 +65,68 @@ export function adaptTools(
|
|
|
57
65
|
for (const { name, tool } of tools) {
|
|
58
66
|
const base = tool;
|
|
59
67
|
|
|
68
|
+
const processedToolErrors = new WeakSet<object>();
|
|
69
|
+
|
|
70
|
+
const persistToolErrorResult = async (
|
|
71
|
+
errorResult: unknown,
|
|
72
|
+
{
|
|
73
|
+
callId,
|
|
74
|
+
startTs,
|
|
75
|
+
stepIndexForEvent,
|
|
76
|
+
args,
|
|
77
|
+
}: {
|
|
78
|
+
callId?: string;
|
|
79
|
+
startTs?: number;
|
|
80
|
+
stepIndexForEvent: number;
|
|
81
|
+
args?: unknown;
|
|
82
|
+
},
|
|
83
|
+
) => {
|
|
84
|
+
const resultPartId = crypto.randomUUID();
|
|
85
|
+
const endTs = Date.now();
|
|
86
|
+
const dur =
|
|
87
|
+
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
88
|
+
|
|
89
|
+
const contentObj: {
|
|
90
|
+
name: string;
|
|
91
|
+
result: unknown;
|
|
92
|
+
callId?: string;
|
|
93
|
+
args?: unknown;
|
|
94
|
+
} = {
|
|
95
|
+
name,
|
|
96
|
+
result: errorResult,
|
|
97
|
+
callId,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (args !== undefined) {
|
|
101
|
+
contentObj.args = args;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const index = await ctx.nextIndex();
|
|
105
|
+
|
|
106
|
+
await ctx.db.insert(messageParts).values({
|
|
107
|
+
id: resultPartId,
|
|
108
|
+
messageId: ctx.messageId,
|
|
109
|
+
index,
|
|
110
|
+
stepIndex: stepIndexForEvent,
|
|
111
|
+
type: 'tool_result',
|
|
112
|
+
content: JSON.stringify(contentObj),
|
|
113
|
+
agent: ctx.agent,
|
|
114
|
+
provider: ctx.provider,
|
|
115
|
+
model: ctx.model,
|
|
116
|
+
startedAt: startTs,
|
|
117
|
+
completedAt: endTs,
|
|
118
|
+
toolName: name,
|
|
119
|
+
toolCallId: callId,
|
|
120
|
+
toolDurationMs: dur ?? undefined,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
publish({
|
|
124
|
+
type: 'tool.result',
|
|
125
|
+
sessionId: ctx.sessionId,
|
|
126
|
+
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
60
130
|
// Add cache control for Anthropic to cache tool definitions (max 2 tools)
|
|
61
131
|
const shouldCache =
|
|
62
132
|
provider === 'anthropic' &&
|
|
@@ -212,263 +282,282 @@ export function adaptTools(
|
|
|
212
282
|
const startTsFromQueue = meta?.startTs;
|
|
213
283
|
const stepIndexForEvent = meta?.stepIndex ?? ctx.stepIndex;
|
|
214
284
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
285
|
+
const stepKey =
|
|
286
|
+
typeof stepIndexForEvent === 'number' &&
|
|
287
|
+
Number.isFinite(stepIndexForEvent)
|
|
288
|
+
? stepIndexForEvent
|
|
289
|
+
: 0;
|
|
290
|
+
let stepState = stepStates.get(stepKey);
|
|
291
|
+
if (!stepState) {
|
|
292
|
+
stepState = { chain: Promise.resolve(), failed: false };
|
|
293
|
+
stepStates.set(stepKey, stepState);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
|
|
297
|
+
try {
|
|
298
|
+
if (stepState.failed) {
|
|
299
|
+
const skipError = {
|
|
300
|
+
ok: false,
|
|
301
|
+
error: `Cannot execute "${name}" because a previous tool call in this step failed. Retry the failing tool before continuing with "${name}".`,
|
|
302
|
+
details: { skippedTool: name },
|
|
303
|
+
};
|
|
304
|
+
throw skipError;
|
|
305
|
+
}
|
|
306
|
+
// Handle session-relative paths and cwd tools
|
|
307
|
+
let res: ToolExecuteReturn | { cwd: string } | null | undefined;
|
|
308
|
+
const cwd = getCwd(ctx.sessionId);
|
|
309
|
+
if (name === 'pwd') {
|
|
310
|
+
res = { cwd };
|
|
311
|
+
} else if (name === 'cd') {
|
|
312
|
+
const next = joinRelative(
|
|
313
|
+
cwd,
|
|
314
|
+
String((input as Record<string, unknown>)?.path ?? '.'),
|
|
315
|
+
);
|
|
316
|
+
setCwd(ctx.sessionId, next);
|
|
317
|
+
res = { cwd: next };
|
|
318
|
+
} else if (
|
|
319
|
+
['read', 'write', 'ls', 'tree'].includes(name) &&
|
|
320
|
+
typeof (input as Record<string, unknown>)?.path === 'string'
|
|
321
|
+
) {
|
|
322
|
+
const rel = joinRelative(
|
|
323
|
+
cwd,
|
|
324
|
+
String((input as Record<string, unknown>).path),
|
|
325
|
+
);
|
|
326
|
+
const nextInput = {
|
|
327
|
+
...(input as Record<string, unknown>),
|
|
328
|
+
path: rel,
|
|
329
|
+
} as ToolExecuteInput;
|
|
330
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
331
|
+
res = base.execute?.(nextInput, options as any);
|
|
332
|
+
} else if (name === 'bash') {
|
|
333
|
+
const needsCwd =
|
|
334
|
+
!input ||
|
|
335
|
+
typeof (input as Record<string, unknown>).cwd !== 'string';
|
|
336
|
+
const nextInput = needsCwd
|
|
337
|
+
? ({
|
|
338
|
+
...(input as Record<string, unknown>),
|
|
339
|
+
cwd,
|
|
340
|
+
} as ToolExecuteInput)
|
|
341
|
+
: input;
|
|
342
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
343
|
+
res = base.execute?.(nextInput, options as any);
|
|
344
|
+
} else {
|
|
345
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
346
|
+
res = base.execute?.(input, options as any);
|
|
347
|
+
}
|
|
348
|
+
let result: unknown = res;
|
|
349
|
+
// If tool returns an async iterable, stream deltas while accumulating
|
|
350
|
+
if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
|
|
351
|
+
const chunks: unknown[] = [];
|
|
352
|
+
for await (const chunk of res as AsyncIterable<unknown>) {
|
|
353
|
+
chunks.push(chunk);
|
|
354
|
+
publish({
|
|
355
|
+
type: 'tool.delta',
|
|
356
|
+
sessionId: ctx.sessionId,
|
|
357
|
+
payload: {
|
|
358
|
+
name,
|
|
359
|
+
channel: 'output',
|
|
360
|
+
delta: chunk,
|
|
361
|
+
stepIndex: stepIndexForEvent,
|
|
362
|
+
callId: callIdFromQueue,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
// Prefer the last chunk as the result if present, otherwise the entire array
|
|
367
|
+
result = chunks.length > 0 ? chunks[chunks.length - 1] : null;
|
|
368
|
+
} else {
|
|
369
|
+
// Await promise or passthrough value
|
|
370
|
+
result = await Promise.resolve(res as ToolExecuteReturn);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isToolError(result)) {
|
|
374
|
+
stepState.failed = true;
|
|
375
|
+
await persistToolErrorResult(result, {
|
|
376
|
+
callId: callIdFromQueue,
|
|
377
|
+
startTs: startTsFromQueue,
|
|
378
|
+
stepIndexForEvent,
|
|
379
|
+
args: meta?.args,
|
|
380
|
+
});
|
|
381
|
+
processedToolErrors.add(result as object);
|
|
382
|
+
throw result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const resultPartId = crypto.randomUUID();
|
|
386
|
+
const callId = callIdFromQueue;
|
|
387
|
+
const startTs = startTsFromQueue;
|
|
388
|
+
const contentObj: {
|
|
389
|
+
name: string;
|
|
390
|
+
result: unknown;
|
|
391
|
+
callId?: string;
|
|
392
|
+
artifact?: unknown;
|
|
393
|
+
args?: unknown;
|
|
394
|
+
} = {
|
|
395
|
+
name,
|
|
396
|
+
result,
|
|
397
|
+
callId,
|
|
398
|
+
};
|
|
399
|
+
if (meta?.args !== undefined) {
|
|
400
|
+
contentObj.args = meta.args;
|
|
401
|
+
}
|
|
402
|
+
if (result && typeof result === 'object' && 'artifact' in result) {
|
|
403
|
+
try {
|
|
404
|
+
const maybeArtifact = (result as { artifact?: unknown })
|
|
405
|
+
.artifact;
|
|
406
|
+
if (maybeArtifact !== undefined)
|
|
407
|
+
contentObj.artifact = maybeArtifact;
|
|
408
|
+
} catch {}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const index = await ctx.nextIndex();
|
|
412
|
+
const endTs = Date.now();
|
|
413
|
+
const dur =
|
|
414
|
+
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
415
|
+
|
|
416
|
+
// Special-case: keep progress_update result lightweight; publish first, persist best-effort
|
|
417
|
+
if (name === 'progress_update') {
|
|
264
418
|
publish({
|
|
265
|
-
type: 'tool.
|
|
419
|
+
type: 'tool.result',
|
|
266
420
|
sessionId: ctx.sessionId,
|
|
267
|
-
payload: {
|
|
268
|
-
name,
|
|
269
|
-
channel: 'output',
|
|
270
|
-
delta: chunk,
|
|
271
|
-
stepIndex: stepIndexForEvent,
|
|
272
|
-
callId: callIdFromQueue,
|
|
273
|
-
},
|
|
421
|
+
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
274
422
|
});
|
|
423
|
+
// Persist without blocking the event loop
|
|
424
|
+
(async () => {
|
|
425
|
+
try {
|
|
426
|
+
await ctx.db.insert(messageParts).values({
|
|
427
|
+
id: resultPartId,
|
|
428
|
+
messageId: ctx.messageId,
|
|
429
|
+
index,
|
|
430
|
+
stepIndex: stepIndexForEvent,
|
|
431
|
+
type: 'tool_result',
|
|
432
|
+
content: JSON.stringify(contentObj),
|
|
433
|
+
agent: ctx.agent,
|
|
434
|
+
provider: ctx.provider,
|
|
435
|
+
model: ctx.model,
|
|
436
|
+
startedAt: startTs,
|
|
437
|
+
completedAt: endTs,
|
|
438
|
+
toolName: name,
|
|
439
|
+
toolCallId: callId,
|
|
440
|
+
toolDurationMs: dur ?? undefined,
|
|
441
|
+
});
|
|
442
|
+
} catch {}
|
|
443
|
+
})();
|
|
444
|
+
return result as ToolExecuteReturn;
|
|
275
445
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
callId,
|
|
295
|
-
};
|
|
296
|
-
if (meta?.args !== undefined) {
|
|
297
|
-
contentObj.args = meta.args;
|
|
298
|
-
}
|
|
299
|
-
if (result && typeof result === 'object' && 'artifact' in result) {
|
|
446
|
+
|
|
447
|
+
await ctx.db.insert(messageParts).values({
|
|
448
|
+
id: resultPartId,
|
|
449
|
+
messageId: ctx.messageId,
|
|
450
|
+
index,
|
|
451
|
+
stepIndex: stepIndexForEvent,
|
|
452
|
+
type: 'tool_result',
|
|
453
|
+
content: JSON.stringify(contentObj),
|
|
454
|
+
agent: ctx.agent,
|
|
455
|
+
provider: ctx.provider,
|
|
456
|
+
model: ctx.model,
|
|
457
|
+
startedAt: startTs,
|
|
458
|
+
completedAt: endTs,
|
|
459
|
+
toolName: name,
|
|
460
|
+
toolCallId: callId,
|
|
461
|
+
toolDurationMs: dur ?? undefined,
|
|
462
|
+
});
|
|
463
|
+
// Update session aggregates: total tool time and counts per tool
|
|
300
464
|
try {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
465
|
+
const sessRows = await ctx.db
|
|
466
|
+
.select()
|
|
467
|
+
.from(sessions)
|
|
468
|
+
.where(eq(sessions.id, ctx.sessionId));
|
|
469
|
+
if (sessRows.length) {
|
|
470
|
+
const row = sessRows[0] as typeof sessions.$inferSelect;
|
|
471
|
+
const totalToolTimeMs =
|
|
472
|
+
Number(row.totalToolTimeMs || 0) + (dur ?? 0);
|
|
473
|
+
let counts: Record<string, number> = {};
|
|
474
|
+
try {
|
|
475
|
+
counts = row.toolCountsJson
|
|
476
|
+
? JSON.parse(row.toolCountsJson)
|
|
477
|
+
: {};
|
|
478
|
+
} catch {}
|
|
479
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
480
|
+
await ctx.db
|
|
481
|
+
.update(sessions)
|
|
482
|
+
.set({
|
|
483
|
+
totalToolTimeMs,
|
|
484
|
+
toolCountsJson: JSON.stringify(counts),
|
|
485
|
+
lastActiveAt: endTs,
|
|
486
|
+
})
|
|
487
|
+
.where(eq(sessions.id, ctx.sessionId));
|
|
488
|
+
}
|
|
304
489
|
} catch {}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const index = await ctx.nextIndex();
|
|
308
|
-
const endTs = Date.now();
|
|
309
|
-
const dur =
|
|
310
|
-
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
311
|
-
|
|
312
|
-
// Special-case: keep progress_update result lightweight; publish first, persist best-effort
|
|
313
|
-
if (name === 'progress_update') {
|
|
314
490
|
publish({
|
|
315
491
|
type: 'tool.result',
|
|
316
492
|
sessionId: ctx.sessionId,
|
|
317
493
|
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
318
494
|
});
|
|
319
|
-
|
|
320
|
-
(async () => {
|
|
495
|
+
if (name === 'update_plan') {
|
|
321
496
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
toolName: name,
|
|
335
|
-
toolCallId: callId,
|
|
336
|
-
toolDurationMs: dur ?? undefined,
|
|
337
|
-
});
|
|
497
|
+
const resultValue = (contentObj as { result?: unknown })
|
|
498
|
+
.result as { items?: unknown; note?: unknown } | undefined;
|
|
499
|
+
if (resultValue && Array.isArray(resultValue.items)) {
|
|
500
|
+
publish({
|
|
501
|
+
type: 'plan.updated',
|
|
502
|
+
sessionId: ctx.sessionId,
|
|
503
|
+
payload: {
|
|
504
|
+
items: resultValue.items,
|
|
505
|
+
note: resultValue.note,
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
338
509
|
} catch {}
|
|
339
|
-
}
|
|
510
|
+
}
|
|
340
511
|
return result as ToolExecuteReturn;
|
|
341
|
-
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
stepState.failed = true;
|
|
342
514
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
content: JSON.stringify(contentObj),
|
|
350
|
-
agent: ctx.agent,
|
|
351
|
-
provider: ctx.provider,
|
|
352
|
-
model: ctx.model,
|
|
353
|
-
startedAt: startTs,
|
|
354
|
-
completedAt: endTs,
|
|
355
|
-
toolName: name,
|
|
356
|
-
toolCallId: callId,
|
|
357
|
-
toolDurationMs: dur ?? undefined,
|
|
358
|
-
});
|
|
359
|
-
// Update session aggregates: total tool time and counts per tool
|
|
360
|
-
try {
|
|
361
|
-
const sessRows = await ctx.db
|
|
362
|
-
.select()
|
|
363
|
-
.from(sessions)
|
|
364
|
-
.where(eq(sessions.id, ctx.sessionId));
|
|
365
|
-
if (sessRows.length) {
|
|
366
|
-
const row = sessRows[0] as typeof sessions.$inferSelect;
|
|
367
|
-
const totalToolTimeMs =
|
|
368
|
-
Number(row.totalToolTimeMs || 0) + (dur ?? 0);
|
|
369
|
-
let counts: Record<string, number> = {};
|
|
370
|
-
try {
|
|
371
|
-
counts = row.toolCountsJson
|
|
372
|
-
? JSON.parse(row.toolCountsJson)
|
|
373
|
-
: {};
|
|
374
|
-
} catch {}
|
|
375
|
-
counts[name] = (counts[name] || 0) + 1;
|
|
376
|
-
await ctx.db
|
|
377
|
-
.update(sessions)
|
|
378
|
-
.set({
|
|
379
|
-
totalToolTimeMs,
|
|
380
|
-
toolCountsJson: JSON.stringify(counts),
|
|
381
|
-
lastActiveAt: endTs,
|
|
382
|
-
})
|
|
383
|
-
.where(eq(sessions.id, ctx.sessionId));
|
|
515
|
+
// Tool execution failed
|
|
516
|
+
if (
|
|
517
|
+
isToolError(error) &&
|
|
518
|
+
processedToolErrors.has(error as object)
|
|
519
|
+
) {
|
|
520
|
+
throw error;
|
|
384
521
|
}
|
|
385
|
-
} catch {}
|
|
386
|
-
publish({
|
|
387
|
-
type: 'tool.result',
|
|
388
|
-
sessionId: ctx.sessionId,
|
|
389
|
-
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
390
|
-
});
|
|
391
|
-
if (name === 'update_plan') {
|
|
392
|
-
try {
|
|
393
|
-
const result = (contentObj as { result?: unknown }).result as
|
|
394
|
-
| { items?: unknown; note?: unknown }
|
|
395
|
-
| undefined;
|
|
396
|
-
if (result && Array.isArray(result.items)) {
|
|
397
|
-
publish({
|
|
398
|
-
type: 'plan.updated',
|
|
399
|
-
sessionId: ctx.sessionId,
|
|
400
|
-
payload: { items: result.items, note: result.note },
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
} catch {}
|
|
404
|
-
}
|
|
405
|
-
return result;
|
|
406
|
-
} catch (error) {
|
|
407
|
-
// Tool execution failed
|
|
408
|
-
// Check if the error is already a structured tool error response
|
|
409
|
-
const resultPartId = crypto.randomUUID();
|
|
410
|
-
const callId = callIdFromQueue;
|
|
411
|
-
const startTs = startTsFromQueue;
|
|
412
|
-
const endTs = Date.now();
|
|
413
|
-
const dur =
|
|
414
|
-
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
415
|
-
|
|
416
|
-
// If the tool returned a structured error response, use it directly
|
|
417
|
-
// Otherwise, wrap the thrown error in our standard format
|
|
418
|
-
let errorResult: unknown;
|
|
419
|
-
if (isToolError(error)) {
|
|
420
|
-
errorResult = error;
|
|
421
|
-
} else {
|
|
422
|
-
const errorMessage =
|
|
423
|
-
error instanceof Error ? error.message : String(error);
|
|
424
|
-
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
425
|
-
errorResult = {
|
|
426
|
-
ok: false,
|
|
427
|
-
error: errorMessage,
|
|
428
|
-
stack: errorStack,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
522
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
523
|
+
const errorResult = isToolError(error)
|
|
524
|
+
? error
|
|
525
|
+
: (() => {
|
|
526
|
+
const errorMessage =
|
|
527
|
+
error instanceof Error ? error.message : String(error);
|
|
528
|
+
const errorStack =
|
|
529
|
+
error instanceof Error ? error.stack : undefined;
|
|
530
|
+
return {
|
|
531
|
+
ok: false,
|
|
532
|
+
error: errorMessage,
|
|
533
|
+
stack: errorStack,
|
|
534
|
+
};
|
|
535
|
+
})();
|
|
437
536
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
537
|
+
await persistToolErrorResult(errorResult, {
|
|
538
|
+
callId: callIdFromQueue,
|
|
539
|
+
startTs: startTsFromQueue,
|
|
540
|
+
stepIndexForEvent,
|
|
541
|
+
args: meta?.args,
|
|
542
|
+
});
|
|
441
543
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
await ctx.db.insert(messageParts).values({
|
|
446
|
-
id: resultPartId,
|
|
447
|
-
messageId: ctx.messageId,
|
|
448
|
-
index,
|
|
449
|
-
stepIndex: stepIndexForEvent,
|
|
450
|
-
type: 'tool_result',
|
|
451
|
-
content: JSON.stringify(contentObj),
|
|
452
|
-
agent: ctx.agent,
|
|
453
|
-
provider: ctx.provider,
|
|
454
|
-
model: ctx.model,
|
|
455
|
-
startedAt: startTs,
|
|
456
|
-
completedAt: endTs,
|
|
457
|
-
toolName: name,
|
|
458
|
-
toolCallId: callId,
|
|
459
|
-
toolDurationMs: dur ?? undefined,
|
|
460
|
-
});
|
|
544
|
+
if (isToolError(error)) {
|
|
545
|
+
processedToolErrors.add(error as object);
|
|
546
|
+
}
|
|
461
547
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
467
|
-
});
|
|
548
|
+
// Re-throw so AI SDK can handle it
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
468
552
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
553
|
+
const queued = stepState.chain
|
|
554
|
+
.catch(() => undefined)
|
|
555
|
+
.then(() => executeWithGuards());
|
|
556
|
+
stepState.chain = queued.then(
|
|
557
|
+
() => undefined,
|
|
558
|
+
() => undefined,
|
|
559
|
+
);
|
|
560
|
+
return queued;
|
|
472
561
|
},
|
|
473
562
|
} as Tool;
|
|
474
563
|
}
|
package/src/runtime/logger.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized logging utility
|
|
3
|
-
*
|
|
4
|
-
* Provides structured logging with debug mode awareness.
|
|
5
|
-
* Replaces scattered console.log calls throughout the codebase.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { isDebugEnabled, isTraceEnabled } from './debug-state';
|
|
9
|
-
|
|
10
|
-
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Format a log message with optional metadata
|
|
14
|
-
*/
|
|
15
|
-
function _formatMessage(
|
|
16
|
-
level: LogLevel,
|
|
17
|
-
message: string,
|
|
18
|
-
meta?: Record<string, unknown>,
|
|
19
|
-
): string {
|
|
20
|
-
const timestamp = new Date().toISOString();
|
|
21
|
-
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
22
|
-
|
|
23
|
-
if (meta && Object.keys(meta).length > 0) {
|
|
24
|
-
return `${prefix} ${message} ${JSON.stringify(meta)}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return `${prefix} ${message}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Log at debug level (only when debug mode is enabled)
|
|
32
|
-
*/
|
|
33
|
-
export function debug(message: string, meta?: Record<string, unknown>): void {
|
|
34
|
-
if (!isDebugEnabled()) return;
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
if (meta && Object.keys(meta).length > 0) {
|
|
38
|
-
console.log(`[debug] ${message}`, meta);
|
|
39
|
-
} else {
|
|
40
|
-
console.log(`[debug] ${message}`);
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
// Silently fail
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Log informational messages (only when debug or trace mode is enabled)
|
|
49
|
-
*/
|
|
50
|
-
export function info(message: string, meta?: Record<string, unknown>): void {
|
|
51
|
-
if (!isDebugEnabled() && !isTraceEnabled()) return;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
if (meta && Object.keys(meta).length > 0) {
|
|
55
|
-
console.log(`[info] ${message}`, meta);
|
|
56
|
-
} else {
|
|
57
|
-
console.log(`[info] ${message}`);
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
// Silently fail
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Log warning messages
|
|
66
|
-
*/
|
|
67
|
-
export function warn(message: string, meta?: Record<string, unknown>): void {
|
|
68
|
-
try {
|
|
69
|
-
if (meta && Object.keys(meta).length > 0) {
|
|
70
|
-
console.warn(`[warn] ${message}`, meta);
|
|
71
|
-
} else {
|
|
72
|
-
console.warn(`[warn] ${message}`);
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
// Silently fail
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Log error messages (only in debug mode, stack trace only with --trace)
|
|
81
|
-
*/
|
|
82
|
-
export function error(
|
|
83
|
-
message: string,
|
|
84
|
-
err?: unknown,
|
|
85
|
-
meta?: Record<string, unknown>,
|
|
86
|
-
): void {
|
|
87
|
-
// Only log errors when debug mode is enabled
|
|
88
|
-
if (!isDebugEnabled()) return;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const logMeta: Record<string, unknown> = { ...meta };
|
|
92
|
-
|
|
93
|
-
if (err) {
|
|
94
|
-
if (err instanceof Error) {
|
|
95
|
-
// Always show error name and message in debug mode
|
|
96
|
-
logMeta.error = {
|
|
97
|
-
name: err.name,
|
|
98
|
-
message: err.message,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// Show full stack trace only with --trace flag
|
|
102
|
-
if (isTraceEnabled() && err.stack) {
|
|
103
|
-
logMeta.error.stack = err.stack;
|
|
104
|
-
}
|
|
105
|
-
} else if (typeof err === 'string') {
|
|
106
|
-
logMeta.error = err;
|
|
107
|
-
} else if (err && typeof err === 'object') {
|
|
108
|
-
// For other error objects, try to extract useful info
|
|
109
|
-
const errObj = err as Record<string, unknown>;
|
|
110
|
-
logMeta.error = {
|
|
111
|
-
...(typeof errObj.name === 'string' ? { name: errObj.name } : {}),
|
|
112
|
-
...(typeof errObj.message === 'string'
|
|
113
|
-
? { message: errObj.message }
|
|
114
|
-
: {}),
|
|
115
|
-
...(typeof errObj.code === 'string' ? { code: errObj.code } : {}),
|
|
116
|
-
...(typeof errObj.status === 'number'
|
|
117
|
-
? { status: errObj.status }
|
|
118
|
-
: {}),
|
|
119
|
-
...(typeof errObj.statusCode === 'number'
|
|
120
|
-
? { statusCode: errObj.statusCode }
|
|
121
|
-
: {}),
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Include stack in trace mode
|
|
125
|
-
if (isTraceEnabled() && typeof errObj.stack === 'string') {
|
|
126
|
-
logMeta.error.stack = errObj.stack;
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
// Fallback for primitive types
|
|
130
|
-
logMeta.error = String(err);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (Object.keys(logMeta).length > 0) {
|
|
135
|
-
console.error(`[error] ${message}`, logMeta);
|
|
136
|
-
} else {
|
|
137
|
-
console.error(`[error] ${message}`);
|
|
138
|
-
}
|
|
139
|
-
} catch (logErr) {
|
|
140
|
-
// Last resort: at least try to log something
|
|
141
|
-
try {
|
|
142
|
-
console.error(`[error] ${message} (logging failed:`, logErr, ')');
|
|
143
|
-
} catch {
|
|
144
|
-
// Give up silently
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Logger object with all methods
|
|
151
|
-
*/
|
|
152
|
-
export const logger = {
|
|
153
|
-
debug,
|
|
154
|
-
info,
|
|
155
|
-
warn,
|
|
156
|
-
error,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Timing utilities (integrates with existing debug.ts timing)
|
|
161
|
-
*/
|
|
162
|
-
function nowMs(): number {
|
|
163
|
-
const perf = (globalThis as { performance?: { now?: () => number } })
|
|
164
|
-
.performance;
|
|
165
|
-
if (perf && typeof perf.now === 'function') return perf.now();
|
|
166
|
-
return Date.now();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
type Timer = {
|
|
170
|
-
end(meta?: Record<string, unknown>): void;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Create a timer for performance measurement
|
|
175
|
-
* Only active when debug mode is enabled
|
|
176
|
-
*/
|
|
177
|
-
export function time(label: string): Timer {
|
|
178
|
-
if (!isDebugEnabled()) {
|
|
179
|
-
return { end() {} };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const start = nowMs();
|
|
183
|
-
let finished = false;
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
end(meta?: Record<string, unknown>) {
|
|
187
|
-
if (finished) return;
|
|
188
|
-
finished = true;
|
|
189
|
-
const duration = nowMs() - start;
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const line = `[timing] ${label} ${duration.toFixed(1)}ms`;
|
|
193
|
-
if (meta && Object.keys(meta).length) {
|
|
194
|
-
console.log(line, meta);
|
|
195
|
-
} else {
|
|
196
|
-
console.log(line);
|
|
197
|
-
}
|
|
198
|
-
} catch {
|
|
199
|
-
// Silently fail
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Export legacy compatibility
|
|
206
|
-
export { isDebugEnabled, isTraceEnabled };
|