@ottocode/server 0.1.237 → 0.1.243
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/skills.ts +11 -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 +46 -25
- package/src/runtime/agent/runner.ts +49 -6
- 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.243",
|
|
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.243",
|
|
53
|
+
"@ottocode/database": "0.1.243",
|
|
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/skills.ts
CHANGED
|
@@ -17,8 +17,18 @@ export function registerSkillsRoutes(app: Hono) {
|
|
|
17
17
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
18
18
|
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
19
19
|
const skills = await discoverSkills(projectRoot, repoRoot);
|
|
20
|
+
// Dedupe by name (same skill may exist in multiple source dirs like
|
|
21
|
+
// ~/.claude/skills and ~/.codex/skills). `discoverSkills` already
|
|
22
|
+
// dedupes via its internal Map, but be defensive here for UI consistency.
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
const unique = skills.filter((s) => {
|
|
25
|
+
const key = s.name.trim();
|
|
26
|
+
if (!key || seen.has(key)) return false;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
20
30
|
return c.json({
|
|
21
|
-
skills:
|
|
31
|
+
skills: unique.map((s) => ({
|
|
22
32
|
name: s.name,
|
|
23
33
|
description: s.description,
|
|
24
34
|
scope: s.scope,
|
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(
|
|
@@ -121,7 +153,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
121
153
|
oneShot: opts.oneShot,
|
|
122
154
|
guidedMode: cfg.defaults.guidedMode,
|
|
123
155
|
spoofPrompt: undefined,
|
|
124
|
-
includeProjectTree:
|
|
156
|
+
includeProjectTree: false,
|
|
125
157
|
userContext: opts.userContext,
|
|
126
158
|
contextSummary,
|
|
127
159
|
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
@@ -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
|
);
|
|
@@ -305,24 +347,3 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
305
347
|
mcpToolsRecord,
|
|
306
348
|
};
|
|
307
349
|
}
|
|
308
|
-
|
|
309
|
-
export function buildMessages(
|
|
310
|
-
additionalSystemMessages: Array<{ role: string; content: string }>,
|
|
311
|
-
history: Array<{ role: string; content: string | Array<unknown> }>,
|
|
312
|
-
isFirstMessage: boolean,
|
|
313
|
-
): Array<{ role: string; content: string | Array<unknown> }> {
|
|
314
|
-
const messagesWithSystemInstructions: Array<{
|
|
315
|
-
role: string;
|
|
316
|
-
content: string | Array<unknown>;
|
|
317
|
-
}> = [...additionalSystemMessages, ...history];
|
|
318
|
-
|
|
319
|
-
if (!isFirstMessage) {
|
|
320
|
-
messagesWithSystemInstructions.push({
|
|
321
|
-
role: 'user',
|
|
322
|
-
content:
|
|
323
|
-
'SYSTEM REMINDER: You are continuing an existing session. When you have completed the task, you MUST stream a text summary of what you did to the user, and THEN call the `finish` tool. Do not call `finish` without a summary.',
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return messagesWithSystemInstructions;
|
|
328
|
-
}
|
|
@@ -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
|
|
|
@@ -162,13 +189,13 @@ async function runAssistant(opts: RunOpts) {
|
|
|
162
189
|
messagesWithSystemInstructions.push({
|
|
163
190
|
role: 'system',
|
|
164
191
|
content:
|
|
165
|
-
'
|
|
192
|
+
'[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.',
|
|
166
193
|
});
|
|
167
194
|
} else {
|
|
168
195
|
messagesWithSystemInstructions.push({
|
|
169
196
|
role: 'user',
|
|
170
197
|
content:
|
|
171
|
-
'
|
|
198
|
+
'<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>',
|
|
172
199
|
});
|
|
173
200
|
}
|
|
174
201
|
}
|
|
@@ -177,13 +204,13 @@ async function runAssistant(opts: RunOpts) {
|
|
|
177
204
|
messagesWithSystemInstructions.push({
|
|
178
205
|
role: 'system',
|
|
179
206
|
content:
|
|
180
|
-
'
|
|
207
|
+
'[system-reminder] Your previous response stopped mid-task. Resume from where you left off and complete the actual work — not a plan-only update.',
|
|
181
208
|
});
|
|
182
209
|
} else {
|
|
183
210
|
messagesWithSystemInstructions.push({
|
|
184
211
|
role: 'user',
|
|
185
212
|
content:
|
|
186
|
-
'
|
|
213
|
+
'<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>',
|
|
187
214
|
});
|
|
188
215
|
}
|
|
189
216
|
}
|
|
@@ -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;
|