@agi-cli/server 0.1.118 → 0.1.119

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/server",
3
- "version": "0.1.118",
3
+ "version": "0.1.119",
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.118",
33
- "@agi-cli/database": "0.1.118",
32
+ "@agi-cli/sdk": "0.1.119",
33
+ "@agi-cli/database": "0.1.119",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -11,6 +11,7 @@ export type AGIEventType =
11
11
  | 'plan.updated'
12
12
  | 'finish-step'
13
13
  | 'usage'
14
+ | 'queue.updated'
14
15
  | 'error'
15
16
  | 'heartbeat';
16
17
 
@@ -1,8 +1,8 @@
1
1
  import type { Hono } from 'hono';
2
2
  import { loadConfig } from '@agi-cli/sdk';
3
3
  import { getDb } from '@agi-cli/database';
4
- import { sessions } from '@agi-cli/database/schema';
5
- import { desc, eq } from 'drizzle-orm';
4
+ import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
+ import { desc, eq, and, inArray } from 'drizzle-orm';
6
6
  import type { ProviderId } from '@agi-cli/sdk';
7
7
  import { isProviderId, catalog } from '@agi-cli/sdk';
8
8
  import { resolveAgentConfig } from '../runtime/agent-registry.ts';
@@ -194,8 +194,108 @@ export function registerSessionsRoutes(app: Hono) {
194
194
  // Abort session stream
195
195
  app.delete('/v1/sessions/:sessionId/abort', async (c) => {
196
196
  const sessionId = c.req.param('sessionId');
197
- const { abortSession } = await import('../runtime/runner.ts');
198
- abortSession(sessionId);
197
+ const body = (await c.req.json().catch(() => ({}))) as Record<
198
+ string,
199
+ unknown
200
+ >;
201
+ const messageId =
202
+ typeof body.messageId === 'string' ? body.messageId : undefined;
203
+ const clearQueue = body.clearQueue === true;
204
+
205
+ const { abortSession, abortMessage } = await import('../runtime/runner.ts');
206
+
207
+ if (messageId) {
208
+ const result = abortMessage(sessionId, messageId);
209
+ return c.json({
210
+ success: result.removed,
211
+ wasRunning: result.wasRunning,
212
+ messageId,
213
+ });
214
+ }
215
+
216
+ abortSession(sessionId, clearQueue);
199
217
  return c.json({ success: true });
200
218
  });
219
+
220
+ // Get queue state for a session
221
+ app.get('/v1/sessions/:sessionId/queue', async (c) => {
222
+ const sessionId = c.req.param('sessionId');
223
+ const { getQueueState } = await import('../runtime/session-queue.ts');
224
+ const state = getQueueState(sessionId);
225
+ return c.json(
226
+ state ?? {
227
+ currentMessageId: null,
228
+ queuedMessages: [],
229
+ isRunning: false,
230
+ },
231
+ );
232
+ });
233
+
234
+ // Remove a message from the queue
235
+ app.delete('/v1/sessions/:sessionId/queue/:messageId', async (c) => {
236
+ const sessionId = c.req.param('sessionId');
237
+ const messageId = c.req.param('messageId');
238
+ const projectRoot = c.req.query('project') || process.cwd();
239
+ const cfg = await loadConfig(projectRoot);
240
+ const db = await getDb(cfg.projectRoot);
241
+ const { removeFromQueue, abortMessage } = await import(
242
+ '../runtime/session-queue.ts'
243
+ );
244
+
245
+ // First try to remove from queue (queued messages)
246
+ const removed = removeFromQueue(sessionId, messageId);
247
+ if (removed) {
248
+ // Delete messages from database
249
+ try {
250
+ // Find the assistant message to get its creation time
251
+ const assistantMsg = await db
252
+ .select()
253
+ .from(messages)
254
+ .where(eq(messages.id, messageId))
255
+ .limit(1);
256
+
257
+ if (assistantMsg.length > 0) {
258
+ // Find the user message that came right before (same session, created just before)
259
+ const userMsg = await db
260
+ .select()
261
+ .from(messages)
262
+ .where(
263
+ and(eq(messages.sessionId, sessionId), eq(messages.role, 'user')),
264
+ )
265
+ .orderBy(desc(messages.createdAt))
266
+ .limit(1);
267
+
268
+ const messageIdsToDelete = [messageId];
269
+ if (userMsg.length > 0) {
270
+ messageIdsToDelete.push(userMsg[0].id);
271
+ }
272
+
273
+ // Delete message parts first (foreign key constraint)
274
+ await db
275
+ .delete(messageParts)
276
+ .where(inArray(messageParts.messageId, messageIdsToDelete));
277
+ // Delete messages
278
+ await db
279
+ .delete(messages)
280
+ .where(inArray(messages.id, messageIdsToDelete));
281
+ }
282
+ } catch (err) {
283
+ logger.error('Failed to delete queued messages from DB', err);
284
+ }
285
+ return c.json({ success: true, removed: true, wasQueued: true });
286
+ }
287
+
288
+ // If not in queue, try to abort (might be running)
289
+ const result = abortMessage(sessionId, messageId);
290
+ if (result.removed) {
291
+ return c.json({
292
+ success: true,
293
+ removed: true,
294
+ wasQueued: false,
295
+ wasRunning: result.wasRunning,
296
+ });
297
+ }
298
+
299
+ return c.json({ success: false, removed: false }, 404);
300
+ });
201
301
  }
