@ottocode/server 0.1.225 → 0.1.227

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.
@@ -63,7 +63,8 @@ export async function getAuthTypeForProvider(
63
63
  projectRoot: string,
64
64
  ): Promise<'api' | 'oauth' | 'wallet' | undefined> {
65
65
  if (embeddedConfig?.auth?.[provider]) {
66
- return embeddedConfig.auth[provider].type as 'api' | 'oauth' | 'wallet';
66
+ const embeddedAuth = embeddedConfig.auth[provider];
67
+ return 'type' in embeddedAuth ? embeddedAuth.type : 'api';
67
68
  }
68
69
  const auth = await getAuth(provider, projectRoot);
69
70
  return auth?.type as 'api' | 'oauth' | 'wallet' | undefined;
@@ -83,7 +84,9 @@ export async function discoverAllAgents(
83
84
  }
84
85
  }
85
86
  } catch (err) {
86
- logger.debug('Failed to load agents.json', err);
87
+ logger.debug('Failed to load agents.json', {
88
+ error: err instanceof Error ? err.message : String(err),
89
+ });
87
90
  }
88
91
 
89
92
  try {
@@ -98,7 +101,9 @@ export async function discoverAllAgents(
98
101
  }
99
102
  }
100
103
  } catch (err) {
101
- logger.debug('Failed to read local agents directory', err);
104
+ logger.debug('Failed to read local agents directory', {
105
+ error: err instanceof Error ? err.message : String(err),
106
+ });
102
107
  }
103
108
 
104
109
  try {
@@ -113,7 +118,9 @@ export async function discoverAllAgents(
113
118
  }
114
119
  }
115
120
  } catch (err) {
116
- logger.debug('Failed to read global agents directory', err);
121
+ logger.debug('Failed to read global agents directory', {
122
+ error: err instanceof Error ? err.message : String(err),
123
+ });
117
124
  }
118
125
 
119
126
  return Array.from(agentSet).sort();
@@ -87,12 +87,14 @@ export function registerTerminalsRoutes(
87
87
  return c.json({ error: 'Terminal not found' }, 404);
88
88
  }
89
89
 
