@ottocode/server 0.1.227 → 0.1.230

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/server",
3
- "version": "0.1.227",
3
+ "version": "0.1.230",
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.227",
53
- "@ottocode/database": "0.1.227",
52
+ "@ottocode/sdk": "0.1.230",
53
+ "@ottocode/database": "0.1.230",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.3.6"
@@ -35,6 +35,17 @@ export const askPaths = {
35
35
  description:
36
36
  'Optional model override for the selected provider.',
37
37
  },
38
+ reasoningText: {
39
+ type: 'boolean',
40
+ description:
41
+ 'Enable extended thinking / reasoning for models that support it.',
42
+ },
43
+ reasoningLevel: {
44
+ type: 'string',
45
+ enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
46
+ description:
47
+ 'Optional reasoning intensity override for supported providers/models.',
48
+ },
38
49
  sessionId: {
39
50
  type: 'string',
40
51
  description: 'Send the prompt to a specific session.',
@@ -179,6 +179,10 @@ export const configPaths = {
179
179
  provider: { type: 'string' },
180
180
  model: { type: 'string' },
181
181
  reasoningText: { type: 'boolean' },
182
+ reasoningLevel: {
183
+ type: 'string',
184
+ enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
185
+ },
182
186
  scope: {
183
187
  type: 'string',
184
188
  enum: ['global', 'local'],
@@ -205,6 +209,17 @@ export const configPaths = {
205
209
  provider: { type: 'string' },
206
210
  model: { type: 'string' },
207
211
  reasoningText: { type: 'boolean' },
212
+ reasoningLevel: {
213
+ type: 'string',
214
+ enum: [
215
+ 'minimal',
216
+ 'low',
217
+ 'medium',
218
+ 'high',
219
+ 'max',
220
+ 'xhigh',
221
+ ],
222
+ },
208
223
  },
209
224
  required: ['agent', 'provider', 'model'],
210
225
  },
@@ -72,6 +72,12 @@ export const messagesPaths = {
72
72
  description:
73
73
  'Enable extended thinking / reasoning for models that support it.',
74
74
  },
75
+ reasoningLevel: {
76
+ type: 'string',
77
+ enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
78
+ description:
79
+ 'Reasoning intensity level for providers/models that support it.',
80
+ },
75
81
  },
76
82
  },
77
83
  },
@@ -73,6 +73,7 @@ export const schemas = {
73
73
  additionalProperties: { type: 'integer' },
74
74
  nullable: true,
75
75
  },
76
+ isRunning: { type: 'boolean' },
76
77
  },
77
78
  required: ['id', 'agent', 'provider', 'model', 'projectPath', 'createdAt'],
78
79
  },
@@ -196,6 +197,10 @@ export const schemas = {
196
197
  provider: { $ref: '#/components/schemas/Provider' },
197
198
  model: { type: 'string' },
198
199
  reasoningText: { type: 'boolean' },
200
+ reasoningLevel: {
201
+ type: 'string',
202
+ enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
203
+ },
199
204
  },
200
205
  required: ['agent', 'provider', 'model'],
201
206
  },
