@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.
- package/package.json +41 -0
- package/src/events/bus.ts +28 -0
- package/src/events/types.ts +20 -0
- package/src/index.ts +183 -0
- package/src/openapi/spec.ts +474 -0
- package/src/routes/ask.ts +59 -0
- package/src/routes/config.ts +124 -0
- package/src/routes/git.ts +736 -0
- package/src/routes/openapi.ts +6 -0
- package/src/routes/root.ts +5 -0
- package/src/routes/session-messages.ts +123 -0
- package/src/routes/session-stream.ts +45 -0
- package/src/routes/sessions.ts +87 -0
- package/src/runtime/agent-registry.ts +327 -0
- package/src/runtime/ask-service.ts +363 -0
- package/src/runtime/cwd.ts +69 -0
- package/src/runtime/db-operations.ts +94 -0
- package/src/runtime/debug.ts +104 -0
- package/src/runtime/environment.ts +131 -0
- package/src/runtime/error-handling.ts +196 -0
- package/src/runtime/history-builder.ts +156 -0
- package/src/runtime/message-service.ts +392 -0
- package/src/runtime/prompt.ts +79 -0
- package/src/runtime/provider-selection.ts +123 -0
- package/src/runtime/provider.ts +138 -0
- package/src/runtime/runner.ts +313 -0
- package/src/runtime/session-manager.ts +95 -0
- package/src/runtime/session-queue.ts +82 -0
- package/src/runtime/stream-handlers.ts +275 -0
- package/src/runtime/token-utils.ts +35 -0
- package/src/runtime/tool-context-setup.ts +58 -0
- package/src/runtime/tool-context.ts +72 -0
- package/src/tools/adapter.ts +380 -0
- package/src/types/sql-imports.d.ts +5 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
import { messageParts, sessions } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { publish } from '../events/bus.ts';
|
|
5
|
+
import type { DiscoveredTool } from '@agi-cli/sdk';
|
|
6
|
+
import { getCwd, setCwd, joinRelative } from '../runtime/cwd.ts';
|
|
7
|
+
import type { ToolAdapterContext } from '../runtime/tool-context.ts';
|
|
8
|
+
|
|
9
|
+
export type { ToolAdapterContext } from '../runtime/tool-context.ts';
|
|
10
|
+
|
|
11
|
+
type ToolExecuteSignature = Tool['execute'] extends (
|
|
12
|
+
input: infer Input,
|
|
13
|
+
options: infer Options,
|
|
14
|
+
) => infer Result
|
|
15
|
+
? { input: Input; options: Options; result: Result }
|
|
16
|
+
: { input: unknown; options: unknown; result: unknown };
|
|
17
|
+
type ToolExecuteInput = ToolExecuteSignature['input'];
|
|
18
|
+
type ToolExecuteOptions = ToolExecuteSignature['options'] extends never
|
|
19
|
+
? undefined
|
|
20
|
+
: ToolExecuteSignature['options'];
|
|
21
|
+
type ToolExecuteReturn = ToolExecuteSignature['result'];
|
|
22
|
+
|
|
23
|
+
type PendingCallMeta = {
|
|
24
|
+
callId: string;
|
|
25
|
+
startTs: number;
|
|
26
|
+
stepIndex?: number;
|
|
27
|
+
args?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function getPendingQueue(
|
|
31
|
+
map: Map<string, PendingCallMeta[]>,
|
|
32
|
+
name: string,
|
|
33
|
+
): PendingCallMeta[] {
|
|
34
|
+
let queue = map.get(name);
|
|
35
|
+
if (!queue) {
|
|
36
|
+
queue = [];
|
|
37
|
+
map.set(name, queue);
|
|
38
|
+
}
|
|
39
|
+
return queue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function adaptTools(tools: DiscoveredTool[], ctx: ToolAdapterContext) {
|
|
43
|
+
const out: Record<string, Tool> = {};
|
|
44
|
+
const pendingCalls = new Map<string, PendingCallMeta[]>();
|
|
45
|
+
let firstToolCallReported = false;
|
|
46
|
+
|
|
47
|
+
for (const { name, tool } of tools) {
|
|
48
|
+
const base = tool;
|
|
49
|
+
out[name] = {
|
|
50
|
+
...base,
|
|
51
|
+
async onInputStart(options: unknown) {
|
|
52
|
+
const queue = getPendingQueue(pendingCalls, name);
|
|
53
|
+
queue.push({
|
|
54
|
+
callId: crypto.randomUUID(),
|
|
55
|
+
startTs: Date.now(),
|
|
56
|
+
stepIndex: ctx.stepIndex,
|
|
57
|
+
});
|
|
58
|
+
if (typeof base.onInputStart === 'function')
|
|
59
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
60
|
+
await base.onInputStart(options as any);
|
|
61
|
+
},
|
|
62
|
+
async onInputDelta(options: unknown) {
|
|
63
|
+
const delta = (options as { inputTextDelta?: string } | undefined)
|
|
64
|
+
?.inputTextDelta;
|
|
65
|
+
const queue = pendingCalls.get(name);
|
|
66
|
+
const meta = queue?.length ? queue[queue.length - 1] : undefined;
|
|
67
|
+
// Stream tool argument deltas as events if needed
|
|
68
|
+
publish({
|
|
69
|
+
type: 'tool.delta',
|
|
70
|
+
sessionId: ctx.sessionId,
|
|
71
|
+
payload: {
|
|
72
|
+
name,
|
|
73
|
+
channel: 'input',
|
|
74
|
+
delta,
|
|
75
|
+
stepIndex: meta?.stepIndex ?? ctx.stepIndex,
|
|
76
|
+
callId: meta?.callId,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (typeof base.onInputDelta === 'function')
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
81
|
+
await base.onInputDelta(options as any);
|
|
82
|
+
},
|
|
83
|
+
async onInputAvailable(options: unknown) {
|
|
84
|
+
const args = (options as { input?: unknown } | undefined)?.input;
|
|
85
|
+
const queue = getPendingQueue(pendingCalls, name);
|
|
86
|
+
let meta = queue.length ? queue[queue.length - 1] : undefined;
|
|
87
|
+
if (!meta) {
|
|
88
|
+
meta = {
|
|
89
|
+
callId: crypto.randomUUID(),
|
|
90
|
+
startTs: Date.now(),
|
|
91
|
+
stepIndex: ctx.stepIndex,
|
|
92
|
+
};
|
|
93
|
+
queue.push(meta);
|
|
94
|
+
}
|
|
95
|
+
meta.stepIndex = ctx.stepIndex;
|
|
96
|
+
meta.args = args;
|
|
97
|
+
const callId = meta.callId;
|
|
98
|
+
const callPartId = crypto.randomUUID();
|
|
99
|
+
const startTs = meta.startTs;
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
!firstToolCallReported &&
|
|
103
|
+
typeof ctx.onFirstToolCall === 'function'
|
|
104
|
+
) {
|
|
105
|
+
firstToolCallReported = true;
|
|
106
|
+
try {
|
|
107
|
+
ctx.onFirstToolCall();
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Special-case: progress updates must render instantly. Publish before any DB work.
|
|
112
|
+
if (name === 'progress_update') {
|
|
113
|
+
publish({
|
|
114
|
+
type: 'tool.call',
|
|
115
|
+
sessionId: ctx.sessionId,
|
|
116
|
+
payload: {
|
|
117
|
+
name,
|
|
118
|
+
args,
|
|
119
|
+
callId,
|
|
120
|
+
stepIndex: ctx.stepIndex,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
// Optionally persist in the background without blocking ordering
|
|
124
|
+
(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const index = await ctx.nextIndex();
|
|
127
|
+
await ctx.db.insert(messageParts).values({
|
|
128
|
+
id: callPartId,
|
|
129
|
+
messageId: ctx.messageId,
|
|
130
|
+
index,
|
|
131
|
+
stepIndex: ctx.stepIndex,
|
|
132
|
+
type: 'tool_call',
|
|
133
|
+
content: JSON.stringify({ name, args, callId }),
|
|
134
|
+
agent: ctx.agent,
|
|
135
|
+
provider: ctx.provider,
|
|
136
|
+
model: ctx.model,
|
|
137
|
+
startedAt: startTs,
|
|
138
|
+
toolName: name,
|
|
139
|
+
toolCallId: callId,
|
|
140
|
+
});
|
|
141
|
+
} catch {}
|
|
142
|
+
})();
|
|
143
|
+
if (typeof base.onInputAvailable === 'function') {
|
|
144
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
145
|
+
await base.onInputAvailable(options as any);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Publish promptly so UI shows the call header before results
|
|
151
|
+
publish({
|
|
152
|
+
type: 'tool.call',
|
|
153
|
+
sessionId: ctx.sessionId,
|
|
154
|
+
payload: { name, args, callId, stepIndex: ctx.stepIndex },
|
|
155
|
+
});
|
|
156
|
+
// Persist best-effort in the background to avoid delaying output
|
|
157
|
+
(async () => {
|
|
158
|
+
try {
|
|
159
|
+
const index = await ctx.nextIndex();
|
|
160
|
+
await ctx.db.insert(messageParts).values({
|
|
161
|
+
id: callPartId,
|
|
162
|
+
messageId: ctx.messageId,
|
|
163
|
+
index,
|
|
164
|
+
stepIndex: ctx.stepIndex,
|
|
165
|
+
type: 'tool_call',
|
|
166
|
+
content: JSON.stringify({ name, args, callId }),
|
|
167
|
+
agent: ctx.agent,
|
|
168
|
+
provider: ctx.provider,
|
|
169
|
+
model: ctx.model,
|
|
170
|
+
startedAt: startTs,
|
|
171
|
+
toolName: name,
|
|
172
|
+
toolCallId: callId,
|
|
173
|
+
});
|
|
174
|
+
} catch {}
|
|
175
|
+
})();
|
|
176
|
+
if (typeof base.onInputAvailable === 'function') {
|
|
177
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
178
|
+
await base.onInputAvailable(options as any);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
async execute(input: ToolExecuteInput, options: ToolExecuteOptions) {
|
|
182
|
+
const queue = pendingCalls.get(name);
|
|
183
|
+
const meta = queue?.shift();
|
|
184
|
+
if (queue && queue.length === 0) pendingCalls.delete(name);
|
|
185
|
+
const callIdFromQueue = meta?.callId;
|
|
186
|
+
const startTsFromQueue = meta?.startTs;
|
|
187
|
+
const stepIndexForEvent = meta?.stepIndex ?? ctx.stepIndex;
|
|
188
|
+
// Handle session-relative paths and cwd tools
|
|
189
|
+
let res: ToolExecuteReturn | { cwd: string } | null | undefined;
|
|
190
|
+
const cwd = getCwd(ctx.sessionId);
|
|
191
|
+
if (name === 'pwd') {
|
|
192
|
+
res = { cwd };
|
|
193
|
+
} else if (name === 'cd') {
|
|
194
|
+
const next = joinRelative(
|
|
195
|
+
cwd,
|
|
196
|
+
String((input as Record<string, unknown>)?.path ?? '.'),
|
|
197
|
+
);
|
|
198
|
+
setCwd(ctx.sessionId, next);
|
|
199
|
+
res = { cwd: next };
|
|
200
|
+
} else if (
|
|
201
|
+
['read', 'write', 'ls', 'tree'].includes(name) &&
|
|
202
|
+
typeof (input as Record<string, unknown>)?.path === 'string'
|
|
203
|
+
) {
|
|
204
|
+
const rel = joinRelative(
|
|
205
|
+
cwd,
|
|
206
|
+
String((input as Record<string, unknown>).path),
|
|
207
|
+
);
|
|
208
|
+
const nextInput = {
|
|
209
|
+
...(input as Record<string, unknown>),
|
|
210
|
+
path: rel,
|
|
211
|
+
} as ToolExecuteInput;
|
|
212
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
213
|
+
res = base.execute?.(nextInput, options as any);
|
|
214
|
+
} else if (name === 'bash') {
|
|
215
|
+
const needsCwd =
|
|
216
|
+
!input ||
|
|
217
|
+
typeof (input as Record<string, unknown>).cwd !== 'string';
|
|
218
|
+
const nextInput = needsCwd
|
|
219
|
+
? ({
|
|
220
|
+
...(input as Record<string, unknown>),
|
|
221
|
+
cwd,
|
|
222
|
+
} as ToolExecuteInput)
|
|
223
|
+
: input;
|
|
224
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
225
|
+
res = base.execute?.(nextInput, options as any);
|
|
226
|
+
} else {
|
|
227
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
228
|
+
res = base.execute?.(input, options as any);
|
|
229
|
+
}
|
|
230
|
+
let result: unknown = res;
|
|
231
|
+
// If tool returns an async iterable, stream deltas while accumulating
|
|
232
|
+
if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
|
|
233
|
+
const chunks: unknown[] = [];
|
|
234
|
+
for await (const chunk of res as AsyncIterable<unknown>) {
|
|
235
|
+
chunks.push(chunk);
|
|
236
|
+
publish({
|
|
237
|
+
type: 'tool.delta',
|
|
238
|
+
sessionId: ctx.sessionId,
|
|
239
|
+
payload: {
|
|
240
|
+
name,
|
|
241
|
+
channel: 'output',
|
|
242
|
+
delta: chunk,
|
|
243
|
+
stepIndex: stepIndexForEvent,
|
|
244
|
+
callId: callIdFromQueue,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// Prefer the last chunk as the result if present, otherwise the entire array
|
|
249
|
+
result = chunks.length > 0 ? chunks[chunks.length - 1] : null;
|
|
250
|
+
} else {
|
|
251
|
+
// Await promise or passthrough value
|
|
252
|
+
result = await Promise.resolve(res as ToolExecuteReturn);
|
|
253
|
+
}
|
|
254
|
+
const resultPartId = crypto.randomUUID();
|
|
255
|
+
const callId = callIdFromQueue;
|
|
256
|
+
const startTs = startTsFromQueue;
|
|
257
|
+
const contentObj: {
|
|
258
|
+
name: string;
|
|
259
|
+
result: unknown;
|
|
260
|
+
callId?: string;
|
|
261
|
+
artifact?: unknown;
|
|
262
|
+
args?: unknown;
|
|
263
|
+
} = {
|
|
264
|
+
name,
|
|
265
|
+
result,
|
|
266
|
+
callId,
|
|
267
|
+
};
|
|
268
|
+
if (meta?.args !== undefined) {
|
|
269
|
+
contentObj.args = meta.args;
|
|
270
|
+
}
|
|
271
|
+
if (result && typeof result === 'object' && 'artifact' in result) {
|
|
272
|
+
try {
|
|
273
|
+
const maybeArtifact = (result as { artifact?: unknown }).artifact;
|
|
274
|
+
if (maybeArtifact !== undefined)
|
|
275
|
+
contentObj.artifact = maybeArtifact;
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const index = await ctx.nextIndex();
|
|
280
|
+
const endTs = Date.now();
|
|
281
|
+
const dur =
|
|
282
|
+
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
283
|
+
|
|
284
|
+
// Special-case: keep progress_update result lightweight; publish first, persist best-effort
|
|
285
|
+
if (name === 'progress_update') {
|
|
286
|
+
publish({
|
|
287
|
+
type: 'tool.result',
|
|
288
|
+
sessionId: ctx.sessionId,
|
|
289
|
+
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
290
|
+
});
|
|
291
|
+
// Persist without blocking the event loop
|
|
292
|
+
(async () => {
|
|
293
|
+
try {
|
|
294
|
+
await ctx.db.insert(messageParts).values({
|
|
295
|
+
id: resultPartId,
|
|
296
|
+
messageId: ctx.messageId,
|
|
297
|
+
index,
|
|
298
|
+
stepIndex: stepIndexForEvent,
|
|
299
|
+
type: 'tool_result',
|
|
300
|
+
content: JSON.stringify(contentObj),
|
|
301
|
+
agent: ctx.agent,
|
|
302
|
+
provider: ctx.provider,
|
|
303
|
+
model: ctx.model,
|
|
304
|
+
startedAt: startTs,
|
|
305
|
+
completedAt: endTs,
|
|
306
|
+
toolName: name,
|
|
307
|
+
toolCallId: callId,
|
|
308
|
+
toolDurationMs: dur ?? undefined,
|
|
309
|
+
});
|
|
310
|
+
} catch {}
|
|
311
|
+
})();
|
|
312
|
+
return result as ToolExecuteReturn;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await ctx.db.insert(messageParts).values({
|
|
316
|
+
id: resultPartId,
|
|
317
|
+
messageId: ctx.messageId,
|
|
318
|
+
index,
|
|
319
|
+
stepIndex: stepIndexForEvent,
|
|
320
|
+
type: 'tool_result',
|
|
321
|
+
content: JSON.stringify(contentObj),
|
|
322
|
+
agent: ctx.agent,
|
|
323
|
+
provider: ctx.provider,
|
|
324
|
+
model: ctx.model,
|
|
325
|
+
startedAt: startTs,
|
|
326
|
+
completedAt: endTs,
|
|
327
|
+
toolName: name,
|
|
328
|
+
toolCallId: callId,
|
|
329
|
+
toolDurationMs: dur ?? undefined,
|
|
330
|
+
});
|
|
331
|
+
// Update session aggregates: total tool time and counts per tool
|
|
332
|
+
try {
|
|
333
|
+
const sessRows = await ctx.db
|
|
334
|
+
.select()
|
|
335
|
+
.from(sessions)
|
|
336
|
+
.where(eq(sessions.id, ctx.sessionId));
|
|
337
|
+
if (sessRows.length) {
|
|
338
|
+
const row = sessRows[0] as typeof sessions.$inferSelect;
|
|
339
|
+
const totalToolTimeMs =
|
|
340
|
+
Number(row.totalToolTimeMs || 0) + (dur ?? 0);
|
|
341
|
+
let counts: Record<string, number> = {};
|
|
342
|
+
try {
|
|
343
|
+
counts = row.toolCountsJson ? JSON.parse(row.toolCountsJson) : {};
|
|
344
|
+
} catch {}
|
|
345
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
346
|
+
await ctx.db
|
|
347
|
+
.update(sessions)
|
|
348
|
+
.set({
|
|
349
|
+
totalToolTimeMs,
|
|
350
|
+
toolCountsJson: JSON.stringify(counts),
|
|
351
|
+
lastActiveAt: endTs,
|
|
352
|
+
})
|
|
353
|
+
.where(eq(sessions.id, ctx.sessionId));
|
|
354
|
+
}
|
|
355
|
+
} catch {}
|
|
356
|
+
publish({
|
|
357
|
+
type: 'tool.result',
|
|
358
|
+
sessionId: ctx.sessionId,
|
|
359
|
+
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
360
|
+
});
|
|
361
|
+
if (name === 'update_plan') {
|
|
362
|
+
try {
|
|
363
|
+
const result = (contentObj as { result?: unknown }).result as
|
|
364
|
+
| { items?: unknown; note?: unknown }
|
|
365
|
+
| undefined;
|
|
366
|
+
if (result && Array.isArray(result.items)) {
|
|
367
|
+
publish({
|
|
368
|
+
type: 'plan.updated',
|
|
369
|
+
sessionId: ctx.sessionId,
|
|
370
|
+
payload: { items: result.items, note: result.note },
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} catch {}
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
},
|
|
377
|
+
} as Tool;
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|