@@ -437,6 +437,7 @@ export async function resolveModel(
437
437
  provider: ProviderName,
438
438
  model: string,
439
439
  cfg: AGIConfig,
440
+ options?: { systemPrompt?: string },
440
441
  ) {
441
442
  if (provider === 'openai') {
442
443
  const auth = await getAuth('openai', cfg.projectRoot);
@@ -447,6 +448,7 @@ export async function resolveModel(
447
448
  projectRoot: cfg.projectRoot,
448
449
  reasoningEffort: isCodexModel ? 'high' : 'medium',
449
450
  reasoningSummary: 'auto',
451
+ instructions: options?.systemPrompt,
450
452
  });
451
453
  }
452
454
  if (auth?.type === 'api' && auth.key) {
@@ -34,8 +34,14 @@ import {
34
34
  } from './stream-handlers.ts';
35
35
  import { getCompactionSystemPrompt, pruneSession } from './compaction.ts';
36
36
 
37
- export { enqueueAssistantRun, abortSession } from './session-queue.ts';
38
- export { getRunnerState } from './session-queue.ts';
37
+ export {
38
+ enqueueAssistantRun,
39
+ abortSession,
40
+ abortMessage,
41
+ removeFromQueue,
42
+ getQueueState,
43
+ getRunnerState,
44
+ } from './session-queue.ts';
39
45
 
40
46
  /**
41
47
  * Main loop that processes the queue for a given session.
@@ -253,7 +259,14 @@ async function runAssistant(opts: RunOpts) {
253
259
  );
254
260
  }
255
261
 
256
- const model = await resolveModel(opts.provider, opts.model, cfg);
262
+ // For OpenAI OAuth, pass the full system prompt as instructions
263
+ const oauthSystemPrompt =
264
+ needsSpoof && opts.provider === 'openai' && additionalSystemMessages[0]
265
+ ? additionalSystemMessages[0].content
266
+ : undefined;
267
+ const model = await resolveModel(opts.provider, opts.model, cfg, {
268
+ systemPrompt: oauthSystemPrompt,
269
+ });
257
270
  debugLog(
258
271
  `[RUNNER] Model created: ${JSON.stringify({ id: model.modelId, provider: model.provider })}`,
259
272
  );
@@ -1,4 +1,5 @@
1
1
  import type { ProviderName } from './provider.ts';
2
+ import { publish } from '../events/bus.ts';
2
3
 
3
4
  export type RunOpts = {
4
5
  sessionId: string;
@@ -15,45 +16,190 @@ export type RunOpts = {
15
16
  compactionContext?: string;
16
17
  };
17
18
 
18
- type RunnerState = { queue: RunOpts[]; running: boolean };
19
+ export type QueuedMessage = {
20
+ messageId: string;
21
+ position: number;
22
+ };
23
+
24
+ type RunnerState = {
25
+ queue: RunOpts[];
26
+ running: boolean;
27
+ currentMessageId: string | null;
28
+ };
19
29
 
20
30
  // Global state for session queues
21
31
  const runners = new Map<string, RunnerState>();
22
32
 
23
- // Track active abort controllers per session
24
- const sessionAbortControllers = new Map<string, AbortController>();
33
+ // Track active abort controllers per MESSAGE (not session)
34
+ const messageAbortControllers = new Map<string, AbortController>();
35
+
36
+ function publishQueueState(sessionId: string) {
37
+ const state = runners.get(sessionId);
38
+ if (!state) return;
39
+
40
+ const queuedMessages: QueuedMessage[] = state.queue.map((opts, index) => ({
41
+ messageId: opts.assistantMessageId,
42
+ position: index,
43
+ }));
44
+
45
+ publish({
46
+ type: 'queue.updated',
47
+ sessionId,
48
+ payload: {
49
+ currentMessageId: state.currentMessageId,
50
+ queuedMessages,
51
+ queueLength: state.queue.length,
52
+ },
53
+ });
54
+ }
25
55
 
26
56
  /**
27
57
  * Enqueues an assistant run for a given session.
28
- * Creates an abort controller for the session if one doesn't exist.
58
+ * Creates an abort controller per message.
29
59
  */
30
60
  export function enqueueAssistantRun(
31
61
  opts: Omit<RunOpts, 'abortSignal'>,
32
62
  processQueueFn: (sessionId: string) => Promise<void>,
33
63
  ) {
34
- // Create abort controller for this session
35
64
  const abortController = new AbortController();
36
- sessionAbortControllers.set(opts.sessionId, abortController);
65
+ messageAbortControllers.set(opts.assistantMessageId, abortController);
37
66
 
38
- const state = runners.get(opts.sessionId) ?? { queue: [], running: false };
67
+ const state = runners.get(opts.sessionId) ?? {
68
+ queue: [],
69
+ running: false,
70
+ currentMessageId: null,
71
+ };
39
72
  state.queue.push({ ...opts, abortSignal: abortController.signal });
40
73
  runners.set(opts.sessionId, state);
74
+
75
+ publishQueueState(opts.sessionId);
76
+
41
77
  if (!state.running) void processQueueFn(opts.sessionId);
42
78
  }
43
79
 
44
80
  /**
45
- * Signals the abort controller for a session.
46
- * This will trigger the abortSignal in the streamText call.
81
+ * Aborts the currently running message for a session.
82
+ * Optionally clears the queue.
83
+ */
84
+ export function abortSession(sessionId: string, clearQueue = false) {
85
+ const state = runners.get(sessionId);
86
+ if (!state) return;
87
+
88
+ // Abort the currently running message
89
+ if (state.currentMessageId) {
90
+ const controller = messageAbortControllers.get(state.currentMessageId);
91
+ if (controller) {
92
+ controller.abort();
93
+ messageAbortControllers.delete(state.currentMessageId);
94
+ }
95
+ }
96
+
97
+ // Optionally clear the queue and abort all queued messages
98
+ if (clearQueue && state.queue.length > 0) {
99
+ for (const opts of state.queue) {
100
+ const controller = messageAbortControllers.get(opts.assistantMessageId);
101
+ if (controller) {
102
+ controller.abort();
103
+ messageAbortControllers.delete(opts.assistantMessageId);
104
+ }
105
+ }
106
+ state.queue = [];
107
+ publishQueueState(sessionId);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Aborts a specific message by its ID.
113
+ * If it's currently running, aborts the stream.
114
+ * If it's queued, removes it from the queue.
115
+ */
116
+ export function abortMessage(
117
+ sessionId: string,
118
+ messageId: string,
119
+ ): { removed: boolean; wasRunning: boolean } {
120
+ const state = runners.get(sessionId);
121
+ if (!state) return { removed: false, wasRunning: false };
122
+
123
+ // Check if this is the currently running message
124
+ if (state.currentMessageId === messageId) {
125
+ const controller = messageAbortControllers.get(messageId);
126
+ if (controller) {
127
+ controller.abort();
128
+ messageAbortControllers.delete(messageId);
129
+ }
130
+ return { removed: true, wasRunning: true };
131
+ }
132
+
133
+ // Check if it's in the queue
134
+ const index = state.queue.findIndex(
135
+ (opts) => opts.assistantMessageId === messageId,
136
+ );
137
+ if (index !== -1) {
138
+ state.queue.splice(index, 1);
139
+ const controller = messageAbortControllers.get(messageId);
140
+ if (controller) {
141
+ controller.abort();
142
+ messageAbortControllers.delete(messageId);
143
+ }
144
+ publishQueueState(sessionId);
145
+ return { removed: true, wasRunning: false };
146
+ }
147
+
148
+ return { removed: false, wasRunning: false };
149
+ }
150
+
151
+ /**
152
+ * Removes a queued message (not the currently running one).
47
153
  */
48
- export function abortSession(sessionId: string) {
49
- const controller = sessionAbortControllers.get(sessionId);
154
+ export function removeFromQueue(sessionId: string, messageId: string): boolean {
155
+ const state = runners.get(sessionId);
156
+ if (!state) return false;
157
+
158
+ // Don't allow removing the currently running message via this function
159
+ if (state.currentMessageId === messageId) {
160
+ return false;
161
+ }
162
+
163
+ const index = state.queue.findIndex(
164
+ (opts) => opts.assistantMessageId === messageId,
165
+ );
166
+ if (index === -1) return false;
167
+
168
+ state.queue.splice(index, 1);
169
+ const controller = messageAbortControllers.get(messageId);
50
170
  if (controller) {
51
171
  controller.abort();
52
- sessionAbortControllers.delete(sessionId);
172
+ messageAbortControllers.delete(messageId);
53
173
  }
174
+
175
+ publishQueueState(sessionId);
176
+ return true;
54
177
  }
55
178
 
56
- export function getRunnerState(sessionId: string): RunnerState | undefined {
179
+ /**
180
+ * Gets the current queue state for a session.
181
+ */
182
+ export function getQueueState(sessionId: string): {
183
+ currentMessageId: string | null;
184
+ queuedMessages: QueuedMessage[];
185
+ isRunning: boolean;
186
+ } | null {
187
+ const state = runners.get(sessionId);
188
+ if (!state) return null;
189
+
190
+ return {
191
+ currentMessageId: state.currentMessageId,
192
+ queuedMessages: state.queue.map((opts, index) => ({
193
+ messageId: opts.assistantMessageId,
194
+ position: index,
195
+ })),
196
+ isRunning: state.running,
197
+ };
198
+ }
199
+
200
+ export function getRunnerState(
201
+ sessionId: string,
202
+ ): { queue: RunOpts[]; running: boolean } | undefined {
57
203
  return runners.get(sessionId);
58
204
  }
59
205
 
@@ -62,16 +208,33 @@ export function setRunning(sessionId: string, running: boolean) {
62
208
  if (state) state.running = running;
63
209
  }
64
210
 
211
+ export function setCurrentMessage(sessionId: string, messageId: string | null) {
212
+ const state = runners.get(sessionId);
213
+ if (state) {
214
+ state.currentMessageId = messageId;
215
+ publishQueueState(sessionId);
216
+ }
217
+ }
218
+
65
219
  export function dequeueJob(sessionId: string): RunOpts | undefined {
66
220
  const state = runners.get(sessionId);
67
- return state?.queue.shift();
221
+ const job = state?.queue.shift();
222
+ if (job && state) {
223
+ state.currentMessageId = job.assistantMessageId;
224
+ publishQueueState(sessionId);
225
+ }
226
+ return job;
68
227
  }
69
228
 
70
229
  export function cleanupSession(sessionId: string) {
71
230
  const state = runners.get(sessionId);
72
231
  if (state && state.queue.length === 0 && !state.running) {
232
+ // Clean up any lingering abort controller for current message
233
+ if (state.currentMessageId) {
234
+ messageAbortControllers.delete(state.currentMessageId);
235
+ }
236
+ state.currentMessageId = null;
73
237
  runners.delete(sessionId);
74
- // Clean up any lingering abort controller
75
- sessionAbortControllers.delete(sessionId);
238
+ publishQueueState(sessionId);
76
239
  }
77
240
  }
@@ -191,6 +191,7 @@ export function adaptTools(
191
191
  delta,
192
192
  stepIndex: meta?.stepIndex ?? ctx.stepIndex,
193
193
  callId: meta?.callId,
194
+ messageId: ctx.messageId,
194
195
  },
195
196
  });
196
197
  if (typeof base.onInputDelta === 'function')
@@ -235,6 +236,7 @@ export function adaptTools(
235
236
  args,
236
237
  callId,
237
238
  stepIndex: ctx.stepIndex,
239
+ messageId: ctx.messageId,
238
240
  },
239
241
  });
240
242
  // Persist synchronously to maintain correct ordering
@@ -266,7 +268,13 @@ export function adaptTools(
266
268
  publish({
267
269
  type: 'tool.call',
268
270
  sessionId: ctx.sessionId,
269
- payload: { name, args, callId, stepIndex: ctx.stepIndex },
271
+ payload: {
272
+ name,
273
+ args,
274
+ callId,
275
+ stepIndex: ctx.stepIndex,
276
+ messageId: ctx.messageId,
277
+ },
270
278
  });
271
279
  // Persist synchronously to maintain correct ordering
272
280
  try {
@@ -373,6 +381,7 @@ export function adaptTools(
373
381
  delta: chunk,
374
382
  stepIndex: stepIndexForEvent,
375
383
  callId: callIdFromQueue,
384
+ messageId: ctx.messageId,
376
385
  },
377
386
  });
378
387
  }