@agi-cli/server 0.1.55

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.
@@ -0,0 +1,131 @@
1
+ import { getHomeDir } from '@agi-cli/sdk';
2
+
3
+ export async function getEnvironmentContext(
4
+ projectRoot: string,
5
+ ): Promise<string> {
6
+ const parts: string[] = [];
7
+
8
+ parts.push(
9
+ 'Here is some useful information about the environment you are running in:',
10
+ );
11
+ parts.push('<env>');
12
+ parts.push(` Working directory: ${projectRoot}`);
13
+
14
+ try {
15
+ const { existsSync } = await import('node:fs');
16
+ const isGitRepo = existsSync(`${projectRoot}/.git`);
17
+ parts.push(` Is directory a git repo: ${isGitRepo ? 'yes' : 'no'}`);
18
+ } catch {
19
+ parts.push(' Is directory a git repo: unknown');
20
+ }
21
+
22
+ parts.push(` Platform: ${process.platform}`);
23
+ parts.push(` Today's date: ${new Date().toDateString()}`);
24
+ parts.push('</env>');
25
+
26
+ return parts.join('\n');
27
+ }
28
+
29
+ export async function getProjectTree(projectRoot: string): Promise<string> {
30
+ try {
31
+ const { promisify } = await import('node:util');
32
+ const { exec } = await import('node:child_process');
33
+ const execAsync = promisify(exec);
34
+
35
+ const { stdout } = await execAsync(
36
+ 'git ls-files 2>/dev/null || find . -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" 2>/dev/null | head -100',
37
+ { cwd: projectRoot, maxBuffer: 1024 * 1024 },
38
+ );
39
+
40
+ if (!stdout.trim()) return '';
41
+
42
+ const files = stdout.trim().split('\n').slice(0, 100);
43
+ return `<project>\n${files.join('\n')}\n</project>`;
44
+ } catch {
45
+ return '';
46
+ }
47
+ }
48
+
49
+ export async function findInstructionFiles(
50
+ projectRoot: string,
51
+ ): Promise<string[]> {
52
+ const { existsSync } = await import('node:fs');
53
+ const { join } = await import('node:path');
54
+ const foundPaths: string[] = [];
55
+
56
+ const localFiles = ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md'];
57
+ for (const filename of localFiles) {
58
+ let currentDir = projectRoot;
59
+ for (let i = 0; i < 5; i++) {
60
+ const filePath = join(currentDir, filename);
61
+ if (existsSync(filePath)) {
62
+ foundPaths.push(filePath);
63
+ break;
64
+ }
65
+ const parentDir = join(currentDir, '..');
66
+ if (parentDir === currentDir) break;
67
+ currentDir = parentDir;
68
+ }
69
+ }
70
+
71
+ const homeDir = getHomeDir();
72
+ const globalFiles = [
73
+ join(homeDir, '.config', 'agi', 'AGENTS.md'),
74
+ join(homeDir, '.claude', 'CLAUDE.md'),
75
+ ];
76
+
77
+ for (const filePath of globalFiles) {
78
+ if (existsSync(filePath) && !foundPaths.includes(filePath)) {
79
+ foundPaths.push(filePath);
80
+ }
81
+ }
82
+
83
+ return foundPaths;
84
+ }
85
+
86
+ export async function loadInstructionFiles(
87
+ projectRoot: string,
88
+ ): Promise<string> {
89
+ const paths = await findInstructionFiles(projectRoot);
90
+ if (paths.length === 0) return '';
91
+
92
+ const contents: string[] = [];
93
+
94
+ for (const path of paths) {
95
+ try {
96
+ const file = Bun.file(path);
97
+ const content = await file.text();
98
+ if (content.trim()) {
99
+ contents.push(
100
+ `\n--- Custom Instructions from ${path} ---\n${content.trim()}`,
101
+ );
102
+ }
103
+ } catch {}
104
+ }
105
+
106
+ return contents.join('\n');
107
+ }
108
+
109
+ export async function composeEnvironmentAndInstructions(
110
+ projectRoot: string,
111
+ options?: { includeProjectTree?: boolean },
112
+ ): Promise<string> {
113
+ const parts: string[] = [];
114
+
115
+ const envContext = await getEnvironmentContext(projectRoot);
116
+ parts.push(envContext);
117
+
118
+ if (options?.includeProjectTree !== false) {
119
+ const projectTree = await getProjectTree(projectRoot);
120
+ if (projectTree) {
121
+ parts.push(projectTree);
122
+ }
123
+ }
124
+
125
+ const customInstructions = await loadInstructionFiles(projectRoot);
126
+ if (customInstructions) {
127
+ parts.push(customInstructions);
128
+ }
129
+
130
+ return parts.filter(Boolean).join('\n\n');
131
+ }
@@ -0,0 +1,196 @@
1
+ import { APICallError } from 'ai';
2
+
3
+ export type ErrorPayload = {
4
+ message: string;
5
+ type: string;
6
+ details?: Record<string, unknown>;
7
+ };
8
+
9
+ export function toErrorPayload(err: unknown): ErrorPayload {
10
+ let actualError = err;
11
+ if (
12
+ err &&
13
+ typeof err === 'object' &&
14
+ 'error' in err &&
15
+ Object.keys(err).length === 1
16
+ ) {
17
+ actualError = (err as { error: unknown }).error;
18
+ }
19
+
20
+ const asObj =
21
+ actualError && typeof actualError === 'object'
22
+ ? (actualError as Record<string, unknown>)
23
+ : undefined;
24
+ let message = '';
25
+ let errorType = 'unknown';
26
+ const details: Record<string, unknown> = {};
27
+
28
+ if (APICallError.isInstance(actualError)) {
29
+ errorType = 'api_error';
30
+ message = actualError.message || 'API call failed';
31
+
32
+ details.name = actualError.name;
33
+ details.statusCode = actualError.statusCode;
34
+ details.url = actualError.url;
35
+ details.isRetryable = actualError.isRetryable;
36
+
37
+ if (actualError.responseBody) {
38
+ details.responseBody = actualError.responseBody;
39
+ try {
40
+ const parsed = JSON.parse(actualError.responseBody);
41
+ if (parsed.error) {
42
+ if (typeof parsed.error === 'string') {
43
+ message = parsed.error;
44
+ } else if (parsed.error.message) {
45
+ message = parsed.error.message;
46
+ }
47
+ }
48
+ if (parsed.error?.type) {
49
+ details.apiErrorType = parsed.error.type;
50
+ }
51
+ } catch {}
52
+ }
53
+
54
+ if (actualError.requestBodyValues) {
55
+ details.requestBodyValues = actualError.requestBodyValues;
56
+ }
57
+
58
+ if (actualError.responseHeaders) {
59
+ details.responseHeaders = actualError.responseHeaders;
60
+ }
61
+
62
+ if (actualError.cause) {
63
+ const cause = actualError.cause as Record<string, unknown> | undefined;
64
+ details.cause = {
65
+ message: typeof cause?.message === 'string' ? cause.message : undefined,
66
+ code: cause?.code,
67
+ status: cause?.status ?? cause?.statusCode,
68
+ };
69
+ }
70
+
71
+ return { message, type: errorType, details };
72
+ }
73
+
74
+ if (
75
+ asObj &&
76
+ 'type' in asObj &&
77
+ asObj.type === 'error' &&
78
+ 'error' in asObj &&
79
+ typeof asObj.error === 'object' &&
80
+ asObj.error
81
+ ) {
82
+ const errorObj = asObj.error as Record<string, unknown>;
83
+
84
+ if (typeof errorObj.message === 'string') {
85
+ message = errorObj.message;
86
+ }
87
+ if (typeof errorObj.type === 'string') {
88
+ errorType = errorObj.type;
89
+ details.errorType = errorObj.type;
90
+ }
91
+ if (typeof errorObj.code === 'string') {
92
+ details.code = errorObj.code;
93
+ }
94
+ if ('param' in errorObj) {
95
+ details.param = errorObj.param;
96
+ }
97
+
98
+ return { message, type: errorType, details };
99
+ }
100
+
101
+ if (asObj) {
102
+ if ('name' in asObj && typeof asObj.name === 'string') {
103
+ errorType = asObj.name;
104
+ details.name = asObj.name;
105
+ }
106
+
107
+ if ('type' in asObj && typeof asObj.type === 'string') {
108
+ errorType = asObj.type;
109
+ details.type = asObj.type;
110
+ }
111
+
112
+ if ('code' in asObj && asObj.code != null) {
113
+ details.code = asObj.code;
114
+ }
115
+
116
+ if ('status' in asObj && asObj.status != null) {
117
+ details.status = asObj.status;
118
+ }
119
+
120
+ if ('statusCode' in asObj && asObj.statusCode != null) {
121
+ details.statusCode = asObj.statusCode;
122
+ }
123
+ }
124
+
125
+ if (asObj && typeof asObj.message === 'string' && asObj.message) {
126
+ message = asObj.message;
127
+ } else if (typeof actualError === 'string') {
128
+ message = actualError;
129
+ } else if (asObj && typeof asObj.error === 'string' && asObj.error) {
130
+ message = asObj.error;
131
+ } else if (
132
+ asObj &&
133
+ typeof asObj.responseBody === 'string' &&
134
+ asObj.responseBody
135
+ ) {
136
+ details.responseBody = asObj.responseBody;
137
+ try {
138
+ const parsed = JSON.parse(asObj.responseBody);
139
+ if (parsed.error) {
140
+ if (typeof parsed.error === 'string') {
141
+ message = parsed.error;
142
+ } else if (typeof parsed.error.message === 'string') {
143
+ message = parsed.error.message;
144
+ } else {
145
+ message = asObj.responseBody;
146
+ }
147
+ } else {
148
+ message = asObj.responseBody;
149
+ }
150
+ } catch {
151
+ message = asObj.responseBody;
152
+ }
153
+ } else if (asObj?.statusCode && asObj.url) {
154
+ message = `HTTP ${String(asObj.statusCode)} error at ${String(asObj.url)}`;
155
+ details.url = asObj.url;
156
+ } else if (asObj?.name) {
157
+ message = String(asObj.name);
158
+ } else {
159
+ message = 'An error occurred';
160
+ }
161
+
162
+ if (asObj) {
163
+ if ('url' in asObj) details.url = asObj.url;
164
+ if ('isRetryable' in asObj) details.isRetryable = asObj.isRetryable;
165
+ if ('data' in asObj) details.data = asObj.data;
166
+
167
+ if (asObj.cause) {
168
+ const c = asObj.cause as Record<string, unknown> | undefined;
169
+ details.cause = {
170
+ message: typeof c?.message === 'string' ? c.message : undefined,
171
+ code: c?.code,
172
+ status: c?.status ?? c?.statusCode,
173
+ };
174
+ }
175
+
176
+ if (
177
+ (asObj as { response?: { status?: unknown; statusText?: unknown } })
178
+ ?.response?.status
179
+ ) {
180
+ details.response = {
181
+ status: (
182
+ asObj as { response?: { status?: unknown; statusText?: unknown } }
183
+ ).response?.status,
184
+ statusText: (
185
+ asObj as { response?: { status?: unknown; statusText?: unknown } }
186
+ ).response?.statusText,
187
+ };
188
+ }
189
+ }
190
+
191
+ return {
192
+ message,
193
+ type: errorType,
194
+ details: Object.keys(details).length ? details : undefined,
195
+ };
196
+ }
@@ -0,0 +1,156 @@
1
+ import { convertToModelMessages, type ModelMessage, type UIMessage } from 'ai';
2
+ import type { getDb } from '@agi-cli/database';
3
+ import { messages, messageParts } from '@agi-cli/database/schema';
4
+ import { eq, asc } from 'drizzle-orm';
5
+ import { debugLog } from './debug.ts';
6
+
7
+ /**
8
+ * Builds the conversation history for a session from the database,
9
+ * converting it to the format expected by the AI SDK.
10
+ */
11
+ export async function buildHistoryMessages(
12
+ db: Awaited<ReturnType<typeof getDb>>,
13
+ sessionId: string,
14
+ ): Promise<ModelMessage[]> {
15
+ const rows = await db
16
+ .select()
17
+ .from(messages)
18
+ .where(eq(messages.sessionId, sessionId))
19
+ .orderBy(asc(messages.createdAt));
20
+
21
+ const ui: UIMessage[] = [];
22
+
23
+ for (const m of rows) {
24
+ const parts = await db
25
+ .select()
26
+ .from(messageParts)
27
+ .where(eq(messageParts.messageId, m.id))
28
+ .orderBy(asc(messageParts.index));
29
+
30
+ if (m.role === 'user') {
31
+ const uparts: UIMessage['parts'] = [];
32
+ for (const p of parts) {
33
+ if (p.type !== 'text') continue;
34
+ try {
35
+ const obj = JSON.parse(p.content ?? '{}');
36
+ const t = String(obj.text ?? '');
37
+ if (t) uparts.push({ type: 'text', text: t });
38
+ } catch {}
39
+ }
40
+ if (uparts.length) {
41
+ ui.push({ id: m.id, role: 'user', parts: uparts });
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if (m.role === 'assistant') {
47
+ const assistantParts: UIMessage['parts'] = [];
48
+ const toolCalls: Array<{ name: string; callId: string; args: unknown }> =
49
+ [];
50
+ const toolResults: Array<{
51
+ name: string;
52
+ callId: string;
53
+ result: unknown;
54
+ }> = [];
55
+
56
+ for (const p of parts) {
57
+ if (p.type === 'text') {
58
+ try {
59
+ const obj = JSON.parse(p.content ?? '{}');
60
+ const t = String(obj.text ?? '');
61
+ if (t) assistantParts.push({ type: 'text', text: t });
62
+ } catch {}
63
+ } else if (p.type === 'tool_call') {
64
+ try {
65
+ const obj = JSON.parse(p.content ?? '{}') as {
66
+ name?: string;
67
+ callId?: string;
68
+ args?: unknown;
69
+ };
70
+ if (obj.callId && obj.name) {
71
+ toolCalls.push({
72
+ name: obj.name,
73
+ callId: obj.callId,
74
+ args: obj.args,
75
+ });
76
+ }
77
+ } catch {}
78
+ } else if (p.type === 'tool_result') {
79
+ try {
80
+ const obj = JSON.parse(p.content ?? '{}') as {
81
+ name?: string;
82
+ callId?: string;
83
+ result?: unknown;
84
+ };
85
+ if (obj.callId) {
86
+ toolResults.push({
87
+ name: obj.name ?? 'tool',
88
+ callId: obj.callId,
89
+ result: obj.result,
90
+ });
91
+ }
92
+ } catch {}
93
+ }
94
+ // Skip error parts in history
95
+ }
96
+
97
+ const hasIncompleteTools = toolCalls.some(
98
+ (call) => !toolResults.find((result) => result.callId === call.callId),
99
+ );
100
+
101
+ if (hasIncompleteTools) {
102
+ debugLog(
103
+ `[buildHistoryMessages] Incomplete tool calls for assistant message ${m.id}, pushing text only`,
104
+ );
105
+ if (assistantParts.length) {
106
+ ui.push({ id: m.id, role: 'assistant', parts: assistantParts });
107
+ }
108
+ continue;
109
+ }
110
+
111
+ for (const call of toolCalls) {
112
+ const toolType = `tool-${call.name}` as `tool-${string}`;
113
+ const result = toolResults.find((r) => r.callId === call.callId);
114
+
115
+ if (result) {
116
+ const outputStr = (() => {
117
+ const r = result.result;
118
+ if (typeof r === 'string') return r;
119
+ try {
120
+ return JSON.stringify(r);
121
+ } catch {
122
+ return String(r);
123
+ }
124
+ })();
125
+
126
+ assistantParts.push({
127
+ type: toolType,
128
+ state: 'output-available',
129
+ toolCallId: call.callId,
130
+ input: call.args,
131
+ output: outputStr,
132
+ } as never);
133
+ }
134
+ }
135
+
136
+ if (assistantParts.length) {
137
+ ui.push({ id: m.id, role: 'assistant', parts: assistantParts });
138
+
139
+ if (toolResults.length) {
140
+ const userParts: UIMessage['parts'] = toolResults.map((r) => {
141
+ const out =
142
+ typeof r.result === 'string'
143
+ ? r.result
144
+ : JSON.stringify(r.result);
145
+ return { type: 'text', text: out };
146
+ });
147
+ if (userParts.length) {
148
+ ui.push({ id: m.id, role: 'user', parts: userParts });
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ return convertToModelMessages(ui);
156
+ }