@ottocode/sdk 0.1.305 → 0.1.306

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/sdk",
3
- "version": "0.1.305",
3
+ "version": "0.1.306",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -55,6 +55,7 @@ export { buildTerminalTool } from './tools/builtin/terminal';
55
55
  export {
56
56
  buildLazyToolsRecord,
57
57
  buildLoadFirstPartyToolsTool,
58
+ buildMCPManagerTool,
58
59
  buildSimulatorTool,
59
60
  getLazyToolDefinitions,
60
61
  } from './tools/lazy/index';
@@ -26,7 +26,10 @@ export const IGNORE_PATTERNS: string[] = [
26
26
  ];
27
27
 
28
28
  export function defaultIgnoreGlobs(extra?: string[]): string[] {
29
- const base = IGNORE_PATTERNS.map((p) => `!${p}*`);
29
+ const base = IGNORE_PATTERNS.flatMap((p) => {
30
+ const pattern = p.replace(/\/$/, '');
31
+ return [`${pattern}/**`, `**/${pattern}/**`];
32
+ });
30
33
  if (Array.isArray(extra) && extra.length) return base.concat(extra);
31
34
  return base;
32
35
  }
@@ -10,3 +10,4 @@ export {
10
10
  type LazyToolDefinition,
11
11
  } from './registry.ts';
12
12
  export { buildSimulatorTool } from './simulator.ts';
