@ottocode/server 0.1.236 → 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 +9 -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/session-files.ts +19 -2
- package/src/routes/terminals.ts +94 -0
- package/src/runtime/agent/registry.ts +25 -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 +51 -3
- 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 +8 -2
- package/src/runtime/tools/mapping.ts +4 -0
- package/src/tools/adapter.ts +12 -2
- package/src/ws.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.242",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.242",
|
|
53
|
+
"@ottocode/database": "0.1.242",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.3.6"
|
package/src/index.ts
CHANGED
|
@@ -25,10 +25,14 @@ import { registerProviderUsageRoutes } from './routes/provider-usage.ts';
|
|
|
25
25
|
import { registerDoctorRoutes } from './routes/doctor.ts';
|
|
26
26
|
import { registerSkillsRoutes } from './routes/skills.ts';
|
|
27
27
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
28
|
+
import { installAiSdkWarningHandler } from './runtime/ai-sdk-warnings.ts';
|
|
28
29
|
|
|
29
30
|
const globalTerminalManager = new TerminalManager();
|
|
30
31
|
setTerminalManager(globalTerminalManager);
|
|
31
32
|
|
|
33
|
+
// Suppress noisy AI SDK provider warnings unless debug mode is enabled.
|
|
34
|
+
installAiSdkWarningHandler();
|
|
35
|
+
|
|
32
36
|
function initApp() {
|
|
33
37
|
const app = new Hono();
|
|
34
38
|
|
|
@@ -190,8 +194,9 @@ export type EmbeddedAppConfig = {
|
|
|
190
194
|
provider?: ProviderId;
|
|
191
195
|
model?: string;
|
|
192
196
|
agent?: string;
|
|
193
|
-
toolApproval?: 'auto' | 'dangerous' | 'all';
|
|
197
|
+
toolApproval?: 'auto' | 'dangerous' | 'all' | 'yolo';
|
|
194
198
|
fullWidthContent?: boolean;
|
|
199
|
+
autoCompactThresholdTokens?: number | null;
|
|
195
200
|
};
|
|
196
201
|
/** Additional CORS origins for proxies/Tailscale (e.g., ['https://myapp.ts.net', 'https://example.com']) */
|
|
197
202
|
corsOrigins?: string[];
|
|
@@ -305,3 +310,6 @@ export { logger } from '@ottocode/sdk';
|
|
|
305
310
|
|
|
306
311
|
// Export server state management
|
|
307
312
|
export { setServerPort, getServerPort, getServerInfo } from './state.ts';
|
|
313
|
+
|
|
314
|
+
// Export WebSocket handler for Bun.serve()
|
|
315
|
+
export { websocket as bunWebSocket } from './ws.ts';
|
|
@@ -179,6 +179,10 @@ export const configPaths = {
|
|
|
179
179
|
provider: { type: 'string' },
|
|
180
180
|
model: { type: 'string' },
|
|
181
181
|
fullWidthContent: { type: 'boolean' },
|
|
182
|
+
autoCompactThresholdTokens: {
|
|
183
|
+
type: 'integer',
|
|
184
|
+
nullable: true,
|
|
185
|
+
},
|
|
182
186
|
reasoningText: { type: 'boolean' },
|
|
183
187
|
reasoningLevel: {
|
|
184
188
|
type: 'string',
|
|
@@ -210,6 +214,10 @@ export const configPaths = {
|
|
|
210
214
|
provider: { type: 'string' },
|
|
211
215
|
model: { type: 'string' },
|
|
212
216
|
fullWidthContent: { type: 'boolean' },
|
|
217
|
+
autoCompactThresholdTokens: {
|
|
218
|
+
type: 'integer',
|
|
219
|
+
nullable: true,
|
|
220
|
+
},
|
|
213
221
|
reasoningText: { type: 'boolean' },
|
|
214
222
|
reasoningLevel: {
|
|
215
223
|
type: 'string',
|
package/src/openapi/schemas.ts
CHANGED
|
@@ -197,6 +197,7 @@ export const schemas = {
|
|
|
197
197
|
provider: { $ref: '#/components/schemas/Provider' },
|
|
198
198
|
model: { type: 'string' },
|
|
199
199
|
fullWidthContent: { type: 'boolean' },
|
|
200
|
+
autoCompactThresholdTokens: { type: 'integer', nullable: true },
|
|
200
201
|
reasoningText: { type: 'boolean' },
|
|
201
202
|
reasoningLevel: {
|
|
202
203
|
type: 'string',
|
package/src/presets.ts
CHANGED
|
@@ -7,6 +7,9 @@ import AGENT_PLAN from '@ottocode/sdk/prompts/agents/plan.txt' with {
|
|
|
7
7
|
import AGENT_GENERAL from '@ottocode/sdk/prompts/agents/general.txt' with {
|
|
8
8
|
type: 'text',
|
|
9
9
|
};
|
|
10
|
+
import AGENT_INIT from '@ottocode/sdk/prompts/agents/init.txt' with {
|
|
11
|
+
type: 'text',
|
|
12
|
+
};
|
|
10
13
|
import AGENT_RESEARCH from '@ottocode/sdk/prompts/agents/research.txt' with {
|
|
11
14
|
type: 'text',
|
|
12
15
|
};
|
|
@@ -36,6 +39,10 @@ export const BUILTIN_AGENTS = {
|
|
|
36
39
|
prompt: AGENT_GENERAL,
|
|
37
40
|
tools: defaultToolsForAgent('general'),
|
|
38
41
|
},
|
|
42
|
+
init: {
|
|
43
|
+
prompt: AGENT_INIT,
|
|
44
|
+
tools: defaultToolsForAgent('init'),
|
|
45
|
+
},
|
|
39
46
|
research: {
|
|
40
47
|
prompt: AGENT_RESEARCH,
|
|
41
48
|
tools: defaultToolsForAgent('research'),
|
|
@@ -54,6 +61,8 @@ export const BUILTIN_AGENTS = {
|
|
|
54
61
|
*/
|
|
55
62
|
export const BUILTIN_TOOLS = [
|
|
56
63
|
'read',
|
|
64
|
+
'edit',
|
|
65
|
+
'multiedit',
|
|
57
66
|
'write',
|
|
58
67
|
'ls',
|
|
59
68
|
'tree',
|
|
@@ -17,7 +17,7 @@ export function registerAgentsRoute(app: Hono) {
|
|
|
17
17
|
if (embeddedConfig) {
|
|
18
18
|
const agents = embeddedConfig.agents
|
|
19
19
|
? Object.keys(embeddedConfig.agents)
|
|
20
|
-
: ['general', 'build', 'plan'];
|
|
20
|
+
: ['general', 'build', 'plan', 'init'];
|
|
21
21
|
return c.json({
|
|
22
22
|
agents,
|
|
23
23
|
default: getDefault(
|
|
@@ -16,12 +16,13 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
16
16
|
agent?: string;
|
|
17
17
|
provider?: string;
|
|
18
18
|
model?: string;
|
|
19
|
-
toolApproval?: 'auto' | 'dangerous' | 'all';
|
|
19
|
+
toolApproval?: 'auto' | 'dangerous' | 'all' | 'yolo';
|
|
20
20
|
guidedMode?: boolean;
|
|
21
21
|
reasoningText?: boolean;
|
|
22
22
|
reasoningLevel?: ReasoningLevel;
|
|
23
23
|
theme?: string;
|
|
24
24
|
fullWidthContent?: boolean;
|
|
25
|
+
autoCompactThresholdTokens?: number | null;
|
|
25
26
|
scope?: 'global' | 'local';
|
|
26
27
|
}>();
|
|
27
28
|
|
|
@@ -30,12 +31,13 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
30
31
|
agent: string;
|
|
31
32
|
provider: ProviderId;
|
|
32
33
|
model: string;
|
|
33
|
-
toolApproval: 'auto' | 'dangerous' | 'all';
|
|
34
|
+
toolApproval: 'auto' | 'dangerous' | 'all' | 'yolo';
|
|
34
35
|
guidedMode: boolean;
|
|
35
36
|
reasoningText: boolean;
|
|
36
37
|
reasoningLevel: ReasoningLevel;
|
|
37
38
|
theme: string;
|
|
38
39
|
fullWidthContent: boolean;
|
|
40
|
+
autoCompactThresholdTokens: number | null;
|
|
39
41
|
}> = {};
|
|
40
42
|
|
|
41
43
|
if (body.agent) updates.agent = body.agent;
|
|
@@ -49,6 +51,14 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
49
51
|
if (body.theme) updates.theme = body.theme;
|
|
50
52
|
if (body.fullWidthContent !== undefined)
|
|
51
53
|
updates.fullWidthContent = body.fullWidthContent;
|
|
54
|
+
if (body.autoCompactThresholdTokens !== undefined) {
|
|
55
|
+
const threshold = body.autoCompactThresholdTokens;
|
|
56
|
+
if (threshold === null) {
|
|
57
|
+
updates.autoCompactThresholdTokens = null;
|
|
58
|
+
} else if (Number.isFinite(threshold) && threshold > 0) {
|
|
59
|
+
updates.autoCompactThresholdTokens = Math.floor(threshold);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
52
62
|
|
|
53
63
|
await setConfig(scope, updates, projectRoot);
|
|
54
64
|
|
|
@@ -58,7 +58,7 @@ export function registerMainConfigRoute(app: Hono) {
|
|
|
58
58
|
undefined,
|
|
59
59
|
embeddedConfig?.defaults?.toolApproval,
|
|
60
60
|
cfg.defaults.toolApproval,
|
|
61
|
-
) as 'auto' | 'dangerous' | 'all',
|
|
61
|
+
) as 'auto' | 'dangerous' | 'all' | 'yolo',
|
|
62
62
|
guidedMode: cfg.defaults.guidedMode ?? false,
|
|
63
63
|
reasoningText: cfg.defaults.reasoningText ?? true,
|
|
64
64
|
reasoningLevel: cfg.defaults.reasoningLevel ?? 'high',
|
|
@@ -69,6 +69,12 @@ export function registerMainConfigRoute(app: Hono) {
|
|
|
69
69
|
embeddedConfig?.defaults?.fullWidthContent,
|
|
70
70
|
cfg.defaults.fullWidthContent,
|
|
71
71
|
) ?? false,
|
|
72
|
+
autoCompactThresholdTokens:
|
|
73
|
+
getDefault(
|
|
74
|
+
undefined,
|
|
75
|
+
embeddedConfig?.defaults?.autoCompactThresholdTokens,
|
|
76
|
+
cfg.defaults.autoCompactThresholdTokens,
|
|
77
|
+
) ?? null,
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
return c.json({
|
|
@@ -72,7 +72,7 @@ export async function getAuthTypeForProvider(
|
|
|
72
72
|
export async function discoverAllAgents(
|
|
73
73
|
projectRoot: string,
|
|
74
74
|
): Promise<string[]> {
|
|
75
|
-
const builtInAgents = ['general', 'build', 'plan'];
|
|
75
|
+
const builtInAgents = ['general', 'build', 'plan', 'init'];
|
|
76
76
|
const agentSet = new Set<string>(builtInAgents);
|
|
77
77
|
|
|
78
78
|
try {
|
|
@@ -6,7 +6,16 @@ import { eq, and, inArray } from 'drizzle-orm';
|
|
|
6
6
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
7
7
|
import { logger } from '@ottocode/sdk';
|
|
8
8
|
|
|
9
|
-
const FILE_EDIT_TOOLS = [
|
|
9
|
+
const FILE_EDIT_TOOLS = [
|
|
10
|
+
'Write',
|
|
11
|
+
'Edit',
|
|
12
|
+
'MultiEdit',
|
|
13
|
+
'ApplyPatch',
|
|
14
|
+
'write',
|
|
15
|
+
'edit',
|
|
16
|
+
'multiedit',
|
|
17
|
+
'apply_patch',
|
|
18
|
+
];
|
|
10
19
|
|
|
11
20
|
interface FileOperation {
|
|
12
21
|
path: string;
|
|
@@ -62,7 +71,7 @@ function extractFilePathFromToolCall(
|
|
|
62
71
|
|
|
63
72
|
const name = toolName.toLowerCase();
|
|
64
73
|
|
|
65
|
-
if (name === 'write') {
|
|
74
|
+
if (name === 'write' || name === 'edit' || name === 'multiedit') {
|
|
66
75
|
if (args && typeof args.path === 'string') return args.path;
|
|
67
76
|
if (typeof c.path === 'string') return c.path;
|
|
68
77
|
}
|
|
@@ -189,6 +198,13 @@ function extractDataFromToolResult(
|
|
|
189
198
|
patch = (args?.patch as string | undefined) ?? c.patch;
|
|
190
199
|
}
|
|
191
200
|
|
|
201
|
+
if (
|
|
202
|
+
(name === 'edit' || name === 'multiedit') &&
|
|
203
|
+
typeof c.result?.artifact?.patch === 'string'
|
|
204
|
+
) {
|
|
205
|
+
patch = c.result.artifact.patch;
|
|
206
|
+
}
|
|
207
|
+
|
|
192
208
|
if (name === 'write') {
|
|
193
209
|
writeContent = args?.content as string | undefined;
|
|
194
210
|
}
|
|
@@ -213,6 +229,7 @@ function extractDataFromToolResult(
|
|
|
213
229
|
function getOperationType(toolName: string): 'write' | 'patch' | 'create' {
|
|
214
230
|
const name = toolName.toLowerCase();
|
|
215
231
|
if (name === 'write') return 'write';
|
|
232
|
+
if (name === 'edit' || name === 'multiedit') return 'patch';
|
|
216
233
|
if (name === 'applypatch' || name === 'apply_patch') return 'patch';
|
|
217
234
|
return 'write';
|
|
218
235
|
}
|
package/src/routes/terminals.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Hono } from 'hono';
|
|
|
3
3
|
import { streamSSE } from 'hono/streaming';
|
|
4
4
|
import type { TerminalManager } from '@ottocode/sdk';
|
|
5
5
|
import { logger } from '@ottocode/sdk';
|
|
6
|
+
import { upgradeWebSocket } from '../ws.ts';
|
|
6
7
|
|
|
7
8
|
export function registerTerminalsRoutes(
|
|
8
9
|
app: Hono,
|
|
@@ -67,6 +68,99 @@ export function registerTerminalsRoutes(
|
|
|
67
68
|
return c.json({ terminal: terminal.toJSON() });
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
app.get(
|
|
72
|
+
'/v1/terminals/:id/ws',
|
|
73
|
+
upgradeWebSocket((c) => {
|
|
74
|
+
const id = c.req.param('id');
|
|
75
|
+
|
|
76
|
+
let onData: ((data: string) => void) | null = null;
|
|
77
|
+
let onExit: ((exitCode: number) => void) | null = null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
onOpen(_event, ws) {
|
|
81
|
+
const terminal = terminalManager.get(id);
|
|
82
|
+
if (!terminal) {
|
|
83
|
+
ws.close(4004, 'Terminal not found');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const history = terminal.read();
|
|
88
|
+
for (const chunk of history) {
|
|
89
|
+
ws.send(chunk);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onData = (data: string) => {
|
|
93
|
+
try {
|
|
94
|
+
ws.send(data);
|
|
95
|
+
} catch {
|
|
96
|
+
// ws may be closed
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
onExit = (exitCode: number) => {
|
|
101
|
+
try {
|
|
102
|
+
ws.send(JSON.stringify({ type: 'exit', exitCode }));
|
|
103
|
+
ws.close(1000, 'Process exited');
|
|
104
|
+
} catch {
|
|
105
|
+
// ws may already be closed
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
terminal.onData(onData);
|
|
110
|
+
terminal.onExit(onExit);
|
|
111
|
+
|
|
112
|
+
if (terminal.status === 'exited') {
|
|
113
|
+
onExit(terminal.exitCode ?? 0);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
onMessage(event, _ws) {
|
|
117
|
+
const terminal = terminalManager.get(id);
|
|
118
|
+
if (!terminal) return;
|
|
119
|
+
|
|
120
|
+
const raw = event.data;
|
|
121
|
+
const message =
|
|
122
|
+
typeof raw === 'string'
|
|
123
|
+
? raw
|
|
124
|
+
: raw instanceof ArrayBuffer
|
|
125
|
+
? new TextDecoder().decode(raw)
|
|
126
|
+
: String(raw);
|
|
127
|
+
|
|
128
|
+
if (message.startsWith('{')) {
|
|
129
|
+
try {
|
|
130
|
+
const msg = JSON.parse(message);
|
|
131
|
+
if (msg.type === 'resize' && msg.cols > 0 && msg.rows > 0) {
|
|
132
|
+
terminal.resize(msg.cols, msg.rows);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// not JSON, treat as input
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
terminal.write(message);
|
|
141
|
+
},
|
|
142
|
+
onClose() {
|
|
143
|
+
const terminal = terminalManager.get(id);
|
|
144
|
+
if (terminal) {
|
|
145
|
+
if (onData) terminal.removeDataListener(onData);
|
|
146
|
+
if (onExit) terminal.removeExitListener(onExit);
|
|
147
|
+
}
|
|
148
|
+
onData = null;
|
|
149
|
+
onExit = null;
|
|
150
|
+
},
|
|
151
|
+
onError() {
|
|
152
|
+
const terminal = terminalManager.get(id);
|
|
153
|
+
if (terminal) {
|
|
154
|
+
if (onData) terminal.removeDataListener(onData);
|
|
155
|
+
if (onExit) terminal.removeExitListener(onExit);
|
|
156
|
+
}
|
|
157
|
+
onData = null;
|
|
158
|
+
onExit = null;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
70
164
|
const handleTerminalOutput = async (c: Context) => {
|
|
71
165
|
const id = c.req.param('id');
|
|
72
166
|
const terminal = terminalManager.get(id);
|
|
@@ -14,6 +14,10 @@ import AGENT_PLAN from '@ottocode/sdk/prompts/agents/plan.txt' with {
|
|
|
14
14
|
import AGENT_GENERAL from '@ottocode/sdk/prompts/agents/general.txt' with {
|
|
15
15
|
type: 'text',
|
|
16
16
|
};
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
18
|
+
import AGENT_INIT from '@ottocode/sdk/prompts/agents/init.txt' with {
|
|
19
|
+
type: 'text',
|
|
20
|
+
};
|
|
17
21
|
import AGENT_RESEARCH from '@ottocode/sdk/prompts/agents/research.txt' with {
|
|
18
22
|
type: 'text',
|
|
19
23
|
};
|
|
@@ -116,6 +120,8 @@ const baseToolSet = ['progress_update', 'finish', 'skill'] as const;
|
|
|
116
120
|
const defaultToolExtras: Record<string, string[]> = {
|
|
117
121
|
build: [
|
|
118
122
|
'read',
|
|
123
|
+
'edit',
|
|
124
|
+
'multiedit',
|
|
119
125
|
'write',
|
|
120
126
|
'ls',
|
|
121
127
|
'tree',
|
|
@@ -131,6 +137,8 @@ const defaultToolExtras: Record<string, string[]> = {
|
|
|
131
137
|
plan: ['read', 'ls', 'tree', 'ripgrep', 'update_todos', 'websearch'],
|
|
132
138
|
general: [
|
|
133
139
|
'read',
|
|
140
|
+
'edit',
|
|
141
|
+
'multiedit',
|
|
134
142
|
'write',
|
|
135
143
|
'ls',
|
|
136
144
|
'tree',
|
|
@@ -140,6 +148,22 @@ const defaultToolExtras: Record<string, string[]> = {
|
|
|
140
148
|
'websearch',
|
|
141
149
|
'update_todos',
|
|
142
150
|
],
|
|
151
|
+
init: [
|
|
152
|
+
'read',
|
|
153
|
+
'edit',
|
|
154
|
+
'multiedit',
|
|
155
|
+
'write',
|
|
156
|
+
'ls',
|
|
157
|
+
'tree',
|
|
158
|
+
'bash',
|
|
159
|
+
'update_todos',
|
|
160
|
+
'glob',
|
|
161
|
+
'ripgrep',
|
|
162
|
+
'git_status',
|
|
163
|
+
'terminal',
|
|
164
|
+
'apply_patch',
|
|
165
|
+
'websearch',
|
|
166
|
+
],
|
|
143
167
|
git: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
144
168
|
commit: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
145
169
|
research: [
|
|
@@ -304,6 +328,7 @@ export async function resolveAgentConfig(
|
|
|
304
328
|
if (n === 'build') return AGENT_BUILD;
|
|
305
329
|
if (n === 'plan') return AGENT_PLAN;
|
|
306
330
|
if (n === 'general') return AGENT_GENERAL;
|
|
331
|
+
if (n === 'init') return AGENT_INIT;
|
|
307
332
|
if (n === 'research') return AGENT_RESEARCH;
|
|
308
333
|
return undefined;
|
|
309
334
|
};
|
|
@@ -9,6 +9,10 @@ export type ReasoningState = {
|
|
|
9
9
|
partId: string;
|
|
10
10
|
text: string;
|
|
11
11
|
providerMetadata?: unknown;
|
|
12
|
+
persisted: boolean;
|
|
13
|
+
opts: RunOpts;
|
|
14
|
+
sharedCtx: ToolAdapterContext;
|
|
15
|
+
getStepIndex: () => number;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
export function serializeReasoningContent(state: ReasoningState): string {
|
|
@@ -23,7 +27,7 @@ export async function handleReasoningStart(
|
|
|
23
27
|
reasoningId: string,
|
|
24
28
|
providerMetadata: unknown,
|
|
25
29
|
opts: RunOpts,
|
|
26
|
-
|
|
30
|
+
_db: Awaited<ReturnType<typeof getDb>>,
|
|
27
31
|
sharedCtx: ToolAdapterContext,
|
|
28
32
|
getStepIndex: () => number,
|
|
29
33
|
reasoningStates: Map<string, ReasoningState>,
|
|
@@ -33,21 +37,33 @@ export async function handleReasoningStart(
|
|
|
33
37
|
partId: reasoningPartId,
|
|
34
38
|
text: '',
|
|
35
39
|
providerMetadata,
|
|
40
|
+
persisted: false,
|
|
41
|
+
opts,
|
|
42
|
+
sharedCtx,
|
|
43
|
+
getStepIndex,
|
|
36
44
|
};
|
|
37
45
|
reasoningStates.set(reasoningId, state);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function persistReasoningPart(
|
|
49
|
+
state: ReasoningState,
|
|
50
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
if (state.persisted) return;
|
|
38
53
|
try {
|
|
39
54
|
await db.insert(messageParts).values({
|
|
40
|
-
id:
|
|
41
|
-
messageId: opts.assistantMessageId,
|
|
42
|
-
index: await sharedCtx.nextIndex(),
|
|
43
|
-
stepIndex: getStepIndex(),
|
|
55
|
+
id: state.partId,
|
|
56
|
+
messageId: state.opts.assistantMessageId,
|
|
57
|
+
index: await state.sharedCtx.nextIndex(),
|
|
58
|
+
stepIndex: state.getStepIndex(),
|
|
44
59
|
type: 'reasoning',
|
|
45
60
|
content: serializeReasoningContent(state),
|
|
46
|
-
agent: opts.agent,
|
|
47
|
-
provider: opts.provider,
|
|
48
|
-
model: opts.model,
|
|
61
|
+
agent: state.opts.agent,
|
|
62
|
+
provider: state.opts.provider,
|
|
63
|
+
model: state.opts.model,
|
|
49
64
|
startedAt: Date.now(),
|
|
50
65
|
});
|
|
66
|
+
state.persisted = true;
|
|
51
67
|
} catch {}
|
|
52
68
|
}
|
|
53
69
|
|
|
@@ -66,6 +82,14 @@ export async function handleReasoningDelta(
|
|
|
66
82
|
if (providerMetadata != null) {
|
|
67
83
|
state.providerMetadata = providerMetadata;
|
|
68
84
|
}
|
|
85
|
+
|
|
86
|
+
// Skip empty-text updates (e.g. Anthropic signature_delta from adaptive
|
|
87
|
+
// thinking emits reasoning-delta with `text: ""`). Publishing/persisting
|
|
88
|
+
// these would create an empty reasoning placeholder shown as `{"text":""}`.
|
|
89
|
+
if (!text) return;
|
|
90
|
+
|
|
91
|
+
await persistReasoningPart(state, db);
|
|
92
|
+
|
|
69
93
|
publish({
|
|
70
94
|
type: 'reasoning.delta',
|
|
71
95
|
sessionId: opts.sessionId,
|
|
@@ -92,9 +116,11 @@ export async function handleReasoningEnd(
|
|
|
92
116
|
const state = reasoningStates.get(reasoningId);
|
|
93
117
|
if (!state) return;
|
|
94
118
|
if (!state.text || state.text.trim() === '') {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
if (state.persisted) {
|
|
120
|
+
try {
|
|
121
|
+
await db.delete(messageParts).where(eq(messageParts.id, state.partId));
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
98
124
|
reasoningStates.delete(reasoningId);
|
|
99
125
|
return;
|
|
100
126
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
loadConfig,
|
|
3
|
+
logger,
|
|
4
|
+
getSessionSystemPromptPath,
|
|
5
|
+
getModelFamily,
|
|
6
|
+
} from '@ottocode/sdk';
|
|
2
7
|
import { wrapLanguageModel } from 'ai';
|
|
3
8
|
import { devToolsMiddleware } from '@ai-sdk/devtools';
|
|
4
9
|
import { getDb } from '@ottocode/database';
|
|
@@ -74,6 +79,33 @@ export function mergeProviderOptions(
|
|
|
74
79
|
return base;
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
const EDITING_TOOL_NAMES = ['edit', 'multiedit', 'write', 'apply_patch'];
|
|
83
|
+
const MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS = new Set([
|
|
84
|
+
'build',
|
|
85
|
+
'general',
|
|
86
|
+
'init',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export function applyModelFamilyEditToolPolicy(
|
|
90
|
+
agent: string,
|
|
91
|
+
tools: string[],
|
|
92
|
+
provider: RunOpts['provider'],
|
|
93
|
+
model: string,
|
|
94
|
+
): string[] {
|
|
95
|
+
if (!MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS.has(agent)) return tools;
|
|
96
|
+
|
|
97
|
+
const family = getModelFamily(provider, model);
|
|
98
|
+
const next = tools.filter(
|
|
99
|
+
(toolName) => !EDITING_TOOL_NAMES.includes(toolName),
|
|
100
|
+
);
|
|
101
|
+
const preferredEditingTools =
|
|
102
|
+
family === 'anthropic' || family === 'openai'
|
|
103
|
+
? ['write', 'apply_patch']
|
|
104
|
+
: ['write', 'edit', 'multiedit'];
|
|
105
|
+
|
|
106
|
+
return Array.from(new Set([...next, ...preferredEditingTools]));
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
78
110
|
const cfgTimer = time('runner:loadConfig+db');
|
|
79
111
|
const cfg = await loadConfig(opts.projectRoot);
|
|
@@ -88,7 +120,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
88
120
|
|
|
89
121
|
const historyTimer = time('runner:buildHistory');
|
|
90
122
|
let history: Awaited<ReturnType<typeof buildHistoryMessages>>;
|
|
91
|
-
if (opts.isCompactCommand && opts.compactionContext) {
|
|
123
|
+
if (opts.omitHistory || (opts.isCompactCommand && opts.compactionContext)) {
|
|
92
124
|
history = [];
|
|
93
125
|
} else {
|
|
94
126
|
history = await buildHistoryMessages(
|
|
@@ -218,6 +250,10 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
218
250
|
});
|
|
219
251
|
}
|
|
220
252
|
|
|
253
|
+
if (opts.additionalPromptMessages?.length) {
|
|
254
|
+
additionalSystemMessages.push(...opts.additionalPromptMessages);
|
|
255
|
+
}
|
|
256
|
+
|
|
221
257
|
const toolsTimer = time('runner:discoverTools');
|
|
222
258
|
const discovered = await discoverProjectTools(cfg.projectRoot);
|
|
223
259
|
const allTools = discovered.tools;
|
|
@@ -236,7 +272,13 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
236
272
|
toolsTimer.end({
|
|
237
273
|
count: allTools.length + Object.keys(mcpToolsRecord).length,
|
|
238
274
|
});
|
|
239
|
-
const
|
|
275
|
+
const allowedToolNames = applyModelFamilyEditToolPolicy(
|
|
276
|
+
agentCfg.name,
|
|
277
|
+
agentCfg.tools || [],
|
|
278
|
+
opts.provider,
|
|
279
|
+
opts.model,
|
|
280
|
+
);
|
|
281
|
+
const allowedNames = new Set([...allowedToolNames, 'finish']);
|
|
240
282
|
const gated = allTools.filter(
|
|
241
283
|
(tool) => allowedNames.has(tool.name) || tool.name === 'load_mcp_tools',
|
|
242
284
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hasToolCall, streamText } from 'ai';
|
|
2
|
-
import {
|
|
2
|
+
import type { getDb } from '@ottocode/database';
|
|
3
|
+
import { messageParts, sessions } from '@ottocode/database/schema';
|
|
3
4
|
import { eq } from 'drizzle-orm';
|
|
4
5
|
import { publish, subscribe } from '../../events/bus.ts';
|
|
5
6
|
import { time } from '../debug/index.ts';
|
|
@@ -22,7 +23,11 @@ import {
|
|
|
22
23
|
createAbortHandler,
|
|
23
24
|
createFinishHandler,
|
|
24
25
|
} from '../stream/handlers.ts';
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
pruneSession,
|
|
28
|
+
getModelLimits,
|
|
29
|
+
shouldAutoCompactBeforeOverflow,
|
|
30
|
+
} from '../message/compaction.ts';
|
|
26
31
|
import { triggerDeferredTitleGeneration } from '../message/service.ts';
|
|
27
32
|
import { setupRunner } from './runner-setup.ts';
|
|
28
33
|
import {
|
|
@@ -51,7 +56,12 @@ export {
|
|
|
51
56
|
getRunnerState,
|
|
52
57
|
} from '../session/queue.ts';
|
|
53
58
|
|
|
54
|
-
const DEFAULT_TRACED_TOOL_INPUTS = new Set([
|
|
59
|
+
const DEFAULT_TRACED_TOOL_INPUTS = new Set([
|
|
60
|
+
'write',
|
|
61
|
+
'edit',
|
|
62
|
+
'multiedit',
|
|
63
|
+
'apply_patch',
|
|
64
|
+
]);
|
|
55
65
|
|
|
56
66
|
function shouldTraceToolInput(name: string): boolean {
|
|
57
67
|
void DEFAULT_TRACED_TOOL_INPUTS;
|
|
@@ -70,6 +80,28 @@ function summarizeTraceValue(value: unknown, max = 160): string {
|
|
|
70
80
|
return fallback.length > max ? `${fallback.slice(0, max)}…` : fallback;
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
async function shouldPreemptivelyAutoCompact(
|
|
84
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
85
|
+
opts: RunOpts,
|
|
86
|
+
threshold: number | null | undefined,
|
|
87
|
+
): Promise<boolean> {
|
|
88
|
+
const limits = getModelLimits(opts.provider, opts.model);
|
|
89
|
+
const sessionRows = await db
|
|
90
|
+
.select({ currentContextTokens: sessions.currentContextTokens })
|
|
91
|
+
.from(sessions)
|
|
92
|
+
.where(eq(sessions.id, opts.sessionId))
|
|
93
|
+
.limit(1);
|
|
94
|
+
|
|
95
|
+
return shouldAutoCompactBeforeOverflow({
|
|
96
|
+
autoCompactThresholdTokens: threshold,
|
|
97
|
+
modelContextWindow: limits?.context ?? null,
|
|
98
|
+
currentContextTokens: sessionRows[0]?.currentContextTokens ?? 0,
|
|
99
|
+
estimatedInputTokens: opts.estimatedInputTokens ?? 0,
|
|
100
|
+
isCompactCommand: opts.isCompactCommand,
|
|
101
|
+
compactionRetries: opts.compactionRetries,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
export async function runSessionLoop(sessionId: string) {
|
|
74
106
|
setRunning(sessionId, true);
|
|
75
107
|
|
|
@@ -327,6 +359,22 @@ async function runAssistant(opts: RunOpts) {
|
|
|
327
359
|
runSessionLoop,
|
|
328
360
|
);
|
|
329
361
|
|
|
362
|
+
if (
|
|
363
|
+
await shouldPreemptivelyAutoCompact(
|
|
364
|
+
db,
|
|
365
|
+
opts,
|
|
366
|
+
cfg.defaults.autoCompactThresholdTokens,
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
const autoCompactError = Object.assign(
|
|
370
|
+
new Error('Configured auto-compaction threshold reached'),
|
|
371
|
+
{ code: 'context_length_exceeded' },
|
|
372
|
+
);
|
|
373
|
+
await onError(autoCompactError);
|
|
374
|
+
unsubscribeFinish();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
330
378
|
const baseOnAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
|
|
331
379
|
const onAbort = async (event: Parameters<typeof baseOnAbort>[0]) => {
|
|
332
380
|
_abortedByUser = true;
|