package/src/routes/ask.ts CHANGED
@@ -79,6 +79,14 @@ export function registerAskRoutes(app: Hono) {
79
79
  agent: typeof body.agent === 'string' ? body.agent : undefined,
80
80
  provider: typeof body.provider === 'string' ? body.provider : undefined,
81
81
  model: typeof body.model === 'string' ? body.model : undefined,
82
+ reasoningText:
83
+ typeof body.reasoningText === 'boolean'
84
+ ? body.reasoningText
85
+ : undefined,
86
+ reasoningLevel:
87
+ typeof body.reasoningLevel === 'string'
88
+ ? (body.reasoningLevel as AskServerRequest['reasoningLevel'])
89
+ : undefined,
82
90
  sessionId:
83
91
  typeof body.sessionId === 'string' ? body.sessionId : undefined,
84
92
  last: Boolean(body.last),
@@ -1,5 +1,10 @@
1
1
  import type { Hono } from 'hono';
2
- import { setConfig, loadConfig, type ProviderId } from '@ottocode/sdk';
2
+ import {
3
+ setConfig,
4
+ loadConfig,
5
+ type ProviderId,
6
+ type ReasoningLevel,
7
+ } from '@ottocode/sdk';
3
8
  import { logger } from '@ottocode/sdk';
4
9
  import { serializeError } from '../../runtime/errors/api-error.ts';
5
10
 
@@ -14,6 +19,7 @@ export function registerDefaultsRoute(app: Hono) {
14
19
  toolApproval?: 'auto' | 'dangerous' | 'all';
15
20
  guidedMode?: boolean;
16
21
  reasoningText?: boolean;
22
+ reasoningLevel?: ReasoningLevel;
17
23
  theme?: string;
18
24
  scope?: 'global' | 'local';
19
25
  }>();
@@ -26,6 +32,7 @@ export function registerDefaultsRoute(app: Hono) {
26
32
  toolApproval: 'auto' | 'dangerous' | 'all';
27
33
  guidedMode: boolean;
28
34
  reasoningText: boolean;
35
+ reasoningLevel: ReasoningLevel;
29
36
  theme: string;
30
37
  }> = {};
31
38
 
@@ -36,6 +43,7 @@ export function registerDefaultsRoute(app: Hono) {
36
43
  if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
37
44
  if (body.reasoningText !== undefined)
38
45
  updates.reasoningText = body.reasoningText;
46
+ if (body.reasoningLevel) updates.reasoningLevel = body.reasoningLevel;
39
47
  if (body.theme) updates.theme = body.theme;
40
48
 
41
49
  await setConfig(scope, updates, projectRoot);
@@ -61,6 +61,7 @@ export function registerMainConfigRoute(app: Hono) {
61
61
  ) as 'auto' | 'dangerous' | 'all',
62
62
  guidedMode: cfg.defaults.guidedMode ?? false,
63
63
  reasoningText: cfg.defaults.reasoningText ?? true,
64
+ reasoningLevel: cfg.defaults.reasoningLevel ?? 'high',
64
65
  theme: cfg.defaults.theme,
65
66
  };
66
67
 
@@ -1,5 +1,5 @@
1
1
  import type { Hono } from 'hono';
2
- import { loadConfig } from '@ottocode/sdk';
2
+ import { loadConfig, type ReasoningLevel } from '@ottocode/sdk';
3
3
  import { getDb } from '@ottocode/database';
4
4
  import { messages, messageParts, sessions } from '@ottocode/database/schema';
5
5
  import { eq, inArray } from 'drizzle-orm';
@@ -124,6 +124,10 @@ export function registerSessionMessagesRoutes(app: Hono) {
124
124
 
125
125
  const reasoning =
126
126
  body?.reasoningText ?? cfg.defaults.reasoningText ?? false;
127
+ const reasoningLevel =
128
+ (body?.reasoningLevel as ReasoningLevel | undefined) ??
129
+ cfg.defaults.reasoningLevel ??
130
+ 'high';
127
131
 
128
132
  // Validate model capabilities if tools are allowed for this agent
129
133
  const wantsToolCalls = true; // agent toolset may be non-empty
@@ -158,6 +162,7 @@ export function registerSessionMessagesRoutes(app: Hono) {
158
162
  oneShot: Boolean(body?.oneShot),
159
163
  userContext,
160
164
  reasoningText: reasoning,
165
+ reasoningLevel,
161
166
  images,
162
167
  files,
163
168
  });
@@ -15,6 +15,7 @@ import { resolveAgentConfig } from '../runtime/agent/registry.ts';
15
15
  import { createSession as createSessionRow } from '../runtime/session/manager.ts';
16
16
  import { serializeError } from '../runtime/errors/api-error.ts';
17
17
  import { logger } from '@ottocode/sdk';
18
+ import { getRunnerState } from '../runtime/session/queue.ts';
18
19
 
19
20
  export function registerSessionsRoutes(app: Hono) {
20
21
  // List sessions
@@ -53,7 +54,9 @@ export function registerSessionsRoutes(app: Hono) {
53
54
  } catch {}
54
55
  }
55
56
  const { toolCountsJson: _toolCountsJson, ...rest } = r;
56
- return counts ? { ...rest, toolCounts: counts } : rest;
57
+ const isRunning = getRunnerState(r.id)?.running ?? false;
58
+ const base = counts ? { ...rest, toolCounts: counts } : rest;
59
+ return { ...base, isRunning };
57
60
  });