13
+ export { buildMCPManagerTool } from './mcp-manager.ts';
@@ -0,0 +1,385 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import { getGlobalConfigDir } from '../../../../config/src/paths.ts';
4
+ import {
5
+ addMCPServerToConfig,
6
+ getMCPManager,
7
+ initializeMCP,
8
+ loadMCPConfig,
9
+ removeMCPServerFromConfig,
10
+ } from '../../mcp/lifecycle.ts';
11
+ import type {
12
+ MCPScope,
13
+ MCPServerConfig,
14
+ MCPServerStatus,
15
+ MCPTransport,
16
+ } from '../../mcp/types.ts';
17
+ import { createToolError } from '../error.ts';
18
+
19
+ const mcpActions = [
20
+ 'list',
21
+ 'add',
22
+ 'update',
23
+ 'remove',
24
+ 'enable',
25
+ 'disable',
26
+ ] as const;
27
+
28
+ type MCPManagerAction = (typeof mcpActions)[number];
29
+
30
+ type ServerSummary = {
31
+ name: string;
32
+ transport: MCPTransport;
33
+ scope: MCPScope;
34
+ disabled: boolean;
35
+ command?: string;
36
+ args?: string[];
37
+ url?: string;
38
+ connected: boolean;
39
+ tools: string[];
40
+ error?: string;
41
+ };
42
+
43
+ function summarizeServer(
44
+ server: MCPServerConfig,
45
+ status?: MCPServerStatus,
46
+ ): ServerSummary {
47
+ return {
48
+ name: server.name,
49
+ transport: server.transport ?? 'stdio',
50
+ scope: server.scope ?? 'global',
51
+ disabled: server.disabled ?? false,
52
+ ...(server.command ? { command: server.command } : {}),
53
+ ...(server.args?.length ? { args: server.args } : {}),
54
+ ...(server.url ? { url: server.url } : {}),
55
+ connected: status?.connected ?? false,
56
+ tools: status?.tools ?? [],
57
+ ...(status?.error ? { error: status.error } : {}),
58
+ };
59
+ }
60
+
61
+ async function getStatuses(): Promise<MCPServerStatus[]> {
62
+ const manager = getMCPManager();
63
+ if (!manager) return [];
64
+ try {
65
+ return await manager.getStatusAsync();
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ async function startServer(
72
+ projectRoot: string,
73
+ server: MCPServerConfig,
74
+ ): Promise<MCPServerStatus | undefined> {
75
+ let manager = getMCPManager();
76
+ if (!manager) {
77
+ manager = await initializeMCP({ servers: [] }, projectRoot);
78
+ }
79
+ if (!manager.started) {
80
+ manager.setProjectRoot(projectRoot);
81
+ }
82
+ await manager.restartServer(server);
83
+ const statuses = await manager.getStatusAsync();
84
+ return statuses.find((status) => status.name === server.name);
85
+ }
86
+
87
+ async function stopServer(name: string): Promise<void> {
88
+ const manager = getMCPManager();
89
+ if (!manager) return;
90
+ await manager.stopServer(name);
91
+ }
92
+
93
+ type MCPManagerInput = {
94
+ action: MCPManagerAction;
95
+ name?: string;
96
+ scope?: MCPScope;
97
+ transport?: MCPTransport;
98
+ command?: string;
99
+ args?: string[];
100
+ env?: Record<string, string>;
101
+ url?: string;
102
+ headers?: Record<string, string>;
103
+ start?: boolean;
104
+ };
105
+
106
+ function validateServerInput(
107
+ input: MCPManagerInput,
108
+ existing?: MCPServerConfig,
109
+ ):
110
+ | { ok: true; server: MCPServerConfig }
111
+ | { ok: false; error: ReturnType<typeof createToolError> } {
112
+ const name = input.name?.trim();
113
+ if (!name) {
114
+ return {
115
+ ok: false,
116
+ error: createToolError('name is required', 'validation', {
117
+ parameter: 'name',
118
+ }),
119
+ };
120
+ }
121
+
122
+ const transport: MCPTransport =
123
+ input.transport ?? existing?.transport ?? 'stdio';
124
+ const command = input.command ?? existing?.command;
125
+ const url = input.url ?? existing?.url;
126
+
127
+ if (transport === 'stdio' && !command) {
128
+ return {
129
+ ok: false,
130
+ error: createToolError(
131
+ 'command is required for stdio transport',
132
+ 'validation',
133
+ { parameter: 'command' },
134
+ ),
135
+ };
136
+ }
137
+ if (transport === 'stdio' && command && /^https?:\/\//i.test(command)) {
138
+ return {
139
+ ok: false,
140
+ error: createToolError(
141
+ 'stdio transport requires a local command, not a URL. Use http or sse transport for remote servers.',
142
+ 'validation',
143
+ { parameter: 'command', value: command },
144
+ ),
145
+ };
146
+ }
147
+ if ((transport === 'http' || transport === 'sse') && !url) {
148
+ return {
149
+ ok: false,
150
+ error: createToolError(
151
+ 'url is required for http/sse transport',
152
+ 'validation',
153
+ { parameter: 'url' },
154
+ ),
155
+ };
156
+ }
157
+
158
+ const scope: MCPScope =
159
+ input.scope ?? existing?.scope ?? ('global' as MCPScope);
160
+ const args = input.args ?? existing?.args;
161
+ const env = input.env ?? existing?.env;
162
+ const headers = input.headers ?? existing?.headers;
163
+
164
+ const server: MCPServerConfig = {
165
+ name,
166
+ transport,
167
+ scope,
168
+ ...(command ? { command } : {}),
169
+ ...(args?.length ? { args } : {}),
170
+ ...(env && Object.keys(env).length ? { env } : {}),
171
+ ...(url ? { url } : {}),
172
+ ...(headers && Object.keys(headers).length ? { headers } : {}),
173
+ ...(existing?.oauth ? { oauth: existing.oauth } : {}),
174
+ ...(existing?.cwd ? { cwd: existing.cwd } : {}),
175
+ ...(existing?.disabled ? { disabled: existing.disabled } : {}),
176
+ };
177
+
178
+ return { ok: true, server };
179
+ }
180
+
181
+ /**
182
+ * Build the `mcp_manager` lazy tool which lets an agent list, add, update,
183
+ * remove, enable, or disable MCP servers in the otto project (.otto/config.json)
184
+ * or global (~/.config/otto/config.json) configuration.
185
+ */
186
+ export function buildMCPManagerTool(projectRoot: string): {
187
+ name: string;
188
+ tool: Tool;
189
+ } {
190
+ return {
191
+ name: 'mcp_manager',
192
+ tool: tool({
193
+ description: `Manage otto MCP (Model Context Protocol) servers.
194
+
195
+ Actions:
196
+ - list: list configured MCP servers with scope, disabled flag, and connection status
197
+ - add: add a new MCP server (upserts if the name already exists)
198
+ - update: update an existing MCP server; omitted fields keep their current values
199
+ - remove: stop and delete an MCP server from config
200
+ - enable: clear the disabled flag and start the server
201
+ - disable: set the disabled flag and stop the server
202
+
203
+ Scope:
204
+ - "project" writes to <project>/.otto/config.json (shared with the repo)
205
+ - "global" writes to the user's global otto config (default)
206
+
207
+ Transports:
208
+ - stdio requires "command" (plus optional "args"/"env")
209
+ - http/sse require "url" (plus optional "headers")
210
+
211
+ Set "start": true on add/update to start the server immediately. Newly started servers expose their tools via load_mcp_tools on the next session turn.`,
212
+ inputSchema: z.object({
213
+ action: z.enum(mcpActions).describe('Operation to perform.'),
214
+ name: z
215
+ .string()
216
+ .optional()
217
+ .describe('MCP server name (required for all actions except list).'),
218
+ scope: z
219
+ .enum(['global', 'project'])
220
+ .optional()
221
+ .describe(
222
+ 'Config scope: "project" (.otto/config.json) or "global" (default for new servers).',
223
+ ),
224
+ transport: z
225
+ .enum(['stdio', 'http', 'sse'])
226
+ .optional()
227
+ .describe('Transport type (default stdio).'),
228
+ command: z
229
+ .string()
230
+ .optional()
231
+ .describe('Executable for stdio transport (e.g. "bunx").'),
232
+ args: z
233
+ .array(z.string())
234
+ .optional()
235
+ .describe('Arguments for the stdio command.'),
236
+ env: z
237
+ .record(z.string())
238
+ .optional()
239
+ .describe('Environment variables for the stdio command.'),
240
+ url: z
241
+ .string()
242
+ .optional()
243
+ .describe('Server URL for http/sse transport.'),
244
+ headers: z
245
+ .record(z.string())
246
+ .optional()
247
+ .describe('HTTP headers for http/sse transport.'),
248
+ start: z
249
+ .boolean()
250
+ .optional()
251
+ .describe('Start the server immediately after add/update.'),
252
+ }),
253
+ execute: async (input: MCPManagerInput) => {
254
+ const globalConfigDir = getGlobalConfigDir();
255
+ const action = input.action;
256
+
257
+ if (action === 'list') {
258
+ const config = await loadMCPConfig(projectRoot, globalConfigDir);
259
+ const statuses = await getStatuses();
260
+ return {
261
+ ok: true,
262
+ servers: config.servers.map((server) =>
263
+ summarizeServer(
264
+ server,
265
+ statuses.find((status) => status.name === server.name),
266
+ ),
267
+ ),
268
+ };
269
+ }
270
+
271
+ const name = input.name?.trim();
272
+ if (!name) {
273
+ return createToolError(
274
+ `name is required for action "${action}"`,
275
+ 'validation',
276
+ { parameter: 'name' },
277
+ );
278
+ }
279
+
280
+ const config = await loadMCPConfig(projectRoot, globalConfigDir);
281
+ const existing = config.servers.find((server) => server.name === name);
282
+
283
+ if (action === 'add' || action === 'update') {
284
+ if (action === 'update' && !existing) {
285
+ return createToolError(
286
+ `MCP server "${name}" not found`,
287
+ 'not_found',
288
+ { parameter: 'name', value: name },
289
+ );
290
+ }
291
+ const validated = validateServerInput(input, existing);
292
+ if (!validated.ok) return validated.error;
293
+ const server = validated.server;
294
+
295
+ if (existing && existing.scope !== server.scope) {
296
+ await removeMCPServerFromConfig(projectRoot, name, globalConfigDir);
297
+ }
298
+ await addMCPServerToConfig(projectRoot, server, globalConfigDir);
299
+
300
+ if (input.start) {
301
+ try {
302
+ const status = await startServer(projectRoot, server);
303
+ return {
304
+ ok: true,
305
+ action,
306
+ server: summarizeServer(server, status),
307
+ };
308
+ } catch (err) {
309
+ const msg = err instanceof Error ? err.message : String(err);
310
+ return {
311
+ ok: true,
312
+ action,
313
+ server: summarizeServer(server),
314
+ startError: msg,
315
+ };
316
+ }
317
+ }
318
+ return { ok: true, action, server: summarizeServer(server) };
319
+ }
320
+
321
+ if (!existing) {
322
+ return createToolError(
323
+ `MCP server "${name}" not found`,
324
+ 'not_found',
325
+ { parameter: 'name', value: name },
326
+ );
327
+ }
328
+
329
+ if (action === 'remove') {
330
+ try {
331
+ await stopServer(name);
332
+ } catch {}
333
+ const removed = await removeMCPServerFromConfig(
334
+ projectRoot,
335
+ name,
336
+ globalConfigDir,
337
+ );
338
+ if (!removed) {
339
+ return createToolError(
340
+ `MCP server "${name}" not found in any config file`,
341
+ 'not_found',
342
+ { parameter: 'name', value: name },
343
+ );
344
+ }
345
+ return { ok: true, action, name };
346
+ }
347
+
348
+ if (action === 'enable') {
349
+ const server: MCPServerConfig = { ...existing, disabled: false };
350
+ await addMCPServerToConfig(projectRoot, server, globalConfigDir);
351
+ try {
352
+ const status = await startServer(projectRoot, server);
353
+ return {
354
+ ok: true,
355
+ action,
356
+ server: summarizeServer(server, status),
357
+ };
358
+ } catch (err) {
359
+ const msg = err instanceof Error ? err.message : String(err);
360
+ return {
361
+ ok: true,
362
+ action,
363
+ server: summarizeServer(server),
364
+ startError: msg,
365
+ };
366
+ }
367
+ }
368
+
369
+ if (action === 'disable') {
370
+ const server: MCPServerConfig = { ...existing, disabled: true };
371
+ await addMCPServerToConfig(projectRoot, server, globalConfigDir);
372
+ try {
373
+ await stopServer(name);
374
+ } catch {}
375
+ return { ok: true, action, server: summarizeServer(server) };
376
+ }
377
+
378
+ return createToolError(`Unknown action "${action}"`, 'validation', {
379
+ parameter: 'action',
380
+ value: action,
381
+ });
382
+ },
383
+ }),
384
+ };
385
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Tool } from 'ai';
2
2
  import { buildCopyAttachmentTool } from '../builtin/fs/copy-attachment.ts';
3
3
  import { buildReadImageTool } from '../builtin/fs/read-image.ts';
4
+ import { buildMCPManagerTool } from './mcp-manager.ts';
4
5
  import { buildSimulatorTool } from './simulator.ts';
5
6
  import { buildLoadToolsTool, type LazyToolBrief } from './load-tools.ts';
6
7
 
@@ -28,6 +29,12 @@ export function getLazyToolDefinitions(): LazyToolDefinition[] {
28
29
  'Copy an original uploaded chat attachment into the project without recompression.',
29
30
  build: buildCopyAttachmentTool,
30
31
  },
32
+ {
33
+ name: 'mcp_manager',
34
+ description:
35
+ 'Manage otto MCP servers: list, add, update, remove, enable, or disable servers in project (.otto/config.json) or global config.',
36
+ build: buildMCPManagerTool,
37
+ },
31
38
  ];
32
39
  }
