@agi-cli/server 0.1.59 → 0.1.61

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.
@@ -1,4 +1,4 @@
1
- import { hasToolCall, streamText } from 'ai';
1
+ import { streamText } from 'ai';
2
2
  import { loadConfig } from '@agi-cli/sdk';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { messageParts } from '@agi-cli/database/schema';
@@ -7,11 +7,8 @@ import { resolveModel } from './provider.ts';
7
7
  import { resolveAgentConfig } from './agent-registry.ts';
8
8
  import { composeSystemPrompt } from './prompt.ts';
9
9
  import { discoverProjectTools } from '@agi-cli/sdk';
10
- import { adaptTools } from '../tools/adapter.ts';
11
- import { publish, subscribe } from '../events/bus.ts';
12
- import { debugLog, time } from './debug.ts';
10
+ import { publish } from '../events/bus.ts';
13
11
  import { buildHistoryMessages } from './history-builder.ts';
14
- import { toErrorPayload } from './error-handling.ts';
15
12
  import { getMaxOutputTokens } from './token-utils.ts';
16
13
  import {
17
14
  type RunOpts,
@@ -22,16 +19,11 @@ import {
22
19
  dequeueJob,
23
20
  cleanupSession,
24
21
  } from './session-queue.ts';
22
+ import { setupToolContext } from './tool-context-setup.ts';
25
23
  import {
26
- setupToolContext,
27
- type RunnerToolContext,
28
- } from './tool-context-setup.ts';
29
- import {
30
- updateSessionTokens,
31
24
  updateSessionTokensIncremental,
32
25
  updateMessageTokensIncremental,
33
26
  completeAssistantMessage,
34
- cleanupEmptyTextParts,
35
27
  } from './db-operations.ts';
36
28
  import {
37
29
  createStepFinishHandler,
@@ -39,175 +31,38 @@ import {
39
31
  createAbortHandler,
40
32
  createFinishHandler,
41
33
  } from './stream-handlers.ts';
34
+ import { addCacheControl } from './cache-optimizer.ts';
35
+ import { optimizeContext } from './context-optimizer.ts';
36
+ import { truncateHistory } from './history-truncator.ts';
42
37
 
43
38
  /**
44
- * Enqueues an assistant run for processing.
45
- */
46
- export function enqueueAssistantRun(opts: Omit<RunOpts, 'abortSignal'>) {
47
- enqueueRun(opts, processQueue);
48
- }
49
-
50
- /**
51
- * Aborts an active session.
52
- */
53
- export function abortSession(sessionId: string) {
54
- abortSessionQueue(sessionId);
55
- }
56
-
57
- /**
58
- * Processes the queue of assistant runs for a session.
59
- */
60
- async function processQueue(sessionId: string) {
61
- const state = getRunnerState(sessionId);
62
- if (!state) return;
63
- if (state.running) return;
64
- setRunning(sessionId, true);
65
-
66
- while (state.queue.length > 0) {
67
- const job = dequeueJob(sessionId);
68
- if (!job) break;
69
- try {
70
- await runAssistant(job);
71
- } catch (_err) {
72
- // Swallow to keep the loop alive; event published by runner
73
- }
74
- }
75
-
76
- setRunning(sessionId, false);
77
- cleanupSession(sessionId);
78
- }
79
-
80
- /**
81
- * Ensures the finish tool is called if not already observed.
82
- */
83
- async function ensureFinishToolCalled(
84
- finishObserved: boolean,
85
- toolset: ReturnType<typeof adaptTools>,
86
- sharedCtx: RunnerToolContext,
87
- stepIndex: number,
88
- ) {
89
- if (finishObserved || !toolset?.finish?.execute) return;
90
-
91
- const finishInput = {} as const;
92
- const callOptions = { input: finishInput } as const;
93
-
94
- sharedCtx.stepIndex = stepIndex;
95
-
96
- try {
97
- await toolset.finish.onInputStart?.(callOptions as never);
98
- } catch {}
99
-
100
- try {
101
- await toolset.finish.onInputAvailable?.(callOptions as never);
102
- } catch {}
103
-
104
- await toolset.finish.execute(finishInput, {} as never);
105
- }
106
-
107
- /**
108
- * Main function to run the assistant for a given request.
39
+ * Main runner that executes the LLM streaming loop with tools
109
40
  */
110
- async function runAssistant(opts: RunOpts) {
111
- const cfgTimer = time('runner:loadConfig+db');
112
- const cfg = await loadConfig(opts.projectRoot);
113
- const db = await getDb(cfg.projectRoot);
114
- cfgTimer.end();
115
-
116
- const agentTimer = time('runner:resolveAgentConfig');
117
- const agentCfg = await resolveAgentConfig(cfg.projectRoot, opts.agent);
118
- agentTimer.end({ agent: opts.agent });
119
-
120
- const agentPrompt = agentCfg.prompt || '';
121
-
122
- const historyTimer = time('runner:buildHistory');
123
- const history = await buildHistoryMessages(db, opts.sessionId);
124
- historyTimer.end({ messages: history.length });
125
-
126
- const isFirstMessage = history.length === 0;
127
-
128
- const systemTimer = time('runner:composeSystemPrompt');
129
- const { getAuth } = await import('@agi-cli/sdk');
130
- const { getProviderSpoofPrompt } = await import('./prompt.ts');
131
- const auth = await getAuth(opts.provider, cfg.projectRoot);
132
- const needsSpoof = auth?.type === 'oauth';
133
- const spoofPrompt = needsSpoof
134
- ? getProviderSpoofPrompt(opts.provider)
135
- : undefined;
136
-
137
- let system: string;
138
- let additionalSystemMessages: Array<{ role: 'system'; content: string }> = [];
139
-
140
- if (spoofPrompt) {
141
- system = spoofPrompt;
142
- const fullPrompt = await composeSystemPrompt({
143
- provider: opts.provider,
144
- model: opts.model,
145
- projectRoot: cfg.projectRoot,
146
- agentPrompt,
147
- oneShot: opts.oneShot,
148
- spoofPrompt: undefined,
149
- includeProjectTree: isFirstMessage,
150
- });
151
- additionalSystemMessages = [{ role: 'system', content: fullPrompt }];
152
- } else {
153
- system = await composeSystemPrompt({
154
- provider: opts.provider,
155
- model: opts.model,
156
- projectRoot: cfg.projectRoot,
157
- agentPrompt,
158
- oneShot: opts.oneShot,
159
- spoofPrompt: undefined,
160
- includeProjectTree: isFirstMessage,
161
- });
162
- }
163
- systemTimer.end();
164
- debugLog('[system] composed prompt (provider+base+agent):');
165
- debugLog(system);
166
-
167
- const toolsTimer = time('runner:discoverTools');
168
- const allTools = await discoverProjectTools(cfg.projectRoot);
169
- toolsTimer.end({ count: allTools.length });
170
- const allowedNames = new Set([
171
- ...(agentCfg.tools || []),
172
- 'finish',
173
- 'progress_update',
174
- ]);
175
- const gated = allTools.filter((t) => allowedNames.has(t.name));
176
- const messagesWithSystemInstructions = [
177
- ...(isFirstMessage ? additionalSystemMessages : []),
178
- ...history,
179
- ];
180
-
181
- const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
182
- opts,
183
- db,
184
- );
185
- const toolset = adaptTools(gated, sharedCtx, opts.provider);
186
-
187
- const modelTimer = time('runner:resolveModel');
188
- const model = await resolveModel(opts.provider, opts.model, cfg);
189
- modelTimer.end();
190
-
191
- const maxOutputTokens = getMaxOutputTokens(opts.provider, opts.model);
192
-
193
- let currentPartId = opts.assistantPartId;
41
+ export async function runAssistant(opts: RunOpts) {
42
+ const db = await getDb();
43
+ const config = await loadConfig();
44
+ const [provider, modelName] = opts.model.split('/', 2);
45
+ const model = resolveModel(provider, modelName);
46
+
47
+ // Build agent + system prompt
48
+ const agentConfig = resolveAgentConfig(opts.agent);
49
+ const availableTools = await discoverProjectTools(config.project.root);
50
+ const system = composeSystemPrompt(agentConfig, availableTools);
51
+
52
+ // Build message history
53
+ const history = await buildHistoryMessages(opts, db);
54
+
55
+ // Setup tool context
56
+ const toolContext = await setupToolContext(opts, db);
57
+ const { tools, sharedCtx } = toolContext;
58
+
59
+ // State
60
+ let currentPartId = sharedCtx.assistantPartId;
61
+ let stepIndex = sharedCtx.stepIndex;
194
62
  let accumulated = '';
195
- let stepIndex = 0;
196
-
197
- let finishObserved = false;
198
- const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
199
- if (evt.type !== 'tool.result') return;
200
- try {
201
- const name = (evt.payload as { name?: string } | undefined)?.name;
202
- if (name === 'finish') finishObserved = true;
203
- } catch {}
204
- });
63
+ const abortController = new AbortController();
205
64
 
206
- const streamStartTimer = time('runner:first-delta');
207
- let firstDeltaSeen = false;
208
- debugLog(`[streamText] Calling with maxOutputTokens: ${maxOutputTokens}`);
209
-
210
- // State management helpers
65
+ // State getters/setters
211
66
  const getCurrentPartId = () => currentPartId;
212
67
  const getStepIndex = () => stepIndex;
213
68
  const updateCurrentPartId = (id: string) => {
@@ -216,12 +71,10 @@ async function runAssistant(opts: RunOpts) {
216
71
  const updateAccumulated = (text: string) => {
217
72
  accumulated = text;
218
73
  };
219
- const incrementStepIndex = () => {
220
- stepIndex += 1;
221
- return stepIndex;
222
- };
74
+ const getAccumulated = () => accumulated;
75
+ const incrementStepIndex = () => ++stepIndex;
223
76
 
224
- // Create stream handlers
77
+ // Handlers
225
78
  const onStepFinish = createStepFinishHandler(
226
79
  opts,
227
80
  db,
@@ -235,104 +88,102 @@ async function runAssistant(opts: RunOpts) {
235
88
  updateMessageTokensIncremental,
236
89
  );
237
90
 
238
- const onError = createErrorHandler(opts, db, getStepIndex, sharedCtx);
239
-
240
- const onAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
241
-
242
91
  const onFinish = createFinishHandler(
243
92
  opts,
244
93
  db,
245
- () => ensureFinishToolCalled(finishObserved, toolset, sharedCtx, stepIndex),
246
94
  completeAssistantMessage,
95
+ getAccumulated,
96
+ abortController,
247
97
  );
248
98
 
249
- // Apply optimizations: deduplication, pruning, cache control, and truncation
250
- const { addCacheControl, truncateHistory } = await import(
251
- './cache-optimizer.ts'
252
- );
253
- const { optimizeContext } = await import('./context-optimizer.ts');
99
+ const _onAbort = createAbortHandler(opts, db, abortController);
100
+ const onError = createErrorHandler(opts, db);
254
101
 
255
- // 1. Optimize context (deduplicate file reads, prune old tool results)
256
- const contextOptimized = optimizeContext(messagesWithSystemInstructions, {
102
+ // Context optimization
103
+ const contextOptimized = optimizeContext(history, {
257
104
  deduplicateFiles: true,
258
105
  maxToolResults: 30,
259
106
  });
260
107
 
261
- // 2. Truncate history
108
+ // Truncate history
262
109
  const truncatedMessages = truncateHistory(contextOptimized, 20);
263
110
 
264
- // 3. Add cache control
111
+ // Add cache control
265
112
  const { system: cachedSystem, messages: optimizedMessages } = addCacheControl(
266
- opts.provider as any,
113
+ opts.provider,
267
114
  system,
268
115
  truncatedMessages,
269
116
  );
270
117
 
271
118
  try {
272
- const result = streamText({
119
+ const maxTokens = getMaxOutputTokens(provider, modelName);
120
+ const result = await streamText({
273
121
  model,
274
- tools: toolset,
275
- ...(cachedSystem ? { system: cachedSystem } : {}),
122
+ system: cachedSystem,
276
123
  messages: optimizedMessages,
277
- ...(maxOutputTokens ? { maxOutputTokens } : {}),
278
- abortSignal: opts.abortSignal,
279
- stopWhen: hasToolCall('finish'),
124
+ tools,
125
+ maxSteps: 50,
126
+ maxTokens,
127
+ temperature: agentConfig.temperature ?? 0.7,
128
+ abortSignal: abortController.signal,
280
129
  onStepFinish,
281
- onError,
282
- onAbort,
283
130
  onFinish,
131
+ experimental_continueSteps: true,
284
132
  });
285
133
 
134
+ // Process the stream
286
135
  for await (const delta of result.textStream) {
287
- if (!delta) continue;
288
- if (!firstDeltaSeen) {
289
- firstDeltaSeen = true;
290
- streamStartTimer.end();
291
- }
136
+ if (abortController.signal.aborted) break;
137
+
292
138
  accumulated += delta;
293
- publish({
294
- type: 'message.part.delta',
139
+ if (currentPartId) {
140
+ await db
141
+ .update(messageParts)
142
+ .set({ content: accumulated })
143
+ .where(eq(messageParts.id, currentPartId));
144
+ }
145
+
146
+ publish('stream:text-delta', {
295
147
  sessionId: opts.sessionId,
296
- payload: {
297
- messageId: opts.assistantMessageId,
298
- partId: currentPartId,
299
- stepIndex,
300
- delta,
301
- },
148
+ messageId: opts.assistantMessageId,
149
+ assistantMessageId: opts.assistantMessageId,
150
+ stepIndex,
151
+ textDelta: delta,
152
+ fullText: accumulated,
302
153
  });
303
- await db
304
- .update(messageParts)
305
- .set({ content: JSON.stringify({ text: accumulated }) })
306
- .where(eq(messageParts.id, currentPartId));
307
154
  }
308
- } catch (error) {
309
- const errorPayload = toErrorPayload(error);
310
- await db
311
- .update(messageParts)
312
- .set({
313
- content: JSON.stringify({
314
- text: accumulated,
315
- error: errorPayload.message,
316
- }),
317
- })
318
- .where(eq(messageParts.messageId, opts.assistantMessageId));
319
- publish({
320
- type: 'error',
321
- sessionId: opts.sessionId,
322
- payload: {
323
- messageId: opts.assistantMessageId,
324
- error: errorPayload.message,
325
- details: errorPayload.details,
326
- },
327
- });
328
- throw error;
155
+ } catch (err) {
156
+ await onError(err);
329
157
  } finally {
330
- if (!firstToolSeen()) firstToolTimer.end({ skipped: true });
331
- try {
332
- unsubscribeFinish();
333
- } catch {}
334
- try {
335
- await cleanupEmptyTextParts(opts, db);
336
- } catch {}
158
+ setRunning(opts.sessionId, false);
159
+ dequeueJob(opts.sessionId);
337
160
  }
338
161
  }
162
+
163
+ /**
164
+ * Enqueues an assistant run
165
+ */
166
+ export async function enqueueAssistantRun(opts: RunOpts) {
167
+ return enqueueRun(opts);
168
+ }
169
+
170
+ /**
171
+ * Aborts a running session
172
+ */
173
+ export async function abortSession(sessionId: number) {
174
+ return abortSessionQueue(sessionId);
175
+ }
176
+
177
+ /**
178
+ * Gets the current runner state for a session
179
+ */
180
+ export function getSessionState(sessionId: number) {
181
+ return getRunnerState(sessionId);
182
+ }
183
+
184
+ /**
185
+ * Cleanup session resources
186
+ */
187
+ export function cleanupSessionResources(sessionId: number) {
188
+ return cleanupSession(sessionId);
189
+ }