@ottocode/server 0.1.245 → 0.1.247
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 +4 -3
- package/src/index.ts +5 -0
- package/src/openapi/paths/config.ts +118 -2
- package/src/openapi/paths/skills.ts +122 -0
- package/src/openapi/schemas.ts +35 -3
- package/src/presets.ts +1 -1
- package/src/routes/auth.ts +24 -30
- package/src/routes/branch.ts +3 -2
- package/src/routes/config/defaults.ts +10 -3
- package/src/routes/config/main.ts +3 -0
- package/src/routes/config/models.ts +84 -14
- package/src/routes/config/providers.ts +137 -4
- package/src/routes/config/utils.ts +72 -2
- package/src/routes/doctor.ts +15 -27
- package/src/routes/git/commit.ts +16 -5
- package/src/routes/research.ts +3 -3
- package/src/routes/session-messages.ts +14 -8
- package/src/routes/sessions.ts +12 -18
- package/src/routes/skills.ts +140 -59
- package/src/runtime/agent/registry.ts +8 -5
- package/src/runtime/agent/runner-setup.ts +136 -39
- package/src/runtime/agent/runner.ts +140 -4
- package/src/runtime/ask/service.ts +13 -10
- package/src/runtime/message/history-builder.ts +22 -6
- package/src/runtime/message/service.ts +7 -1
- package/src/runtime/prompt/builder.ts +12 -0
- package/src/runtime/prompt/capabilities.ts +200 -0
- package/src/runtime/provider/index.ts +98 -0
- package/src/runtime/provider/reasoning.ts +73 -17
- package/src/runtime/provider/selection.ts +16 -14
- package/src/runtime/session/manager.ts +1 -1
- package/src/runtime/session/queue.ts +7 -2
- package/src/runtime/tools/approval.ts +1 -0
- package/src/runtime/tools/guards.ts +4 -3
- package/src/runtime/tools/mapping.ts +4 -2
- package/src/tools/adapter.ts +3 -3
package/src/routes/skills.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import {
|
|
3
3
|
discoverSkills,
|
|
4
|
+
filterDiscoveredSkills,
|
|
4
5
|
loadSkill,
|
|
5
6
|
loadSkillFile,
|
|
6
7
|
discoverSkillFiles,
|
|
@@ -8,32 +9,56 @@ import {
|
|
|
8
9
|
validateSkillName,
|
|
9
10
|
parseSkillFile,
|
|
10
11
|
logger,
|
|
12
|
+
loadConfig,
|
|
13
|
+
writeSkillSettings,
|
|
11
14
|
} from '@ottocode/sdk';
|
|
12
15
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
13
16
|
|
|
17
|
+
function dedupeSkillsByName<T extends { name: string }>(skills: T[]): T[] {
|
|
18
|
+
const seen = new Set<string>();
|
|
19
|
+
return skills.filter((skill) => {
|
|
20
|
+
const key = skill.name.trim();
|
|
21
|
+
if (!key || seen.has(key)) return false;
|
|
22
|
+
seen.add(key);
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sortSkillsByName<T extends { name: string }>(skills: T[]): T[] {
|
|
28
|
+
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mapSkillsWithEnabled(
|
|
32
|
+
discovered: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
scope: string;
|
|
36
|
+
path: string;
|
|
37
|
+
}>,
|
|
38
|
+
cfg: Awaited<ReturnType<typeof loadConfig>>,
|
|
39
|
+
) {
|
|
40
|
+
return discovered.map((skill) => ({
|
|
41
|
+
name: skill.name,
|
|
42
|
+
description: skill.description,
|
|
43
|
+
scope: skill.scope,
|
|
44
|
+
path: skill.path,
|
|
45
|
+
enabled: cfg.skills?.items?.[skill.name]?.enabled !== false,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
export function registerSkillsRoutes(app: Hono) {
|
|
15
50
|
app.get('/v1/skills', async (c) => {
|
|
16
51
|
try {
|
|
17
52
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
53
|
+
const cfg = await loadConfig(projectRoot);
|
|
18
54
|
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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
|
-
});
|
|
55
|
+
const discovered = sortSkillsByName(
|
|
56
|
+
await discoverSkills(projectRoot, repoRoot),
|
|
57
|
+
);
|
|
58
|
+
const filtered = filterDiscoveredSkills(discovered, cfg.skills);
|
|
59
|
+
const unique = sortSkillsByName(dedupeSkillsByName(filtered));
|
|
30
60
|
return c.json({
|
|
31
|
-
skills: unique
|
|
32
|
-
name: s.name,
|
|
33
|
-
description: s.description,
|
|
34
|
-
scope: s.scope,
|
|
35
|
-
path: s.path,
|
|
36
|
-
})),
|
|
61
|
+
skills: mapSkillsWithEnabled(unique, cfg),
|
|
37
62
|
});
|
|
38
63
|
} catch (error) {
|
|
39
64
|
logger.error('Failed to list skills', error);
|
|
@@ -42,6 +67,68 @@ export function registerSkillsRoutes(app: Hono) {
|
|
|
42
67
|
}
|
|
43
68
|
});
|
|
44
69
|
|
|
70
|
+
app.get('/v1/config/skills', async (c) => {
|
|
71
|
+
try {
|
|
72
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
73
|
+
const cfg = await loadConfig(projectRoot);
|
|
74
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
75
|
+
const discovered = sortSkillsByName(
|
|
76
|
+
dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
|
|
77
|
+
);
|
|
78
|
+
const filtered = sortSkillsByName(
|
|
79
|
+
filterDiscoveredSkills(discovered, cfg.skills),
|
|
80
|
+
);
|
|
81
|
+
return c.json({
|
|
82
|
+
enabled: cfg.skills?.enabled !== false,
|
|
83
|
+
totalCount: discovered.length,
|
|
84
|
+
enabledCount: filtered.length,
|
|
85
|
+
items: mapSkillsWithEnabled(discovered, cfg),
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error('Failed to get skills config', error);
|
|
89
|
+
const errorResponse = serializeError(error);
|
|
90
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
app.put('/v1/config/skills', async (c) => {
|
|
95
|
+
try {
|
|
96
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
97
|
+
const body = await c.req.json<{
|
|
98
|
+
enabled?: boolean;
|
|
99
|
+
items?: Record<string, { enabled?: boolean }>;
|
|
100
|
+
scope?: 'global' | 'local';
|
|
101
|
+
}>();
|
|
102
|
+
await writeSkillSettings(
|
|
103
|
+
body.scope || 'local',
|
|
104
|
+
{
|
|
105
|
+
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
|
|
106
|
+
...(body.items ? { items: body.items } : {}),
|
|
107
|
+
},
|
|
108
|
+
projectRoot,
|
|
109
|
+
);
|
|
110
|
+
const cfg = await loadConfig(projectRoot);
|
|
111
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
112
|
+
const discovered = sortSkillsByName(
|
|
113
|
+
dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
|
|
114
|
+
);
|
|
115
|
+
const filtered = sortSkillsByName(
|
|
116
|
+
filterDiscoveredSkills(discovered, cfg.skills),
|
|
117
|
+
);
|
|
118
|
+
return c.json({
|
|
119
|
+
success: true,
|
|
120
|
+
enabled: cfg.skills?.enabled !== false,
|
|
121
|
+
totalCount: discovered.length,
|
|
122
|
+
enabledCount: filtered.length,
|
|
123
|
+
items: mapSkillsWithEnabled(discovered, cfg),
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error('Failed to update skills config', error);
|
|
127
|
+
const errorResponse = serializeError(error);
|
|
128
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
45
132
|
app.get('/v1/skills/:name', async (c) => {
|
|
46
133
|
try {
|
|
47
134
|
const name = c.req.param('name');
|
|
@@ -72,52 +159,46 @@ export function registerSkillsRoutes(app: Hono) {
|
|
|
72
159
|
}
|
|
73
160
|
});
|
|
74
161
|
|
|
75
|
-
app.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await discoverSkills(projectRoot, repoRoot);
|
|
102
|
-
|
|
103
|
-
const result = await loadSkillFile(name, filePath);
|
|
104
|
-
if (!result) {
|
|
105
|
-
return c.json(
|
|
106
|
-
{ error: `File '${filePath}' not found in skill '${name}'` },
|
|
107
|
-
404,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
return c.json({ content: result.content, path: result.resolvedPath });
|
|
111
|
-
} catch (error) {
|
|
112
|
-
logger.error('Failed to load skill file', error);
|
|
113
|
-
const errorResponse = serializeError(error);
|
|
162
|
+
app.get('/v1/skills/:name/files', async (c) => {
|
|
163
|
+
try {
|
|
164
|
+
const name = c.req.param('name');
|
|
165
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
166
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
167
|
+
await discoverSkills(projectRoot, repoRoot);
|
|
168
|
+
|
|
169
|
+
const files = await discoverSkillFiles(name);
|
|
170
|
+
return c.json({ files });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error('Failed to list skill files', error);
|
|
173
|
+
const errorResponse = serializeError(error);
|
|
174
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
app.get('/v1/skills/:name/files/*', async (c) => {
|
|
179
|
+
try {
|
|
180
|
+
const name = c.req.param('name');
|
|
181
|
+
const filePath = c.req.path.replace(`/v1/skills/${name}/files/`, '');
|
|
182
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
183
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
184
|
+
await discoverSkills(projectRoot, repoRoot);
|
|
185
|
+
|
|
186
|
+
const result = await loadSkillFile(name, filePath);
|
|
187
|
+
if (!result) {
|
|
114
188
|
return c.json(
|
|
115
|
-
|
|
116
|
-
|
|
189
|
+
{ error: `File '${filePath}' not found in skill '${name}'` },
|
|
190
|
+
404,
|
|
117
191
|
);
|
|
118
192
|
}
|
|
119
|
-
|
|
193
|
+
return c.json({ content: result.content, path: result.resolvedPath });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logger.error('Failed to load skill file', error);
|
|
196
|
+
const errorResponse = serializeError(error);
|
|
197
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
120
200
|
|
|
201
|
+
app.post('/v1/skills/validate', async (c) => {
|
|
121
202
|
try {
|
|
122
203
|
const body = await c.req.json<{ content: string; path?: string }>();
|
|
123
204
|
if (!body.content) {
|
|
@@ -115,17 +115,18 @@ function mergeAgentEntries(
|
|
|
115
115
|
return merged;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const baseToolSet = ['progress_update', 'finish'
|
|
118
|
+
const baseToolSet = ['progress_update', 'finish'] as const;
|
|
119
119
|
|
|
120
120
|
const defaultToolExtras: Record<string, string[]> = {
|
|
121
121
|
build: [
|
|
122
|
+
'skill',
|
|
122
123
|
'read',
|
|
123
124
|
'edit',
|
|
124
125
|
'multiedit',
|
|
125
126
|
'write',
|
|
126
127
|
'ls',
|
|
127
128
|
'tree',
|
|
128
|
-
'
|
|
129
|
+
'shell',
|
|
129
130
|
'update_todos',
|
|
130
131
|
'glob',
|
|
131
132
|
'ripgrep',
|
|
@@ -134,28 +135,30 @@ const defaultToolExtras: Record<string, string[]> = {
|
|
|
134
135
|
'apply_patch',
|
|
135
136
|
'websearch',
|
|
136
137
|
],
|
|
137
|
-
plan: ['read', 'ls', 'tree', 'ripgrep', 'update_todos', 'websearch'],
|
|
138
|
+
plan: ['skill', 'read', 'ls', 'tree', 'ripgrep', 'update_todos', 'websearch'],
|
|
138
139
|
general: [
|
|
140
|
+
'skill',
|
|
139
141
|
'read',
|
|
140
142
|
'edit',
|
|
141
143
|
'multiedit',
|
|
142
144
|
'write',
|
|
143
145
|
'ls',
|
|
144
146
|
'tree',
|
|
145
|
-
'
|
|
147
|
+
'shell',
|
|
146
148
|
'ripgrep',
|
|
147
149
|
'glob',
|
|
148
150
|
'websearch',
|
|
149
151
|
'update_todos',
|
|
150
152
|
],
|
|
151
153
|
init: [
|
|
154
|
+
'skill',
|
|
152
155
|
'read',
|
|
153
156
|
'edit',
|
|
154
157
|
'multiedit',
|
|
155
158
|
'write',
|
|
156
159
|
'ls',
|
|
157
160
|
'tree',
|
|
158
|
-
'
|
|
161
|
+
'shell',
|
|
159
162
|
'update_todos',
|
|
160
163
|
'glob',
|
|
161
164
|
'ripgrep',
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadConfig,
|
|
3
3
|
logger,
|
|
4
|
+
getConfiguredProviderFamily,
|
|
4
5
|
getSessionSystemPromptPath,
|
|
5
6
|
getModelFamily,
|
|
7
|
+
type OttoConfig,
|
|
6
8
|
} from '@ottocode/sdk';
|
|
7
9
|
import { wrapLanguageModel } from 'ai';
|
|
8
10
|
import { devToolsMiddleware } from '@ai-sdk/devtools';
|
|
@@ -19,7 +21,7 @@ import type { Tool } from 'ai';
|
|
|
19
21
|
import { adaptTools } from '../../tools/adapter.ts';
|
|
20
22
|
import { buildDatabaseTools } from '../../tools/database/index.ts';
|
|
21
23
|
import { time } from '../debug/index.ts';
|
|
22
|
-
import { isDevtoolsEnabled } from '../debug/state.ts';
|
|
24
|
+
import { isDebugEnabled, isDevtoolsEnabled } from '../debug/state.ts';
|
|
23
25
|
import { buildHistoryMessages } from '../message/history-builder.ts';
|
|
24
26
|
import { getMaxOutputTokens } from '../utils/token.ts';
|
|
25
27
|
import { setupToolContext } from '../tools/setup.ts';
|
|
@@ -29,6 +31,39 @@ import { buildReasoningConfig } from '../provider/reasoning.ts';
|
|
|
29
31
|
import type { RunOpts } from '../session/queue.ts';
|
|
30
32
|
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
31
33
|
|
|
34
|
+
type RunnerSetupTimings = {
|
|
35
|
+
loadConfigAndDbMs: number;
|
|
36
|
+
resolveAgentConfigMs: number;
|
|
37
|
+
buildHistoryMs: number;
|
|
38
|
+
loadSessionMs: number;
|
|
39
|
+
composeSystemPromptMs: number;
|
|
40
|
+
discoverToolsMs: number;
|
|
41
|
+
resolveModelMs: number;
|
|
42
|
+
setupToolContextMs: number;
|
|
43
|
+
buildToolsetMs: number;
|
|
44
|
+
totalMs: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type TimedResult<T> = {
|
|
48
|
+
value: T;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function nowMs(): number {
|
|
53
|
+
const perf = globalThis.performance;
|
|
54
|
+
if (perf && typeof perf.now === 'function') return perf.now();
|
|
55
|
+
return Date.now();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function timePromise<T>(promise: Promise<T>): Promise<TimedResult<T>> {
|
|
59
|
+
const startedAt = nowMs();
|
|
60
|
+
const value = await promise;
|
|
61
|
+
return {
|
|
62
|
+
value,
|
|
63
|
+
durationMs: nowMs() - startedAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
export interface SetupResult {
|
|
33
68
|
cfg: Awaited<ReturnType<typeof loadConfig>>;
|
|
34
69
|
db: Awaited<ReturnType<typeof getDb>>;
|
|
@@ -50,6 +85,7 @@ export interface SetupResult {
|
|
|
50
85
|
needsSpoof: boolean;
|
|
51
86
|
isOpenAIOAuth: boolean;
|
|
52
87
|
mcpToolsRecord: Record<string, Tool>;
|
|
88
|
+
timings: RunnerSetupTimings;
|
|
53
89
|
}
|
|
54
90
|
|
|
55
91
|
export function mergeProviderOptions(
|
|
@@ -86,15 +122,27 @@ const MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS = new Set([
|
|
|
86
122
|
'init',
|
|
87
123
|
]);
|
|
88
124
|
|
|
125
|
+
function normalizeToolName(toolName: string): string {
|
|
126
|
+
return toolName === 'bash' ? 'shell' : toolName;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeToolNames(toolNames: string[]): string[] {
|
|
130
|
+
return Array.from(new Set(toolNames.map(normalizeToolName)));
|
|
131
|
+
}
|
|
132
|
+
|
|
89
133
|
export function applyModelFamilyEditToolPolicy(
|
|
90
134
|
agent: string,
|
|
91
135
|
tools: string[],
|
|
92
136
|
provider: RunOpts['provider'],
|
|
93
137
|
model: string,
|
|
138
|
+
cfg?: OttoConfig,
|
|
94
139
|
): string[] {
|
|
140
|
+
tools = normalizeToolNames(tools);
|
|
95
141
|
if (!MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS.has(agent)) return tools;
|
|
96
142
|
|
|
97
|
-
const family =
|
|
143
|
+
const family = cfg
|
|
144
|
+
? getConfiguredProviderFamily(cfg, provider, model)
|
|
145
|
+
: getModelFamily(provider, model);
|
|
98
146
|
const next = tools.filter(
|
|
99
147
|
(toolName) => !EDITING_TOOL_NAMES.includes(toolName),
|
|
100
148
|
);
|
|
@@ -107,40 +155,67 @@ export function applyModelFamilyEditToolPolicy(
|
|
|
107
155
|
}
|
|
108
156
|
|
|
109
157
|
export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
158
|
+
const setupStartedAt = nowMs();
|
|
110
159
|
const cfgTimer = time('runner:loadConfig+db');
|
|
160
|
+
const loadConfigAndDbStartedAt = nowMs();
|
|
111
161
|
const cfg = await loadConfig(opts.projectRoot);
|
|
112
162
|
const db = await getDb(cfg.projectRoot);
|
|
163
|
+
const loadConfigAndDbMs = nowMs() - loadConfigAndDbStartedAt;
|
|
113
164
|
cfgTimer.end();
|
|
114
165
|
|
|
115
166
|
const agentTimer = time('runner:resolveAgentConfig');
|
|
116
|
-
const
|
|
167
|
+
const agentCfgPromise = timePromise(
|
|
168
|
+
resolveAgentConfig(cfg.projectRoot, opts.agent),
|
|
169
|
+
);
|
|
170
|
+
const historyPromise =
|
|
171
|
+
opts.omitHistory || (opts.isCompactCommand && opts.compactionContext)
|
|
172
|
+
? Promise.resolve({ value: [], durationMs: 0 })
|
|
173
|
+
: timePromise(
|
|
174
|
+
buildHistoryMessages(db, opts.sessionId, opts.assistantMessageId),
|
|
175
|
+
);
|
|
176
|
+
const sessionRowsPromise = timePromise(
|
|
177
|
+
db.select().from(sessions).where(eq(sessions.id, opts.sessionId)).limit(1),
|
|
178
|
+
);
|
|
179
|
+
const discoveredToolsPromise = timePromise(
|
|
180
|
+
discoverProjectTools(cfg.projectRoot, undefined, cfg.skills),
|
|
181
|
+
);
|
|
182
|
+
const { value: agentCfg, durationMs: resolveAgentConfigMs } =
|
|
183
|
+
await agentCfgPromise;
|
|
117
184
|
agentTimer.end({ agent: opts.agent });
|
|
118
185
|
|
|
119
186
|
const agentPrompt = agentCfg.prompt || '';
|
|
120
187
|
|
|
121
188
|
const historyTimer = time('runner:buildHistory');
|
|
122
|
-
|
|
123
|
-
if (opts.omitHistory || (opts.isCompactCommand && opts.compactionContext)) {
|
|
124
|
-
history = [];
|
|
125
|
-
} else {
|
|
126
|
-
history = await buildHistoryMessages(
|
|
127
|
-
db,
|
|
128
|
-
opts.sessionId,
|
|
129
|
-
opts.assistantMessageId,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
189
|
+
const { value: history, durationMs: buildHistoryMs } = await historyPromise;
|
|
132
190
|
historyTimer.end({ messages: history.length });
|
|
133
191
|
|
|
134
|
-
const sessionRows
|
|
135
|
-
|
|
136
|
-
.from(sessions)
|
|
137
|
-
.where(eq(sessions.id, opts.sessionId))
|
|
138
|
-
.limit(1);
|
|
192
|
+
const { value: sessionRows, durationMs: loadSessionMs } =
|
|
193
|
+
await sessionRowsPromise;
|
|
139
194
|
const contextSummary = sessionRows[0]?.contextSummary ?? undefined;
|
|
195
|
+
const toolsTimer = time('runner:discoverTools');
|
|
196
|
+
const { value: discovered, durationMs: discoverToolsMs } =
|
|
197
|
+
await discoveredToolsPromise;
|
|
198
|
+
const allTools = discovered.tools;
|
|
199
|
+
const { mcpToolsRecord } = discovered;
|
|
200
|
+
|
|
201
|
+
if (opts.agent === 'research') {
|
|
202
|
+
const currentSession = sessionRows[0];
|
|
203
|
+
const parentSessionId = currentSession?.parentSessionId ?? null;
|
|
204
|
+
|
|
205
|
+
const dbTools = buildDatabaseTools(cfg.projectRoot, parentSessionId);
|
|
206
|
+
for (const dt of dbTools) {
|
|
207
|
+
discovered.tools.push(dt);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
toolsTimer.end({
|
|
212
|
+
count: allTools.length + Object.keys(mcpToolsRecord).length,
|
|
213
|
+
});
|
|
140
214
|
|
|
141
215
|
const isFirstMessage = !history.some((m) => m.role === 'assistant');
|
|
142
216
|
|
|
143
217
|
const systemTimer = time('runner:composeSystemPrompt');
|
|
218
|
+
const composeSystemPromptStartedAt = nowMs();
|
|
144
219
|
const { getAuth } = await import('@ottocode/sdk');
|
|
145
220
|
const auth = await getAuth(opts.provider, cfg.projectRoot);
|
|
146
221
|
const oauth = detectOAuth(opts.provider, auth);
|
|
@@ -148,6 +223,8 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
148
223
|
const composed = await composeSystemPrompt({
|
|
149
224
|
provider: opts.provider,
|
|
150
225
|
model: opts.model,
|
|
226
|
+
promptFamily: getConfiguredProviderFamily(cfg, opts.provider, opts.model),
|
|
227
|
+
skillSettings: cfg.skills,
|
|
151
228
|
projectRoot: cfg.projectRoot,
|
|
152
229
|
agentPrompt,
|
|
153
230
|
oneShot: opts.oneShot,
|
|
@@ -180,6 +257,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
180
257
|
: oauth.needsSpoof
|
|
181
258
|
? 'spoof'
|
|
182
259
|
: 'standard';
|
|
260
|
+
const composeSystemPromptMs = nowMs() - composeSystemPromptStartedAt;
|
|
183
261
|
systemTimer.end();
|
|
184
262
|
logger.debug('[prompt] system prompt assembled', {
|
|
185
263
|
sessionId: opts.sessionId,
|
|
@@ -216,7 +294,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
216
294
|
(message) => message.role,
|
|
217
295
|
),
|
|
218
296
|
});
|
|
219
|
-
if (effectiveSystemPrompt) {
|
|
297
|
+
if (effectiveSystemPrompt && isDebugEnabled()) {
|
|
220
298
|
const systemPromptPath = getSessionSystemPromptPath(opts.sessionId);
|
|
221
299
|
try {
|
|
222
300
|
await mkdir(dirname(systemPromptPath), { recursive: true });
|
|
@@ -253,40 +331,27 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
253
331
|
if (opts.additionalPromptMessages?.length) {
|
|
254
332
|
additionalSystemMessages.push(...opts.additionalPromptMessages);
|
|
255
333
|
}
|
|
256
|
-
|
|
257
|
-
const toolsTimer = time('runner:discoverTools');
|
|
258
|
-
const discovered = await discoverProjectTools(cfg.projectRoot);
|
|
259
|
-
const allTools = discovered.tools;
|
|
260
|
-
const { mcpToolsRecord } = discovered;
|
|
261
|
-
|
|
262
|
-
if (opts.agent === 'research') {
|
|
263
|
-
const currentSession = sessionRows[0];
|
|
264
|
-
const parentSessionId = currentSession?.parentSessionId ?? null;
|
|
265
|
-
|
|
266
|
-
const dbTools = buildDatabaseTools(cfg.projectRoot, parentSessionId);
|
|
267
|
-
for (const dt of dbTools) {
|
|
268
|
-
discovered.tools.push(dt);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
toolsTimer.end({
|
|
273
|
-
count: allTools.length + Object.keys(mcpToolsRecord).length,
|
|
274
|
-
});
|
|
275
334
|
const allowedToolNames = applyModelFamilyEditToolPolicy(
|
|
276
335
|
agentCfg.name,
|
|
277
336
|
agentCfg.tools || [],
|
|
278
337
|
opts.provider,
|
|
279
338
|
opts.model,
|
|
280
339
|
);
|
|
281
|
-
const allowedNames = new Set([
|
|
340
|
+
const allowedNames = new Set([
|
|
341
|
+
...normalizeToolNames(allowedToolNames),
|
|
342
|
+
'finish',
|
|
343
|
+
]);
|
|
282
344
|
const gated = allTools.filter(
|
|
283
345
|
(tool) => allowedNames.has(tool.name) || tool.name === 'load_mcp_tools',
|
|
284
346
|
);
|
|
285
347
|
|
|
348
|
+
const resolveModelStartedAt = nowMs();
|
|
286
349
|
const model = await resolveModel(opts.provider, opts.model, cfg, {
|
|
287
350
|
sessionId: opts.sessionId,
|
|
288
351
|
messageId: opts.assistantMessageId,
|
|
352
|
+
reasoningText: opts.reasoningText,
|
|
289
353
|
});
|
|
354
|
+
const resolveModelMs = nowMs() - resolveModelStartedAt;
|
|
290
355
|
const wrappedModel = isDevtoolsEnabled()
|
|
291
356
|
? wrapLanguageModel({
|
|
292
357
|
// biome-ignore lint/suspicious/noExplicitAny: OpenRouter provider uses v2 spec
|
|
@@ -297,14 +362,18 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
297
362
|
|
|
298
363
|
const maxOutputTokens = adapted.maxOutputTokens;
|
|
299
364
|
|
|
365
|
+
const setupToolContextStartedAt = nowMs();
|
|
300
366
|
const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
|
|
301
367
|
opts,
|
|
302
368
|
db,
|
|
303
369
|
);
|
|
370
|
+
const setupToolContextMs = nowMs() - setupToolContextStartedAt;
|
|
304
371
|
|
|
372
|
+
const buildToolsetStartedAt = nowMs();
|
|
305
373
|
const providerAuth = await getAuth(opts.provider, opts.projectRoot);
|
|
306
374
|
const authType = providerAuth?.type;
|
|
307
375
|
const toolset = adaptTools(gated, sharedCtx, opts.provider, authType);
|
|
376
|
+
const buildToolsetMs = nowMs() - buildToolsetStartedAt;
|
|
308
377
|
|
|
309
378
|
const providerOptions = { ...adapted.providerOptions };
|
|
310
379
|
let effectiveMaxOutputTokens = maxOutputTokens;
|
|
@@ -317,6 +386,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
317
386
|
}
|
|
318
387
|
|
|
319
388
|
const reasoningConfig = buildReasoningConfig({
|
|
389
|
+
cfg,
|
|
320
390
|
provider: opts.provider,
|
|
321
391
|
model: opts.model,
|
|
322
392
|
reasoningText: opts.reasoningText,
|
|
@@ -326,6 +396,32 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
326
396
|
mergeProviderOptions(providerOptions, reasoningConfig.providerOptions);
|
|
327
397
|
effectiveMaxOutputTokens = reasoningConfig.effectiveMaxOutputTokens;
|
|
328
398
|
|
|
399
|
+
const timings: RunnerSetupTimings = {
|
|
400
|
+
loadConfigAndDbMs,
|
|
401
|
+
resolveAgentConfigMs,
|
|
402
|
+
buildHistoryMs,
|
|
403
|
+
loadSessionMs,
|
|
404
|
+
composeSystemPromptMs,
|
|
405
|
+
discoverToolsMs,
|
|
406
|
+
resolveModelMs,
|
|
407
|
+
setupToolContextMs,
|
|
408
|
+
buildToolsetMs,
|
|
409
|
+
totalMs: nowMs() - setupStartedAt,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
logger.info('[latency] runner setup', {
|
|
413
|
+
sessionId: opts.sessionId,
|
|
414
|
+
messageId: opts.assistantMessageId,
|
|
415
|
+
agent: opts.agent,
|
|
416
|
+
provider: opts.provider,
|
|
417
|
+
model: opts.model,
|
|
418
|
+
historyMessages: history.length,
|
|
419
|
+
systemPromptChars: effectiveSystemPrompt.length,
|
|
420
|
+
additionalPromptMessages: additionalSystemMessages.length,
|
|
421
|
+
allowedToolCount: gated.length,
|
|
422
|
+
timings,
|
|
423
|
+
});
|
|
424
|
+
|
|
329
425
|
return {
|
|
330
426
|
cfg,
|
|
331
427
|
db,
|
|
@@ -345,5 +441,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
345
441
|
needsSpoof: oauth.needsSpoof,
|
|
346
442
|
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
347
443
|
mcpToolsRecord,
|
|
444
|
+
timings,
|
|
348
445
|
};
|
|
349
446
|
}
|