90
+ const activeTerminal = terminal;
91
+
90
92
  return streamSSE(c, async (stream) => {
91
93
  logger.debug('SSE stream started for terminal', { id });
92
94
  // Send historical buffer first (unless skipHistory is set)
93
95
  const skipHistory = c.req.query('skipHistory') === 'true';
94
96
  if (!skipHistory) {
95
- const history = terminal.read();
97
+ const history = activeTerminal.read();
96
98
  logger.debug('SSE sending terminal history', {
97
99
  id,
98
100
  lines: history.length,
@@ -120,8 +122,8 @@ export function registerTerminalsRoutes(
120
122
  let finished = false;
121
123
 
122
124
  function cleanup() {
123
- terminal.removeDataListener(onData);
124
- terminal.removeExitListener(onExit);
125
+ activeTerminal.removeDataListener(onData);
126
+ activeTerminal.removeExitListener(onExit);
125
127
  c.req.raw.signal.removeEventListener('abort', onAbort);
126
128
  }
127
129
 
@@ -145,7 +147,7 @@ export function registerTerminalsRoutes(
145
147
 
146
148
  function onAbort() {
147
149
  logger.debug('SSE client disconnected from terminal', {
148
- id: terminal.id,
150
+ id: activeTerminal.id,
149
151
  });
150
152
  stream.close();
151
153
  finish();
@@ -59,7 +59,7 @@ export function registerTunnelRoutes(app: Hono) {
59
59
 
60
60
  const url = await activeTunnel.start(port, (msg) => {
61
61
  progressMessage = msg;
62
- logger.debug('Tunnel progress:', msg);
62
+ logger.debug('Tunnel progress', { message: msg });
63
63
  });
64
64
 
65
65
  tunnelUrl = url;
@@ -2,12 +2,14 @@ export type OauthCodexContinuationInput = {
2
2
  provider: string;
3
3
  isOpenAIOAuth: boolean;
4
4
  finishObserved: boolean;
5
+ abortedByUser?: boolean;
5
6
  continuationCount: number;
6
7
  maxContinuations: number;
7
8
  finishReason?: string;
8
9
  rawFinishReason?: string;
9
10
  firstToolSeen: boolean;
10
11
  hasTrailingAssistantText: boolean;
12
+ endedWithToolActivity?: boolean;
11
13
  droppedPseudoToolText: boolean;
12
14
  lastAssistantText: string;
13
15
  };
@@ -68,6 +70,10 @@ export function decideOauthCodexContinuation(
68
70
  return { shouldContinue: false };
69
71
  }
70
72
 
73
+ if (input.abortedByUser) {
74
+ return { shouldContinue: false, reason: 'aborted-by-user' };
75
+ }
76
+
71
77
  if (input.continuationCount >= input.maxContinuations) {
72
78
  return { shouldContinue: false, reason: 'max-continuations-reached' };
73
79
  }
@@ -76,6 +82,10 @@ export function decideOauthCodexContinuation(
76
82
  return { shouldContinue: true, reason: 'truncated' };
77
83
  }
78
84
 
85
+ if (input.endedWithToolActivity) {
86
+ return { shouldContinue: true, reason: 'ended-on-tool-activity' };
87
+ }
88
+
79
89
  if (isMissingAssistantSummary(input)) {
80
90
  return { shouldContinue: true, reason: 'no-trailing-assistant-text' };
81
91
  }
@@ -211,6 +211,13 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
211
211
  const providerOptions = { ...adapted.providerOptions };
212
212
  let effectiveMaxOutputTokens = maxOutputTokens;
213
213
 
214
+ if (opts.provider === 'copilot') {
215
+ providerOptions.openai = {
216
+ ...((providerOptions.openai as Record<string, unknown>) || {}),
217
+ store: false,
218
+ };
219
+ }
220
+
214
221
  if (opts.reasoningText) {
215
222
  const underlyingProvider = getUnderlyingProviderKey(
216
223
  opts.provider,
@@ -182,11 +182,16 @@ async function runAssistant(opts: RunOpts) {
182
182
  let _finishObserved = false;
183
183
  let _toolActivityObserved = false;
184
184
  let _trailingAssistantTextAfterTool = false;
185
+ let _abortedByUser = false;
186
+ let titleGenerationTriggered = false;
185
187
  const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
186
188
  if (evt.type === 'tool.call' || evt.type === 'tool.result') {
187
189
  _toolActivityObserved = true;
188
190
  _trailingAssistantTextAfterTool = false;
189
191
  }
192
+ if (evt.type === 'tool.call') {
193
+ triggerTitleGenerationWhenReady();
194
+ }
190
195
  if (evt.type !== 'tool.result') return;
191
196
  try {
192
197
  const name = (evt.payload as { name?: string } | undefined)?.name;
@@ -221,6 +226,22 @@ async function runAssistant(opts: RunOpts) {
221
226
  stepIndex += 1;
222
227
  return stepIndex;
223
228
  };
229
+ const triggerTitleGenerationWhenReady = () => {
230
+ if (titleGenerationTriggered) {
231
+ return;
232
+ }
233
+
234
+ titleGenerationTriggered = true;
235
+ if (!isFirstMessage) {
236
+ return;
237
+ }
238
+
239
+ void triggerDeferredTitleGeneration({
240
+ cfg,
241
+ db,
242
+ sessionId: opts.sessionId,
243
+ });
244
+ };
224
245
 
225
246
  const reasoningStates = new Map<string, ReasoningState>();
226
247
 
@@ -232,6 +253,7 @@ async function runAssistant(opts: RunOpts) {
232
253
  getCurrentPartId,
233
254
  updateCurrentPartId,
234
255
  updateAccumulated,
256
+ triggerTitleGenerationWhenReady,
235
257
  sharedCtx,
236
258
  updateSessionTokensIncremental,
237
259
  updateMessageTokensIncremental,
@@ -245,12 +267,19 @@ async function runAssistant(opts: RunOpts) {
245
267
  runSessionLoop,
246
268
  );
247
269
 
248
- const onAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
270
+ const baseOnAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
271
+ const onAbort = async (event: Parameters<typeof baseOnAbort>[0]) => {
272
+ _abortedByUser = true;
273
+ await baseOnAbort(event);
274
+ };
249
275
 
250
276
  const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
251
- const stopWhenCondition = isOpenAIOAuth
252
- ? stepCountIs(20)
253
- : hasToolCall('finish');
277
+ const isCopilotResponsesApi =
278
+ opts.provider === 'copilot' && !opts.model.startsWith('gpt-5-mini');
279
+ const stopWhenCondition =
280
+ isOpenAIOAuth || isCopilotResponsesApi
281
+ ? stepCountIs(20)
282
+ : hasToolCall('finish');
254
283
 
255
284
  try {
256
285
  const result = streamText({
@@ -307,13 +336,6 @@ async function runAssistant(opts: RunOpts) {
307
336
  if (!firstDeltaSeen) {
308
337
  firstDeltaSeen = true;
309
338
  streamStartTimer.end();
310
- if (isFirstMessage) {
311
- void triggerDeferredTitleGeneration({
312
- cfg,
313
- db,
314
- sessionId: opts.sessionId,
315
- });
316
- }
317
339
  }
318
340
 
319
341
  if (!currentPartId) {
@@ -421,16 +443,20 @@ async function runAssistant(opts: RunOpts) {
421
443
 
422
444
  const MAX_CONTINUATIONS = 6;
423
445
  const continuationCount = opts.continuationCount ?? 0;
446
+ const endedWithToolActivity =
447
+ _toolActivityObserved && !_trailingAssistantTextAfterTool;
424
448
  const continuationDecision = decideOauthCodexContinuation({
425
449
  provider: opts.provider,
426
450
  isOpenAIOAuth,
427
451
  finishObserved: _finishObserved,
452
+ abortedByUser: _abortedByUser,
428
453
  continuationCount,
429
454
  maxContinuations: MAX_CONTINUATIONS,
430
455
  finishReason: streamFinishReason,
431
456
  rawFinishReason: streamRawFinishReason,
432
457
  firstToolSeen: fs,
433
458
  hasTrailingAssistantText: _trailingAssistantTextAfterTool,
459
+ endedWithToolActivity,
434
460
  droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
435
461
  lastAssistantText: latestAssistantText,
436
462
  });
@@ -127,7 +127,12 @@ async function processAskRequest(
127
127
  google: { enabled: true },
128
128
  openrouter: { enabled: true },
129
129
  opencode: { enabled: true },
130
+ copilot: { enabled: true },
130
131
  setu: { enabled: true },
132
+ zai: { enabled: true },
133
+ 'zai-coding': { enabled: true },
134
+ moonshot: { enabled: true },
135
+ minimax: { enabled: true },
131
136
  },
132
137
  paths: {
133
138
  dataDir: `${projectRoot}/.otto`,
@@ -5,6 +5,7 @@
5
5
  * across all API endpoints.
6
6
  */
7
7
 
8
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
8
9
  import { isDebugEnabled } from '../debug/state.ts';
9
10
  import { toErrorPayload } from './handling.ts';
10
11
 
@@ -16,7 +17,7 @@ export type APIErrorResponse = {
16
17
  message: string;
17
18
  type: string;
18
19
  code?: string;
19
- status?: number;
20
+ status?: ContentfulStatusCode;
20
21
  details?: Record<string, unknown>;
21
22
  stack?: string;
22
23
  };
@@ -27,29 +28,33 @@ export type APIErrorResponse = {
27
28
  */
28
29
  export class APIError extends Error {
29
30
  public readonly code?: string;
30
- public readonly status: number;
31
+ public readonly status: ContentfulStatusCode;
31
32
  public readonly type: string;
32
33
  public readonly details?: Record<string, unknown>;
33
34
 
34
35
  constructor(
35
36
  message: string,
36
- options?: {
37
- code?: string;
38
- status?: number;
39
- type?: string;
40
- details?: Record<string, unknown>;
41
- cause?: unknown;
42
- },
37
+ options?:
38
+ | ContentfulStatusCode
39
+ | {
40
+ code?: string;
41
+ status?: ContentfulStatusCode;
42
+ type?: string;
43
+ details?: Record<string, unknown>;
44
+ cause?: unknown;
45
+ },
43
46
  ) {
44
47
  super(message);
45
48
  this.name = 'APIError';
46
- this.code = options?.code;
47
- this.status = options?.status ?? 500;
48
- this.type = options?.type ?? 'api_error';
49
- this.details = options?.details;
50
-
51
- if (options?.cause) {
52
- this.cause = options.cause;
49
+ const normalizedOptions =
50
+ typeof options === 'number' ? { status: options } : options;
51
+ this.code = normalizedOptions?.code;
52
+ this.status = normalizedOptions?.status ?? 500;
53
+ this.type = normalizedOptions?.type ?? 'api_error';
54
+ this.details = normalizedOptions?.details;
55
+
56
+ if (normalizedOptions?.cause) {
57
+ this.cause = normalizedOptions.cause;
53
58
  }
54
59
 
55
60
  // Maintain proper stack trace
@@ -72,7 +77,7 @@ export function serializeError(err: unknown): APIErrorResponse {
72
77
  // Determine HTTP status code
73
78
  // Default to 400 for generic errors (client errors)
74
79
  // Only use 500 if explicitly set or for APIError instances without a status
75
- let status = 400;
80
+ let status: ContentfulStatusCode = 400;
76
81
 
77
82
  // Handle APIError instances first
78
83
  if (err instanceof APIError) {
@@ -80,15 +85,16 @@ export function serializeError(err: unknown): APIErrorResponse {
80
85
  } else if (err && typeof err === 'object') {
81
86
  const errObj = err as Record<string, unknown>;
82
87
  if (typeof errObj.status === 'number') {
83
- status = errObj.status;
88
+ status = errObj.status as ContentfulStatusCode;
84
89
  } else if (typeof errObj.statusCode === 'number') {
85
- status = errObj.statusCode;
90
+ status = errObj.statusCode as ContentfulStatusCode;
86
91
  } else if (
87
92
  errObj.details &&
88
93
  typeof errObj.details === 'object' &&
89
94
  typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
90
95
  ) {
91
- status = (errObj.details as Record<string, unknown>).statusCode as number;
96
+ status = (errObj.details as Record<string, unknown>)
97
+ .statusCode as ContentfulStatusCode;
92
98
  }
93
99
  }
94
100
 
@@ -130,7 +136,9 @@ export function serializeError(err: unknown): APIErrorResponse {
130
136
  * @param err - The error to convert
131
137
  * @returns Tuple of [APIErrorResponse, HTTP status code]
132
138
  */
133
- export function createErrorResponse(err: unknown): [APIErrorResponse, number] {
139
+ export function createErrorResponse(
140
+ err: unknown,
141
+ ): [APIErrorResponse, ContentfulStatusCode] {
134
142
  const response = serializeError(err);
135
143
  return [response, response.error.status ?? 500];
136
144
  }
@@ -284,7 +284,7 @@ async function generateSessionTitle(args: {
284
284
  return;
285
285
  }
286
286
 
287
- const provider = sess.provider ?? cfg.defaults.provider;
287
+ const provider = (sess.provider ?? cfg.defaults.provider) as ProviderId;
288
288
  const modelName = sess.model ?? cfg.defaults.model;
289
289
 
290
290
  debugLog('[TITLE_GEN] Generating title for session');
@@ -365,7 +365,7 @@ Output ONLY the title, nothing else.`;
365
365
 
366
366
  await db
367
367
  .update(sessions)
368
- .set({ title: sanitized, updatedAt: Date.now() })
368
+ .set({ title: sanitized, lastActiveAt: Date.now() })
369
369
  .where(eq(sessions.id, sessionId));
370
370
 
371
371
  debugLog(`[TITLE_GEN] Setting final title: "${sanitized}"`);
@@ -1,12 +1,123 @@
1
- import { getAuth, createCopilotModel } from '@ottocode/sdk';
2
- import type { OttoConfig } from '@ottocode/sdk';
1
+ import { getAuth, createCopilotModel, readEnvKey } from '@ottocode/sdk';
2
+ import type { OttoConfig, OAuth } from '@ottocode/sdk';
3
3
 
4
- export async function resolveCopilotModel(model: string, cfg: OttoConfig) {
5
- const auth = await getAuth('copilot', cfg.projectRoot);
4
+ const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
5
+ const COPILOT_MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
6
+
7
+ type CachedCopilotModels = {
8
+ expiresAt: number;
9
+ models: Set<string>;
10
+ };
11
+
12
+ const copilotModelsCache = new Map<string, CachedCopilotModels>();
13
+
14
+ type CopilotTokenCandidate = {
15
+ source: 'env' | 'oauth';
16
+ token: string;
17
+ oauth: OAuth;
18
+ };
19
+
20
+ async function getCopilotTokenCandidates(
21
+ projectRoot: string,
22
+ ): Promise<CopilotTokenCandidate[]> {
23
+ const candidates: CopilotTokenCandidate[] = [];
24
+
25
+ const envToken = readEnvKey('copilot');
26
+ if (envToken) {
27
+ candidates.push({
28
+ source: 'env',
29
+ token: envToken,
30
+ oauth: {
31
+ type: 'oauth',
32
+ access: envToken,
33
+ refresh: envToken,
34
+ expires: 0,
35
+ },
36
+ });
37
+ }
38
+
39
+ const auth = await getAuth('copilot', projectRoot);
6
40
  if (auth?.type === 'oauth') {
7
- return createCopilotModel(model, { oauth: auth });
41
+ if (auth.refresh !== envToken) {
42
+ candidates.push({ source: 'oauth', token: auth.refresh, oauth: auth });
43
+ }
44
+ }
45
+
46
+ return candidates;
47
+ }
48
+
49
+ async function getCopilotAvailableModels(
50
+ token: string,
51
+ ): Promise<Set<string> | null> {
52
+ const cached = copilotModelsCache.get(token);
53
+ if (cached && cached.expiresAt > Date.now()) {
54
+ return cached.models;
55
+ }
56
+
57
+ try {
58
+ const response = await fetch(COPILOT_MODELS_URL, {
59
+ headers: {
60
+ Authorization: `Bearer ${token}`,
61
+ 'Openai-Intent': 'conversation-edits',
62
+ 'User-Agent': 'ottocode',
63
+ },
64
+ });
65
+
66
+ if (!response.ok) return null;
67
+
68
+ const payload = (await response.json()) as {
69
+ data?: Array<{ id?: string }>;
70
+ };
71
+ const models = new Set(
72
+ (payload.data ?? [])
73
+ .map((item) => item.id)
74
+ .filter((id): id is string => Boolean(id)),
75
+ );
76
+
77
+ copilotModelsCache.set(token, {
78
+ expiresAt: Date.now() + COPILOT_MODELS_CACHE_TTL_MS,
79
+ models,
80
+ });
81
+
82
+ return models;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export async function resolveCopilotModel(model: string, cfg: OttoConfig) {
89
+ const candidates = await getCopilotTokenCandidates(cfg.projectRoot);
90
+ if (!candidates.length) {
91
+ throw new Error(
92
+ 'Copilot provider requires OAuth or GITHUB_TOKEN. Run `otto auth login copilot` or set GITHUB_TOKEN.',
93
+ );
94
+ }
95
+
96
+ let selected: CopilotTokenCandidate | null = null;
97
+ const unionAvailableModels = new Set<string>();
98
+
99
+ for (const candidate of candidates) {
100
+ const availableModels = await getCopilotAvailableModels(candidate.token);
101
+ if (!availableModels || availableModels.size === 0) continue;
102
+
103
+ for (const availableModel of availableModels) {
104
+ unionAvailableModels.add(availableModel);
105
+ }
106
+
107
+ if (!selected && availableModels.has(model)) {
108
+ selected = candidate;
109
+ }
110
+ }
111
+
112
+ if (selected) {
113
+ return createCopilotModel(model, { oauth: selected.oauth });
114
+ }
115
+
116
+ if (unionAvailableModels.size > 0) {
117
+ throw new Error(
118
+ `Copilot model '${model}' is not available for this account/organization token. Available models: ${Array.from(unionAvailableModels).join(', ')}`,
119
+ );
8
120
  }
9
- throw new Error(
10
- 'Copilot provider requires OAuth. Run `otto auth login copilot`.',
11
- );
121
+
122
+ return createCopilotModel(model, { oauth: candidates[0].oauth });
12
123
  }
@@ -69,11 +69,10 @@ export function detectOAuth(
69
69
  ): OAuthContext {
70
70
  const isOAuth = auth?.type === 'oauth';
71
71
  const needsSpoof = !!isOAuth && provider === 'anthropic';
72
- const isCopilot = provider === 'copilot';
73
72
  return {
74
- isOAuth: !!isOAuth || isCopilot,
73
+ isOAuth: !!isOAuth,
75
74
  needsSpoof,
76
- isOpenAIOAuth: (!!isOAuth && provider === 'openai') || isCopilot,
75
+ isOpenAIOAuth: !!isOAuth && provider === 'openai',
77
76
  spoofPrompt: needsSpoof ? getProviderSpoofPrompt(provider) : undefined,
78
77
  };
79
78
  }
@@ -160,7 +160,14 @@ export async function createBranch({
160
160
  }
161
161
 
162
162
  const result: SessionRow = {
163
- ...newSession,
163
+ id: newSession.id,
164
+ title: newSession.title ?? null,
165
+ agent: newSession.agent,
166
+ provider: newSession.provider,
167
+ model: newSession.model,
168
+ projectPath: newSession.projectPath,
169
+ createdAt: newSession.createdAt,
170
+ lastActiveAt: newSession.lastActiveAt ?? null,
164
171
  totalInputTokens: null,
165
172
  totalOutputTokens: null,
166
173
  totalCachedTokens: null,
@@ -171,6 +178,9 @@ export async function createBranch({
171
178
  currentContextTokens: null,
172
179
  contextSummary: null,
173
180
  lastCompactedAt: null,
181
+ parentSessionId: newSession.parentSessionId ?? null,
182
+ branchPointMessageId: newSession.branchPointMessageId ?? null,
183
+ sessionType: newSession.sessionType ?? null,
174
184
  };
175
185
 
176
186
  publish({
@@ -39,7 +39,7 @@ export async function createSession({
39
39
  await ensureProviderEnv(cfg, provider);
40
40
  const id = crypto.randomUUID();
41
41
  const now = Date.now();
42
- const row = {
42
+ const row: SessionRow = {
43
43
  id,
44
44
  title: title ?? null,
45
45
  agent,
@@ -56,6 +56,11 @@ export async function createSession({
56
56
  totalToolTimeMs: null,
57
57
  toolCountsJson: null,
58
58
  currentContextTokens: null,
59
+ contextSummary: null,
60
+ lastCompactedAt: null,
61
+ parentSessionId: null,
62
+ branchPointMessageId: null,
63
+ sessionType: 'main',
59
64
  };
60
65
  await db.insert(sessions).values(row);
61
66
  publish({ type: 'session.created', sessionId: id, payload: row });
@@ -16,6 +16,7 @@ export function createStepFinishHandler(
16
16
  getCurrentPartId: () => string | null,
17
17
  updateCurrentPartId: (id: string | null) => void,
18
18
  updateAccumulated: (text: string) => void,
19
+ triggerTitleGenerationWhenReady: () => void,
19
20
  sharedCtx: ToolAdapterContext,
20
21
  updateSessionTokensIncrementalFn: (
21
22
  usage: UsageData,
@@ -31,6 +32,8 @@ export function createStepFinishHandler(
31
32
  ) => Promise<void>,
32
33
  ) {
33
34
  return async (step: StepFinishEvent) => {
35
+ triggerTitleGenerationWhenReady();
36
+
34
37
  const finishedAt = Date.now();
35
38
  const currentPartId = getCurrentPartId();
36
39
  const stepIndex = getStepIndex();
@@ -10,6 +10,9 @@ export function getMaxOutputTokens(
10
10
  provider: ProviderName,
11
11
  modelId: string,
12
12
  ): number | undefined {
13
+ if (provider === 'copilot') {
14
+ return undefined;
15
+ }
13
16
  try {
14
17
  const providerCatalog = catalog[provider];
15
18
  if (!providerCatalog) {
@@ -130,12 +130,13 @@ export function adaptTools(
130
130
  }: {
131
131
  callId?: string;
132
132
  startTs?: number;
133
- stepIndexForEvent: number;
133
+ stepIndexForEvent?: number;
134
134
  args?: unknown;
135
135
  },
136
136
  ) => {
137
137
  const resultPartId = crypto.randomUUID();
138
138
  const endTs = Date.now();
139
+ const effectiveStepIndex = stepIndexForEvent ?? ctx.stepIndex;
139
140
  const dur =
140
141
  typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
141
142
 
@@ -160,7 +161,7 @@ export function adaptTools(
160
161
  id: resultPartId,
161
162
  messageId: ctx.messageId,
162
163
  index,
163
- stepIndex: stepIndexForEvent,
164
+ stepIndex: effectiveStepIndex,
164
165
  type: 'tool_result',
165
166
  content: JSON.stringify(contentObj),
166
167
  agent: ctx.agent,
@@ -176,7 +177,7 @@ export function adaptTools(
176
177
  publish({
177
178
  type: 'tool.result',
178
179
  sessionId: ctx.sessionId,
179
- payload: { ...contentObj, stepIndex: stepIndexForEvent },
180
+ payload: { ...contentObj, stepIndex: effectiveStepIndex },
180
181
  });
181
182
  };
182
183