@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 +1 -1
- package/src/core/src/index.ts +1 -0
- package/src/core/src/tools/builtin/ignore.ts +4 -1
- package/src/core/src/tools/lazy/index.ts +1 -0
- package/src/core/src/tools/lazy/mcp-manager.ts +385 -0
- package/src/core/src/tools/lazy/registry.ts +7 -0
- package/src/core/src/tools/loader.ts +0 -2
- package/src/index.ts +1 -0
- package/src/prompts/src/agents/build.txt +1 -1
- package/src/prompts/src/base.txt +3 -4
- package/src/prompts/src/providers/default.txt +1 -1
- package/src/prompts/src/providers/glm.txt +1 -1
- package/src/prompts/src/providers/moonshot.txt +1 -1
- package/src/providers/src/openai-oauth-client.ts +0 -61
- package/src/core/src/tools/builtin/finish.ts +0 -12
- package/src/core/src/tools/builtin/finish.txt +0 -15
package/package.json
CHANGED
package/src/core/src/index.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|
|
@@ -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
|
@@ -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
|
|
5
|
+
- Stream a short summary of what you did, then stop.
|
|
6
6
|
|
|
7
7
|
## Editing workflow
|
|
8
8
|
|
package/src/prompts/src/base.txt
CHANGED
|
@@ -26,9 +26,8 @@ When instructions conflict, obey (highest → lowest):
|
|
|
26
26
|
|
|
27
27
|
## Finishing your turn
|
|
28
28
|
|
|
29
|
-
|
|
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
|
|
32
|
-
- For substantive work (edits, multi-tool runs): briefly describe the outcome (what changed, key files, how to verify), then
|
|
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
|
|
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
|
|
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
|
|
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.
|