58
61
  return c.json({
59
62
  items: normalized,
@@ -10,6 +10,7 @@ export type OauthCodexContinuationInput = {
10
10
  firstToolSeen: boolean;
11
11
  hasTrailingAssistantText: boolean;
12
12
  endedWithToolActivity?: boolean;
13
+ lastToolName?: string;
13
14
  droppedPseudoToolText: boolean;
14
15
  lastAssistantText: string;
15
16
  };
@@ -54,7 +55,7 @@ const MAX_UNCLEAN_EOF_RETRIES = 1;
54
55
 
55
56
  function isUncleanEof(input: OauthCodexContinuationInput): boolean {
56
57
  if (input.finishReason && input.finishReason !== 'unknown') return false;
57
- if (input.firstToolSeen) return true;
58
+ if (isMissingAssistantSummary(input)) return true;
58
59
  if (looksLikeIntermediateProgressText(input.lastAssistantText)) return true;
59
60
  return false;
60
61
  }
@@ -82,6 +83,10 @@ export function decideOauthCodexContinuation(
82
83
  return { shouldContinue: true, reason: 'truncated' };
83
84
  }
84
85
 
86
+ if (input.lastToolName === 'finish') {
87
+ return { shouldContinue: false };
88
+ }
89
+
85
90
  if (input.endedWithToolActivity) {
86
91
  return { shouldContinue: true, reason: 'ended-on-tool-activity' };
87
92
  }
@@ -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,27 +207,86 @@ 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;
238
+ let _endedWithToolActivity = false;
239
+ let _lastToolName: string | undefined;
185
240
  let _abortedByUser = false;
186
241
  let titleGenerationTriggered = false;
187
242
  const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
188
243
  if (evt.type === 'tool.call' || evt.type === 'tool.result') {
189
244
  _toolActivityObserved = true;
190
245
  _trailingAssistantTextAfterTool = false;
246
+ _endedWithToolActivity = true;
247
+ try {
248
+ _lastToolName = (evt.payload as { name?: string } | undefined)?.name;
249
+ } catch {
250
+ _lastToolName = undefined;
251
+ }
191
252
  }
192
253
  if (evt.type === 'tool.call') {
193
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
+ }
194
265
  }
195
- if (evt.type !== 'tool.result') return;
196
- try {
197
- const name = (evt.payload as { name?: string } | undefined)?.name;
198
- if (name === 'finish') _finishObserved = true;
199
- } catch (err) {
200
- debugLog(
201
- `[RUNNER] finish observer error: ${err instanceof Error ? err.message : String(err)}`,
202
- );
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
+ }
203
290
  }
204
291
  });
205
292
 
@@ -209,6 +296,7 @@ async function runAssistant(opts: RunOpts) {
209
296
  let currentPartId: string | null = null;
210
297
  let accumulated = '';
211
298
  let latestAssistantText = '';
299
+ let lastTextDeltaStepIndex: number | null = null;
212
300
  let stepIndex = 0;
213
301
  const oauthTextGuard = isOpenAIOAuth
214
302
  ? createOauthCodexTextGuardState()
@@ -305,10 +393,61 @@ async function runAssistant(opts: RunOpts) {
305
393
  onFinish: onFinish as any,
306
394
  // biome-ignore lint/suspicious/noExplicitAny: AI SDK streamText options type
307
395
  } as any);
396
+ const tracedToolInputNamesById = new Map<string, string>();
308
397
 
309
398
  for await (const part of result.fullStream) {
310
399
  if (!part) continue;
311
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
+
312
451
  if (part.type === 'text-delta') {
313
452
  const rawDelta = part.text;
314
453
  if (!rawDelta) continue;
@@ -322,11 +461,16 @@ async function runAssistant(opts: RunOpts) {
322
461
  if (accumulated.trim()) {
323
462
  latestAssistantText = accumulated;
324
463
  }
464
+ if (accumulated.length > 0) {
465
+ lastTextDeltaStepIndex = stepIndex;
466
+ }
467
+ dump?.recordTextDelta(stepIndex, accumulated);
325
468
  if (
326
469
  (delta.trim().length > 0 && _toolActivityObserved) ||
327
470
  (delta.trim().length > 0 && firstToolSeen())
328
471
  ) {
329
472
  _trailingAssistantTextAfterTool = true;
473
+ _endedWithToolActivity = false;
330
474
  }
331
475
 
332
476
  if (!currentPartId && !accumulated.trim()) {
@@ -441,10 +585,25 @@ async function runAssistant(opts: RunOpts) {
441
585
  `[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, trailingAssistantTextAfterTool=${_trailingAssistantTextAfterTool}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}`,
442
586
  );
443
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
+
444
605
  const MAX_CONTINUATIONS = 6;
445
606
  const continuationCount = opts.continuationCount ?? 0;
446
- const endedWithToolActivity =
447
- _toolActivityObserved && !_trailingAssistantTextAfterTool;
448
607
  const continuationDecision = decideOauthCodexContinuation({
449
608
  provider: opts.provider,
450
609
  isOpenAIOAuth,
@@ -456,7 +615,8 @@ async function runAssistant(opts: RunOpts) {
456
615
  rawFinishReason: streamRawFinishReason,
457
616
  firstToolSeen: fs,
458
617
  hasTrailingAssistantText: _trailingAssistantTextAfterTool,
459
- endedWithToolActivity,
618
+ endedWithToolActivity: _endedWithToolActivity,
619
+ lastToolName: _lastToolName,
460
620
  droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
461
621
  lastAssistantText: latestAssistantText,
462
622
  });
@@ -535,6 +695,7 @@ async function runAssistant(opts: RunOpts) {
535
695
  }
536
696
  } catch (err) {
537
697
  unsubscribeFinish();
698
+ dump?.recordError(err);
538
699
  const payload = toErrorPayload(err);
539
700
 
540
701
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -619,6 +780,16 @@ async function runAssistant(opts: RunOpts) {
619
780
  }
620
781
  throw err;
621
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
+ }
622
793
  debugLog(
623
794
  `[RUNNER] Turn complete for session ${opts.sessionId}, message ${opts.assistantMessageId}`,
624
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;