@ottocode/server 0.1.265 → 0.1.266
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/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +2 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +67 -264
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -1,35 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadConfig,
|
|
3
|
-
logger,
|
|
4
|
-
getConfiguredProviderFamily,
|
|
5
|
-
getSessionSystemPromptPath,
|
|
6
|
-
getModelFamily,
|
|
7
|
-
type OttoConfig,
|
|
8
|
-
} from '@ottocode/sdk';
|
|
9
|
-
import { wrapLanguageModel } from 'ai';
|
|
10
|
-
import { devToolsMiddleware } from '@ai-sdk/devtools';
|
|
1
|
+
import { discoverProjectTools, loadConfig, logger } from '@ottocode/sdk';
|
|
11
2
|
import { getDb } from '@ottocode/database';
|
|
12
3
|
import { sessions } from '@ottocode/database/schema';
|
|
13
4
|
import { eq } from 'drizzle-orm';
|
|
14
|
-
import { mkdir } from 'node:fs/promises';
|
|
15
|
-
import { dirname } from 'node:path';
|
|
16
|
-
import { resolveModel } from '../provider/index.ts';
|
|
17
|
-
import { resolveAgentConfig } from './registry.ts';
|
|
18
|
-
import { composeSystemPrompt } from '../prompt/builder.ts';
|
|
19
|
-
import { discoverProjectTools } from '@ottocode/sdk';
|
|
20
5
|
import type { Tool } from 'ai';
|
|
21
6
|
import { adaptTools } from '../../tools/adapter.ts';
|
|
7
|
+
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
22
8
|
import { buildDatabaseTools } from '../../tools/database/index.ts';
|
|
23
9
|
import { time } from '../debug/index.ts';
|
|
24
|
-
import { isDebugEnabled, isDevtoolsEnabled } from '../debug/state.ts';
|
|
25
10
|
import { buildHistoryMessages } from '../message/history-builder.ts';
|
|
26
|
-
import { getMaxOutputTokens } from '../utils/token.ts';
|
|
27
11
|
import { setupToolContext } from '../tools/setup.ts';
|
|
28
|
-
import { getCompactionSystemPrompt } from '../message/compaction.ts';
|
|
29
|
-
import { detectOAuth, adaptRunnerCall } from '../provider/oauth-adapter.ts';
|
|
30
|
-
import { buildReasoningConfig } from '../provider/reasoning.ts';
|
|
31
12
|
import type { RunOpts } from '../session/queue.ts';
|
|
32
|
-
import
|
|
13
|
+
import { resolveAgentConfig } from './registry.ts';
|
|
14
|
+
import {
|
|
15
|
+
appendRunnerPromptMessages,
|
|
16
|
+
buildRunnerPrompt,
|
|
17
|
+
} from './runner-setup-prompt.ts';
|
|
18
|
+
import {
|
|
19
|
+
buildAllowedTools,
|
|
20
|
+
applyModelFamilyEditToolPolicy,
|
|
21
|
+
mergeProviderOptions,
|
|
22
|
+
} from './runner-setup-tools.ts';
|
|
23
|
+
import {
|
|
24
|
+
buildRunnerProviderOptions,
|
|
25
|
+
resolveRunnerModel,
|
|
26
|
+
} from './runner-setup-model.ts';
|
|
27
|
+
import { nowMs, timePromise } from './runner-setup-utils.ts';
|
|
28
|
+
|
|
29
|
+
export { applyModelFamilyEditToolPolicy, mergeProviderOptions };
|
|
33
30
|
|
|
34
31
|
type RunnerSetupTimings = {
|
|
35
32
|
loadConfigAndDbMs: number;
|
|
@@ -44,26 +41,6 @@ type RunnerSetupTimings = {
|
|
|
44
41
|
totalMs: number;
|
|
45
42
|
};
|
|
46
43
|
|
|
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
|
-
|
|
67
44
|
export interface SetupResult {
|
|
68
45
|
cfg: Awaited<ReturnType<typeof loadConfig>>;
|
|
69
46
|
db: Awaited<ReturnType<typeof getDb>>;
|
|
@@ -72,9 +49,7 @@ export interface SetupResult {
|
|
|
72
49
|
system: string;
|
|
73
50
|
systemComponents: string[];
|
|
74
51
|
additionalSystemMessages: Array<{ role: 'system' | 'user'; content: string }>;
|
|
75
|
-
model:
|
|
76
|
-
| Awaited<ReturnType<typeof resolveModel>>
|
|
77
|
-
| ReturnType<typeof wrapLanguageModel>;
|
|
52
|
+
model: Awaited<ReturnType<typeof resolveRunnerModel>>['model'];
|
|
78
53
|
maxOutputTokens: number | undefined;
|
|
79
54
|
effectiveMaxOutputTokens: number | undefined;
|
|
80
55
|
toolset: ReturnType<typeof adaptTools>;
|
|
@@ -88,78 +63,6 @@ export interface SetupResult {
|
|
|
88
63
|
timings: RunnerSetupTimings;
|
|
89
64
|
}
|
|
90
65
|
|
|
91
|
-
export function mergeProviderOptions(
|
|
92
|
-
base: Record<string, unknown>,
|
|
93
|
-
incoming: Record<string, unknown>,
|
|
94
|
-
): Record<string, unknown> {
|
|
95
|
-
for (const [key, value] of Object.entries(incoming)) {
|
|
96
|
-
const existing = base[key];
|
|
97
|
-
if (
|
|
98
|
-
existing &&
|
|
99
|
-
typeof existing === 'object' &&
|
|
100
|
-
!Array.isArray(existing) &&
|
|
101
|
-
value &&
|
|
102
|
-
typeof value === 'object' &&
|
|
103
|
-
!Array.isArray(value)
|
|
104
|
-
) {
|
|
105
|
-
base[key] = {
|
|
106
|
-
...(existing as Record<string, unknown>),
|
|
107
|
-
...(value as Record<string, unknown>),
|
|
108
|
-
};
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
base[key] = value;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return base;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const EDITING_TOOL_NAMES = [
|
|
119
|
-
'edit',
|
|
120
|
-
'multiedit',
|
|
121
|
-
'write',
|
|
122
|
-
'copy_into',
|
|
123
|
-
'apply_patch',
|
|
124
|
-
];
|
|
125
|
-
const MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS = new Set([
|
|
126
|
-
'build',
|
|
127
|
-
'general',
|
|
128
|
-
'init',
|
|
129
|
-
]);
|
|
130
|
-
|
|
131
|
-
function normalizeToolName(toolName: string): string {
|
|
132
|
-
return toolName === 'bash' ? 'shell' : toolName;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function normalizeToolNames(toolNames: string[]): string[] {
|
|
136
|
-
return Array.from(new Set(toolNames.map(normalizeToolName)));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function applyModelFamilyEditToolPolicy(
|
|
140
|
-
agent: string,
|
|
141
|
-
tools: string[],
|
|
142
|
-
provider: RunOpts['provider'],
|
|
143
|
-
model: string,
|
|
144
|
-
cfg?: OttoConfig,
|
|
145
|
-
): string[] {
|
|
146
|
-
tools = normalizeToolNames(tools);
|
|
147
|
-
if (!MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS.has(agent)) return tools;
|
|
148
|
-
|
|
149
|
-
const family = cfg
|
|
150
|
-
? getConfiguredProviderFamily(cfg, provider, model)
|
|
151
|
-
: getModelFamily(provider, model);
|
|
152
|
-
const next = tools.filter(
|
|
153
|
-
(toolName) => !EDITING_TOOL_NAMES.includes(toolName),
|
|
154
|
-
);
|
|
155
|
-
const preferredEditingTools =
|
|
156
|
-
family === 'anthropic' || family === 'openai'
|
|
157
|
-
? ['write', 'copy_into', 'apply_patch']
|
|
158
|
-
: ['write', 'edit', 'multiedit', 'copy_into'];
|
|
159
|
-
|
|
160
|
-
return Array.from(new Set([...next, ...preferredEditingTools]));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
66
|
export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
164
67
|
const setupStartedAt = nowMs();
|
|
165
68
|
const cfgTimer = time('runner:loadConfig+db');
|
|
@@ -189,8 +92,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
189
92
|
await agentCfgPromise;
|
|
190
93
|
agentTimer.end({ agent: opts.agent });
|
|
191
94
|
|
|
192
|
-
const agentPrompt = agentCfg.prompt || '';
|
|
193
|
-
|
|
194
95
|
const historyTimer = time('runner:buildHistory');
|
|
195
96
|
const { value: history, durationMs: buildHistoryMs } = await historyPromise;
|
|
196
97
|
historyTimer.end({ messages: history.length });
|
|
@@ -207,7 +108,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
207
108
|
if (opts.agent === 'research') {
|
|
208
109
|
const currentSession = sessionRows[0];
|
|
209
110
|
const parentSessionId = currentSession?.parentSessionId ?? null;
|
|
210
|
-
|
|
211
111
|
const dbTools = buildDatabaseTools(cfg.projectRoot, parentSessionId);
|
|
212
112
|
for (const dt of dbTools) {
|
|
213
113
|
discovered.tools.push(dt);
|
|
@@ -221,152 +121,30 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
221
121
|
const isFirstMessage = !history.some((m) => m.role === 'assistant');
|
|
222
122
|
|
|
223
123
|
const systemTimer = time('runner:composeSystemPrompt');
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const composed = await composeSystemPrompt({
|
|
230
|
-
provider: opts.provider,
|
|
231
|
-
model: opts.model,
|
|
232
|
-
promptFamily: getConfiguredProviderFamily(cfg, opts.provider, opts.model),
|
|
233
|
-
skillSettings: cfg.skills,
|
|
234
|
-
projectRoot: cfg.projectRoot,
|
|
235
|
-
agentPrompt,
|
|
236
|
-
oneShot: opts.oneShot,
|
|
237
|
-
guidedMode: cfg.defaults.guidedMode,
|
|
238
|
-
spoofPrompt: undefined,
|
|
239
|
-
includeProjectTree: false,
|
|
240
|
-
userContext: opts.userContext,
|
|
124
|
+
const prompt = await buildRunnerPrompt({
|
|
125
|
+
opts,
|
|
126
|
+
cfg,
|
|
127
|
+
agentPrompt: agentCfg.prompt || '',
|
|
241
128
|
contextSummary,
|
|
242
|
-
|
|
129
|
+
historyLength: history.length,
|
|
130
|
+
isFirstMessage,
|
|
243
131
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
rawMaxOutputTokens,
|
|
132
|
+
systemTimer.end();
|
|
133
|
+
appendRunnerPromptMessages({
|
|
134
|
+
opts,
|
|
135
|
+
additionalSystemMessages: prompt.additionalSystemMessages,
|
|
249
136
|
});
|
|
250
137
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
| Record<string, unknown>
|
|
255
|
-
| undefined;
|
|
256
|
-
const openAIInstructions =
|
|
257
|
-
typeof openAIProviderOptions?.instructions === 'string'
|
|
258
|
-
? openAIProviderOptions.instructions
|
|
259
|
-
: '';
|
|
260
|
-
const effectiveSystemPrompt = system || openAIInstructions || composed.prompt;
|
|
261
|
-
const promptMode = oauth.isOpenAIOAuth
|
|
262
|
-
? 'openai-oauth'
|
|
263
|
-
: oauth.needsSpoof
|
|
264
|
-
? 'spoof'
|
|
265
|
-
: 'standard';
|
|
266
|
-
const composeSystemPromptMs = nowMs() - composeSystemPromptStartedAt;
|
|
267
|
-
systemTimer.end();
|
|
268
|
-
logger.debug('[prompt] system prompt assembled', {
|
|
269
|
-
sessionId: opts.sessionId,
|
|
270
|
-
messageId: opts.assistantMessageId,
|
|
271
|
-
agent: opts.agent,
|
|
138
|
+
const gated = buildAllowedTools({
|
|
139
|
+
agentName: agentCfg.name,
|
|
140
|
+
agentTools: agentCfg.tools || [],
|
|
272
141
|
provider: opts.provider,
|
|
273
142
|
model: opts.model,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
systemLength: effectiveSystemPrompt.length,
|
|
277
|
-
historyMessages: history.length,
|
|
278
|
-
additionalSystemMessages: additionalSystemMessages.length,
|
|
279
|
-
isFirstMessage,
|
|
280
|
-
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
281
|
-
needsSpoof: oauth.needsSpoof,
|
|
282
|
-
});
|
|
283
|
-
logger.debug('[prompt] detailed prompt context', {
|
|
284
|
-
sessionId: opts.sessionId,
|
|
285
|
-
messageId: opts.assistantMessageId,
|
|
286
|
-
debugDetail: true,
|
|
287
|
-
agentPromptLength: agentPrompt.length,
|
|
288
|
-
contextSummaryLength: contextSummary?.length ?? 0,
|
|
289
|
-
userContextLength: opts.userContext?.length ?? 0,
|
|
290
|
-
oneShot: Boolean(opts.oneShot),
|
|
291
|
-
guidedMode: Boolean(cfg.defaults.guidedMode),
|
|
292
|
-
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
293
|
-
needsSpoof: oauth.needsSpoof,
|
|
294
|
-
promptMode,
|
|
295
|
-
rawSystemLength: system.length,
|
|
296
|
-
openAIInstructionsLength: openAIInstructions.length,
|
|
297
|
-
effectiveSystemPromptLength: effectiveSystemPrompt.length,
|
|
298
|
-
systemComponents,
|
|
299
|
-
additionalSystemMessageRoles: additionalSystemMessages.map(
|
|
300
|
-
(message) => message.role,
|
|
301
|
-
),
|
|
302
|
-
});
|
|
303
|
-
if (effectiveSystemPrompt && isDebugEnabled()) {
|
|
304
|
-
const systemPromptPath = getSessionSystemPromptPath(opts.sessionId);
|
|
305
|
-
try {
|
|
306
|
-
await mkdir(dirname(systemPromptPath), { recursive: true });
|
|
307
|
-
await Bun.write(systemPromptPath, effectiveSystemPrompt);
|
|
308
|
-
logger.debug('[prompt] wrote system prompt file', {
|
|
309
|
-
sessionId: opts.sessionId,
|
|
310
|
-
messageId: opts.assistantMessageId,
|
|
311
|
-
path: systemPromptPath,
|
|
312
|
-
debugDetail: true,
|
|
313
|
-
promptMode,
|
|
314
|
-
effectiveSystemPromptLength: effectiveSystemPrompt.length,
|
|
315
|
-
});
|
|
316
|
-
} catch (error) {
|
|
317
|
-
logger.warn('[prompt] failed to write system prompt file', {
|
|
318
|
-
sessionId: opts.sessionId,
|
|
319
|
-
messageId: opts.assistantMessageId,
|
|
320
|
-
error: error instanceof Error ? error.message : String(error),
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (opts.isCompactCommand && opts.compactionContext) {
|
|
326
|
-
const compactPrompt = getCompactionSystemPrompt();
|
|
327
|
-
additionalSystemMessages.push({
|
|
328
|
-
role: 'system',
|
|
329
|
-
content: compactPrompt,
|
|
330
|
-
});
|
|
331
|
-
additionalSystemMessages.push({
|
|
332
|
-
role: 'user',
|
|
333
|
-
content: `Please summarize this conversation:\n\n<conversation-to-summarize>\n${opts.compactionContext}\n</conversation-to-summarize>`,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (opts.additionalPromptMessages?.length) {
|
|
338
|
-
additionalSystemMessages.push(...opts.additionalPromptMessages);
|
|
339
|
-
}
|
|
340
|
-
const allowedToolNames = applyModelFamilyEditToolPolicy(
|
|
341
|
-
agentCfg.name,
|
|
342
|
-
agentCfg.tools || [],
|
|
343
|
-
opts.provider,
|
|
344
|
-
opts.model,
|
|
345
|
-
);
|
|
346
|
-
const allowedNames = new Set([
|
|
347
|
-
...normalizeToolNames(allowedToolNames),
|
|
348
|
-
'finish',
|
|
349
|
-
]);
|
|
350
|
-
const gated = allTools.filter(
|
|
351
|
-
(tool) => allowedNames.has(tool.name) || tool.name === 'load_mcp_tools',
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
const resolveModelStartedAt = nowMs();
|
|
355
|
-
const model = await resolveModel(opts.provider, opts.model, cfg, {
|
|
356
|
-
sessionId: opts.sessionId,
|
|
357
|
-
messageId: opts.assistantMessageId,
|
|
358
|
-
reasoningText: opts.reasoningText,
|
|
143
|
+
cfg,
|
|
144
|
+
allTools,
|
|
359
145
|
});
|
|
360
|
-
const resolveModelMs = nowMs() - resolveModelStartedAt;
|
|
361
|
-
const wrappedModel = isDevtoolsEnabled()
|
|
362
|
-
? wrapLanguageModel({
|
|
363
|
-
// biome-ignore lint/suspicious/noExplicitAny: OpenRouter provider uses v2 spec
|
|
364
|
-
model: model as any,
|
|
365
|
-
middleware: devToolsMiddleware(),
|
|
366
|
-
})
|
|
367
|
-
: model;
|
|
368
146
|
|
|
369
|
-
const
|
|
147
|
+
const { model, resolveModelMs } = await resolveRunnerModel({ opts, cfg });
|
|
370
148
|
|
|
371
149
|
const setupToolContextStartedAt = nowMs();
|
|
372
150
|
const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
|
|
@@ -376,38 +154,26 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
376
154
|
const setupToolContextMs = nowMs() - setupToolContextStartedAt;
|
|
377
155
|
|
|
378
156
|
const buildToolsetStartedAt = nowMs();
|
|
157
|
+
const { getAuth } = await import('@ottocode/sdk');
|
|
379
158
|
const providerAuth = await getAuth(opts.provider, opts.projectRoot);
|
|
380
159
|
const authType = providerAuth?.type;
|
|
381
160
|
const toolset = adaptTools(gated, sharedCtx, opts.provider, authType);
|
|
382
161
|
const buildToolsetMs = nowMs() - buildToolsetStartedAt;
|
|
383
162
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const reasoningConfig = buildReasoningConfig({
|
|
395
|
-
cfg,
|
|
396
|
-
provider: opts.provider,
|
|
397
|
-
model: opts.model,
|
|
398
|
-
reasoningText: opts.reasoningText,
|
|
399
|
-
reasoningLevel: opts.reasoningLevel,
|
|
400
|
-
maxOutputTokens,
|
|
401
|
-
});
|
|
402
|
-
mergeProviderOptions(providerOptions, reasoningConfig.providerOptions);
|
|
403
|
-
effectiveMaxOutputTokens = reasoningConfig.effectiveMaxOutputTokens;
|
|
163
|
+
const { providerOptions, effectiveMaxOutputTokens } =
|
|
164
|
+
buildRunnerProviderOptions({
|
|
165
|
+
cfg,
|
|
166
|
+
opts,
|
|
167
|
+
adaptedProviderOptions: prompt.providerOptions,
|
|
168
|
+
maxOutputTokens: prompt.maxOutputTokens,
|
|
169
|
+
});
|
|
404
170
|
|
|
405
171
|
const timings: RunnerSetupTimings = {
|
|
406
172
|
loadConfigAndDbMs,
|
|
407
173
|
resolveAgentConfigMs,
|
|
408
174
|
buildHistoryMs,
|
|
409
175
|
loadSessionMs,
|
|
410
|
-
composeSystemPromptMs,
|
|
176
|
+
composeSystemPromptMs: prompt.composeSystemPromptMs,
|
|
411
177
|
discoverToolsMs,
|
|
412
178
|
resolveModelMs,
|
|
413
179
|
setupToolContextMs,
|
|
@@ -422,8 +188,8 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
422
188
|
provider: opts.provider,
|
|
423
189
|
model: opts.model,
|
|
424
190
|
historyMessages: history.length,
|
|
425
|
-
systemPromptChars: effectiveSystemPrompt.length,
|
|
426
|
-
additionalPromptMessages: additionalSystemMessages.length,
|
|
191
|
+
systemPromptChars: prompt.effectiveSystemPrompt.length,
|
|
192
|
+
additionalPromptMessages: prompt.additionalSystemMessages.length,
|
|
427
193
|
allowedToolCount: gated.length,
|
|
428
194
|
timings,
|
|
429
195
|
});
|
|
@@ -433,19 +199,19 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
433
199
|
db,
|
|
434
200
|
agentCfg,
|
|
435
201
|
history,
|
|
436
|
-
system,
|
|
437
|
-
systemComponents,
|
|
438
|
-
additionalSystemMessages,
|
|
439
|
-
model
|
|
440
|
-
maxOutputTokens,
|
|
202
|
+
system: prompt.system,
|
|
203
|
+
systemComponents: prompt.systemComponents,
|
|
204
|
+
additionalSystemMessages: prompt.additionalSystemMessages,
|
|
205
|
+
model,
|
|
206
|
+
maxOutputTokens: prompt.maxOutputTokens,
|
|
441
207
|
effectiveMaxOutputTokens,
|
|
442
208
|
toolset,
|
|
443
209
|
sharedCtx,
|
|
444
210
|
firstToolTimer,
|
|
445
211
|
firstToolSeen,
|
|
446
212
|
providerOptions,
|
|
447
|
-
needsSpoof:
|
|
448
|
-
isOpenAIOAuth:
|
|
213
|
+
needsSpoof: prompt.needsSpoof,
|
|
214
|
+
isOpenAIOAuth: prompt.isOpenAIOAuth,
|
|
449
215
|
mcpToolsRecord,
|
|
450
216
|
timings,
|
|
451
217
|
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { logger } from '@ottocode/sdk';
|
|
2
|
+
import { time } from '../debug/index.ts';
|
|
3
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
4
|
+
import type { RunnerMessage } from './runner-reminders.ts';
|
|
5
|
+
import type { SetupResult } from './runner-setup.ts';
|
|
6
|
+
|
|
7
|
+
export function nowMs(): number {
|
|
8
|
+
const perf = globalThis.performance;
|
|
9
|
+
if (perf && typeof perf.now === 'function') return perf.now();
|
|
10
|
+
return Date.now();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function approximateMessageChars(messages: RunnerMessage[]): number {
|
|
14
|
+
let total = 0;
|
|
15
|
+
for (const message of messages) {
|
|
16
|
+
total += message.role.length;
|
|
17
|
+
if (typeof message.content === 'string') {
|
|
18
|
+
total += message.content.length;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
total += JSON.stringify(message.content).length;
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
return total;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function summarizeToolShape(tools: Record<string, unknown>) {
|
|
29
|
+
const names = Object.keys(tools);
|
|
30
|
+
const entries = names.map((name) => {
|
|
31
|
+
const toolValue = tools[name];
|
|
32
|
+
let approxChars = 0;
|
|
33
|
+
try {
|
|
34
|
+
approxChars = JSON.stringify(toolValue).length;
|
|
35
|
+
} catch {}
|
|
36
|
+
return { name, approxChars };
|
|
37
|
+
});
|
|
38
|
+
entries.sort((a, b) => b.approxChars - a.approxChars);
|
|
39
|
+
return {
|
|
40
|
+
toolNames: names,
|
|
41
|
+
toolSchemaCharsApprox: entries.reduce(
|
|
42
|
+
(total, entry) => total + entry.approxChars,
|
|
43
|
+
0,
|
|
44
|
+
),
|
|
45
|
+
largestTools: entries.slice(0, 8),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createFirstOutputLatencyLogger(args: {
|
|
50
|
+
opts: RunOpts;
|
|
51
|
+
runStartedAt: number;
|
|
52
|
+
queueWaitMs: number;
|
|
53
|
+
timings: SetupResult['timings'];
|
|
54
|
+
}) {
|
|
55
|
+
const streamStartTimer = time('runner:first-delta');
|
|
56
|
+
let firstDeltaSeen = false;
|
|
57
|
+
return (kind: 'text' | 'reasoning') => {
|
|
58
|
+
if (firstDeltaSeen) return;
|
|
59
|
+
firstDeltaSeen = true;
|
|
60
|
+
const firstOutputMs = nowMs() - args.runStartedAt;
|
|
61
|
+
streamStartTimer.end({
|
|
62
|
+
kind,
|
|
63
|
+
queueWaitMs: args.queueWaitMs,
|
|
64
|
+
setupMs: args.timings.totalMs,
|
|
65
|
+
});
|
|
66
|
+
logger.info('[latency] first output', {
|
|
67
|
+
sessionId: args.opts.sessionId,
|
|
68
|
+
messageId: args.opts.assistantMessageId,
|
|
69
|
+
agent: args.opts.agent,
|
|
70
|
+
provider: args.opts.provider,
|
|
71
|
+
model: args.opts.model,
|
|
72
|
+
kind,
|
|
73
|
+
queueWaitMs: args.queueWaitMs,
|
|
74
|
+
firstOutputMs,
|
|
75
|
+
setupMs: args.timings.totalMs,
|
|
76
|
+
totalSinceEnqueueMs: args.queueWaitMs + firstOutputMs,
|
|
77
|
+
timings: args.timings,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function logStreamRequestReady(args: {
|
|
83
|
+
opts: RunOpts;
|
|
84
|
+
setup: SetupResult;
|
|
85
|
+
queueWaitMs: number;
|
|
86
|
+
messages: RunnerMessage[];
|
|
87
|
+
toolset: Record<string, unknown>;
|
|
88
|
+
hasPrepareStep: boolean;
|
|
89
|
+
}): void {
|
|
90
|
+
const { opts, setup, queueWaitMs, messages, toolset, hasPrepareStep } = args;
|
|
91
|
+
const toolShape = summarizeToolShape(toolset);
|
|
92
|
+
logger.info('[latency] stream request ready', {
|
|
93
|
+
sessionId: opts.sessionId,
|
|
94
|
+
messageId: opts.assistantMessageId,
|
|
95
|
+
agent: opts.agent,
|
|
96
|
+
provider: opts.provider,
|
|
97
|
+
model: opts.model,
|
|
98
|
+
queueWaitMs,
|
|
99
|
+
setupMs: setup.timings.totalMs,
|
|
100
|
+
messageCount: messages.length,
|
|
101
|
+
toolCount: Object.keys(toolset).length,
|
|
102
|
+
toolNames: toolShape.toolNames,
|
|
103
|
+
toolSchemaCharsApprox: toolShape.toolSchemaCharsApprox,
|
|
104
|
+
largestTools: toolShape.largestTools,
|
|
105
|
+
hasPrepareStep,
|
|
106
|
+
providerOptionsKeys: Object.keys(setup.providerOptions),
|
|
107
|
+
systemPromptChars: setup.system.length,
|
|
108
|
+
messageCharsApprox: approximateMessageChars(messages),
|
|
109
|
+
additionalSystemMessages: setup.additionalSystemMessages.length,
|
|
110
|
+
historyMessages: setup.history.length,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { messageParts } from '@ottocode/database/schema';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { logger } from '@ottocode/sdk';
|
|
4
|
+
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import type { getDb } from '@ottocode/database';
|
|
6
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
7
|
+
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
8
|
+
import type { createTurnDumpCollector } from '../debug/turn-dump.ts';
|
|
9
|
+
import type { RunnerToolObserverState } from './runner-tool-observer.ts';
|
|
10
|
+
import { nowMs } from './runner-telemetry.ts';
|
|
11
|
+
|
|
12
|
+
type TurnDumpCollector = NonNullable<
|
|
13
|
+
ReturnType<typeof createTurnDumpCollector>
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export type RunnerTextState = {
|
|
17
|
+
currentPartId: string | null;
|
|
18
|
+
accumulated: string;
|
|
19
|
+
latestAssistantText: string;
|
|
20
|
+
lastTextDeltaStepIndex: number | null;
|
|
21
|
+
firstPublishedDeltaSeen: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function handleRunnerTextDelta(args: {
|
|
25
|
+
delta: string;
|
|
26
|
+
state: RunnerTextState;
|
|
27
|
+
toolObserver: RunnerToolObserverState;
|
|
28
|
+
opts: RunOpts;
|
|
29
|
+
db: Awaited<ReturnType<typeof getDb>>;
|
|
30
|
+
sharedCtx: ToolAdapterContext;
|
|
31
|
+
stepIndex: number;
|
|
32
|
+
dump: TurnDumpCollector | null;
|
|
33
|
+
firstToolSeen: () => boolean;
|
|
34
|
+
logFirstOutputLatency: (kind: 'text' | 'reasoning') => void;
|
|
35
|
+
runStartedAt: number;
|
|
36
|
+
queueWaitMs: number;
|
|
37
|
+
setupMs: number;
|
|
38
|
+
}): Promise<boolean> {
|
|
39
|
+
const { delta, state, opts, db, sharedCtx, stepIndex, dump } = args;
|
|
40
|
+
state.accumulated += delta;
|
|
41
|
+
if (state.accumulated.trim()) {
|
|
42
|
+
state.latestAssistantText = state.accumulated;
|
|
43
|
+
}
|
|
44
|
+
if (state.accumulated.length > 0) {
|
|
45
|
+
state.lastTextDeltaStepIndex = stepIndex;
|
|
46
|
+
}
|
|
47
|
+
dump?.recordTextDelta(stepIndex, state.accumulated);
|
|
48
|
+
if (
|
|
49
|
+
(delta.trim().length > 0 && args.toolObserver.toolActivityObserved) ||
|
|
50
|
+
(delta.trim().length > 0 && args.firstToolSeen())
|
|
51
|
+
) {
|
|
52
|
+
args.toolObserver.trailingAssistantTextAfterTool = true;
|
|
53
|
+
args.toolObserver.endedWithToolActivity = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!state.currentPartId && !state.accumulated.trim()) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
args.logFirstOutputLatency('text');
|
|
61
|
+
|
|
62
|
+
if (!state.currentPartId) {
|
|
63
|
+
state.currentPartId = crypto.randomUUID();
|
|
64
|
+
sharedCtx.assistantPartId = state.currentPartId;
|
|
65
|
+
await db.insert(messageParts).values({
|
|
66
|
+
id: state.currentPartId,
|
|
67
|
+
messageId: opts.assistantMessageId,
|
|
68
|
+
index: await sharedCtx.nextIndex(),
|
|
69
|
+
stepIndex: null,
|
|
70
|
+
type: 'text',
|
|
71
|
+
content: JSON.stringify({ text: state.accumulated }),
|
|
72
|
+
agent: opts.agent,
|
|
73
|
+
provider: opts.provider,
|
|
74
|
+
model: opts.model,
|
|
75
|
+
startedAt: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
publish({
|
|
80
|
+
type: 'message.part.delta',
|
|
81
|
+
sessionId: opts.sessionId,
|
|
82
|
+
payload: {
|
|
83
|
+
messageId: opts.assistantMessageId,
|
|
84
|
+
partId: state.currentPartId,
|
|
85
|
+
stepIndex,
|
|
86
|
+
delta,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (!state.firstPublishedDeltaSeen) {
|
|
90
|
+
state.firstPublishedDeltaSeen = true;
|
|
91
|
+
logger.info('[latency] first published delta', {
|
|
92
|
+
sessionId: opts.sessionId,
|
|
93
|
+
messageId: opts.assistantMessageId,
|
|
94
|
+
agent: opts.agent,
|
|
95
|
+
provider: opts.provider,
|
|
96
|
+
model: opts.model,
|
|
97
|
+
sinceRunStartMs: nowMs() - args.runStartedAt,
|
|
98
|
+
queueWaitMs: args.queueWaitMs,
|
|
99
|
+
setupMs: args.setupMs,
|
|
100
|
+
deltaPreview: delta.length > 80 ? `${delta.slice(0, 80)}…` : delta,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
await db
|
|
104
|
+
.update(messageParts)
|
|
105
|
+
.set({ content: JSON.stringify({ text: state.accumulated }) })
|
|
106
|
+
.where(eq(messageParts.id, state.currentPartId));
|
|
107
|
+
return true;
|
|
108
|
+
}
|