@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.
Files changed (70) hide show
  1. package/package.json +3 -3
  2. package/src/index.ts +9 -5
  3. package/src/openapi/paths/git.ts +4 -0
  4. package/src/routes/ask.ts +13 -14
  5. package/src/routes/branch.ts +106 -0
  6. package/src/routes/config/agents.ts +1 -1
  7. package/src/routes/config/cwd.ts +1 -1
  8. package/src/routes/config/main.ts +1 -1
  9. package/src/routes/config/models.ts +32 -4
  10. package/src/routes/config/providers.ts +1 -1
  11. package/src/routes/config/utils.ts +14 -1
  12. package/src/routes/files.ts +1 -1
  13. package/src/routes/git/commit.ts +23 -6
  14. package/src/routes/git/schemas.ts +1 -0
  15. package/src/routes/session-files.ts +1 -1
  16. package/src/routes/session-messages.ts +2 -2
  17. package/src/routes/sessions.ts +8 -6
  18. package/src/runtime/agent/registry.ts +333 -0
  19. package/src/runtime/agent/runner-reasoning.ts +108 -0
  20. package/src/runtime/agent/runner-setup.ts +265 -0
  21. package/src/runtime/agent/runner.ts +356 -0
  22. package/src/runtime/agent-registry.ts +6 -333
  23. package/src/runtime/{ask-service.ts → ask/service.ts} +5 -5
  24. package/src/runtime/{debug.ts → debug/index.ts} +1 -1
  25. package/src/runtime/{api-error.ts → errors/api-error.ts} +2 -2
  26. package/src/runtime/message/compaction-auto.ts +137 -0
  27. package/src/runtime/message/compaction-context.ts +64 -0
  28. package/src/runtime/message/compaction-detect.ts +19 -0
  29. package/src/runtime/message/compaction-limits.ts +58 -0
  30. package/src/runtime/message/compaction-mark.ts +115 -0
  31. package/src/runtime/message/compaction-prune.ts +75 -0
  32. package/src/runtime/message/compaction.ts +23 -0
  33. package/src/runtime/{history-builder.ts → message/history-builder.ts} +2 -2
  34. package/src/runtime/{message-service.ts → message/service.ts} +8 -14
  35. package/src/runtime/{history → message}/tool-history-tracker.ts +1 -1
  36. package/src/runtime/{prompt.ts → prompt/builder.ts} +1 -1
  37. package/src/runtime/{provider.ts → provider/anthropic.ts} +4 -219
  38. package/src/runtime/provider/google.ts +12 -0
  39. package/src/runtime/provider/index.ts +44 -0
  40. package/src/runtime/provider/openai.ts +26 -0
  41. package/src/runtime/provider/opencode.ts +61 -0
  42. package/src/runtime/provider/openrouter.ts +11 -0
  43. package/src/runtime/provider/solforge.ts +22 -0
  44. package/src/runtime/provider/zai.ts +53 -0
  45. package/src/runtime/session/branch.ts +277 -0
  46. package/src/runtime/{db-operations.ts → session/db-operations.ts} +1 -1
  47. package/src/runtime/{session-manager.ts → session/manager.ts} +1 -1
  48. package/src/runtime/{session-queue.ts → session/queue.ts} +2 -2
  49. package/src/runtime/stream/abort-handler.ts +65 -0
  50. package/src/runtime/stream/error-handler.ts +200 -0
  51. package/src/runtime/stream/finish-handler.ts +123 -0
  52. package/src/runtime/stream/handlers.ts +5 -0
  53. package/src/runtime/stream/step-finish.ts +93 -0
  54. package/src/runtime/stream/types.ts +17 -0
  55. package/src/runtime/{tool-context.ts → tools/context.ts} +1 -1
  56. package/src/runtime/{tool-context-setup.ts → tools/setup.ts} +3 -3
  57. package/src/runtime/{token-utils.ts → utils/token.ts} +2 -2
  58. package/src/tools/adapter.ts +4 -4
  59. package/src/runtime/compaction.ts +0 -536
  60. package/src/runtime/runner.ts +0 -654
  61. package/src/runtime/stream-handlers.ts +0 -508
  62. /package/src/runtime/{cache-optimizer.ts → context/cache-optimizer.ts} +0 -0
  63. /package/src/runtime/{environment.ts → context/environment.ts} +0 -0
  64. /package/src/runtime/{context-optimizer.ts → context/optimizer.ts} +0 -0
  65. /package/src/runtime/{debug-state.ts → debug/state.ts} +0 -0
  66. /package/src/runtime/{error-handling.ts → errors/handling.ts} +0 -0
  67. /package/src/runtime/{history-truncator.ts → message/history-truncator.ts} +0 -0
  68. /package/src/runtime/{provider-selection.ts → provider/selection.ts} +0 -0
  69. /package/src/runtime/{tool-mapping.ts → tools/mapping.ts} +0 -0
  70. /package/src/runtime/{cwd.ts → utils/cwd.ts} +0 -0
