@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.
@@ -1,4 +1,4 @@
1
- import { loadConfig, getUnderlyingProviderKey } from '@ottocode/sdk';
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
- const THINKING_BUDGET = 16000;
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
- if (opts.reasoningText) {
222
- const underlyingProvider = getUnderlyingProviderKey(
223
- opts.provider,
224
- opts.model,
225
- );
226
-
227
- if (underlyingProvider === 'anthropic') {
228
- providerOptions.anthropic = {
229
- thinking: { type: 'enabled', budgetTokens: THINKING_BUDGET },
230
- };
231
- if (maxOutputTokens && maxOutputTokens > THINKING_BUDGET) {
232
- effectiveMaxOutputTokens = maxOutputTokens - THINKING_BUDGET;
233
- }
234
- } else if (underlyingProvider === 'openai') {
235
- providerOptions.openai = {
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 !== 'tool.result') return;
204
- try {
205
- const name = (evt.payload as { name?: string } | undefined)?.name;
206
- if (name === 'finish') _finishObserved = true;
207
- } catch (err) {
208
- debugLog(
209
- `[RUNNER] finish observer error: ${err instanceof Error ? err.message : String(err)}`,
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;