@ottocode/server 0.1.237 → 0.1.242
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 -1
- package/src/openapi/paths/config.ts +8 -0
- package/src/openapi/schemas.ts +1 -0
- package/src/presets.ts +7 -0
- package/src/routes/config/agents.ts +1 -1
- package/src/routes/config/defaults.ts +12 -2
- package/src/routes/config/main.ts +7 -1
- package/src/routes/config/utils.ts +1 -1
- package/src/routes/terminals.ts +94 -0
- package/src/runtime/agent/registry.ts +21 -0
- package/src/runtime/agent/runner-reasoning.ts +37 -11
- package/src/runtime/agent/runner-setup.ts +45 -3
- package/src/runtime/agent/runner.ts +45 -2
- package/src/runtime/ai-sdk-warnings.ts +70 -0
- package/src/runtime/commands/builtins.ts +84 -0
- package/src/runtime/commands/init.ts +358 -0
- package/src/runtime/message/compaction-limits.ts +40 -0
- package/src/runtime/message/compaction.ts +1 -0
- package/src/runtime/message/service.ts +43 -31
- package/src/runtime/provider/reasoning.ts +2 -0
- package/src/runtime/session/queue.ts +10 -0
- package/src/runtime/tools/approval.ts +6 -2
- package/src/tools/adapter.ts +6 -1
- package/src/ws.ts +5 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { DB } from '@ottocode/database';
|
|
2
|
+
import type { OttoConfig, ProviderId } from '@ottocode/sdk';
|
|
3
|
+
import {
|
|
4
|
+
buildCompactionContext,
|
|
5
|
+
getModelLimits,
|
|
6
|
+
isCompactCommand,
|
|
7
|
+
} from '../message/compaction.ts';
|
|
8
|
+
import {
|
|
9
|
+
buildInitCommandUserPrompt,
|
|
10
|
+
buildInitProjectSnapshot,
|
|
11
|
+
getInitCommandSystemPrompt,
|
|
12
|
+
isInitCommand,
|
|
13
|
+
} from './init.ts';
|
|
14
|
+
|
|
15
|
+
export type BuiltinCommandPromptMessage = {
|
|
16
|
+
role: 'system' | 'user';
|
|
17
|
+
content: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BuiltinCommandSpec = {
|
|
21
|
+
id: 'compact' | 'init';
|
|
22
|
+
agent?: string;
|
|
23
|
+
oneShot?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Controls prompt construction only. The run still executes in the current
|
|
26
|
+
* session and persists messages/tool activity through the normal pipeline.
|
|
27
|
+
*/
|
|
28
|
+
omitHistory?: boolean;
|
|
29
|
+
isCompactCommand?: boolean;
|
|
30
|
+
compactionContext?: string;
|
|
31
|
+
additionalPromptMessages?: BuiltinCommandPromptMessage[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns a prepared built-in slash command spec when the message matches one.
|
|
36
|
+
*
|
|
37
|
+
* These commands still run through the normal current-session agent pipeline;
|
|
38
|
+
* this only customizes the prompt setup and agent selection.
|
|
39
|
+
*/
|
|
40
|
+
export async function prepareBuiltinCommand(args: {
|
|
41
|
+
cfg: OttoConfig;
|
|
42
|
+
db: DB;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
provider: ProviderId;
|
|
45
|
+
model: string;
|
|
46
|
+
content: string;
|
|
47
|
+
}): Promise<BuiltinCommandSpec | null> {
|
|
48
|
+
if (isCompactCommand(args.content)) {
|
|
49
|
+
const limits = getModelLimits(args.provider, args.model);
|
|
50
|
+
const contextTokenLimit = limits
|
|
51
|
+
? Math.max(Math.floor(limits.context * 0.5), 15000)
|
|
52
|
+
: 15000;
|
|
53
|
+
const compactionContext = await buildCompactionContext(
|
|
54
|
+
args.db,
|
|
55
|
+
args.sessionId,
|
|
56
|
+
contextTokenLimit,
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
id: 'compact',
|
|
60
|
+
isCompactCommand: true,
|
|
61
|
+
omitHistory: true,
|
|
62
|
+
compactionContext,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isInitCommand(args.content)) {
|
|
67
|
+
const snapshot = await buildInitProjectSnapshot(args.cfg.projectRoot);
|
|
68
|
+
return {
|
|
69
|
+
id: 'init',
|
|
70
|
+
agent: 'init',
|
|
71
|
+
oneShot: true,
|
|
72
|
+
omitHistory: true,
|
|
73
|
+
additionalPromptMessages: [
|
|
74
|
+
{ role: 'system', content: getInitCommandSystemPrompt() },
|
|
75
|
+
{
|
|
76
|
+
role: 'user',
|
|
77
|
+
content: buildInitCommandUserPrompt(args.cfg.projectRoot, snapshot),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const IGNORED_NAMES = new Set([
|
|
5
|
+
'.git',
|
|
6
|
+
'.idea',
|
|
7
|
+
'.next',
|
|
8
|
+
'.turbo',
|
|
9
|
+
'.venv',
|
|
10
|
+
'.vscode',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'coverage',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'target',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const KEY_DIRECTORY_LIMIT = 24;
|
|
19
|
+
const WORKSPACE_LIMIT = 24;
|
|
20
|
+
|
|
21
|
+
/** Returns true when the message is the built-in /init command. */
|
|
22
|
+
export function isInitCommand(content: string): boolean {
|
|
23
|
+
return content.trim().toLowerCase() === '/init';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** System instructions for the focused /init repo-analysis run. */
|
|
27
|
+
export function getInitCommandSystemPrompt(): string {
|
|
28
|
+
return [
|
|
29
|
+
'You are generating durable agent instructions for this repository.',
|
|
30
|
+
'',
|
|
31
|
+
'Your job is to inspect the real project structure and source code, then create or refresh AGENTS.md documentation for future coding agents.',
|
|
32
|
+
'',
|
|
33
|
+
'Hard rules:',
|
|
34
|
+
'- Do not trust existing markdown as the source of truth; code, config, and filesystem structure win when there is a conflict.',
|
|
35
|
+
'- Inspect actual package manifests, source directories, route wiring, schemas, app entrypoints, and build configuration before writing.',
|
|
36
|
+
'- Prefer a small number of strong docs over many tiny docs.',
|
|
37
|
+
'- If this is a monorepo, create a root AGENTS.md that acts as the routing/index doc and points to a small set of focused docs under .agents/.',
|
|
38
|
+
'- If this is not a monorepo, root AGENTS.md may stay mostly self-contained with only minimal supporting docs.',
|
|
39
|
+
'- Root AGENTS.md must tell future agents which .agents doc to read for mobile, server/api, web/tui clients, database, and cross-cutting changes when applicable.',
|
|
40
|
+
'- Avoid oversplitting. Only create supporting docs when a domain is meaningfully distinct. Aim for roughly 3-6 supporting docs max when splitting is needed.',
|
|
41
|
+
'- Keep docs actionable for coding agents: architecture, key file paths, workflow rules, and when to consult related docs.',
|
|
42
|
+
'- Reuse and update existing AGENTS.md or .agents docs when appropriate instead of duplicating content.',
|
|
43
|
+
'',
|
|
44
|
+
'Expected process:',
|
|
45
|
+
'1. Scan the repository structure and key code/config files with tools.',
|
|
46
|
+
'2. Decide the minimum useful documentation split.',
|
|
47
|
+
'3. Write or update AGENTS.md and any needed .agents/*.md files.',
|
|
48
|
+
'4. Finish with a concise summary of what you generated and why.',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Builds a concise filesystem-grounded snapshot to seed the /init run. */
|
|
53
|
+
export async function buildInitProjectSnapshot(
|
|
54
|
+
projectRoot: string,
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const rootManifest = await readJson<Record<string, unknown>>(
|
|
57
|
+
join(projectRoot, 'package.json'),
|
|
58
|
+
);
|
|
59
|
+
const workspacePatterns = extractWorkspacePatterns(rootManifest?.workspaces);
|
|
60
|
+
const workspaceDirs = await collectWorkspaceDirs(
|
|
61
|
+
projectRoot,
|
|
62
|
+
workspacePatterns,
|
|
63
|
+
);
|
|
64
|
+
const topLevelDirs = await listDirectoryNames(
|
|
65
|
+
projectRoot,
|
|
66
|
+
true,
|
|
67
|
+
KEY_DIRECTORY_LIMIT,
|
|
68
|
+
);
|
|
69
|
+
const topLevelFiles = await listDirectoryNames(
|
|
70
|
+
projectRoot,
|
|
71
|
+
false,
|
|
72
|
+
KEY_DIRECTORY_LIMIT,
|
|
73
|
+
);
|
|
74
|
+
const appDirs = await listDirectoryNames(
|
|
75
|
+
join(projectRoot, 'apps'),
|
|
76
|
+
true,
|
|
77
|
+
KEY_DIRECTORY_LIMIT,
|
|
78
|
+
);
|
|
79
|
+
const packageDirs = await listDirectoryNames(
|
|
80
|
+
join(projectRoot, 'packages'),
|
|
81
|
+
true,
|
|
82
|
+
KEY_DIRECTORY_LIMIT,
|
|
83
|
+
);
|
|
84
|
+
const docsDirs = await listDirectoryNames(
|
|
85
|
+
join(projectRoot, 'docs'),
|
|
86
|
+
false,
|
|
87
|
+
KEY_DIRECTORY_LIMIT,
|
|
88
|
+
);
|
|
89
|
+
const routeFiles = await listDirectoryNames(
|
|
90
|
+
join(projectRoot, 'packages/server/src/routes'),
|
|
91
|
+
false,
|
|
92
|
+
KEY_DIRECTORY_LIMIT,
|
|
93
|
+
);
|
|
94
|
+
const schemaFiles = await listDirectoryNames(
|
|
95
|
+
join(projectRoot, 'packages/database/src/schema'),
|
|
96
|
+
false,
|
|
97
|
+
KEY_DIRECTORY_LIMIT,
|
|
98
|
+
);
|
|
99
|
+
const sdkDirs = await listDirectoryNames(
|
|
100
|
+
join(projectRoot, 'packages/sdk/src'),
|
|
101
|
+
true,
|
|
102
|
+
KEY_DIRECTORY_LIMIT,
|
|
103
|
+
);
|
|
104
|
+
const webSdkDirs = await listDirectoryNames(
|
|
105
|
+
join(projectRoot, 'packages/web-sdk/src'),
|
|
106
|
+
true,
|
|
107
|
+
KEY_DIRECTORY_LIMIT,
|
|
108
|
+
);
|
|
109
|
+
const existingAgentDocs = await summarizeExistingAgentDocs(projectRoot);
|
|
110
|
+
const workspaceSummaries = await Promise.all(
|
|
111
|
+
workspaceDirs.slice(0, WORKSPACE_LIMIT).map(async (workspaceDir) => {
|
|
112
|
+
const rel = toProjectRelative(projectRoot, workspaceDir);
|
|
113
|
+
const manifest = await readJson<Record<string, unknown>>(
|
|
114
|
+
join(workspaceDir, 'package.json'),
|
|
115
|
+
);
|
|
116
|
+
return summarizeWorkspace(rel, manifest);
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const runtimeSignals = [
|
|
121
|
+
(await exists(join(projectRoot, 'bun.lock'))) ? 'bun.lock' : null,
|
|
122
|
+
(await exists(join(projectRoot, 'bunfig.toml'))) ? 'bunfig.toml' : null,
|
|
123
|
+
(await exists(join(projectRoot, 'biome.json'))) ? 'biome.json' : null,
|
|
124
|
+
(await exists(join(projectRoot, 'tsconfig.base.json')))
|
|
125
|
+
? 'tsconfig.base.json'
|
|
126
|
+
: null,
|
|
127
|
+
(await exists(join(projectRoot, 'docker'))) ? 'docker/' : null,
|
|
128
|
+
].filter((value): value is string => Boolean(value));
|
|
129
|
+
|
|
130
|
+
const lines = [
|
|
131
|
+
`Project root: ${projectRoot}`,
|
|
132
|
+
`Repo shape: ${workspacePatterns.length > 0 || workspaceDirs.length > 1 ? 'monorepo/workspace' : 'single-project or minimal workspace'}`,
|
|
133
|
+
rootManifest?.name ? `Root package: ${String(rootManifest.name)}` : null,
|
|
134
|
+
workspacePatterns.length
|
|
135
|
+
? `Workspace globs: ${workspacePatterns.join(', ')}`
|
|
136
|
+
: 'Workspace globs: none declared',
|
|
137
|
+
runtimeSignals.length
|
|
138
|
+
? `Tooling signals: ${runtimeSignals.join(', ')}`
|
|
139
|
+
: null,
|
|
140
|
+
topLevelDirs.length
|
|
141
|
+
? `Top-level directories: ${topLevelDirs.join(', ')}`
|
|
142
|
+
: null,
|
|
143
|
+
topLevelFiles.length
|
|
144
|
+
? `Top-level files: ${topLevelFiles.join(', ')}`
|
|
145
|
+
: null,
|
|
146
|
+
appDirs.length ? `apps/: ${appDirs.join(', ')}` : null,
|
|
147
|
+
packageDirs.length ? `packages/: ${packageDirs.join(', ')}` : null,
|
|
148
|
+
docsDirs.length ? `docs/: ${docsDirs.join(', ')}` : null,
|
|
149
|
+
routeFiles.length
|
|
150
|
+
? `packages/server/src/routes: ${routeFiles.join(', ')}`
|
|
151
|
+
: null,
|
|
152
|
+
schemaFiles.length
|
|
153
|
+
? `packages/database/src/schema: ${schemaFiles.join(', ')}`
|
|
154
|
+
: null,
|
|
155
|
+
sdkDirs.length ? `packages/sdk/src dirs: ${sdkDirs.join(', ')}` : null,
|
|
156
|
+
webSdkDirs.length
|
|
157
|
+
? `packages/web-sdk/src dirs: ${webSdkDirs.join(', ')}`
|
|
158
|
+
: null,
|
|
159
|
+
existingAgentDocs,
|
|
160
|
+
workspaceSummaries.length ? 'Workspace packages:' : null,
|
|
161
|
+
...workspaceSummaries.map((summary) => `- ${summary}`),
|
|
162
|
+
]
|
|
163
|
+
.filter((line): line is string => Boolean(line))
|
|
164
|
+
.join('\n');
|
|
165
|
+
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Builds the primary user prompt for /init after the repo snapshot is collected. */
|
|
170
|
+
export function buildInitCommandUserPrompt(
|
|
171
|
+
projectRoot: string,
|
|
172
|
+
snapshot: string,
|
|
173
|
+
): string {
|
|
174
|
+
return [
|
|
175
|
+
'Generate or refresh the repository agent documentation for this project.',
|
|
176
|
+
'',
|
|
177
|
+
'Deliverables:',
|
|
178
|
+
'- Root AGENTS.md at the repository root.',
|
|
179
|
+
'- Supporting docs under .agents/ only when needed.',
|
|
180
|
+
'',
|
|
181
|
+
'What to analyze before writing:',
|
|
182
|
+
'- monorepo boundaries and workspace responsibilities',
|
|
183
|
+
'- how server/API routes are wired',
|
|
184
|
+
'- how web and TUI clients are wired',
|
|
185
|
+
'- whether mobile deserves its own doc',
|
|
186
|
+
'- database/schema/migration locations',
|
|
187
|
+
'- SDK/shared packages and build/test tooling',
|
|
188
|
+
'',
|
|
189
|
+
'Document design rules:',
|
|
190
|
+
'- Root AGENTS.md should be the primary entrypoint and routing guide for future LLM agents.',
|
|
191
|
+
'- Root AGENTS.md should point to the exact .agents docs to read for different task types.',
|
|
192
|
+
'- If a task touches multiple layers, root AGENTS.md should say to read multiple docs.',
|
|
193
|
+
'- Prefer fewer, stronger docs. Do not scatter tiny markdown files.',
|
|
194
|
+
'- Keep instructions concrete and file-path-aware.',
|
|
195
|
+
'- Mention repository-specific norms that matter for safe changes.',
|
|
196
|
+
'',
|
|
197
|
+
'Repository snapshot (filesystem-grounded, still verify with tools):',
|
|
198
|
+
'<project-snapshot>',
|
|
199
|
+
snapshot,
|
|
200
|
+
'</project-snapshot>',
|
|
201
|
+
'',
|
|
202
|
+
`Project root for file writes: ${projectRoot}`,
|
|
203
|
+
'',
|
|
204
|
+
'Now inspect the real code/config with tools, decide the right doc split, and write the docs.',
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function summarizeExistingAgentDocs(
|
|
209
|
+
projectRoot: string,
|
|
210
|
+
): Promise<string | null> {
|
|
211
|
+
const rootAgents = (await exists(join(projectRoot, 'AGENTS.md')))
|
|
212
|
+
? 'AGENTS.md'
|
|
213
|
+
: null;
|
|
214
|
+
const subdocs = await listDirectoryNames(
|
|
215
|
+
join(projectRoot, '.agents'),
|
|
216
|
+
false,
|
|
217
|
+
KEY_DIRECTORY_LIMIT,
|
|
218
|
+
);
|
|
219
|
+
if (!rootAgents && subdocs.length === 0) return null;
|
|
220
|
+
return `Existing agent docs: ${[rootAgents, ...subdocs].filter(Boolean).join(', ')}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function collectWorkspaceDirs(
|
|
224
|
+
projectRoot: string,
|
|
225
|
+
patterns: string[],
|
|
226
|
+
): Promise<string[]> {
|
|
227
|
+
const found = new Set<string>();
|
|
228
|
+
for (const pattern of patterns) {
|
|
229
|
+
for (const match of await expandWorkspacePattern(projectRoot, pattern)) {
|
|
230
|
+
found.add(match);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (found.size === 0) {
|
|
235
|
+
for (const fallbackRoot of ['apps', 'packages', 'examples']) {
|
|
236
|
+
for (const dir of await listSubdirectories(
|
|
237
|
+
join(projectRoot, fallbackRoot),
|
|
238
|
+
)) {
|
|
239
|
+
found.add(dir);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return Array.from(found).sort((a, b) => a.localeCompare(b));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function expandWorkspacePattern(
|
|
248
|
+
projectRoot: string,
|
|
249
|
+
pattern: string,
|
|
250
|
+
): Promise<string[]> {
|
|
251
|
+
const normalized = pattern.replace(/\\/g, '/').replace(/\/$/, '');
|
|
252
|
+
if (!normalized.endsWith('/*')) return [];
|
|
253
|
+
const base = normalized.slice(0, -2);
|
|
254
|
+
if (!base || base.includes('*')) return [];
|
|
255
|
+
return listSubdirectories(join(projectRoot, base));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function extractWorkspacePatterns(workspaces: unknown): string[] {
|
|
259
|
+
if (Array.isArray(workspaces)) {
|
|
260
|
+
return workspaces.filter(
|
|
261
|
+
(value): value is string => typeof value === 'string',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
workspaces &&
|
|
266
|
+
typeof workspaces === 'object' &&
|
|
267
|
+
Array.isArray((workspaces as { packages?: unknown }).packages)
|
|
268
|
+
) {
|
|
269
|
+
return (workspaces as { packages: unknown[] }).packages.filter(
|
|
270
|
+
(value): value is string => typeof value === 'string',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function summarizeWorkspace(
|
|
277
|
+
relPath: string,
|
|
278
|
+
manifest?: Record<string, unknown>,
|
|
279
|
+
): string {
|
|
280
|
+
const name =
|
|
281
|
+
typeof manifest?.name === 'string'
|
|
282
|
+
? manifest.name
|
|
283
|
+
: `(${basename(relPath)})`;
|
|
284
|
+
const scripts =
|
|
285
|
+
manifest?.scripts && typeof manifest.scripts === 'object'
|
|
286
|
+
? Object.keys(manifest.scripts as Record<string, unknown>).slice(0, 6)
|
|
287
|
+
: [];
|
|
288
|
+
const flags = [
|
|
289
|
+
typeof manifest?.private === 'boolean'
|
|
290
|
+
? manifest.private
|
|
291
|
+
? 'private'
|
|
292
|
+
: 'public'
|
|
293
|
+
: null,
|
|
294
|
+
scripts.length ? `scripts: ${scripts.join(', ')}` : null,
|
|
295
|
+
]
|
|
296
|
+
.filter((value): value is string => Boolean(value))
|
|
297
|
+
.join(' | ');
|
|
298
|
+
return flags ? `${relPath} → ${name} (${flags})` : `${relPath} → ${name}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function listSubdirectories(path: string): Promise<string[]> {
|
|
302
|
+
try {
|
|
303
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
304
|
+
return entries
|
|
305
|
+
.filter((entry) => entry.isDirectory() && !shouldIgnore(entry.name))
|
|
306
|
+
.map((entry) => join(path, entry.name))
|
|
307
|
+
.sort((a, b) => a.localeCompare(b));
|
|
308
|
+
} catch {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function listDirectoryNames(
|
|
314
|
+
path: string,
|
|
315
|
+
directoriesOnly: boolean,
|
|
316
|
+
limit: number,
|
|
317
|
+
): Promise<string[]> {
|
|
318
|
+
try {
|
|
319
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
320
|
+
return entries
|
|
321
|
+
.filter((entry) => !shouldIgnore(entry.name))
|
|
322
|
+
.filter((entry) =>
|
|
323
|
+
directoriesOnly ? entry.isDirectory() : !entry.isDirectory(),
|
|
324
|
+
)
|
|
325
|
+
.map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
|
|
326
|
+
.sort((a, b) => a.localeCompare(b))
|
|
327
|
+
.slice(0, limit);
|
|
328
|
+
} catch {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function shouldIgnore(name: string): boolean {
|
|
334
|
+
return IGNORED_NAMES.has(name) || name.startsWith('.DS_Store');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function readJson<T>(path: string): Promise<T | undefined> {
|
|
338
|
+
try {
|
|
339
|
+
const file = Bun.file(path);
|
|
340
|
+
if (!(await file.exists())) return undefined;
|
|
341
|
+
return (await file.json()) as T;
|
|
342
|
+
} catch {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function exists(path: string): Promise<boolean> {
|
|
348
|
+
try {
|
|
349
|
+
return await Bun.file(path).exists();
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function toProjectRelative(projectRoot: string, absPath: string): string {
|
|
356
|
+
const rel = relative(projectRoot, absPath).replace(/\\/g, '/');
|
|
357
|
+
return rel.length > 0 ? rel : '.';
|
|
358
|
+
}
|
|
@@ -12,6 +12,46 @@ export interface ModelLimits {
|
|
|
12
12
|
output: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function shouldAutoCompactBeforeOverflow(args: {
|
|
16
|
+
autoCompactThresholdTokens?: number | null;
|
|
17
|
+
modelContextWindow?: number | null;
|
|
18
|
+
currentContextTokens?: number | null;
|
|
19
|
+
estimatedInputTokens?: number | null;
|
|
20
|
+
isCompactCommand?: boolean;
|
|
21
|
+
compactionRetries?: number;
|
|
22
|
+
}): boolean {
|
|
23
|
+
const threshold = Number(args.autoCompactThresholdTokens ?? 0);
|
|
24
|
+
if (!Number.isFinite(threshold) || threshold <= 0) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (args.isCompactCommand) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if ((args.compactionRetries ?? 0) > 0) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const modelContextWindow = Number(args.modelContextWindow ?? 0);
|
|
35
|
+
if (!Number.isFinite(modelContextWindow) || modelContextWindow <= threshold) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const currentContextTokens = Math.max(
|
|
40
|
+
0,
|
|
41
|
+
Math.floor(Number(args.currentContextTokens ?? 0)),
|
|
42
|
+
);
|
|
43
|
+
if (currentContextTokens <= 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const estimatedInputTokens = Math.max(
|
|
48
|
+
0,
|
|
49
|
+
Math.floor(Number(args.estimatedInputTokens ?? 0)),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return currentContextTokens + estimatedInputTokens >= threshold;
|
|
53
|
+
}
|
|
54
|
+
|
|
15
55
|
export function getModelLimits(
|
|
16
56
|
provider: string,
|
|
17
57
|
model: string,
|
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
type ProviderId,
|
|
14
14
|
type ReasoningLevel,
|
|
15
15
|
} from '@ottocode/sdk';
|
|
16
|
-
import {
|
|
16
|
+
import { estimateTokens } from './compaction.ts';
|
|
17
17
|
import { detectOAuth, adaptSimpleCall } from '../provider/oauth-adapter.ts';
|
|
18
|
+
import { prepareBuiltinCommand } from '../commands/builtins.ts';
|
|
18
19
|
|
|
19
20
|
type SessionRow = typeof sessions.$inferSelect;
|
|
20
21
|
|
|
@@ -61,14 +62,25 @@ export async function dispatchAssistantMessage(
|
|
|
61
62
|
|
|
62
63
|
const sessionId = session.id;
|
|
63
64
|
const now = Date.now();
|
|
65
|
+
const builtinCommand = await prepareBuiltinCommand({
|
|
66
|
+
cfg,
|
|
67
|
+
db,
|
|
68
|
+
sessionId,
|
|
69
|
+
provider,
|
|
70
|
+
model,
|
|
71
|
+
content,
|
|
72
|
+
});
|
|
73
|
+
const effectiveAgent = builtinCommand?.agent ?? agent;
|
|
74
|
+
const effectiveOneShot = builtinCommand?.oneShot ?? oneShot;
|
|
64
75
|
const userMessageId = crypto.randomUUID();
|
|
65
76
|
logger.debug('[agent] dispatching assistant message', {
|
|
66
77
|
sessionId,
|
|
67
|
-
agent,
|
|
78
|
+
agent: effectiveAgent,
|
|
68
79
|
provider,
|
|
69
80
|
model,
|
|
70
|
-
oneShot: Boolean(
|
|
81
|
+
oneShot: Boolean(effectiveOneShot),
|
|
71
82
|
hasUserContext: Boolean(userContext),
|
|
83
|
+
builtinCommand: builtinCommand?.id,
|
|
72
84
|
});
|
|
73
85
|
|
|
74
86
|
await db.insert(messages).values({
|
|
@@ -76,7 +88,7 @@ export async function dispatchAssistantMessage(
|
|
|
76
88
|
sessionId,
|
|
77
89
|
role: 'user',
|
|
78
90
|
status: 'complete',
|
|
79
|
-
agent,
|
|
91
|
+
agent: effectiveAgent,
|
|
80
92
|
provider,
|
|
81
93
|
model,
|
|
82
94
|
createdAt: now,
|
|
@@ -87,7 +99,7 @@ export async function dispatchAssistantMessage(
|
|
|
87
99
|
index: 0,
|
|
88
100
|
type: 'text',
|
|
89
101
|
content: JSON.stringify({ text: String(content) }),
|
|
90
|
-
agent,
|
|
102
|
+
agent: effectiveAgent,
|
|
91
103
|
provider,
|
|
92
104
|
model,
|
|
93
105
|
});
|
|
@@ -101,7 +113,7 @@ export async function dispatchAssistantMessage(
|
|
|
101
113
|
index: i + 1,
|
|
102
114
|
type: 'image',
|
|
103
115
|
content: JSON.stringify({ data: img.data, mediaType: img.mediaType }),
|
|
104
|
-
agent,
|
|
116
|
+
agent: effectiveAgent,
|
|
105
117
|
provider,
|
|
106
118
|
model,
|
|
107
119
|
});
|
|
@@ -124,7 +136,7 @@ export async function dispatchAssistantMessage(
|
|
|
124
136
|
mediaType: file.mediaType,
|
|
125
137
|
textContent: file.textContent,
|
|
126
138
|
}),
|
|
127
|
-
agent,
|
|
139
|
+
agent: effectiveAgent,
|
|
128
140
|
provider,
|
|
129
141
|
model,
|
|
130
142
|
});
|
|
@@ -137,7 +149,7 @@ export async function dispatchAssistantMessage(
|
|
|
137
149
|
payload: {
|
|
138
150
|
id: userMessageId,
|
|
139
151
|
role: 'user',
|
|
140
|
-
agent,
|
|
152
|
+
agent: effectiveAgent,
|
|
141
153
|
provider,
|
|
142
154
|
model,
|
|
143
155
|
content: String(content),
|
|
@@ -150,7 +162,7 @@ export async function dispatchAssistantMessage(
|
|
|
150
162
|
sessionId,
|
|
151
163
|
role: 'assistant',
|
|
152
164
|
status: 'pending',
|
|
153
|
-
agent,
|
|
165
|
+
agent: effectiveAgent,
|
|
154
166
|
provider,
|
|
155
167
|
model,
|
|
156
168
|
createdAt: Date.now(),
|
|
@@ -161,27 +173,23 @@ export async function dispatchAssistantMessage(
|
|
|
161
173
|
payload: {
|
|
162
174
|
id: assistantMessageId,
|
|
163
175
|
role: 'assistant',
|
|
164
|
-
agent,
|
|
176
|
+
agent: effectiveAgent,
|
|
165
177
|
provider,
|
|
166
178
|
model,
|
|
167
179
|
},
|
|
168
180
|
});
|
|
169
181
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
sessionId,
|
|
182
|
-
contextTokenLimit,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
182
|
+
const commandPromptText =
|
|
183
|
+
builtinCommand?.additionalPromptMessages
|
|
184
|
+
?.map((message) => message.content)
|
|
185
|
+
.join('\n\n') ?? content;
|
|
186
|
+
const estimatedInputTokens =
|
|
187
|
+
estimateTokens(commandPromptText) +
|
|
188
|
+
estimateTokens(userContext ?? '') +
|
|
189
|
+
(files?.reduce(
|
|
190
|
+
(total, file) => total + estimateTokens(file.textContent ?? ''),
|
|
191
|
+
0,
|
|
192
|
+
) ?? 0);
|
|
185
193
|
|
|
186
194
|
const toolApprovalMode = cfg.defaults.toolApproval ?? 'dangerous';
|
|
187
195
|
|
|
@@ -189,16 +197,19 @@ export async function dispatchAssistantMessage(
|
|
|
189
197
|
{
|
|
190
198
|
sessionId,
|
|
191
199
|
assistantMessageId,
|
|
192
|
-
agent,
|
|
200
|
+
agent: effectiveAgent,
|
|
193
201
|
provider,
|
|
194
202
|
model,
|
|
195
203
|
projectRoot: cfg.projectRoot,
|
|
196
|
-
oneShot: Boolean(
|
|
204
|
+
oneShot: Boolean(effectiveOneShot),
|
|
197
205
|
userContext,
|
|
206
|
+
estimatedInputTokens,
|
|
198
207
|
reasoningText,
|
|
199
208
|
reasoningLevel,
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
omitHistory: builtinCommand?.omitHistory,
|
|
210
|
+
isCompactCommand: builtinCommand?.isCompactCommand,
|
|
211
|
+
compactionContext: builtinCommand?.compactionContext,
|
|
212
|
+
additionalPromptMessages: builtinCommand?.additionalPromptMessages,
|
|
202
213
|
toolApprovalMode,
|
|
203
214
|
},
|
|
204
215
|
runSessionLoop,
|
|
@@ -206,10 +217,11 @@ export async function dispatchAssistantMessage(
|
|
|
206
217
|
logger.debug('[agent] assistant run enqueued', {
|
|
207
218
|
sessionId,
|
|
208
219
|
assistantMessageId,
|
|
209
|
-
agent,
|
|
220
|
+
agent: effectiveAgent,
|
|
210
221
|
provider,
|
|
211
222
|
model,
|
|
212
|
-
|
|
223
|
+
builtinCommand: builtinCommand?.id,
|
|
224
|
+
isCompactCommand: builtinCommand?.isCompactCommand,
|
|
213
225
|
});
|
|
214
226
|
|
|
215
227
|
void touchSessionLastActive({ db, sessionId });
|
|
@@ -134,6 +134,8 @@ function buildSharedProviderOptions(
|
|
|
134
134
|
function usesAdaptiveAnthropicThinking(model: string): boolean {
|
|
135
135
|
const lower = model.toLowerCase();
|
|
136
136
|
return (
|
|
137
|
+
lower.includes('claude-opus-4-7') ||
|
|
138
|
+
lower.includes('claude-opus-4.7') ||
|
|
137
139
|
lower.includes('claude-opus-4-6') ||
|
|
138
140
|
lower.includes('claude-opus-4.6') ||
|
|
139
141
|
lower.includes('claude-sonnet-4-6') ||
|
|
@@ -12,11 +12,21 @@ export type RunOpts = {
|
|
|
12
12
|
projectRoot: string;
|
|
13
13
|
oneShot?: boolean;
|
|
14
14
|
userContext?: string;
|
|
15
|
+
estimatedInputTokens?: number;
|
|
15
16
|
reasoningText?: boolean;
|
|
16
17
|
reasoningLevel?: ReasoningLevel;
|
|
17
18
|
abortSignal?: AbortSignal;
|
|
19
|
+
/**
|
|
20
|
+
* Omits prior session history from prompt assembly only. The run still emits
|
|
21
|
+
* events, tool calls, and persisted message parts in the current session.
|
|
22
|
+
*/
|
|
23
|
+
omitHistory?: boolean;
|
|
18
24
|
isCompactCommand?: boolean;
|
|
19
25
|
compactionContext?: string;
|
|
26
|
+
additionalPromptMessages?: Array<{
|
|
27
|
+
role: 'system' | 'user';
|
|
28
|
+
content: string;
|
|
29
|
+
}>;
|
|
20
30
|
toolApprovalMode?: ToolApprovalMode;
|
|
21
31
|
compactionRetries?: number;
|
|
22
32
|
continuationCount?: number;
|