@@ -0,0 +1,333 @@
1
+ import { getGlobalAgentsJsonPath, getGlobalAgentsDir } from '@agi-cli/sdk';
2
+ import { debugLog } from '../debug/index.ts';
3
+ import type { ProviderName } from '@agi-cli/sdk';
4
+ import { catalog } from '@agi-cli/sdk';
5
+ // Embed default agent prompts; only user overrides read from disk.
6
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
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
+ }
@@ -0,0 +1,108 @@
1
+ import type { getDb } from '@agi-cli/database';
2
+ import { messageParts } from '@agi-cli/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { publish } from '../../events/bus.ts';
5
+ import type { RunOpts } from '../session/queue.ts';
6
+ import type { ToolAdapterContext } from '../../tools/adapter.ts';
7
+
8
+ export type ReasoningState = {
9
+ partId: string;
10
+ text: string;
11
+ providerMetadata?: unknown;
12
+ };
13
+
14
+ export function serializeReasoningContent(state: ReasoningState): string {
15
+ return JSON.stringify(
16
+ state.providerMetadata != null
17
+ ? { text: state.text, providerMetadata: state.providerMetadata }
18
+ : { text: state.text },
19
+ );
20
+ }
21
+
22
+ export async function handleReasoningStart(
23
+ reasoningId: string,
24
+ providerMetadata: unknown,
25
+ opts: RunOpts,
26
+ db: Awaited<ReturnType<typeof getDb>>,
27
+ sharedCtx: ToolAdapterContext,
28
+ getStepIndex: () => number,
29
+ reasoningStates: Map<string, ReasoningState>,
30
+ ): Promise<void> {
31
+ const reasoningPartId = crypto.randomUUID();
32
+ const state: ReasoningState = {
33
+ partId: reasoningPartId,
34
+ text: '',
35
+ providerMetadata,
36
+ };
37
+ reasoningStates.set(reasoningId, state);
38
+ try {
39
+ await db.insert(messageParts).values({
40
+ id: reasoningPartId,
41
+ messageId: opts.assistantMessageId,
42
+ index: await sharedCtx.nextIndex(),
43
+ stepIndex: getStepIndex(),
44
+ type: 'reasoning',
45
+ content: serializeReasoningContent(state),
46
+ agent: opts.agent,
47
+ provider: opts.provider,
48
+ model: opts.model,
49
+ startedAt: Date.now(),
50
+ });
51
+ } catch {}
52
+ }
53
+
54
+ export async function handleReasoningDelta(
55
+ reasoningId: string,
56
+ text: string,
57
+ providerMetadata: unknown,
58
+ opts: RunOpts,
59
+ db: Awaited<ReturnType<typeof getDb>>,
60
+ getStepIndex: () => number,
61
+ reasoningStates: Map<string, ReasoningState>,
62
+ ): Promise<void> {
63
+ const state = reasoningStates.get(reasoningId);
64
+ if (!state) return;
65
+ state.text += text;
66
+ if (providerMetadata != null) {
67
+ state.providerMetadata = providerMetadata;
68
+ }
69
+ publish({
70
+ type: 'reasoning.delta',
71
+ sessionId: opts.sessionId,
72
+ payload: {
73
+ messageId: opts.assistantMessageId,
74
+ partId: state.partId,
75
+ stepIndex: getStepIndex(),
76
+ delta: text,
77
+ },
78
+ });
79
+ try {
80
+ await db
81
+ .update(messageParts)
82
+ .set({ content: serializeReasoningContent(state) })
83
+ .where(eq(messageParts.id, state.partId));
84
+ } catch {}
85
+ }
86
+
87
+ export async function handleReasoningEnd(
88
+ reasoningId: string,
89
+ db: Awaited<ReturnType<typeof getDb>>,
90
+ reasoningStates: Map<string, ReasoningState>,
91
+ ): Promise<void> {
92
+ const state = reasoningStates.get(reasoningId);
93
+ if (!state) return;
94
+ if (!state.text || state.text.trim() === '') {
95
+ try {
96
+ await db.delete(messageParts).where(eq(messageParts.id, state.partId));
97
+ } catch {}
98
+ reasoningStates.delete(reasoningId);
99
+ return;
100
+ }
101
+ try {
102
+ await db
103
+ .update(messageParts)
104
+ .set({ completedAt: Date.now() })
105
+ .where(eq(messageParts.id, state.partId));
106
+ } catch {}
107
+ reasoningStates.delete(reasoningId);
108
+ }
@@ -0,0 +1,265 @@
1
+ import { loadConfig } from '@agi-cli/sdk';
2
+ import { getDb } from '@agi-cli/database';
3
+ import { sessions } from '@agi-cli/database/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { resolveModel } from '../provider/index.ts';
6
+ import { resolveAgentConfig } from './registry.ts';
7
+ import {
8
+ composeSystemPrompt,
9
+ getProviderSpoofPrompt,
10
+ } from '../prompt/builder.ts';
11
+ import { discoverProjectTools } from '@agi-cli/sdk';
12
+ import { adaptTools } from '../../tools/adapter.ts';
13
+ import { debugLog, time } from '../debug/index.ts';
14
+ import { buildHistoryMessages } from '../message/history-builder.ts';
15
+ import { getMaxOutputTokens } from '../utils/token.ts';
16
+ import { setupToolContext } from '../tools/setup.ts';
17
+ import { getCompactionSystemPrompt } from '../message/compaction.ts';
18
+ import type { RunOpts } from '../session/queue.ts';
19
+ import type { ToolAdapterContext } from '../../tools/adapter.ts';
20
+
21
+ export interface SetupResult {
22
+ cfg: Awaited<ReturnType<typeof loadConfig>>;
23
+ db: Awaited<ReturnType<typeof getDb>>;
24
+ agentCfg: Awaited<ReturnType<typeof resolveAgentConfig>>;
25
+ history: Awaited<ReturnType<typeof buildHistoryMessages>>;
26
+ system: string;
27
+ systemComponents: string[];
28
+ additionalSystemMessages: Array<{ role: 'system' | 'user'; content: string }>;
29
+ model: Awaited<ReturnType<typeof resolveModel>>;
30
+ maxOutputTokens: number | undefined;
31
+ effectiveMaxOutputTokens: number | undefined;
32
+ toolset: ReturnType<typeof adaptTools>;
33
+ sharedCtx: ToolAdapterContext;
34
+ firstToolTimer: ReturnType<typeof time>;
35
+ firstToolSeen: () => boolean;
36
+ providerOptions: Record<string, unknown>;
37
+ needsSpoof: boolean;
38
+ }
39
+
40
+ const THINKING_BUDGET = 16000;
41
+
42
+ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
43
+ const cfgTimer = time('runner:loadConfig+db');
44
+ const cfg = await loadConfig(opts.projectRoot);
45
+ const db = await getDb(cfg.projectRoot);
46
+ cfgTimer.end();
47
+
48
+ const agentTimer = time('runner:resolveAgentConfig');
49
+ const agentCfg = await resolveAgentConfig(cfg.projectRoot, opts.agent);
50
+ agentTimer.end({ agent: opts.agent });
51
+
52
+ const agentPrompt = agentCfg.prompt || '';
53
+
54
+ const historyTimer = time('runner:buildHistory');
55
+ let history: Awaited<ReturnType<typeof buildHistoryMessages>>;
56
+ if (opts.isCompactCommand && opts.compactionContext) {
57
+ debugLog('[RUNNER] Using minimal history for /compact command');
58
+ history = [];
59
+ } else {
60
+ history = await buildHistoryMessages(db, opts.sessionId);
61
+ }
62
+ historyTimer.end({ messages: history.length });
63
+
64
+ const sessionRows = await db
65
+ .select()
66
+ .from(sessions)
67
+ .where(eq(sessions.id, opts.sessionId))
68
+ .limit(1);
69
+ const contextSummary = sessionRows[0]?.contextSummary ?? undefined;
70
+ if (contextSummary) {
71
+ debugLog(
72
+ `[RUNNER] Using context summary from compaction (${contextSummary.length} chars)`,
73
+ );
74
+ }
75
+
76
+ const isFirstMessage = !history.some((m) => m.role === 'assistant');
77
+
78
+ debugLog(`[RUNNER] isFirstMessage: ${isFirstMessage}`);
79
+ debugLog(`[RUNNER] userContext provided: ${opts.userContext ? 'YES' : 'NO'}`);
80
+ if (opts.userContext) {
81
+ debugLog(
82
+ `[RUNNER] userContext value: ${opts.userContext.substring(0, 100)}${opts.userContext.length > 100 ? '...' : ''}`,
83
+ );
84
+ }
85
+
86
+ const systemTimer = time('runner:composeSystemPrompt');
87
+ const { getAuth } = await import('@agi-cli/sdk');
88
+ const auth = await getAuth(opts.provider, cfg.projectRoot);
89
+ const needsSpoof = auth?.type === 'oauth';
90
+ const spoofPrompt = needsSpoof
91
+ ? getProviderSpoofPrompt(opts.provider)
92
+ : undefined;
93
+
94
+ debugLog(`[RUNNER] needsSpoof (OAuth): ${needsSpoof}`);
95
+ debugLog(
96
+ `[RUNNER] spoofPrompt: ${spoofPrompt ? `present (${opts.provider})` : 'none'}`,
97
+ );
98
+
99
+ let system: string;
100
+ let systemComponents: string[] = [];
101
+ let oauthFullPromptComponents: string[] | undefined;
102
+ let additionalSystemMessages: Array<{
103
+ role: 'system' | 'user';
104
+ content: string;
105
+ }> = [];
106
+
107
+ if (spoofPrompt) {
108
+ system = spoofPrompt;
109
+ systemComponents = [`spoof:${opts.provider || 'unknown'}`];
110
+ const fullPrompt = await composeSystemPrompt({
111
+ provider: opts.provider,
112
+ model: opts.model,
113
+ projectRoot: cfg.projectRoot,
114
+ agentPrompt,
115
+ oneShot: opts.oneShot,
116
+ spoofPrompt: undefined,
117
+ includeProjectTree: isFirstMessage,
118
+ userContext: opts.userContext,
119
+ contextSummary,
120
+ });
121
+ oauthFullPromptComponents = fullPrompt.components;
122
+
123
+ additionalSystemMessages = [{ role: 'system', content: fullPrompt.prompt }];
124
+
125
+ debugLog('[RUNNER] OAuth mode: additionalSystemMessages created');
126
+ const includesUserContext =
127
+ !!opts.userContext && fullPrompt.prompt.includes(opts.userContext);
128
+ debugLog(
129
+ `[system] oauth-full summary: ${JSON.stringify({
130
+ components: oauthFullPromptComponents ?? [],
131
+ length: fullPrompt.prompt.length,
132
+ includesUserContext,
133
+ })}`,
134
+ );
135
+ } else {
136
+ const composed = await composeSystemPrompt({
137
+ provider: opts.provider,
138
+ model: opts.model,
139
+ projectRoot: cfg.projectRoot,
140
+ agentPrompt,
141
+ oneShot: opts.oneShot,
142
+ spoofPrompt: undefined,
143
+ includeProjectTree: isFirstMessage,
144
+ userContext: opts.userContext,
145
+ contextSummary,
146
+ });
147
+ system = composed.prompt;
148
+ systemComponents = composed.components;
149
+ }
150
+ systemTimer.end();
151
+ debugLog(
152
+ `[system] summary: ${JSON.stringify({
153
+ components: systemComponents,
154
+ length: system.length,
155
+ })}`,
156
+ );
157
+
158
+ if (opts.isCompactCommand && opts.compactionContext) {
159
+ debugLog('[RUNNER] Injecting compaction context for /compact command');
160
+ const compactPrompt = getCompactionSystemPrompt();
161
+ additionalSystemMessages.push({
162
+ role: 'system',
163
+ content: compactPrompt,
164
+ });
165
+ additionalSystemMessages.push({
166
+ role: 'user',
167
+ content: `Please summarize this conversation:\n\n<conversation-to-summarize>\n${opts.compactionContext}\n</conversation-to-summarize>`,
168
+ });
169
+ }
170
+
171
+ const toolsTimer = time('runner:discoverTools');
172
+ const allTools = await discoverProjectTools(cfg.projectRoot);
173
+ toolsTimer.end({ count: allTools.length });
174
+ const allowedNames = new Set([...(agentCfg.tools || []), 'finish']);
175
+ const gated = allTools.filter((tool) => allowedNames.has(tool.name));
176
+ debugLog(`[tools] ${gated.length} allowed tools`);
177
+
178
+ debugLog(`[RUNNER] About to create model with provider: ${opts.provider}`);
179
+ debugLog(`[RUNNER] About to create model ID: ${opts.model}`);
180
+
181
+ const oauthSystemPrompt =
182
+ needsSpoof && opts.provider === 'openai' && additionalSystemMessages[0]
183
+ ? additionalSystemMessages[0].content
184
+ : undefined;
185
+ const model = await resolveModel(opts.provider, opts.model, cfg, {
186
+ systemPrompt: oauthSystemPrompt,
187
+ });
188
+ debugLog(
189
+ `[RUNNER] Model created: ${JSON.stringify({ id: model.modelId, provider: model.provider })}`,
190
+ );
191
+
192
+ const maxOutputTokens = getMaxOutputTokens(opts.provider, opts.model);
193
+ debugLog(`[RUNNER] maxOutputTokens for ${opts.model}: ${maxOutputTokens}`);
194
+
195
+ const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
196
+ opts,
197
+ db,
198
+ );
199
+
200
+ const providerAuth = await getAuth(opts.provider, opts.projectRoot);
201
+ const authType = providerAuth?.type;
202
+ const toolset = adaptTools(gated, sharedCtx, opts.provider, authType);
203
+
204
+ const providerOptions: Record<string, unknown> = {};
205
+ let effectiveMaxOutputTokens = maxOutputTokens;
206
+
207
+ if (opts.reasoning) {
208
+ if (opts.provider === 'anthropic') {
209
+ providerOptions.anthropic = {
210
+ thinking: { type: 'enabled', budgetTokens: THINKING_BUDGET },
211
+ };
212
+ if (maxOutputTokens && maxOutputTokens > THINKING_BUDGET) {
213
+ effectiveMaxOutputTokens = maxOutputTokens - THINKING_BUDGET;
214
+ }
215
+ } else if (opts.provider === 'openai') {
216
+ providerOptions.openai = {
217
+ reasoningSummary: 'auto',
218
+ };
219
+ } else if (opts.provider === 'google') {
220
+ providerOptions.google = {
221
+ thinkingConfig: { thinkingBudget: THINKING_BUDGET },
222
+ };
223
+ }
224
+ }
225
+
226
+ return {
227
+ cfg,
228
+ db,
229
+ agentCfg,
230
+ history,
231
+ system,
232
+ systemComponents,
233
+ additionalSystemMessages,
234
+ model,
235
+ maxOutputTokens,
236
+ effectiveMaxOutputTokens,
237
+ toolset,
238
+ sharedCtx,
239
+ firstToolTimer,
240
+ firstToolSeen,
241
+ providerOptions,
242
+ needsSpoof,
243
+ };
244
+ }
245
+
246
+ export function buildMessages(
247
+ additionalSystemMessages: Array<{ role: string; content: string }>,
248
+ history: Array<{ role: string; content: string | Array<unknown> }>,
249
+ isFirstMessage: boolean,
250
+ ): Array<{ role: string; content: string | Array<unknown> }> {
251
+ const messagesWithSystemInstructions: Array<{
252
+ role: string;
253
+ content: string | Array<unknown>;
254
+ }> = [...additionalSystemMessages, ...history];
255
+
256
+ if (!isFirstMessage) {
257
+ messagesWithSystemInstructions.push({
258
+ role: 'user',
259
+ content:
260
+ '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.',
261
+ });
262
+ }
263
+
264
+ return messagesWithSystemInstructions;
265
+ }