@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 +3 -3
- package/src/events/types.ts +1 -0
- package/src/routes/sessions.ts +104 -4
- package/src/runtime/provider.ts +2 -0
- package/src/runtime/runner.ts +16 -3
- package/src/runtime/session-queue.ts +179 -16
- package/src/tools/adapter.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
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"
|
package/src/events/types.ts
CHANGED
package/src/routes/sessions.ts
CHANGED
|
@@ -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
|
|
198
|
-
|
|
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
|
}
|
package/src/runtime/provider.ts
CHANGED
|
@@ -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) {
|
package/src/runtime/runner.ts
CHANGED
|
@@ -34,8 +34,14 @@ import {
|
|
|
34
34
|
} from './stream-handlers.ts';
|
|
35
35
|
import { getCompactionSystemPrompt, pruneSession } from './compaction.ts';
|
|
36
36
|
|
|
37
|
-
export {
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
65
|
+
messageAbortControllers.set(opts.assistantMessageId, abortController);
|
|
37
66
|
|
|
38
|
-
const state = runners.get(opts.sessionId) ?? {
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
|
49
|
-
const
|
|
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
|
-
|
|
172
|
+
messageAbortControllers.delete(messageId);
|
|
53
173
|
}
|
|
174
|
+
|
|
175
|
+
publishQueueState(sessionId);
|
|
176
|
+
return true;
|
|
54
177
|
}
|
|
55
178
|
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
sessionAbortControllers.delete(sessionId);
|
|
238
|
+
publishQueueState(sessionId);
|
|
76
239
|
}
|
|
77
240
|
}
|
package/src/tools/adapter.ts
CHANGED
|
@@ -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: {
|
|
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
|
}
|