@ottocode/server 0.1.265 → 0.1.266
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/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +2 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +67 -264
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -3,26 +3,7 @@ import type { ProviderName } from '@ottocode/sdk';
|
|
|
3
3
|
import { catalog } from '@ottocode/sdk';
|
|
4
4
|
import { readdir } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
8
|
-
import AGENT_BUILD from '@ottocode/sdk/prompts/agents/build.txt' with {
|
|
9
|
-
type: 'text',
|
|
10
|
-
};
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
12
|
-
import AGENT_PLAN from '@ottocode/sdk/prompts/agents/plan.txt' with {
|
|
13
|
-
type: 'text',
|
|
14
|
-
};
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
16
|
-
import AGENT_GENERAL from '@ottocode/sdk/prompts/agents/general.txt' with {
|
|
17
|
-
type: 'text',
|
|
18
|
-
};
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
20
|
-
import AGENT_INIT from '@ottocode/sdk/prompts/agents/init.txt' with {
|
|
21
|
-
type: 'text',
|
|
22
|
-
};
|
|
23
|
-
import AGENT_RESEARCH from '@ottocode/sdk/prompts/agents/research.txt' with {
|
|
24
|
-
type: 'text',
|
|
25
|
-
};
|
|
6
|
+
import { resolveAgentPrompt } from './registry-prompts.ts';
|
|
26
7
|
|
|
27
8
|
export type AgentConfig = {
|
|
28
9
|
name: string;
|
|
@@ -291,109 +272,11 @@ export async function resolveAgentConfig(
|
|
|
291
272
|
}
|
|
292
273
|
const agents = await loadAgentsConfig(projectRoot);
|
|
293
274
|
const entry = agents[name];
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const localDirTxt = `${projectRoot}/.otto/agents/${name}/agent.txt`.replace(
|
|
300
|
-
/\\/g,
|
|
301
|
-
'/',
|
|
302
|
-
);
|
|
303
|
-
const localDirMd = `${projectRoot}/.otto/agents/${name}/agent.md`.replace(
|
|
304
|
-
/\\/g,
|
|
305
|
-
'/',
|
|
306
|
-
);
|
|
307
|
-
const localFlatTxt = `${projectRoot}/.otto/agents/${name}.txt`.replace(
|
|
308
|
-
/\\/g,
|
|
309
|
-
'/',
|
|
310
|
-
);
|
|
311
|
-
const localFlatMd = `${projectRoot}/.otto/agents/${name}.md`.replace(
|
|
312
|
-
/\\/g,
|
|
313
|
-
'/',
|
|
314
|
-
);
|
|
315
|
-
const globalDirTxt = `${globalAgentsDir}/${name}/agent.txt`.replace(
|
|
316
|
-
/\\/g,
|
|
317
|
-
'/',
|
|
318
|
-
);
|
|
319
|
-
const globalDirMd = `${globalAgentsDir}/${name}/agent.md`.replace(/\\/g, '/');
|
|
320
|
-
const globalFlatTxt = `${globalAgentsDir}/${name}.txt`.replace(/\\/g, '/');
|
|
321
|
-
const globalFlatMd = `${globalAgentsDir}/${name}.md`.replace(/\\/g, '/');
|
|
322
|
-
const files = [
|
|
323
|
-
localDirMd,
|
|
324
|
-
localFlatMd,
|
|
325
|
-
localDirTxt,
|
|
326
|
-
localFlatTxt,
|
|
327
|
-
globalDirMd,
|
|
328
|
-
globalFlatMd,
|
|
329
|
-
globalDirTxt,
|
|
330
|
-
globalFlatTxt,
|
|
331
|
-
];
|
|
332
|
-
for (const p of files) {
|
|
333
|
-
try {
|
|
334
|
-
const f = Bun.file(p);
|
|
335
|
-
if (await f.exists()) {
|
|
336
|
-
const text = await f.text();
|
|
337
|
-
if (text.trim()) {
|
|
338
|
-
prompt = text;
|
|
339
|
-
promptSource = `file:${p}`;
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} catch {}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// If agents.json provides a 'prompt' field, accept inline content or a relative/absolute path
|
|
347
|
-
if (entry?.prompt) {
|
|
348
|
-
const p = entry.prompt.trim();
|
|
349
|
-
if (
|
|
350
|
-
/[.](md|txt)$/i.test(p) ||
|
|
351
|
-
p.startsWith('.') ||
|
|
352
|
-
p.startsWith('/') ||
|
|
353
|
-
p.startsWith('~/')
|
|
354
|
-
) {
|
|
355
|
-
const candidates: string[] = [];
|
|
356
|
-
if (p.startsWith('~/')) {
|
|
357
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
358
|
-
candidates.push(`${home}/${p.slice(2)}`);
|
|
359
|
-
} else if (p.startsWith('/')) candidates.push(p);
|
|
360
|
-
else candidates.push(`${projectRoot}/${p}`.replace(/\\/g, '/'));
|
|
361
|
-
for (const candidate of candidates) {
|
|
362
|
-
const pf = Bun.file(candidate);
|
|
363
|
-
if (await pf.exists()) {
|
|
364
|
-
const t = await pf.text();
|
|
365
|
-
if (t.trim()) {
|
|
366
|
-
prompt = t;
|
|
367
|
-
promptSource = `agents.json:file:${candidate}`;
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
} else {
|
|
373
|
-
prompt = p;
|
|
374
|
-
promptSource = 'agents.json:inline';
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Fallback: use embedded defaults (plan/build/general); else default to build
|
|
379
|
-
if (!prompt) {
|
|
380
|
-
const byName = (n: string): string | undefined => {
|
|
381
|
-
if (n === 'build') return AGENT_BUILD;
|
|
382
|
-
if (n === 'plan') return AGENT_PLAN;
|
|
383
|
-
if (n === 'general') return AGENT_GENERAL;
|
|
384
|
-
if (n === 'init') return AGENT_INIT;
|
|
385
|
-
if (n === 'research') return AGENT_RESEARCH;
|
|
386
|
-
return undefined;
|
|
387
|
-
};
|
|
388
|
-
const candidate = byName(name)?.trim();
|
|
389
|
-
if (candidate?.length) {
|
|
390
|
-
prompt = candidate;
|
|
391
|
-
promptSource = `fallback:embedded:${name}.txt`;
|
|
392
|
-
} else {
|
|
393
|
-
prompt = (AGENT_BUILD || '').trim();
|
|
394
|
-
promptSource = 'fallback:embedded:build.txt';
|
|
395
|
-
}
|
|
396
|
-
}
|
|
275
|
+
const { prompt } = await resolveAgentPrompt({
|
|
276
|
+
projectRoot,
|
|
277
|
+
name,
|
|
278
|
+
entryPrompt: entry?.prompt,
|
|
279
|
+
});
|
|
397
280
|
|
|
398
281
|
// Default tool access per agent if not explicitly configured
|
|
399
282
|
let tools = Array.isArray(entry?.tools)
|
|
@@ -411,7 +294,6 @@ export async function resolveAgentConfig(
|
|
|
411
294
|
const deduped = Array.from(new Set([...tools, ...baseToolSet]));
|
|
412
295
|
const provider = normalizeProvider(entry?.provider);
|
|
413
296
|
const model = normalizeModel(entry?.model);
|
|
414
|
-
void promptSource;
|
|
415
297
|
return {
|
|
416
298
|
name,
|
|
417
299
|
prompt,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { getDb } from '@ottocode/database';
|
|
2
|
+
import { sessions } from '@ottocode/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import { toErrorPayload } from '../errors/handling.ts';
|
|
6
|
+
import {
|
|
7
|
+
pruneSession,
|
|
8
|
+
shouldAutoCompactBeforeOverflow,
|
|
9
|
+
} from '../message/compaction.ts';
|
|
10
|
+
import type {
|
|
11
|
+
completeAssistantMessage,
|
|
12
|
+
updateMessageTokensIncremental,
|
|
13
|
+
updateSessionTokensIncremental,
|
|
14
|
+
} from '../session/db-operations.ts';
|
|
15
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
16
|
+
|
|
17
|
+
type CompleteAssistantMessage = typeof completeAssistantMessage;
|
|
18
|
+
type UpdateSessionTokensIncremental = typeof updateSessionTokensIncremental;
|
|
19
|
+
type UpdateMessageTokensIncremental = typeof updateMessageTokensIncremental;
|
|
20
|
+
|
|
21
|
+
export async function shouldPreemptivelyAutoCompact(
|
|
22
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
23
|
+
opts: RunOpts,
|
|
24
|
+
threshold: number | null | undefined,
|
|
25
|
+
): Promise<boolean> {
|
|
26
|
+
const sessionRows = await db
|
|
27
|
+
.select({ currentContextTokens: sessions.currentContextTokens })
|
|
28
|
+
.from(sessions)
|
|
29
|
+
.where(eq(sessions.id, opts.sessionId))
|
|
30
|
+
.limit(1);
|
|
31
|
+
|
|
32
|
+
return shouldAutoCompactBeforeOverflow({
|
|
33
|
+
autoCompactThresholdTokens: threshold,
|
|
34
|
+
currentContextTokens: sessionRows[0]?.currentContextTokens ?? 0,
|
|
35
|
+
estimatedInputTokens: opts.estimatedInputTokens ?? 0,
|
|
36
|
+
isCompactCommand: opts.isCompactCommand,
|
|
37
|
+
compactionRetries: opts.compactionRetries,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isPromptTooLongError(err: unknown): boolean {
|
|
42
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
43
|
+
const errorCode = (err as { code?: string })?.code ?? '';
|
|
44
|
+
const responseBody = (err as { responseBody?: string })?.responseBody ?? '';
|
|
45
|
+
const apiErrorType = (err as { apiErrorType?: string })?.apiErrorType ?? '';
|
|
46
|
+
const combinedError = `${errorMessage} ${responseBody}`.toLowerCase();
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
combinedError.includes('prompt is too long') ||
|
|
50
|
+
combinedError.includes('maximum context length') ||
|
|
51
|
+
combinedError.includes('too many tokens') ||
|
|
52
|
+
combinedError.includes('context_length_exceeded') ||
|
|
53
|
+
combinedError.includes('request too large') ||
|
|
54
|
+
combinedError.includes('exceeds the model') ||
|
|
55
|
+
combinedError.includes('input is too long') ||
|
|
56
|
+
errorCode === 'context_length_exceeded' ||
|
|
57
|
+
apiErrorType === 'invalid_request_error'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function handleRunnerError(args: {
|
|
62
|
+
err: unknown;
|
|
63
|
+
opts: RunOpts;
|
|
64
|
+
db: Awaited<ReturnType<typeof getDb>>;
|
|
65
|
+
completeAssistantMessage: CompleteAssistantMessage;
|
|
66
|
+
updateSessionTokensIncremental: UpdateSessionTokensIncremental;
|
|
67
|
+
updateMessageTokensIncremental: UpdateMessageTokensIncremental;
|
|
68
|
+
}): Promise<'handled' | 'rethrow'> {
|
|
69
|
+
const { err, opts, db } = args;
|
|
70
|
+
const payload = toErrorPayload(err);
|
|
71
|
+
|
|
72
|
+
if (isPromptTooLongError(err) && !opts.isCompactCommand) {
|
|
73
|
+
try {
|
|
74
|
+
const pruneResult = await pruneSession(db, opts.sessionId);
|
|
75
|
+
void pruneResult;
|
|
76
|
+
|
|
77
|
+
publish({
|
|
78
|
+
type: 'error',
|
|
79
|
+
sessionId: opts.sessionId,
|
|
80
|
+
payload: {
|
|
81
|
+
...payload,
|
|
82
|
+
message: `Context too large. Auto-compacted old tool results. Please retry your message.`,
|
|
83
|
+
name: 'ContextOverflow',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await args.completeAssistantMessage({}, opts, db);
|
|
89
|
+
} catch {}
|
|
90
|
+
return 'handled';
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
publish({
|
|
95
|
+
type: 'error',
|
|
96
|
+
sessionId: opts.sessionId,
|
|
97
|
+
payload,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await args.updateSessionTokensIncremental(
|
|
102
|
+
{ inputTokens: 0, outputTokens: 0 },
|
|
103
|
+
undefined,
|
|
104
|
+
opts,
|
|
105
|
+
db,
|
|
106
|
+
);
|
|
107
|
+
await args.updateMessageTokensIncremental(
|
|
108
|
+
{ inputTokens: 0, outputTokens: 0 },
|
|
109
|
+
undefined,
|
|
110
|
+
opts,
|
|
111
|
+
db,
|
|
112
|
+
);
|
|
113
|
+
await args.completeAssistantMessage({}, opts, db);
|
|
114
|
+
} catch {}
|
|
115
|
+
return 'rethrow';
|
|
116
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type RunnerMessage = {
|
|
2
|
+
role: string;
|
|
3
|
+
content: string | Array<unknown>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function appendRunnerReminderMessages(args: {
|
|
7
|
+
messages: RunnerMessage[];
|
|
8
|
+
isFirstMessage: boolean;
|
|
9
|
+
isOpenAIOAuth: boolean;
|
|
10
|
+
continuationCount?: number;
|
|
11
|
+
}): void {
|
|
12
|
+
const { messages, isFirstMessage, isOpenAIOAuth, continuationCount } = args;
|
|
13
|
+
|
|
14
|
+
if (!isFirstMessage) {
|
|
15
|
+
messages.push(
|
|
16
|
+
isOpenAIOAuth
|
|
17
|
+
? {
|
|
18
|
+
role: 'system',
|
|
19
|
+
content:
|
|
20
|
+
'[system-reminder] Continuing an existing session. Execute directly, use tools as needed, and call `finish` at the end. For simple questions, your answer IS the response — do not add a "Summary:" recap.',
|
|
21
|
+
}
|
|
22
|
+
: {
|
|
23
|
+
role: 'user',
|
|
24
|
+
content:
|
|
25
|
+
'<system-reminder>Continuing an existing session. Answer or complete the work directly, then call `finish`. For simple questions, your answer IS the response — do NOT add a labeled "Summary:" line or recap trivial replies.</system-reminder>',
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if ((continuationCount ?? 0) <= 0) return;
|
|
31
|
+
|
|
32
|
+
messages.push(
|
|
33
|
+
isOpenAIOAuth
|
|
34
|
+
? {
|
|
35
|
+
role: 'system',
|
|
36
|
+
content:
|
|
37
|
+
'[system-reminder] Your previous response stopped mid-task. Resume from where you left off and complete the actual work — not a plan-only update.',
|
|
38
|
+
}
|
|
39
|
+
: {
|
|
40
|
+
role: 'user',
|
|
41
|
+
content:
|
|
42
|
+
'<system-reminder>Your previous response stopped before calling `finish`. Resume from where you left off, do the actual work (no plan-only updates), then stream a summary and call `finish`.</system-reminder>',
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { wrapLanguageModel } from 'ai';
|
|
2
|
+
import { devToolsMiddleware } from '@ai-sdk/devtools';
|
|
3
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
4
|
+
import { isDevtoolsEnabled } from '../debug/state.ts';
|
|
5
|
+
import { resolveModel } from '../provider/index.ts';
|
|
6
|
+
import { buildReasoningConfig } from '../provider/reasoning.ts';
|
|
7
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
8
|
+
import { mergeProviderOptions } from './runner-setup-tools.ts';
|
|
9
|
+
import { nowMs } from './runner-setup-utils.ts';
|
|
10
|
+
|
|
11
|
+
export async function resolveRunnerModel(args: {
|
|
12
|
+
opts: RunOpts;
|
|
13
|
+
cfg: OttoConfig;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
model:
|
|
16
|
+
| Awaited<ReturnType<typeof resolveModel>>
|
|
17
|
+
| ReturnType<typeof wrapLanguageModel>;
|
|
18
|
+
resolveModelMs: number;
|
|
19
|
+
}> {
|
|
20
|
+
const resolveModelStartedAt = nowMs();
|
|
21
|
+
const model = await resolveModel(
|
|
22
|
+
args.opts.provider,
|
|
23
|
+
args.opts.model,
|
|
24
|
+
args.cfg,
|
|
25
|
+
{
|
|
26
|
+
sessionId: args.opts.sessionId,
|
|
27
|
+
messageId: args.opts.assistantMessageId,
|
|
28
|
+
reasoningText: args.opts.reasoningText,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
const resolveModelMs = nowMs() - resolveModelStartedAt;
|
|
32
|
+
const wrappedModel = isDevtoolsEnabled()
|
|
33
|
+
? wrapLanguageModel({
|
|
34
|
+
// biome-ignore lint/suspicious/noExplicitAny: OpenRouter provider uses v2 spec
|
|
35
|
+
model: model as any,
|
|
36
|
+
middleware: devToolsMiddleware(),
|
|
37
|
+
})
|
|
38
|
+
: model;
|
|
39
|
+
|
|
40
|
+
return { model: wrappedModel, resolveModelMs };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildRunnerProviderOptions(args: {
|
|
44
|
+
cfg: OttoConfig;
|
|
45
|
+
opts: RunOpts;
|
|
46
|
+
adaptedProviderOptions: Record<string, unknown>;
|
|
47
|
+
maxOutputTokens: number | undefined;
|
|
48
|
+
}): {
|
|
49
|
+
providerOptions: Record<string, unknown>;
|
|
50
|
+
effectiveMaxOutputTokens: number | undefined;
|
|
51
|
+
} {
|
|
52
|
+
const providerOptions = { ...args.adaptedProviderOptions };
|
|
53
|
+
|
|
54
|
+
if (args.opts.provider === 'copilot') {
|
|
55
|
+
providerOptions.openai = {
|
|
56
|
+
...((providerOptions.openai as Record<string, unknown>) || {}),
|
|
57
|
+
store: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const reasoningConfig = buildReasoningConfig({
|
|
62
|
+
cfg: args.cfg,
|
|
63
|
+
provider: args.opts.provider,
|
|
64
|
+
model: args.opts.model,
|
|
65
|
+
reasoningText: args.opts.reasoningText,
|
|
66
|
+
reasoningLevel: args.opts.reasoningLevel,
|
|
67
|
+
maxOutputTokens: args.maxOutputTokens,
|
|
68
|
+
});
|
|
69
|
+
mergeProviderOptions(providerOptions, reasoningConfig.providerOptions);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
providerOptions,
|
|
73
|
+
effectiveMaxOutputTokens: reasoningConfig.effectiveMaxOutputTokens,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfiguredProviderFamily,
|
|
3
|
+
getSessionSystemPromptPath,
|
|
4
|
+
logger,
|
|
5
|
+
type OttoConfig,
|
|
6
|
+
} from '@ottocode/sdk';
|
|
7
|
+
import { mkdir } from 'node:fs/promises';
|
|
8
|
+
import { dirname } from 'node:path';
|
|
9
|
+
import { composeSystemPrompt } from '../prompt/builder.ts';
|
|
10
|
+
import { isDebugEnabled } from '../debug/state.ts';
|
|
11
|
+
import { getMaxOutputTokens } from '../utils/token.ts';
|
|
12
|
+
import { getCompactionSystemPrompt } from '../message/compaction.ts';
|
|
13
|
+
import { adaptRunnerCall, detectOAuth } from '../provider/oauth-adapter.ts';
|
|
14
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
15
|
+
import { nowMs } from './runner-setup-utils.ts';
|
|
16
|
+
|
|
17
|
+
export type RunnerPromptSetup = {
|
|
18
|
+
system: string;
|
|
19
|
+
systemComponents: string[];
|
|
20
|
+
additionalSystemMessages: Array<{ role: 'system' | 'user'; content: string }>;
|
|
21
|
+
maxOutputTokens: number | undefined;
|
|
22
|
+
providerOptions: Record<string, unknown>;
|
|
23
|
+
needsSpoof: boolean;
|
|
24
|
+
isOpenAIOAuth: boolean;
|
|
25
|
+
effectiveSystemPrompt: string;
|
|
26
|
+
composeSystemPromptMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function buildRunnerPrompt(args: {
|
|
30
|
+
opts: RunOpts;
|
|
31
|
+
cfg: OttoConfig;
|
|
32
|
+
agentPrompt: string;
|
|
33
|
+
contextSummary?: string;
|
|
34
|
+
historyLength: number;
|
|
35
|
+
isFirstMessage: boolean;
|
|
36
|
+
}): Promise<RunnerPromptSetup> {
|
|
37
|
+
const { opts, cfg, agentPrompt, contextSummary } = args;
|
|
38
|
+
const composeSystemPromptStartedAt = nowMs();
|
|
39
|
+
const { getAuth } = await import('@ottocode/sdk');
|
|
40
|
+
const auth = await getAuth(opts.provider, cfg.projectRoot);
|
|
41
|
+
const oauth = detectOAuth(opts.provider, auth);
|
|
42
|
+
|
|
43
|
+
const composed = await composeSystemPrompt({
|
|
44
|
+
provider: opts.provider,
|
|
45
|
+
model: opts.model,
|
|
46
|
+
promptFamily: getConfiguredProviderFamily(cfg, opts.provider, opts.model),
|
|
47
|
+
skillSettings: cfg.skills,
|
|
48
|
+
projectRoot: cfg.projectRoot,
|
|
49
|
+
agentPrompt,
|
|
50
|
+
oneShot: opts.oneShot,
|
|
51
|
+
guidedMode: cfg.defaults.guidedMode,
|
|
52
|
+
spoofPrompt: undefined,
|
|
53
|
+
includeProjectTree: false,
|
|
54
|
+
userContext: opts.userContext,
|
|
55
|
+
contextSummary,
|
|
56
|
+
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const rawMaxOutputTokens = getMaxOutputTokens(opts.provider, opts.model);
|
|
60
|
+
const adapted = adaptRunnerCall(oauth, composed, {
|
|
61
|
+
provider: opts.provider,
|
|
62
|
+
rawMaxOutputTokens,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const { system } = adapted;
|
|
66
|
+
const { systemComponents, additionalSystemMessages } = adapted;
|
|
67
|
+
const openAIProviderOptions = adapted.providerOptions.openai as
|
|
68
|
+
| Record<string, unknown>
|
|
69
|
+
| undefined;
|
|
70
|
+
const openAIInstructions =
|
|
71
|
+
typeof openAIProviderOptions?.instructions === 'string'
|
|
72
|
+
? openAIProviderOptions.instructions
|
|
73
|
+
: '';
|
|
74
|
+
const effectiveSystemPrompt = system || openAIInstructions || composed.prompt;
|
|
75
|
+
const promptMode = oauth.isOpenAIOAuth
|
|
76
|
+
? 'openai-oauth'
|
|
77
|
+
: oauth.needsSpoof
|
|
78
|
+
? 'spoof'
|
|
79
|
+
: 'standard';
|
|
80
|
+
const composeSystemPromptMs = nowMs() - composeSystemPromptStartedAt;
|
|
81
|
+
|
|
82
|
+
logger.debug('[prompt] system prompt assembled', {
|
|
83
|
+
sessionId: opts.sessionId,
|
|
84
|
+
messageId: opts.assistantMessageId,
|
|
85
|
+
agent: opts.agent,
|
|
86
|
+
provider: opts.provider,
|
|
87
|
+
model: opts.model,
|
|
88
|
+
promptMode,
|
|
89
|
+
components: systemComponents,
|
|
90
|
+
systemLength: effectiveSystemPrompt.length,
|
|
91
|
+
historyMessages: args.historyLength,
|
|
92
|
+
additionalSystemMessages: additionalSystemMessages.length,
|
|
93
|
+
isFirstMessage: args.isFirstMessage,
|
|
94
|
+
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
95
|
+
needsSpoof: oauth.needsSpoof,
|
|
96
|
+
});
|
|
97
|
+
logger.debug('[prompt] detailed prompt context', {
|
|
98
|
+
sessionId: opts.sessionId,
|
|
99
|
+
messageId: opts.assistantMessageId,
|
|
100
|
+
debugDetail: true,
|
|
101
|
+
agentPromptLength: agentPrompt.length,
|
|
102
|
+
contextSummaryLength: contextSummary?.length ?? 0,
|
|
103
|
+
userContextLength: opts.userContext?.length ?? 0,
|
|
104
|
+
oneShot: Boolean(opts.oneShot),
|
|
105
|
+
guidedMode: Boolean(cfg.defaults.guidedMode),
|
|
106
|
+
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
107
|
+
needsSpoof: oauth.needsSpoof,
|
|
108
|
+
promptMode,
|
|
109
|
+
rawSystemLength: system.length,
|
|
110
|
+
openAIInstructionsLength: openAIInstructions.length,
|
|
111
|
+
effectiveSystemPromptLength: effectiveSystemPrompt.length,
|
|
112
|
+
systemComponents,
|
|
113
|
+
additionalSystemMessageRoles: additionalSystemMessages.map(
|
|
114
|
+
(message) => message.role,
|
|
115
|
+
),
|
|
116
|
+
});
|
|
117
|
+
await writeDebugSystemPrompt({
|
|
118
|
+
opts,
|
|
119
|
+
effectiveSystemPrompt,
|
|
120
|
+
promptMode,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
system,
|
|
125
|
+
systemComponents,
|
|
126
|
+
additionalSystemMessages,
|
|
127
|
+
maxOutputTokens: adapted.maxOutputTokens,
|
|
128
|
+
providerOptions: adapted.providerOptions,
|
|
129
|
+
needsSpoof: oauth.needsSpoof,
|
|
130
|
+
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
131
|
+
effectiveSystemPrompt,
|
|
132
|
+
composeSystemPromptMs,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function writeDebugSystemPrompt(args: {
|
|
137
|
+
opts: RunOpts;
|
|
138
|
+
effectiveSystemPrompt: string;
|
|
139
|
+
promptMode: string;
|
|
140
|
+
}): Promise<void> {
|
|
141
|
+
const { opts, effectiveSystemPrompt, promptMode } = args;
|
|
142
|
+
if (!effectiveSystemPrompt || !isDebugEnabled()) return;
|
|
143
|
+
|
|
144
|
+
const systemPromptPath = getSessionSystemPromptPath(opts.sessionId);
|
|
145
|
+
try {
|
|
146
|
+
await mkdir(dirname(systemPromptPath), { recursive: true });
|
|
147
|
+
await Bun.write(systemPromptPath, effectiveSystemPrompt);
|
|
148
|
+
logger.debug('[prompt] wrote system prompt file', {
|
|
149
|
+
sessionId: opts.sessionId,
|
|
150
|
+
messageId: opts.assistantMessageId,
|
|
151
|
+
path: systemPromptPath,
|
|
152
|
+
debugDetail: true,
|
|
153
|
+
promptMode,
|
|
154
|
+
effectiveSystemPromptLength: effectiveSystemPrompt.length,
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.warn('[prompt] failed to write system prompt file', {
|
|
158
|
+
sessionId: opts.sessionId,
|
|
159
|
+
messageId: opts.assistantMessageId,
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function appendRunnerPromptMessages(args: {
|
|
166
|
+
opts: RunOpts;
|
|
167
|
+
additionalSystemMessages: Array<{ role: 'system' | 'user'; content: string }>;
|
|
168
|
+
}): void {
|
|
169
|
+
const { opts, additionalSystemMessages } = args;
|
|
170
|
+
if (opts.isCompactCommand && opts.compactionContext) {
|
|
171
|
+
const compactPrompt = getCompactionSystemPrompt();
|
|
172
|
+
additionalSystemMessages.push({
|
|
173
|
+
role: 'system',
|
|
174
|
+
content: compactPrompt,
|
|
175
|
+
});
|
|
176
|
+
additionalSystemMessages.push({
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: `Please summarize this conversation:\n\n<conversation-to-summarize>\n${opts.compactionContext}\n</conversation-to-summarize>`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts.additionalPromptMessages?.length) {
|
|
183
|
+
additionalSystemMessages.push(...opts.additionalPromptMessages);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfiguredProviderFamily,
|
|
3
|
+
getModelFamily,
|
|
4
|
+
type OttoConfig,
|
|
5
|
+
} from '@ottocode/sdk';
|
|
6
|
+
import type { DiscoveredTool } from '@ottocode/sdk';
|
|
7
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
8
|
+
|
|
9
|
+
const EDITING_TOOL_NAMES = [
|
|
10
|
+
'edit',
|
|
11
|
+
'multiedit',
|
|
12
|
+
'write',
|
|
13
|
+
'copy_into',
|
|
14
|
+
'apply_patch',
|
|
15
|
+
];
|
|
16
|
+
const MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS = new Set([
|
|
17
|
+
'build',
|
|
18
|
+
'general',
|
|
19
|
+
'init',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function normalizeToolName(toolName: string): string {
|
|
23
|
+
return toolName === 'bash' ? 'shell' : toolName;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeToolNames(toolNames: string[]): string[] {
|
|
27
|
+
return Array.from(new Set(toolNames.map(normalizeToolName)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mergeProviderOptions(
|
|
31
|
+
base: Record<string, unknown>,
|
|
32
|
+
incoming: Record<string, unknown>,
|
|
33
|
+
): Record<string, unknown> {
|
|
34
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
35
|
+
const existing = base[key];
|
|
36
|
+
if (
|
|
37
|
+
existing &&
|
|
38
|
+
typeof existing === 'object' &&
|
|
39
|
+
!Array.isArray(existing) &&
|
|
40
|
+
value &&
|
|
41
|
+
typeof value === 'object' &&
|
|
42
|
+
!Array.isArray(value)
|
|
43
|
+
) {
|
|
44
|
+
base[key] = {
|
|
45
|
+
...(existing as Record<string, unknown>),
|
|
46
|
+
...(value as Record<string, unknown>),
|
|
47
|
+
};
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
base[key] = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function applyModelFamilyEditToolPolicy(
|
|
58
|
+
agent: string,
|
|
59
|
+
tools: string[],
|
|
60
|
+
provider: RunOpts['provider'],
|
|
61
|
+
model: string,
|
|
62
|
+
cfg?: OttoConfig,
|
|
63
|
+
): string[] {
|
|
64
|
+
tools = normalizeToolNames(tools);
|
|
65
|
+
if (!MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS.has(agent)) return tools;
|
|
66
|
+
|
|
67
|
+
const family = cfg
|
|
68
|
+
? getConfiguredProviderFamily(cfg, provider, model)
|
|
69
|
+
: getModelFamily(provider, model);
|
|
70
|
+
const next = tools.filter(
|
|
71
|
+
(toolName) => !EDITING_TOOL_NAMES.includes(toolName),
|
|
72
|
+
);
|
|
73
|
+
const preferredEditingTools =
|
|
74
|
+
family === 'anthropic' || family === 'openai'
|
|
75
|
+
? ['write', 'copy_into', 'apply_patch']
|
|
76
|
+
: ['write', 'edit', 'multiedit', 'copy_into'];
|
|
77
|
+
|
|
78
|
+
return Array.from(new Set([...next, ...preferredEditingTools]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildAllowedTools(args: {
|
|
82
|
+
agentName: string;
|
|
83
|
+
agentTools: string[];
|
|
84
|
+
provider: RunOpts['provider'];
|
|
85
|
+
model: string;
|
|
86
|
+
cfg: OttoConfig;
|
|
87
|
+
allTools: DiscoveredTool[];
|
|
88
|
+
}): DiscoveredTool[] {
|
|
89
|
+
const allowedToolNames = applyModelFamilyEditToolPolicy(
|
|
90
|
+
args.agentName,
|
|
91
|
+
args.agentTools,
|
|
92
|
+
args.provider,
|
|
93
|
+
args.model,
|
|
94
|
+
args.cfg,
|
|
95
|
+
);
|
|
96
|
+
const allowedNames = new Set([
|
|
97
|
+
...normalizeToolNames(allowedToolNames),
|
|
98
|
+
'finish',
|
|
99
|
+
]);
|
|
100
|
+
return args.allTools.filter(
|
|
101
|
+
(tool) => allowedNames.has(tool.name) || tool.name === 'load_mcp_tools',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type TimedResult<T> = {
|
|
2
|
+
value: T;
|
|
3
|
+
durationMs: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function nowMs(): number {
|
|
7
|
+
const perf = globalThis.performance;
|
|
8
|
+
if (perf && typeof perf.now === 'function') return perf.now();
|
|
9
|
+
return Date.now();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function timePromise<T>(
|
|
13
|
+
promise: Promise<T>,
|
|
14
|
+
): Promise<TimedResult<T>> {
|
|
15
|
+
const startedAt = nowMs();
|
|
16
|
+
const value = await promise;
|
|
17
|
+
return {
|
|
18
|
+
value,
|
|
19
|
+
durationMs: nowMs() - startedAt,
|
|
20
|
+
};
|
|
21
|
+
}
|