@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
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'),
|
|
@@ -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 {
|
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
|
};
|
|
@@ -144,6 +148,22 @@ const defaultToolExtras: Record<string, string[]> = {
|
|
|
144
148
|
'websearch',
|
|
145
149
|
'update_todos',
|
|
146
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
|
+
],
|
|
147
167
|
git: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
148
168
|
commit: ['git_status', 'git_diff', 'git_commit', 'read', 'ls'],
|
|
149
169
|
research: [
|
|
@@ -308,6 +328,7 @@ export async function resolveAgentConfig(
|
|
|
308
328
|
if (n === 'build') return AGENT_BUILD;
|
|
309
329
|
if (n === 'plan') return AGENT_PLAN;
|
|
310
330
|
if (n === 'general') return AGENT_GENERAL;
|
|
331
|
+
if (n === 'init') return AGENT_INIT;
|
|
311
332
|
if (n === 'research') return AGENT_RESEARCH;
|
|
312
333
|
return undefined;
|
|
313
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 {
|
|
@@ -75,6 +80,28 @@ function summarizeTraceValue(value: unknown, max = 160): string {
|
|
|
75
80
|
return fallback.length > max ? `${fallback.slice(0, max)}…` : fallback;
|
|
76
81
|
}
|
|
77
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
|
+
|
|
78
105
|
export async function runSessionLoop(sessionId: string) {
|
|
79
106
|
setRunning(sessionId, true);
|
|
80
107
|
|
|
@@ -332,6 +359,22 @@ async function runAssistant(opts: RunOpts) {
|
|
|
332
359
|
runSessionLoop,
|
|
333
360
|
);
|
|
334
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
|
+
|
|
335
378
|
const baseOnAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
|
|
336
379
|
const onAbort = async (event: Parameters<typeof baseOnAbort>[0]) => {
|
|
337
380
|
_abortedByUser = true;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI SDK warning handler.
|
|
3
|
+
*
|
|
4
|
+
* The AI SDK logs provider warnings (e.g. unsupported features like
|
|
5
|
+
* `maxOutputTokens` caps) to the console via `globalThis.AI_SDK_LOG_WARNINGS`.
|
|
6
|
+
* These warnings are noisy during normal operation. We suppress them by
|
|
7
|
+
* default and only surface them when debug mode is enabled.
|
|
8
|
+
*
|
|
9
|
+
* See: https://ai-sdk.dev (AI_SDK_LOG_WARNINGS global)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isDebugEnabled } from './debug/state.ts';
|
|
13
|
+
|
|
14
|
+
type AiSdkWarning =
|
|
15
|
+
| { type: 'unsupported'; feature: string; details?: string }
|
|
16
|
+
| { type: 'compatibility'; feature: string; details?: string }
|
|
17
|
+
| { type: 'other'; message: string };
|
|
18
|
+
|
|
19
|
+
type LogWarningsOptions = {
|
|
20
|
+
warnings: AiSdkWarning[];
|
|
21
|
+
provider: string;
|
|
22
|
+
model: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function formatWarning(
|
|
26
|
+
warning: AiSdkWarning,
|
|
27
|
+
provider: string,
|
|
28
|
+
model: string,
|
|
29
|
+
): string {
|
|
30
|
+
const prefix = `AI SDK Warning (${provider} / ${model}):`;
|
|
31
|
+
switch (warning.type) {
|
|
32
|
+
case 'unsupported': {
|
|
33
|
+
let message = `${prefix} The feature "${warning.feature}" is not supported.`;
|
|
34
|
+
if (warning.details) message += ` ${warning.details}`;
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
37
|
+
case 'compatibility': {
|
|
38
|
+
let message = `${prefix} The feature "${warning.feature}" is used in a compatibility mode.`;
|
|
39
|
+
if (warning.details) message += ` ${warning.details}`;
|
|
40
|
+
return message;
|
|
41
|
+
}
|
|
42
|
+
case 'other':
|
|
43
|
+
return `${prefix} ${warning.message}`;
|
|
44
|
+
default:
|
|
45
|
+
return `${prefix} ${JSON.stringify(warning)}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let installed = false;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Install a custom AI SDK warning handler that suppresses warnings unless
|
|
53
|
+
* debug mode is enabled. Safe to call multiple times (installs once).
|
|
54
|
+
*/
|
|
55
|
+
export function installAiSdkWarningHandler(): void {
|
|
56
|
+
if (installed) return;
|
|
57
|
+
installed = true;
|
|
58
|
+
|
|
59
|
+
(
|
|
60
|
+
globalThis as unknown as {
|
|
61
|
+
AI_SDK_LOG_WARNINGS?: ((options: LogWarningsOptions) => void) | false;
|
|
62
|
+
}
|
|
63
|
+
).AI_SDK_LOG_WARNINGS = (options: LogWarningsOptions) => {
|
|
64
|
+
if (!isDebugEnabled()) return;
|
|
65
|
+
if (!options.warnings?.length) return;
|
|
66
|
+
for (const warning of options.warnings) {
|
|
67
|
+
console.warn(formatWarning(warning, options.provider, options.model));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|