33
40
 
@@ -1,6 +1,5 @@
1
1
  import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
- import { finishTool } from './builtin/finish.ts';
4
3
  import { buildFsTools } from './builtin/fs/index.ts';
5
4
  import { buildGitTools } from './builtin/git.ts';
6
5
  import { progressUpdateTool } from './builtin/progress.ts';
@@ -148,7 +147,6 @@ async function discoverStaticProjectTools(
148
147
  for (const { name, tool } of buildGitTools(projectRoot))
149
148
  tools.set(name, tool);
150
149
  // Built-ins
151
- tools.set('finish', finishTool);
152
150
  tools.set('progress_update', progressUpdateTool);
153
151
  const shell = buildShellTool(projectRoot);
154
152
  tools.set(shell.name, shell.tool);
package/src/index.ts CHANGED
@@ -310,6 +310,7 @@ export { buildGitTools } from './core/src/index.ts';
310
310
  export {
311
311
  buildLazyToolsRecord,
312
312
  buildLoadFirstPartyToolsTool,
313
+ buildMCPManagerTool,
313
314
  buildSimulatorTool,
314
315
  getLazyToolDefinitions,
315
316
  } from './core/src/index.ts';
@@ -2,7 +2,7 @@ You help with coding and build tasks.
2
2
 
3
3
  - Be precise and practical. Inspect before editing; prefer small, targeted diffs.
4
4
  - Keep tool inputs short; avoid long prose inside tool parameters.
5
- - Stream a short summary of what you did, then call `finish`.
5
+ - Stream a short summary of what you did, then stop.
6
6
 
7
7
  ## Editing workflow
8
8
 
@@ -26,9 +26,8 @@ When instructions conflict, obey (highest → lowest):
26
26
 
27
27
  ## Finishing your turn
28
28
 
29
- Every response ends with a call to the `finish` tool. The answer/work you already streamed IS your final response — `finish` just signals the turn is over.
29
+ Your turn ends when you stop calling tools. The answer/work you already streamed IS your final response.
30
30
 
31
- - For questions and conversational replies: answer directly, then call `finish`. The answer IS the response; no separate summary.
32
- - For substantive work (edits, multi-tool runs): briefly describe the outcome (what changed, key files, how to verify), then call `finish`.
31
+ - For questions and conversational replies: answer directly. The answer IS the response; no separate summary.
32
+ - For substantive work (edits, multi-tool runs): briefly describe the outcome (what changed, key files, how to verify), then stop.
33
33
  - NEVER label your response with "Summary:" or similar prefixes. NEVER add a recap to trivial replies — the direct answer is sufficient.
34
- - You MUST call `finish` as your last action. Don't call it before your text response finishes streaming.
@@ -50,4 +50,4 @@ Independent operations (multiple reads, multiple searches, `git_status` + `git_d
50
50
 
51
51
  # Finishing
52
52
 
53
- Stream a short summary of what you did, then call the `finish` tool. Never call `finish` without first streaming a summary.
53
+ Stream a short summary of what you did, then stop. Your turn ends when you stop calling tools.
@@ -62,4 +62,4 @@ Independent operations (multiple reads, multiple searches, `git_status` + `git_d
62
62
 
63
63
  # Finishing
64
64
 
65
- Stream a short summary of what you did, then call the `finish` tool. Never call `finish` without first streaming a summary.
65
+ Stream a short summary of what you did, then stop. Your turn ends when you stop calling tools.
@@ -62,4 +62,4 @@ Independent operations (multiple reads, multiple searches, `git_status` + `git_d
62
62
 
63
63
  # Finishing
64
64
 
65
- Stream a short summary of what you did, then call the `finish` tool. Never call `finish` without first streaming a summary.
65
+ Stream a short summary of what you did, then stop. Your turn ends when you stop calling tools.
@@ -25,9 +25,6 @@ type OpenAIOAuthSessionState = {
25
25
  model?: string;
26
26
  status?: string;
27
27
  incompleteReason?: string;
28
- turnState?: string;
29
- installationId?: string;
30
- windowId?: string;
31
28
  };
32
29
 
33
30
  const openAIOAuthSessionState = new Map<string, OpenAIOAuthSessionState>();
@@ -214,17 +211,6 @@ function writeSessionState(sessionId: string, next: OpenAIOAuthSessionState) {
214
211
  openAIOAuthSessionState.set(sessionId, next);
215
212
  }
216
213
 
217
- function mergeSessionState(sessionId: string, next: OpenAIOAuthSessionState) {
218
- writeSessionState(sessionId, {
219
- ...readSessionState(sessionId),
220
- ...next,
221
- });
222
- }
223
-
224
- function getCodexWindowId(sessionId: string) {
225
- return `${sessionId}:0`;
226
- }
227
-
228
214
  function rewriteRequestBody(
229
215
  body: string,
230
216
  sessionId?: string,
@@ -237,17 +223,6 @@ function rewriteRequestBody(
237
223
  return { body: changed ? JSON.stringify(parsed) : body, model };
238
224
  }
239
225
 
240
- const clientMetadata =
241
- parsed.client_metadata && typeof parsed.client_metadata === 'object'
242
- ? (parsed.client_metadata as Record<string, unknown>)
243
- : {};
244
- if (clientMetadata['x-codex-installation-id'] !== CODEX_INSTALLATION_ID) {
245
- parsed.client_metadata = {
246
- ...clientMetadata,
247
- 'x-codex-installation-id': CODEX_INSTALLATION_ID,
248
- };
249
- changed = true;
250
- }
251
226
  if (typeof parsed.prompt_cache_key !== 'string') {
252
227
  parsed.prompt_cache_key = sessionId;
253
228
  changed = true;
@@ -384,9 +359,6 @@ function trackResponseEvent(data: string, sessionId?: string) {
384
359
  model: responseModel ?? prior?.model,
385
360
  status: responseStatus ?? type,
386
361
  incompleteReason,
387
- turnState: prior?.turnState,
388
- installationId: prior?.installationId,
389
- windowId: prior?.windowId,
390
362
  });
391
363
  logOpenAIOAuth(
392
364
  `tracked response event type=${type ?? 'unknown'} responseId=${responseId} session=${sessionId} status=${responseStatus ?? 'unknown'} incompleteReason=${incompleteReason ?? 'none'}`,
@@ -692,10 +664,6 @@ function buildHeaders(
692
664
  sessionId?: string,
693
665
  ): Headers {
694
666
  const headers = new Headers(init?.headers);
695
- const prior = readSessionState(sessionId);
696
- const windowId = sessionId
697
- ? (prior?.windowId ?? getCodexWindowId(sessionId))
698
- : undefined;
699
667
  headers.delete('Authorization');
700
668
  headers.delete('authorization');
701
669
  headers.set('authorization', `Bearer ${accessToken}`);
@@ -710,30 +678,10 @@ function buildHeaders(
710
678
  }
711
679
  if (sessionId) {
712
680
  headers.set('session_id', sessionId);
713
- headers.set('thread_id', sessionId);
714
- headers.set('x-codex-window-id', windowId ?? getCodexWindowId(sessionId));
715
- if (prior?.turnState) {
716
- headers.set('x-codex-turn-state', prior.turnState);
717
- }
718
681
  }
719
682
  return headers;
720
683
  }
721
684
 
722
- function trackCodexResponseHeaders(response: Response, sessionId?: string) {
723
- if (!sessionId) return;
724
- const turnState = response.headers.get('x-codex-turn-state') ?? undefined;
725
- if (!turnState) return;
726
- const windowId = getCodexWindowId(sessionId);
727
- mergeSessionState(sessionId, {
728
- turnState,
729
- installationId: CODEX_INSTALLATION_ID,
730
- windowId,
731
- });
732
- logOpenAIOAuth(
733
- `tracked x-codex-turn-state for session=${sessionId} window=${windowId}`,
734
- );
735
- }
736
-
737
685
  export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
738
686
  let currentOAuth = config.oauth;
739
687
 
@@ -770,9 +718,6 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
770
718
  model: requestModel,
771
719
  status: prior?.status,
772
720
  incompleteReason: prior?.incompleteReason,
773
- turnState: prior?.turnState,
774
- installationId: prior?.installationId,
775
- windowId: prior?.windowId,
776
721
  });
777
722
  }
778
723
  }
@@ -830,9 +775,6 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
830
775
  bodyCharsApprox: requestBodySize,
831
776
  model: requestModel,
832
777
  });
833
- if (isResponsesRequest) {
834
- trackCodexResponseHeaders(response, config.sessionId);
835
- }
836
778
  if (!response.ok && response.status !== 401) {
837
779
  loggerWarn('[openai-oauth] non-OK response', {
838
780
  sessionId: config.sessionId,
@@ -892,9 +834,6 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
892
834
  requestStartedAt: retryStartedAt,
893
835
  },
894
836
  );
895
- if (isResponsesRequest) {
896
- trackCodexResponseHeaders(retryResponse, config.sessionId);
897
- }
898
837
  loggerDebug('[openai-oauth] retry response received', {
899
838
  sessionId: config.sessionId,
900
839
  target: isResponsesRequest ? 'codex.responses' : 'other',
@@ -1,12 +0,0 @@
1
- import { z } from 'zod/v3';
2
- import { tool } from 'ai';
3
- import DESCRIPTION from './finish.txt' with { type: 'text' };
4
- import type { ToolResponse } from '../error.ts';
5
-
6
- export const finishTool = tool({
7
- description: DESCRIPTION,
8
- inputSchema: z.object({}),
9
- async execute(): Promise<ToolResponse<{ done: true }>> {
10
- return { ok: true, done: true };
11
- },
12
- });
@@ -1,15 +0,0 @@
1
- Signal the end of your turn. Call this as the LAST action of every response, after your text/work has finished streaming.
2
-
3
- ## How it works
4
-
5
- Your text response IS the final answer. `finish` is just the end-of-turn signal — it does NOT produce visible output to the user.
6
-
7
- - For questions and conversational replies: answer directly, then call `finish`. No separate summary.
8
- - For substantive work (edits, multi-tool runs): briefly describe the outcome, then call `finish`.
9
-
10
- ## Never
11
-
12
- - NEVER add a "Summary:" label (or "Result:", "Done:", etc.) to your response. The direct answer is the response.
13
- - NEVER recap trivial single-sentence answers. "15" or "Yes" is a complete reply on its own.
14
- - NEVER call `finish` before your text finishes streaming.
15
- - NEVER forget to call `finish` — without it the system hangs waiting for more output.