@agi-cli/server 0.1.119 → 0.1.121
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/index.ts +9 -5
- package/src/openapi/paths/git.ts +4 -0
- package/src/routes/ask.ts +13 -14
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +1 -1
- package/src/routes/config/cwd.ts +1 -1
- package/src/routes/config/main.ts +1 -1
- package/src/routes/config/models.ts +32 -4
- package/src/routes/config/providers.ts +1 -1
- package/src/routes/config/utils.ts +14 -1
- package/src/routes/files.ts +1 -1
- package/src/routes/git/commit.ts +23 -6
- package/src/routes/git/schemas.ts +1 -0
- package/src/routes/session-files.ts +1 -1
- package/src/routes/session-messages.ts +2 -2
- package/src/routes/sessions.ts +8 -6
- package/src/runtime/agent/registry.ts +333 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +265 -0
- package/src/runtime/agent/runner.ts +356 -0
- package/src/runtime/agent-registry.ts +6 -333
- package/src/runtime/{ask-service.ts → ask/service.ts} +5 -5
- package/src/runtime/{debug.ts → debug/index.ts} +1 -1
- package/src/runtime/{api-error.ts → errors/api-error.ts} +2 -2
- package/src/runtime/message/compaction-auto.ts +137 -0
- package/src/runtime/message/compaction-context.ts +64 -0
- package/src/runtime/message/compaction-detect.ts +19 -0
- package/src/runtime/message/compaction-limits.ts +58 -0
- package/src/runtime/message/compaction-mark.ts +115 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +23 -0
- package/src/runtime/{history-builder.ts → message/history-builder.ts} +2 -2
- package/src/runtime/{message-service.ts → message/service.ts} +8 -14
- package/src/runtime/{history → message}/tool-history-tracker.ts +1 -1
- package/src/runtime/{prompt.ts → prompt/builder.ts} +1 -1
- package/src/runtime/{provider.ts → provider/anthropic.ts} +4 -219
- package/src/runtime/provider/google.ts +12 -0
- package/src/runtime/provider/index.ts +44 -0
- package/src/runtime/provider/openai.ts +26 -0
- package/src/runtime/provider/opencode.ts +61 -0
- package/src/runtime/provider/openrouter.ts +11 -0
- package/src/runtime/provider/solforge.ts +22 -0
- package/src/runtime/provider/zai.ts +53 -0
- package/src/runtime/session/branch.ts +277 -0
- package/src/runtime/{db-operations.ts → session/db-operations.ts} +1 -1
- package/src/runtime/{session-manager.ts → session/manager.ts} +1 -1
- package/src/runtime/{session-queue.ts → session/queue.ts} +2 -2
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +200 -0
- package/src/runtime/stream/finish-handler.ts +123 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +17 -0
- package/src/runtime/{tool-context.ts → tools/context.ts} +1 -1
- package/src/runtime/{tool-context-setup.ts → tools/setup.ts} +3 -3
- package/src/runtime/{token-utils.ts → utils/token.ts} +2 -2
- package/src/tools/adapter.ts +4 -4
- package/src/runtime/compaction.ts +0 -536
- package/src/runtime/runner.ts +0 -654
- package/src/runtime/stream-handlers.ts +0 -508
- /package/src/runtime/{cache-optimizer.ts → context/cache-optimizer.ts} +0 -0
- /package/src/runtime/{environment.ts → context/environment.ts} +0 -0
- /package/src/runtime/{context-optimizer.ts → context/optimizer.ts} +0 -0
- /package/src/runtime/{debug-state.ts → debug/state.ts} +0 -0
- /package/src/runtime/{error-handling.ts → errors/handling.ts} +0 -0
- /package/src/runtime/{history-truncator.ts → message/history-truncator.ts} +0 -0
- /package/src/runtime/{provider-selection.ts → provider/selection.ts} +0 -0
- /package/src/runtime/{tool-mapping.ts → tools/mapping.ts} +0 -0
- /package/src/runtime/{cwd.ts → utils/cwd.ts} +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { hasToolCall, streamText } from 'ai';
|
|
2
|
+
import { messageParts } from '@agi-cli/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { publish, subscribe } from '../../events/bus.ts';
|
|
5
|
+
import { debugLog, time } from '../debug/index.ts';
|
|
6
|
+
import { toErrorPayload } from '../errors/handling.ts';
|
|
7
|
+
import {
|
|
8
|
+
type RunOpts,
|
|
9
|
+
setRunning,
|
|
10
|
+
dequeueJob,
|
|
11
|
+
cleanupSession,
|
|
12
|
+
} from '../session/queue.ts';
|
|
13
|
+
import {
|
|
14
|
+
updateSessionTokensIncremental,
|
|
15
|
+
updateMessageTokensIncremental,
|
|
16
|
+
completeAssistantMessage,
|
|
17
|
+
cleanupEmptyTextParts,
|
|
18
|
+
} from '../session/db-operations.ts';
|
|
19
|
+
import {
|
|
20
|
+
createStepFinishHandler,
|
|
21
|
+
createErrorHandler,
|
|
22
|
+
createAbortHandler,
|
|
23
|
+
createFinishHandler,
|
|
24
|
+
} from '../stream/handlers.ts';
|
|
25
|
+
import { pruneSession } from '../message/compaction.ts';
|
|
26
|
+
import { setupRunner } from './runner-setup.ts';
|
|
27
|
+
import {
|
|
28
|
+
type ReasoningState,
|
|
29
|
+
serializeReasoningContent,
|
|
30
|
+
handleReasoningStart,
|
|
31
|
+
handleReasoningDelta,
|
|
32
|
+
handleReasoningEnd,
|
|
33
|
+
} from './runner-reasoning.ts';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
enqueueAssistantRun,
|
|
37
|
+
abortSession,
|
|
38
|
+
abortMessage,
|
|
39
|
+
removeFromQueue,
|
|
40
|
+
getQueueState,
|
|
41
|
+
getRunnerState,
|
|
42
|
+
} from '../session/queue.ts';
|
|
43
|
+
|
|
44
|
+
export async function runSessionLoop(sessionId: string) {
|
|
45
|
+
setRunning(sessionId, true);
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
const job = await dequeueJob(sessionId);
|
|
49
|
+
if (!job) break;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await runAssistant(job);
|
|
53
|
+
} catch (_err) {
|
|
54
|
+
// Swallow to keep the loop alive; event published by runner
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setRunning(sessionId, false);
|
|
59
|
+
cleanupSession(sessionId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runAssistant(opts: RunOpts) {
|
|
63
|
+
const separator = '='.repeat(72);
|
|
64
|
+
debugLog(separator);
|
|
65
|
+
debugLog(
|
|
66
|
+
`[RUNNER] Starting turn for session ${opts.sessionId}, message ${opts.assistantMessageId}`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const setup = await setupRunner(opts);
|
|
70
|
+
const {
|
|
71
|
+
db,
|
|
72
|
+
history,
|
|
73
|
+
system,
|
|
74
|
+
additionalSystemMessages,
|
|
75
|
+
model,
|
|
76
|
+
effectiveMaxOutputTokens,
|
|
77
|
+
toolset,
|
|
78
|
+
sharedCtx,
|
|
79
|
+
firstToolTimer,
|
|
80
|
+
firstToolSeen,
|
|
81
|
+
providerOptions,
|
|
82
|
+
} = setup;
|
|
83
|
+
|
|
84
|
+
const isFirstMessage = !history.some((m) => m.role === 'assistant');
|
|
85
|
+
|
|
86
|
+
const messagesWithSystemInstructions: Array<{
|
|
87
|
+
role: string;
|
|
88
|
+
content: string | Array<unknown>;
|
|
89
|
+
}> = [...additionalSystemMessages, ...history];
|
|
90
|
+
|
|
91
|
+
if (!isFirstMessage) {
|
|
92
|
+
messagesWithSystemInstructions.push({
|
|
93
|
+
role: 'user',
|
|
94
|
+
content:
|
|
95
|
+
'SYSTEM REMINDER: You are continuing an existing session. When you have completed the task, you MUST stream a text summary of what you did to the user, and THEN call the `finish` tool. Do not call `finish` without a summary.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
debugLog(
|
|
100
|
+
`[RUNNER] messagesWithSystemInstructions length: ${messagesWithSystemInstructions.length}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
let _finishObserved = false;
|
|
104
|
+
const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
|
|
105
|
+
if (evt.type !== 'tool.result') return;
|
|
106
|
+
try {
|
|
107
|
+
const name = (evt.payload as { name?: string } | undefined)?.name;
|
|
108
|
+
if (name === 'finish') _finishObserved = true;
|
|
109
|
+
} catch {}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const streamStartTimer = time('runner:first-delta');
|
|
113
|
+
let firstDeltaSeen = false;
|
|
114
|
+
|
|
115
|
+
let currentPartId: string | null = null;
|
|
116
|
+
let accumulated = '';
|
|
117
|
+
let stepIndex = 0;
|
|
118
|
+
|
|
119
|
+
const getCurrentPartId = () => currentPartId;
|
|
120
|
+
const getStepIndex = () => stepIndex;
|
|
121
|
+
const updateCurrentPartId = (id: string | null) => {
|
|
122
|
+
currentPartId = id;
|
|
123
|
+
};
|
|
124
|
+
const updateAccumulated = (text: string) => {
|
|
125
|
+
accumulated = text;
|
|
126
|
+
};
|
|
127
|
+
const incrementStepIndex = () => {
|
|
128
|
+
stepIndex += 1;
|
|
129
|
+
return stepIndex;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const reasoningStates = new Map<string, ReasoningState>();
|
|
133
|
+
|
|
134
|
+
const onStepFinish = createStepFinishHandler(
|
|
135
|
+
opts,
|
|
136
|
+
db,
|
|
137
|
+
getStepIndex,
|
|
138
|
+
incrementStepIndex,
|
|
139
|
+
getCurrentPartId,
|
|
140
|
+
updateCurrentPartId,
|
|
141
|
+
updateAccumulated,
|
|
142
|
+
sharedCtx,
|
|
143
|
+
updateSessionTokensIncremental,
|
|
144
|
+
updateMessageTokensIncremental,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const onError = createErrorHandler(
|
|
148
|
+
opts,
|
|
149
|
+
db,
|
|
150
|
+
getStepIndex,
|
|
151
|
+
sharedCtx,
|
|
152
|
+
runSessionLoop,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const onAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
|
|
156
|
+
|
|
157
|
+
const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK message types are complex
|
|
161
|
+
const result = streamText({
|
|
162
|
+
model,
|
|
163
|
+
tools: toolset,
|
|
164
|
+
...(system ? { system } : {}),
|
|
165
|
+
messages: messagesWithSystemInstructions as any,
|
|
166
|
+
...(effectiveMaxOutputTokens
|
|
167
|
+
? { maxOutputTokens: effectiveMaxOutputTokens }
|
|
168
|
+
: {}),
|
|
169
|
+
...(Object.keys(providerOptions).length > 0 ? { providerOptions } : {}),
|
|
170
|
+
abortSignal: opts.abortSignal,
|
|
171
|
+
stopWhen: hasToolCall('finish'),
|
|
172
|
+
onStepFinish: onStepFinish as any,
|
|
173
|
+
onError: onError as any,
|
|
174
|
+
onAbort: onAbort as any,
|
|
175
|
+
onFinish: onFinish as any,
|
|
176
|
+
} as any);
|
|
177
|
+
|
|
178
|
+
for await (const part of result.fullStream) {
|
|
179
|
+
if (!part) continue;
|
|
180
|
+
|
|
181
|
+
if (part.type === 'text-delta') {
|
|
182
|
+
const delta = part.text;
|
|
183
|
+
if (!delta) continue;
|
|
184
|
+
if (!firstDeltaSeen) {
|
|
185
|
+
firstDeltaSeen = true;
|
|
186
|
+
streamStartTimer.end();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!currentPartId) {
|
|
190
|
+
currentPartId = crypto.randomUUID();
|
|
191
|
+
sharedCtx.assistantPartId = currentPartId;
|
|
192
|
+
await db.insert(messageParts).values({
|
|
193
|
+
id: currentPartId,
|
|
194
|
+
messageId: opts.assistantMessageId,
|
|
195
|
+
index: await sharedCtx.nextIndex(),
|
|
196
|
+
stepIndex: null,
|
|
197
|
+
type: 'text',
|
|
198
|
+
content: JSON.stringify({ text: '' }),
|
|
199
|
+
agent: opts.agent,
|
|
200
|
+
provider: opts.provider,
|
|
201
|
+
model: opts.model,
|
|
202
|
+
startedAt: Date.now(),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
accumulated += delta;
|
|
207
|
+
publish({
|
|
208
|
+
type: 'message.part.delta',
|
|
209
|
+
sessionId: opts.sessionId,
|
|
210
|
+
payload: {
|
|
211
|
+
messageId: opts.assistantMessageId,
|
|
212
|
+
partId: currentPartId,
|
|
213
|
+
stepIndex,
|
|
214
|
+
delta,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
await db
|
|
218
|
+
.update(messageParts)
|
|
219
|
+
.set({ content: JSON.stringify({ text: accumulated }) })
|
|
220
|
+
.where(eq(messageParts.id, currentPartId));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (part.type === 'reasoning-start') {
|
|
225
|
+
const reasoningId = part.id;
|
|
226
|
+
if (!reasoningId) continue;
|
|
227
|
+
await handleReasoningStart(
|
|
228
|
+
reasoningId,
|
|
229
|
+
part.providerMetadata,
|
|
230
|
+
opts,
|
|
231
|
+
db,
|
|
232
|
+
sharedCtx,
|
|
233
|
+
getStepIndex,
|
|
234
|
+
reasoningStates,
|
|
235
|
+
);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (part.type === 'reasoning-delta') {
|
|
240
|
+
await handleReasoningDelta(
|
|
241
|
+
part.id,
|
|
242
|
+
part.text,
|
|
243
|
+
part.providerMetadata,
|
|
244
|
+
opts,
|
|
245
|
+
db,
|
|
246
|
+
getStepIndex,
|
|
247
|
+
reasoningStates,
|
|
248
|
+
);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (part.type === 'reasoning-end') {
|
|
253
|
+
await handleReasoningEnd(part.id, db, reasoningStates);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const fs = firstToolSeen();
|
|
258
|
+
if (!fs && !_finishObserved) {
|
|
259
|
+
publish({
|
|
260
|
+
type: 'finish-step',
|
|
261
|
+
sessionId: opts.sessionId,
|
|
262
|
+
payload: { reason: 'no-tool-calls' },
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
unsubscribeFinish();
|
|
267
|
+
await cleanupEmptyTextParts(opts, db);
|
|
268
|
+
firstToolTimer.end({ seen: firstToolSeen() });
|
|
269
|
+
|
|
270
|
+
debugLog(
|
|
271
|
+
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}`,
|
|
272
|
+
);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
unsubscribeFinish();
|
|
275
|
+
const payload = toErrorPayload(err);
|
|
276
|
+
|
|
277
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
278
|
+
const errorCode = (err as { code?: string })?.code ?? '';
|
|
279
|
+
const responseBody = (err as { responseBody?: string })?.responseBody ?? '';
|
|
280
|
+
const apiErrorType = (err as { apiErrorType?: string })?.apiErrorType ?? '';
|
|
281
|
+
const combinedError = `${errorMessage} ${responseBody}`.toLowerCase();
|
|
282
|
+
|
|
283
|
+
const isPromptTooLong =
|
|
284
|
+
combinedError.includes('prompt is too long') ||
|
|
285
|
+
combinedError.includes('maximum context length') ||
|
|
286
|
+
combinedError.includes('too many tokens') ||
|
|
287
|
+
combinedError.includes('context_length_exceeded') ||
|
|
288
|
+
combinedError.includes('request too large') ||
|
|
289
|
+
combinedError.includes('exceeds the model') ||
|
|
290
|
+
combinedError.includes('input is too long') ||
|
|
291
|
+
errorCode === 'context_length_exceeded' ||
|
|
292
|
+
apiErrorType === 'invalid_request_error';
|
|
293
|
+
|
|
294
|
+
debugLog(
|
|
295
|
+
`[RUNNER] isPromptTooLong: ${isPromptTooLong}, isCompactCommand: ${opts.isCompactCommand}`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (isPromptTooLong && !opts.isCompactCommand) {
|
|
299
|
+
debugLog('[RUNNER] Prompt too long - auto-compacting');
|
|
300
|
+
try {
|
|
301
|
+
const pruneResult = await pruneSession(db, opts.sessionId);
|
|
302
|
+
debugLog(
|
|
303
|
+
`[RUNNER] Auto-pruned ${pruneResult.pruned} parts, saved ~${pruneResult.saved} tokens`,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
publish({
|
|
307
|
+
type: 'error',
|
|
308
|
+
sessionId: opts.sessionId,
|
|
309
|
+
payload: {
|
|
310
|
+
...payload,
|
|
311
|
+
message: `Context too large. Auto-compacted old tool results. Please retry your message.`,
|
|
312
|
+
name: 'ContextOverflow',
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await completeAssistantMessage({}, opts, db);
|
|
318
|
+
} catch {}
|
|
319
|
+
return;
|
|
320
|
+
} catch (pruneErr) {
|
|
321
|
+
debugLog(
|
|
322
|
+
`[RUNNER] Auto-prune failed: ${pruneErr instanceof Error ? pruneErr.message : String(pruneErr)}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
debugLog(`[RUNNER] Error during stream: ${payload.message}`);
|
|
328
|
+
publish({
|
|
329
|
+
type: 'error',
|
|
330
|
+
sessionId: opts.sessionId,
|
|
331
|
+
payload,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
await updateSessionTokensIncremental(
|
|
336
|
+
{ inputTokens: 0, outputTokens: 0 },
|
|
337
|
+
undefined,
|
|
338
|
+
opts,
|
|
339
|
+
db,
|
|
340
|
+
);
|
|
341
|
+
await updateMessageTokensIncremental(
|
|
342
|
+
{ inputTokens: 0, outputTokens: 0 },
|
|
343
|
+
undefined,
|
|
344
|
+
opts,
|
|
345
|
+
db,
|
|
346
|
+
);
|
|
347
|
+
await completeAssistantMessage({}, opts, db);
|
|
348
|
+
} catch {}
|
|
349
|
+
throw err;
|
|
350
|
+
} finally {
|
|
351
|
+
debugLog(
|
|
352
|
+
`[RUNNER] Turn complete for session ${opts.sessionId}, message ${opts.assistantMessageId}`,
|
|
353
|
+
);
|
|
354
|
+
debugLog(separator);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -1,333 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import AGENT_BUILD from '@agi-cli/sdk/prompts/agents/build.txt' with {
|
|
8
|
-
type: 'text',
|
|
9
|
-
};
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
11
|
-
import AGENT_PLAN from '@agi-cli/sdk/prompts/agents/plan.txt' with {
|
|
12
|
-
type: 'text',
|
|
13
|
-
};
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
15
|
-
import AGENT_GENERAL from '@agi-cli/sdk/prompts/agents/general.txt' with {
|
|
16
|
-
type: 'text',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type AgentConfig = {
|
|
20
|
-
name: string;
|
|
21
|
-
prompt: string;
|
|
22
|
-
tools: string[]; // allowed tool names
|
|
23
|
-
provider?: ProviderName;
|
|
24
|
-
model?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type AgentConfigEntry = {
|
|
28
|
-
tools?: string[];
|
|
29
|
-
appendTools?: string[];
|
|
30
|
-
prompt?: string;
|
|
31
|
-
provider?: string;
|
|
32
|
-
model?: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type AgentsJson = Record<string, AgentConfigEntry>;
|
|
36
|
-
|
|
37
|
-
function normalizeStringList(value: unknown): string[] {
|
|
38
|
-
if (!Array.isArray(value)) return [];
|
|
39
|
-
const seen = new Set<string>();
|
|
40
|
-
const out: string[] = [];
|
|
41
|
-
for (const item of value) {
|
|
42
|
-
if (typeof item !== 'string') continue;
|
|
43
|
-
const trimmed = item.trim();
|
|
44
|
-
if (!trimmed || seen.has(trimmed)) continue;
|
|
45
|
-
seen.add(trimmed);
|
|
46
|
-
out.push(trimmed);
|
|
47
|
-
}
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const providerValues = new Set<ProviderName>(
|
|
52
|
-
Object.keys(catalog) as ProviderName[],
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
function normalizeProvider(value: unknown): ProviderName | undefined {
|
|
56
|
-
if (typeof value !== 'string') return undefined;
|
|
57
|
-
const trimmed = value.trim().toLowerCase();
|
|
58
|
-
if (!trimmed) return undefined;
|
|
59
|
-
return providerValues.has(trimmed as ProviderName)
|
|
60
|
-
? (trimmed as ProviderName)
|
|
61
|
-
: undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeModel(value: unknown): string | undefined {
|
|
65
|
-
if (typeof value !== 'string') return undefined;
|
|
66
|
-
const trimmed = value.trim();
|
|
67
|
-
return trimmed.length ? trimmed : undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function mergeAgentEntries(
|
|
71
|
-
base: AgentConfigEntry | undefined,
|
|
72
|
-
override: AgentConfigEntry,
|
|
73
|
-
): AgentConfigEntry {
|
|
74
|
-
const merged: AgentConfigEntry = {};
|
|
75
|
-
const baseTools = normalizeStringList(base?.tools);
|
|
76
|
-
if (baseTools.length) merged.tools = [...baseTools];
|
|
77
|
-
const baseAppend = normalizeStringList(base?.appendTools);
|
|
78
|
-
if (baseAppend.length) merged.appendTools = [...baseAppend];
|
|
79
|
-
if (base && Object.hasOwn(base, 'prompt')) merged.prompt = base.prompt;
|
|
80
|
-
if (base && Object.hasOwn(base, 'provider'))
|
|
81
|
-
merged.provider = normalizeProvider(base.provider);
|
|
82
|
-
if (base && Object.hasOwn(base, 'model'))
|
|
83
|
-
merged.model = normalizeModel(base.model);
|
|
84
|
-
|
|
85
|
-
if (Array.isArray(override.tools))
|
|
86
|
-
merged.tools = normalizeStringList(override.tools);
|
|
87
|
-
if (Array.isArray(override.appendTools)) {
|
|
88
|
-
const extras = normalizeStringList(override.appendTools);
|
|
89
|
-
const union = new Set([...(merged.appendTools ?? []), ...extras]);
|
|
90
|
-
merged.appendTools = Array.from(union);
|
|
91
|
-
} else if (
|
|
92
|
-
Object.hasOwn(override, 'appendTools') &&
|
|
93
|
-
!Array.isArray(override.appendTools)
|
|
94
|
-
) {
|
|
95
|
-
delete merged.appendTools;
|
|
96
|
-
}
|
|
97
|
-
if (Object.hasOwn(override, 'prompt')) merged.prompt = override.prompt;
|
|
98
|
-
|
|
99
|
-
if (Object.hasOwn(override, 'provider')) {
|
|
100
|
-
const normalized = normalizeProvider(override.provider);
|
|
101
|
-
if (normalized) merged.provider = normalized;
|
|
102
|
-
else delete merged.provider;
|
|
103
|
-
}
|
|
104
|
-
if (Object.hasOwn(override, 'model')) {
|
|
105
|
-
const normalized = normalizeModel(override.model);
|
|
106
|
-
if (normalized) merged.model = normalized;
|
|
107
|
-
else delete merged.model;
|
|
108
|
-
}
|
|
109
|
-
return merged;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const baseToolSet = ['progress_update', 'finish'] as const;
|
|
113
|
-
|
|
114
|
-
const defaultToolExtras: Record<string, string[]> = {
|
|
115
|
-
build: [
|
|
116
|
-
'read',
|
|
117
|
-
'write',
|
|
118
|
-
'ls',
|
|
119
|
-
'tree',
|
|
120
|
-
'bash',
|
|
121
|
-
'update_todos',
|
|
122
|
-
'glob',
|
|
123
|
-
'ripgrep',
|
|
124
|
-
'git_status',
|
|
125
|
-
'terminal',
|
|
126
|
-
'apply_patch',
|
|
127
|
-
'websearch',
|
|
128
|
-
],
|
|
129
|
-
plan: ['read', 'ls', 'tree', 'ripgrep', 'update_todos', 'websearch'],
|
|
130
|
-
general: [
|
|
131
|
-
'read',
|
|
132
|
-
'write',
|
|
133
|
-
'ls',
|
|
134
|
-
'tree',
|
|
135
|
-
'bash',
|
|
136
|
-
'ripgrep',
|
|
137
|
-
'glob',
|
|
138
|
-
'websearch',
|
|
139
|
-
'update_todos',
|
|
140
|
-
],
|
|
141
|
-
git: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
142
|
-
commit: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export function defaultToolsForAgent(name: string): string[] {
|
|
146
|
-
const extras = defaultToolExtras[name] ? [...defaultToolExtras[name]] : [];
|
|
147
|
-
return Array.from(new Set([...baseToolSet, ...extras]));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function loadAgentsConfig(
|
|
151
|
-
projectRoot: string,
|
|
152
|
-
): Promise<AgentsJson> {
|
|
153
|
-
const localPath = `${projectRoot}/.agi/agents.json`.replace(/\\/g, '/');
|
|
154
|
-
const globalPath = getGlobalAgentsJsonPath();
|
|
155
|
-
let globalCfg: AgentsJson = {};
|
|
156
|
-
let localCfg: AgentsJson = {};
|
|
157
|
-
try {
|
|
158
|
-
const gf = Bun.file(globalPath);
|
|
159
|
-
if (await gf.exists())
|
|
160
|
-
globalCfg = (await gf.json().catch(() => ({}))) as AgentsJson;
|
|
161
|
-
} catch {}
|
|
162
|
-
try {
|
|
163
|
-
const lf = Bun.file(localPath);
|
|
164
|
-
if (await lf.exists())
|
|
165
|
-
localCfg = (await lf.json().catch(() => ({}))) as AgentsJson;
|
|
166
|
-
} catch {}
|
|
167
|
-
const merged: AgentsJson = {};
|
|
168
|
-
for (const [name, entry] of Object.entries(globalCfg)) {
|
|
169
|
-
merged[name] = mergeAgentEntries(undefined, entry ?? {});
|
|
170
|
-
}
|
|
171
|
-
for (const [name, entry] of Object.entries(localCfg)) {
|
|
172
|
-
const base = merged[name];
|
|
173
|
-
merged[name] = mergeAgentEntries(base, entry ?? {});
|
|
174
|
-
}
|
|
175
|
-
return merged;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export async function resolveAgentConfig(
|
|
179
|
-
projectRoot: string,
|
|
180
|
-
name: string,
|
|
181
|
-
inlineConfig?: {
|
|
182
|
-
prompt?: string;
|
|
183
|
-
tools?: string[];
|
|
184
|
-
provider?: string;
|
|
185
|
-
model?: string;
|
|
186
|
-
},
|
|
187
|
-
): Promise<AgentConfig> {
|
|
188
|
-
if (inlineConfig?.prompt) {
|
|
189
|
-
const provider = normalizeProvider(inlineConfig.provider);
|
|
190
|
-
const model = normalizeModel(inlineConfig.model);
|
|
191
|
-
return {
|
|
192
|
-
name,
|
|
193
|
-
prompt: inlineConfig.prompt,
|
|
194
|
-
tools: inlineConfig.tools ?? defaultToolsForAgent(name),
|
|
195
|
-
provider,
|
|
196
|
-
model,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
const agents = await loadAgentsConfig(projectRoot);
|
|
200
|
-
const entry = agents[name];
|
|
201
|
-
let prompt = '';
|
|
202
|
-
let promptSource: string = 'none';
|
|
203
|
-
|
|
204
|
-
// Override files: project first, then global
|
|
205
|
-
const globalAgentsDir = getGlobalAgentsDir();
|
|
206
|
-
const localDirTxt = `${projectRoot}/.agi/agents/${name}/agent.txt`.replace(
|
|
207
|
-
/\\/g,
|
|
208
|
-
'/',
|
|
209
|
-
);
|
|
210
|
-
const localDirMd = `${projectRoot}/.agi/agents/${name}/agent.md`.replace(
|
|
211
|
-
/\\/g,
|
|
212
|
-
'/',
|
|
213
|
-
);
|
|
214
|
-
const localFlatTxt = `${projectRoot}/.agi/agents/${name}.txt`.replace(
|
|
215
|
-
/\\/g,
|
|
216
|
-
'/',
|
|
217
|
-
);
|
|
218
|
-
const localFlatMd = `${projectRoot}/.agi/agents/${name}.md`.replace(
|
|
219
|
-
/\\/g,
|
|
220
|
-
'/',
|
|
221
|
-
);
|
|
222
|
-
const globalDirTxt = `${globalAgentsDir}/${name}/agent.txt`.replace(
|
|
223
|
-
/\\/g,
|
|
224
|
-
'/',
|
|
225
|
-
);
|
|
226
|
-
const globalDirMd = `${globalAgentsDir}/${name}/agent.md`.replace(/\\/g, '/');
|
|
227
|
-
const globalFlatTxt = `${globalAgentsDir}/${name}.txt`.replace(/\\/g, '/');
|
|
228
|
-
const globalFlatMd = `${globalAgentsDir}/${name}.md`.replace(/\\/g, '/');
|
|
229
|
-
const files = [
|
|
230
|
-
localDirMd,
|
|
231
|
-
localFlatMd,
|
|
232
|
-
localDirTxt,
|
|
233
|
-
localFlatTxt,
|
|
234
|
-
globalDirMd,
|
|
235
|
-
globalFlatMd,
|
|
236
|
-
globalDirTxt,
|
|
237
|
-
globalFlatTxt,
|
|
238
|
-
];
|
|
239
|
-
for (const p of files) {
|
|
240
|
-
try {
|
|
241
|
-
const f = Bun.file(p);
|
|
242
|
-
if (await f.exists()) {
|
|
243
|
-
const text = await f.text();
|
|
244
|
-
if (text.trim()) {
|
|
245
|
-
prompt = text;
|
|
246
|
-
promptSource = `file:${p}`;
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
} catch {}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// If agents.json provides a 'prompt' field, accept inline content or a relative/absolute path
|
|
254
|
-
if (entry?.prompt) {
|
|
255
|
-
const p = entry.prompt.trim();
|
|
256
|
-
if (
|
|
257
|
-
/[.](md|txt)$/i.test(p) ||
|
|
258
|
-
p.startsWith('.') ||
|
|
259
|
-
p.startsWith('/') ||
|
|
260
|
-
p.startsWith('~/')
|
|
261
|
-
) {
|
|
262
|
-
const candidates: string[] = [];
|
|
263
|
-
if (p.startsWith('~/')) {
|
|
264
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
265
|
-
candidates.push(`${home}/${p.slice(2)}`);
|
|
266
|
-
} else if (p.startsWith('/')) candidates.push(p);
|
|
267
|
-
else candidates.push(`${projectRoot}/${p}`.replace(/\\/g, '/'));
|
|
268
|
-
for (const candidate of candidates) {
|
|
269
|
-
const pf = Bun.file(candidate);
|
|
270
|
-
if (await pf.exists()) {
|
|
271
|
-
const t = await pf.text();
|
|
272
|
-
if (t.trim()) {
|
|
273
|
-
prompt = t;
|
|
274
|
-
promptSource = `agents.json:file:${candidate}`;
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
prompt = p;
|
|
281
|
-
promptSource = 'agents.json:inline';
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Fallback: use embedded defaults (plan/build/general); else default to build
|
|
286
|
-
if (!prompt) {
|
|
287
|
-
const byName = (n: string): string | undefined => {
|
|
288
|
-
if (n === 'build') return AGENT_BUILD;
|
|
289
|
-
if (n === 'plan') return AGENT_PLAN;
|
|
290
|
-
if (n === 'general') return AGENT_GENERAL;
|
|
291
|
-
return undefined;
|
|
292
|
-
};
|
|
293
|
-
const candidate = byName(name)?.trim();
|
|
294
|
-
if (candidate?.length) {
|
|
295
|
-
prompt = candidate;
|
|
296
|
-
promptSource = `fallback:embedded:${name}.txt`;
|
|
297
|
-
} else {
|
|
298
|
-
prompt = (AGENT_BUILD || '').trim();
|
|
299
|
-
promptSource = 'fallback:embedded:build.txt';
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Default tool access per agent if not explicitly configured
|
|
304
|
-
let tools = Array.isArray(entry?.tools)
|
|
305
|
-
? [...(entry?.tools as string[])]
|
|
306
|
-
: defaultToolsForAgent(name);
|
|
307
|
-
if (!entry || !entry.tools) {
|
|
308
|
-
tools = defaultToolsForAgent(name);
|
|
309
|
-
}
|
|
310
|
-
if (Array.isArray(entry?.appendTools) && entry.appendTools.length) {
|
|
311
|
-
for (const t of entry.appendTools) {
|
|
312
|
-
if (typeof t === 'string' && t.trim()) tools.push(t.trim());
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
// Deduplicate and ensure base tools are always available
|
|
316
|
-
const deduped = Array.from(new Set([...tools, ...baseToolSet]));
|
|
317
|
-
const provider = normalizeProvider(entry?.provider);
|
|
318
|
-
const model = normalizeModel(entry?.model);
|
|
319
|
-
debugLog(`[agent] ${name} prompt source: ${promptSource}`);
|
|
320
|
-
debugLog(
|
|
321
|
-
`[agent] ${name} prompt summary: ${JSON.stringify({
|
|
322
|
-
length: prompt.length,
|
|
323
|
-
lines: prompt.split('\n').length,
|
|
324
|
-
})}`,
|
|
325
|
-
);
|
|
326
|
-
return {
|
|
327
|
-
name,
|
|
328
|
-
prompt,
|
|
329
|
-
tools: deduped,
|
|
330
|
-
provider,
|
|
331
|
-
model,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
1
|
+
// Barrel export for backwards compatibility with @agi-cli/server/runtime/agent-registry
|
|
2
|
+
export {
|
|
3
|
+
resolveAgentConfig,
|
|
4
|
+
defaultToolsForAgent,
|
|
5
|
+
type AgentConfigEntry,
|
|
6
|
+
} from './agent/registry.ts';
|