@ottocode/server 0.1.228 → 0.1.231
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 +1 -2
- package/src/openapi/paths/ask.ts +11 -0
- package/src/openapi/paths/config.ts +17 -0
- package/src/openapi/paths/messages.ts +6 -0
- package/src/openapi/schemas.ts +6 -0
- package/src/routes/ask.ts +8 -0
- package/src/routes/config/defaults.ts +13 -1
- package/src/routes/config/main.ts +7 -0
- package/src/routes/config/models.ts +2 -0
- package/src/routes/mcp.ts +62 -58
- package/src/routes/session-messages.ts +6 -1
- package/src/routes/session-stream.ts +46 -45
- package/src/routes/sessions.ts +4 -1
- package/src/routes/terminals.ts +15 -3
- package/src/routes/tunnel.ts +7 -3
- package/src/runtime/agent/runner-setup.ts +43 -34
- package/src/runtime/agent/runner.ts +171 -8
- package/src/runtime/ask/service.ts +16 -0
- package/src/runtime/debug/turn-dump.ts +330 -0
- package/src/runtime/message/history-builder.ts +99 -91
- package/src/runtime/message/service.ts +16 -2
- package/src/runtime/prompt/builder.ts +8 -6
- package/src/runtime/provider/reasoning.ts +291 -0
- package/src/runtime/session/queue.ts +2 -0
- package/src/runtime/tools/guards.ts +52 -4
- package/src/tools/adapter.ts +87 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig
|
|
1
|
+
import { loadConfig } from '@ottocode/sdk';
|
|
2
2
|
import { wrapLanguageModel } from 'ai';
|
|
3
3
|
import { devToolsMiddleware } from '@ai-sdk/devtools';
|
|
4
4
|
import { getDb } from '@ottocode/database';
|
|
@@ -18,6 +18,7 @@ import { getMaxOutputTokens } from '../utils/token.ts';
|
|
|
18
18
|
import { setupToolContext } from '../tools/setup.ts';
|
|
19
19
|
import { getCompactionSystemPrompt } from '../message/compaction.ts';
|
|
20
20
|
import { detectOAuth, adaptRunnerCall } from '../provider/oauth-adapter.ts';
|
|
21
|
+
import { buildReasoningConfig } from '../provider/reasoning.ts';
|
|
21
22
|
import type { RunOpts } from '../session/queue.ts';
|
|
22
23
|
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
23
24
|
|
|
@@ -44,7 +45,32 @@ export interface SetupResult {
|
|
|
44
45
|
mcpToolsRecord: Record<string, Tool>;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
export function mergeProviderOptions(
|
|
49
|
+
base: Record<string, unknown>,
|
|
50
|
+
incoming: Record<string, unknown>,
|
|
51
|
+
): Record<string, unknown> {
|
|
52
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
53
|
+
const existing = base[key];
|
|
54
|
+
if (
|
|
55
|
+
existing &&
|
|
56
|
+
typeof existing === 'object' &&
|
|
57
|
+
!Array.isArray(existing) &&
|
|
58
|
+
value &&
|
|
59
|
+
typeof value === 'object' &&
|
|
60
|
+
!Array.isArray(value)
|
|
61
|
+
) {
|
|
62
|
+
base[key] = {
|
|
63
|
+
...(existing as Record<string, unknown>),
|
|
64
|
+
...(value as Record<string, unknown>),
|
|
65
|
+
};
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
base[key] = value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return base;
|
|
73
|
+
}
|
|
48
74
|
|
|
49
75
|
export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
50
76
|
const cfgTimer = time('runner:loadConfig+db');
|
|
@@ -218,38 +244,21 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
218
244
|
};
|
|
219
245
|
}
|
|
220
246
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
...((providerOptions.openai as Record<string, unknown>) || {}),
|
|
237
|
-
reasoningEffort: 'high',
|
|
238
|
-
reasoningSummary: 'auto',
|
|
239
|
-
};
|
|
240
|
-
} else if (underlyingProvider === 'google') {
|
|
241
|
-
const isGemini3 = opts.model.includes('gemini-3');
|
|
242
|
-
providerOptions.google = {
|
|
243
|
-
thinkingConfig: isGemini3
|
|
244
|
-
? { thinkingLevel: 'high', includeThoughts: true }
|
|
245
|
-
: { thinkingBudget: THINKING_BUDGET },
|
|
246
|
-
};
|
|
247
|
-
} else if (underlyingProvider === 'openai-compatible') {
|
|
248
|
-
providerOptions.openaiCompatible = {
|
|
249
|
-
reasoningEffort: 'high',
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
}
|
|
247
|
+
const reasoningConfig = buildReasoningConfig({
|
|
248
|
+
provider: opts.provider,
|
|
249
|
+
model: opts.model,
|
|
250
|
+
reasoningText: opts.reasoningText,
|
|
251
|
+
reasoningLevel: opts.reasoningLevel,
|
|
252
|
+
maxOutputTokens,
|
|
253
|
+
});
|
|
254
|
+
mergeProviderOptions(providerOptions, reasoningConfig.providerOptions);
|
|
255
|
+
effectiveMaxOutputTokens = reasoningConfig.effectiveMaxOutputTokens;
|
|
256
|
+
debugLog(
|
|
257
|
+
`[RUNNER] reasoning enabled for ${opts.provider}/${opts.model}: ${reasoningConfig.enabled}, level: ${opts.reasoningLevel ?? 'default'}`,
|
|
258
|
+
);
|
|
259
|
+
debugLog(
|
|
260
|
+
`[RUNNER] final providerOptions: ${JSON.stringify(providerOptions)}`,
|
|
261
|
+
);
|
|
253
262
|
|
|
254
263
|
return {
|
|
255
264
|
cfg,
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
consumeOauthCodexTextDelta,
|
|
43
43
|
} from '../stream/text-guard.ts';
|
|
44
44
|
import { decideOauthCodexContinuation } from './oauth-codex-continuation.ts';
|
|
45
|
+
import { createTurnDumpCollector } from '../debug/turn-dump.ts';
|
|
45
46
|
|
|
46
47
|
export {
|
|
47
48
|
enqueueAssistantRun,
|
|
@@ -52,6 +53,33 @@ export {
|
|
|
52
53
|
getRunnerState,
|
|
53
54
|
} from '../session/queue.ts';
|
|
54
55
|
|
|
56
|
+
const DEFAULT_TRACED_TOOL_INPUTS = new Set(['write', 'apply_patch']);
|
|
57
|
+
|
|
58
|
+
function shouldTraceToolInput(name: string): boolean {
|
|
59
|
+
const raw = process.env.OTTO_DEBUG_TOOL_INPUT?.trim();
|
|
60
|
+
if (!raw) return false;
|
|
61
|
+
const normalized = raw.toLowerCase();
|
|
62
|
+
if (['1', 'true', 'yes', 'on', 'all'].includes(normalized)) {
|
|
63
|
+
return DEFAULT_TRACED_TOOL_INPUTS.has(name);
|
|
64
|
+
}
|
|
65
|
+
const tokens = raw
|
|
66
|
+
.split(/[\s,]+/)
|
|
67
|
+
.map((token) => token.trim().toLowerCase())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
return tokens.includes('all') || tokens.includes(name.toLowerCase());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function summarizeTraceValue(value: unknown, max = 160): string {
|
|
73
|
+
try {
|
|
74
|
+
const json = JSON.stringify(value);
|
|
75
|
+
if (typeof json === 'string') {
|
|
76
|
+
return json.length > max ? `${json.slice(0, max)}…` : json;
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
const fallback = String(value);
|
|
80
|
+
return fallback.length > max ? `${fallback.slice(0, max)}…` : fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
export async function runSessionLoop(sessionId: string) {
|
|
56
84
|
setRunning(sessionId, true);
|
|
57
85
|
|
|
@@ -179,6 +207,31 @@ async function runAssistant(opts: RunOpts) {
|
|
|
179
207
|
`[RUNNER] messagesWithSystemInstructions length: ${messagesWithSystemInstructions.length}`,
|
|
180
208
|
);
|
|
181
209
|
|
|
210
|
+
const dump = createTurnDumpCollector({
|
|
211
|
+
sessionId: opts.sessionId,
|
|
212
|
+
messageId: opts.assistantMessageId,
|
|
213
|
+
provider: opts.provider,
|
|
214
|
+
model: opts.model,
|
|
215
|
+
agent: opts.agent,
|
|
216
|
+
continuationCount: opts.continuationCount,
|
|
217
|
+
});
|
|
218
|
+
if (dump) {
|
|
219
|
+
dump.setSystemPrompt(system, setup.systemComponents);
|
|
220
|
+
dump.setAdditionalSystemMessages(
|
|
221
|
+
additionalSystemMessages as Array<{ role: string; content: string }>,
|
|
222
|
+
);
|
|
223
|
+
dump.setHistory(history as Array<{ role: string; content: unknown }>);
|
|
224
|
+
dump.setFinalMessages(messagesWithSystemInstructions);
|
|
225
|
+
dump.setTools(toolset);
|
|
226
|
+
dump.setModelConfig({
|
|
227
|
+
maxOutputTokens: setup.maxOutputTokens,
|
|
228
|
+
effectiveMaxOutputTokens,
|
|
229
|
+
providerOptions,
|
|
230
|
+
isOpenAIOAuth,
|
|
231
|
+
needsSpoof: setup.needsSpoof,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
182
235
|
let _finishObserved = false;
|
|
183
236
|
let _toolActivityObserved = false;
|
|
184
237
|
let _trailingAssistantTextAfterTool = false;
|
|
@@ -199,15 +252,41 @@ async function runAssistant(opts: RunOpts) {
|
|
|
199
252
|
}
|
|
200
253
|
if (evt.type === 'tool.call') {
|
|
201
254
|
triggerTitleGenerationWhenReady();
|
|
255
|
+
if (dump) {
|
|
256
|
+
try {
|
|
257
|
+
const p = evt.payload as {
|
|
258
|
+
name?: string;
|
|
259
|
+
callId?: string;
|
|
260
|
+
args?: unknown;
|
|
261
|
+
};
|
|
262
|
+
dump.recordToolCall(stepIndex, p.name ?? '', p.callId ?? '', p.args);
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
202
265
|
}
|
|
203
|
-
if (evt.type
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
266
|
+
if (evt.type === 'tool.result') {
|
|
267
|
+
if (dump) {
|
|
268
|
+
try {
|
|
269
|
+
const p = evt.payload as {
|
|
270
|
+
name?: string;
|
|
271
|
+
callId?: string;
|
|
272
|
+
result?: unknown;
|
|
273
|
+
};
|
|
274
|
+
dump.recordToolResult(
|
|
275
|
+
stepIndex,
|
|
276
|
+
p.name ?? '',
|
|
277
|
+
p.callId ?? '',
|
|
278
|
+
p.result,
|
|
279
|
+
);
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const name = (evt.payload as { name?: string } | undefined)?.name;
|
|
284
|
+
if (name === 'finish') _finishObserved = true;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
debugLog(
|
|
287
|
+
`[RUNNER] finish observer error: ${err instanceof Error ? err.message : String(err)}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
211
290
|
}
|
|
212
291
|
});
|
|
213
292
|
|
|
@@ -217,6 +296,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
217
296
|
let currentPartId: string | null = null;
|
|
218
297
|
let accumulated = '';
|
|
219
298
|
let latestAssistantText = '';
|
|
299
|
+
let lastTextDeltaStepIndex: number | null = null;
|
|
220
300
|
let stepIndex = 0;
|
|
221
301
|
const oauthTextGuard = isOpenAIOAuth
|
|
222
302
|
? createOauthCodexTextGuardState()
|
|
@@ -313,10 +393,61 @@ async function runAssistant(opts: RunOpts) {
|
|
|
313
393
|
onFinish: onFinish as any,
|
|
314
394
|
// biome-ignore lint/suspicious/noExplicitAny: AI SDK streamText options type
|
|
315
395
|
} as any);
|
|
396
|
+
const tracedToolInputNamesById = new Map<string, string>();
|
|
316
397
|
|
|
317
398
|
for await (const part of result.fullStream) {
|
|
318
399
|
if (!part) continue;
|
|
319
400
|
|
|
401
|
+
if (part.type === 'tool-input-start') {
|
|
402
|
+
if (shouldTraceToolInput(part.toolName)) {
|
|
403
|
+
tracedToolInputNamesById.set(part.id, part.toolName);
|
|
404
|
+
debugLog(
|
|
405
|
+
`[TOOL_INPUT_TRACE][runner] fullStream tool-input-start tool=${part.toolName} callId=${part.id}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (part.type === 'tool-input-delta') {
|
|
412
|
+
const toolName = tracedToolInputNamesById.get(part.id);
|
|
413
|
+
if (toolName) {
|
|
414
|
+
debugLog(
|
|
415
|
+
`[TOOL_INPUT_TRACE][runner] fullStream tool-input-delta tool=${toolName} callId=${part.id} delta=${summarizeTraceValue(part.delta)}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (part.type === 'tool-input-end') {
|
|
422
|
+
const toolName = tracedToolInputNamesById.get(part.id);
|
|
423
|
+
if (toolName) {
|
|
424
|
+
debugLog(
|
|
425
|
+
`[TOOL_INPUT_TRACE][runner] fullStream tool-input-end tool=${toolName} callId=${part.id}`,
|
|
426
|
+
);
|
|
427
|
+
tracedToolInputNamesById.delete(part.id);
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (part.type === 'tool-call') {
|
|
433
|
+
if (shouldTraceToolInput(part.toolName)) {
|
|
434
|
+
tracedToolInputNamesById.delete(part.toolCallId);
|
|
435
|
+
debugLog(
|
|
436
|
+
`[TOOL_INPUT_TRACE][runner] fullStream tool-call tool=${part.toolName} callId=${part.toolCallId} input=${summarizeTraceValue(part.input)}`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (part.type === 'tool-result') {
|
|
443
|
+
if (shouldTraceToolInput(part.toolName)) {
|
|
444
|
+
debugLog(
|
|
445
|
+
`[TOOL_INPUT_TRACE][runner] fullStream tool-result tool=${part.toolName} callId=${part.toolCallId} output=${summarizeTraceValue(part.output)}`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
320
451
|
if (part.type === 'text-delta') {
|
|
321
452
|
const rawDelta = part.text;
|
|
322
453
|
if (!rawDelta) continue;
|
|
@@ -330,6 +461,10 @@ async function runAssistant(opts: RunOpts) {
|
|
|
330
461
|
if (accumulated.trim()) {
|
|
331
462
|
latestAssistantText = accumulated;
|
|
332
463
|
}
|
|
464
|
+
if (accumulated.length > 0) {
|
|
465
|
+
lastTextDeltaStepIndex = stepIndex;
|
|
466
|
+
}
|
|
467
|
+
dump?.recordTextDelta(stepIndex, accumulated);
|
|
333
468
|
if (
|
|
334
469
|
(delta.trim().length > 0 && _toolActivityObserved) ||
|
|
335
470
|
(delta.trim().length > 0 && firstToolSeen())
|
|
@@ -450,6 +585,23 @@ async function runAssistant(opts: RunOpts) {
|
|
|
450
585
|
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, trailingAssistantTextAfterTool=${_trailingAssistantTextAfterTool}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}`,
|
|
451
586
|
);
|
|
452
587
|
|
|
588
|
+
if (dump) {
|
|
589
|
+
const finalTextSnapshot = latestAssistantText || accumulated;
|
|
590
|
+
if (finalTextSnapshot.length > 0) {
|
|
591
|
+
dump.recordTextDelta(
|
|
592
|
+
lastTextDeltaStepIndex ?? stepIndex,
|
|
593
|
+
finalTextSnapshot,
|
|
594
|
+
{ force: true },
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
dump.recordStreamEnd({
|
|
598
|
+
finishReason: streamFinishReason,
|
|
599
|
+
rawFinishReason: streamRawFinishReason,
|
|
600
|
+
finishObserved: _finishObserved,
|
|
601
|
+
aborted: _abortedByUser,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
453
605
|
const MAX_CONTINUATIONS = 6;
|
|
454
606
|
const continuationCount = opts.continuationCount ?? 0;
|
|
455
607
|
const continuationDecision = decideOauthCodexContinuation({
|
|
@@ -543,6 +695,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
543
695
|
}
|
|
544
696
|
} catch (err) {
|
|
545
697
|
unsubscribeFinish();
|
|
698
|
+
dump?.recordError(err);
|
|
546
699
|
const payload = toErrorPayload(err);
|
|
547
700
|
|
|
548
701
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -627,6 +780,16 @@ async function runAssistant(opts: RunOpts) {
|
|
|
627
780
|
}
|
|
628
781
|
throw err;
|
|
629
782
|
} finally {
|
|
783
|
+
if (dump) {
|
|
784
|
+
try {
|
|
785
|
+
const dumpPath = await dump.flush(cfg.projectRoot);
|
|
786
|
+
debugLog(`[RUNNER] Debug dump written to ${dumpPath}`);
|
|
787
|
+
} catch (dumpErr) {
|
|
788
|
+
debugLog(
|
|
789
|
+
`[RUNNER] Failed to write debug dump: ${dumpErr instanceof Error ? dumpErr.message : String(dumpErr)}`,
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
630
793
|
debugLog(
|
|
631
794
|
`[RUNNER] Turn complete for session ${opts.sessionId}, message ${opts.assistantMessageId}`,
|
|
632
795
|
);
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
isProviderId,
|
|
20
20
|
providerEnvVar,
|
|
21
21
|
type ProviderId,
|
|
22
|
+
type ReasoningLevel,
|
|
22
23
|
} from '@ottocode/sdk';
|
|
23
24
|
import { sessions } from '@ottocode/database/schema';
|
|
24
25
|
import { time } from '../debug/index.ts';
|
|
@@ -48,6 +49,8 @@ export type InjectableConfig = {
|
|
|
48
49
|
model?: string;
|
|
49
50
|
apiKey?: string;
|
|
50
51
|
agent?: string;
|
|
52
|
+
reasoningText?: boolean;
|
|
53
|
+
reasoningLevel?: ReasoningLevel;
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
export type InjectableCredentials = Partial<
|
|
@@ -60,6 +63,8 @@ export type AskServerRequest = {
|
|
|
60
63
|
agent?: string;
|
|
61
64
|
provider?: string;
|
|
62
65
|
model?: string;
|
|
66
|
+
reasoningText?: boolean;
|
|
67
|
+
reasoningLevel?: ReasoningLevel;
|
|
63
68
|
sessionId?: string;
|
|
64
69
|
last?: boolean;
|
|
65
70
|
jsonMode?: boolean;
|
|
@@ -120,6 +125,10 @@ async function processAskRequest(
|
|
|
120
125
|
provider: injectedProvider,
|
|
121
126
|
model: injectedModel,
|
|
122
127
|
agent: injectedAgent,
|
|
128
|
+
reasoningText:
|
|
129
|
+
request.config?.reasoningText ?? request.reasoningText ?? true,
|
|
130
|
+
reasoningLevel:
|
|
131
|
+
request.config?.reasoningLevel ?? request.reasoningLevel ?? 'high',
|
|
123
132
|
},
|
|
124
133
|
providers: {
|
|
125
134
|
openai: { enabled: true },
|
|
@@ -299,6 +308,11 @@ async function processAskRequest(
|
|
|
299
308
|
await ensureProviderEnv(cfg, providerForMessage);
|
|
300
309
|
}
|
|
301
310
|
|
|
311
|
+
const reasoningText =
|
|
312
|
+
request.reasoningText ?? cfg.defaults.reasoningText ?? false;
|
|
313
|
+
const reasoningLevel =
|
|
314
|
+
request.reasoningLevel ?? cfg.defaults.reasoningLevel ?? 'high';
|
|
315
|
+
|
|
302
316
|
const assistantMessage = await dispatchAssistantMessage({
|
|
303
317
|
cfg,
|
|
304
318
|
db,
|
|
@@ -308,6 +322,8 @@ async function processAskRequest(
|
|
|
308
322
|
model: modelForMessage,
|
|
309
323
|
content: request.prompt,
|
|
310
324
|
oneShot: !request.sessionId && !request.last,
|
|
325
|
+
reasoningText,
|
|
326
|
+
reasoningLevel,
|
|
311
327
|
});
|
|
312
328
|
|
|
313
329
|
const headerAgent = session.agent ?? agentName;
|