@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,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
+ }
@@ -0,0 +1,5 @@
1
+ // Type declarations for .sql file imports (Bun-specific)
2
+ declare module '*.sql' {
3
+ const content: string;
4
+ export default content;
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src/**/*"]
7
+ }