@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.2

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.
Files changed (71) hide show
  1. package/README.md +50 -0
  2. package/docs/consumer-capability-matrix.md +374 -0
  3. package/package.json +17 -10
  4. package/scripts/generate-codex-protocol.ts +711 -0
  5. package/src/agents/agent-operation.ts +8 -0
  6. package/src/agents/agent-provider.ts +3 -0
  7. package/src/agents/agent-run.ts +14 -2
  8. package/src/agents/agent-runtime-error.ts +28 -0
  9. package/src/agents/agent-session-events.ts +33 -0
  10. package/src/agents/agent-tool-runner.ts +12 -11
  11. package/src/agents/agent-tool.ts +12 -4
  12. package/src/agents/author-context.ts +1 -1
  13. package/src/agents/claude/claude-auth.ts +40 -0
  14. package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
  15. package/src/agents/claude/claude-code-process.ts +98 -0
  16. package/src/agents/claude/claude-command-mapper.ts +63 -3
  17. package/src/agents/claude/claude-desktop.ts +55 -0
  18. package/src/agents/claude/claude-event-mapper.ts +864 -135
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +344 -29
  21. package/src/agents/claude/claude-run.ts +893 -130
  22. package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
  23. package/src/agents/claude/claude-skill-catalog.ts +75 -0
  24. package/src/agents/claude/claude-steered-notifications.ts +180 -0
  25. package/src/agents/claude/claude-tool-mapper.ts +81 -2
  26. package/src/agents/claude/claude-workspace-skills.ts +87 -0
  27. package/src/agents/claude/index.ts +2 -0
  28. package/src/agents/codex/codex-auth-mapper.ts +27 -0
  29. package/src/agents/codex/codex-auth.ts +19 -0
  30. package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
  31. package/src/agents/codex/codex-browser-auth-status.ts +55 -0
  32. package/src/agents/codex/codex-command-mapper.ts +38 -3
  33. package/src/agents/codex/codex-event-mapper.ts +375 -64
  34. package/src/agents/codex/codex-json-rpc-client.ts +66 -21
  35. package/src/agents/codex/codex-provider.ts +118 -236
  36. package/src/agents/codex/codex-run-auth.ts +113 -0
  37. package/src/agents/codex/codex-run.ts +423 -53
  38. package/src/agents/codex/codex-tool-mapper.ts +2 -2
  39. package/src/agents/codex/codex.errors.ts +8 -0
  40. package/src/agents/codex/generated/meta.gen.ts +144 -6
  41. package/src/agents/codex/generated/namespaces.gen.ts +92 -3
  42. package/src/agents/codex/generated/schema.gen.ts +24698 -15638
  43. package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
  44. package/src/agents/index.ts +5 -0
  45. package/src/agents/input-attachment-preparation.ts +13 -0
  46. package/src/agents/mcp-tool-content.ts +82 -0
  47. package/src/agents/model-rebinding-run.ts +14 -3
  48. package/src/agents/selected-repository-instructions.ts +202 -0
  49. package/src/agents/session-context.ts +41 -0
  50. package/src/agents/subscription-token-refresh.ts +73 -0
  51. package/src/agents/user-input-request.ts +19 -20
  52. package/src/errors/index.ts +1 -1
  53. package/src/errors/validation-error.ts +6 -0
  54. package/src/protocol/agent-catalog.ts +159 -0
  55. package/src/protocol/agent-command.ts +52 -7
  56. package/src/protocol/agent-content.ts +36 -16
  57. package/src/protocol/agent-event.ts +210 -73
  58. package/src/protocol/agent-identity.ts +29 -0
  59. package/src/protocol/agent-json.ts +122 -8
  60. package/src/protocol/agent-metadata.ts +2 -2
  61. package/src/protocol/agent-model.ts +13 -1
  62. package/src/protocol/agent-notification.ts +110 -0
  63. package/src/protocol/agent-presentation.ts +71 -20
  64. package/src/protocol/agent-tool.ts +66 -10
  65. package/src/protocol/agent-usage.ts +3 -1
  66. package/src/protocol/agent-user-input.ts +4 -4
  67. package/src/protocol/agent-work.ts +12 -10
  68. package/src/protocol/index.ts +2 -0
  69. package/src/protocol/rejected-keys.ts +11 -0
  70. package/src/provenance.gen.ts +3 -3
  71. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
@@ -0,0 +1,320 @@
1
+ import type { Server, Socket } from 'node:net'
2
+ import type { CodexAuthTokens } from './codex-auth'
3
+ import type { ClientRequestParamsByMethod } from './codex-protocol'
4
+ import type { CodexRunAuth } from './codex-run-auth'
5
+ import { Buffer } from 'node:buffer'
6
+ import { chmod, mkdtemp, rm } from 'node:fs/promises'
7
+ import { createServer } from 'node:net'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { AgentProviderProtocolError } from '../agent-runtime-error'
11
+ import { codexAuthTokensExpireSoon } from './codex-auth'
12
+ import { toAuthRefreshInput, toCodexLoginParams, toCodexRefreshResponse } from './codex-auth-mapper'
13
+ import { projectCodexBrowserAuthStatus } from './codex-browser-auth-status'
14
+ import { CodexJsonRpcClient, CodexJsonRpcResponseError, isCodexServerRequest } from './codex-json-rpc-client'
15
+
16
+ const MAX_BYTES = 1024 * 1024
17
+ const DEADLINE_MS = 30_000
18
+ const MAX_CONNECTIONS = 8
19
+
20
+ /**
21
+ * The credential source a broker serves. Codex runs pass their own `CodexRunAuth`, shared with the
22
+ * main native process; Claude runs pass a `LazyCodexRunAuth` that binds an account on first use.
23
+ */
24
+ export type CodexBrowserAuthSource = Pick<CodexRunAuth, 'getCurrentTokens' | 'refreshAuthTokens' | 'close'>
25
+
26
+ interface BrokerInput {
27
+ executable: string
28
+ cwd: string
29
+ environment: Record<string, string | undefined>
30
+ auth: CodexBrowserAuthSource
31
+ }
32
+ type BrowserMethod = 'initialize' | 'getAuthStatus' | 'configRequirements/read' | 'config/read' | 'config/value/write' | 'config/batchWrite'
33
+ type BrowserRequest = {
34
+ [Method in BrowserMethod]: { id: string | number, method: Method, params: ClientRequestParamsByMethod[Method] }
35
+ }[BrowserMethod]
36
+
37
+ /** Runtime-only capability: never put this socket path into session metadata. */
38
+ export class CodexBrowserAuthBroker {
39
+ readonly socketPath: string
40
+ private readonly server: Server
41
+ private readonly connections = new Set<{ close: () => Promise<void> }>()
42
+ private closing: Promise<void> | undefined
43
+
44
+ private constructor(private readonly directory: string, private readonly input: BrokerInput) {
45
+ this.socketPath = join(directory, 'rpc.sock')
46
+ this.server = createServer((socket) => {
47
+ if (this.closing || this.connections.size >= MAX_CONNECTIONS) {
48
+ socket.destroy()
49
+ return
50
+ }
51
+ const connection = new BrowserConnection(socket, this.input)
52
+ this.connections.add(connection)
53
+ socket.once('close', () => {
54
+ void connection.close().finally(() => this.connections.delete(connection))
55
+ })
56
+ })
57
+ }
58
+
59
+ static async open(input: BrokerInput): Promise<CodexBrowserAuthBroker> {
60
+ const directory = await mkdtemp(join(tmpdir(), 'codex-browser-'))
61
+ const broker = new CodexBrowserAuthBroker(directory, input)
62
+ try {
63
+ await chmod(directory, 0o700)
64
+ await new Promise<void>((resolve, reject) => {
65
+ broker.server.once('error', reject)
66
+ broker.server.listen(broker.socketPath, resolve)
67
+ })
68
+ await chmod(broker.socketPath, 0o600)
69
+ return broker
70
+ }
71
+ catch {
72
+ await broker.close()
73
+ throw brokerFailure()
74
+ }
75
+ }
76
+
77
+ async close(): Promise<void> {
78
+ this.closing ??= (async () => {
79
+ this.input.auth.close()
80
+ const listenerClosed = new Promise<void>(resolve => this.server.close(() => resolve()))
81
+ await Promise.all([...this.connections].map(async connection => await connection.close()))
82
+ await listenerClosed
83
+ await rm(this.directory, { recursive: true, force: true })
84
+ })()
85
+ await this.closing
86
+ }
87
+ }
88
+
89
+ class BrowserConnection {
90
+ private readonly client: CodexJsonRpcClient
91
+ private readonly abort = new AbortController()
92
+ private tokens: CodexAuthTokens | undefined
93
+ private initialized = false
94
+ private buffer = Buffer.alloc(0)
95
+ private queuedBytes = 0
96
+ private queuedFrames = 0
97
+ private queuedRefreshes = 0
98
+ private queue = Promise.resolve()
99
+ private refreshQueue = Promise.resolve()
100
+ private closing: Promise<void> | undefined
101
+ private readonly startupTimer: ReturnType<typeof setTimeout>
102
+
103
+ constructor(private readonly socket: Socket, private readonly input: BrokerInput) {
104
+ this.client = new CodexJsonRpcClient(input.executable, {
105
+ cwd: input.cwd,
106
+ environment: input.environment,
107
+ maxLineBytes: MAX_BYTES,
108
+ onStderr: () => undefined,
109
+ onInvalidLine: () => {
110
+ void this.close()
111
+ },
112
+ onMessage: (message) => {
113
+ if (!isCodexServerRequest(message))
114
+ return
115
+ if (message.method !== 'account/chatgptAuthTokens/refresh') {
116
+ this.client.respondError(message.id, { code: -32601, message: 'Unsupported request.' })
117
+ return
118
+ }
119
+ this.queuedRefreshes += 1
120
+ if (this.queuedRefreshes > 8) {
121
+ void this.close()
122
+ return
123
+ }
124
+ this.refreshQueue = this.refreshQueue.then(async () => {
125
+ this.abort.signal.throwIfAborted()
126
+ if (!this.tokens || message.params.reason !== 'unauthorized')
127
+ throw brokerFailure()
128
+ const refreshed = await this.deadline(input.auth.refreshAuthTokens(toAuthRefreshInput(message.params, this.tokens)))
129
+ if (this.abort.signal.aborted)
130
+ return
131
+ this.tokens = refreshed
132
+ this.client.respond(message.id, toCodexRefreshResponse(refreshed))
133
+ }).catch(() => {
134
+ void this.close()
135
+ }).finally(() => { this.queuedRefreshes -= 1 })
136
+ },
137
+ })
138
+ this.startupTimer = setTimeout(() => {
139
+ void this.close()
140
+ }, DEADLINE_MS)
141
+ this.client.child.once('close', () => {
142
+ void this.close()
143
+ })
144
+ socket.on('error', () => {
145
+ void this.close()
146
+ })
147
+ socket.on('end', () => {
148
+ void this.close()
149
+ })
150
+ socket.on('data', (chunk: Buffer) => {
151
+ this.queuedBytes += chunk.length
152
+ if (this.queuedBytes > MAX_BYTES) {
153
+ void this.close()
154
+ return
155
+ }
156
+ this.buffer = Buffer.concat([this.buffer, chunk])
157
+ let newline = this.buffer.indexOf(10)
158
+ while (newline !== -1) {
159
+ this.queuedFrames += 1
160
+ if (this.queuedFrames > 64) {
161
+ void this.close()
162
+ return
163
+ }
164
+ const line = this.buffer.subarray(0, newline)
165
+ this.buffer = this.buffer.subarray(newline + 1)
166
+ newline = this.buffer.indexOf(10)
167
+ this.queue = this.queue.then(async () => {
168
+ if (!this.closing)
169
+ await this.deadline(this.handleLine(line))
170
+ }).catch(() => {
171
+ void this.close()
172
+ }).finally(() => {
173
+ this.queuedBytes -= line.length + 1
174
+ this.queuedFrames -= 1
175
+ })
176
+ }
177
+ })
178
+ }
179
+
180
+ async close(): Promise<void> {
181
+ this.closing ??= (async () => {
182
+ clearTimeout(this.startupTimer)
183
+ this.tokens = undefined
184
+ this.abort.abort()
185
+ this.socket.destroy()
186
+ await this.client.stop()
187
+ this.buffer = Buffer.alloc(0)
188
+ })()
189
+ await this.closing
190
+ }
191
+
192
+ private async handleLine(line: Buffer): Promise<void> {
193
+ const raw: unknown = JSON.parse(line.toString('utf8'))
194
+ if (isInitializedNotification(raw))
195
+ return
196
+ const request = parseRequest(raw)
197
+ let result: unknown
198
+ if (request.method === 'initialize') {
199
+ if (this.initialized)
200
+ throw brokerFailure()
201
+ result = await this.client.request(request.method, request.params)
202
+ this.abort.signal.throwIfAborted()
203
+ this.client.notify('initialized', undefined)
204
+ const tokens = await this.input.auth.getCurrentTokens({ signal: this.abort.signal })
205
+ await this.installTokens(tokens)
206
+ this.initialized = true
207
+ clearTimeout(this.startupTimer)
208
+ }
209
+ else {
210
+ if (!this.initialized)
211
+ throw brokerFailure()
212
+ try {
213
+ if (request.method === 'getAuthStatus') {
214
+ await this.prepareAuthStatus(request.params)
215
+ const response = await this.client.request(request.method, request.params)
216
+ if (!this.tokens)
217
+ throw brokerFailure()
218
+ result = projectCodexBrowserAuthStatus({ response, params: request.params, installedTokens: this.tokens })
219
+ }
220
+ else {
221
+ result = await this.client.request(request.method, request.params)
222
+ }
223
+ }
224
+ catch (error) {
225
+ // Native config errors (including version conflicts) are command results, not
226
+ // transport failures. Keep only the numeric code; messages/data may hold secrets.
227
+ if (request.method === 'getAuthStatus' || !(error instanceof CodexJsonRpcResponseError)
228
+ || error.responseErrorCode === undefined || !Number.isSafeInteger(error.responseErrorCode)
229
+ || error.responseErrorCode < -2147483648 || error.responseErrorCode > 2147483647) {
230
+ throw brokerFailure()
231
+ }
232
+ await this.writeResponse({ id: request.id, error: { code: error.responseErrorCode, message: 'Codex configuration request failed.' } })
233
+ return
234
+ }
235
+ }
236
+ await this.writeResponse({ id: request.id, result })
237
+ }
238
+
239
+ private async prepareAuthStatus(params: ClientRequestParamsByMethod['getAuthStatus']): Promise<void> {
240
+ const current = await this.input.auth.getCurrentTokens({ signal: this.abort.signal })
241
+ await this.installTokens(current)
242
+ const installed = this.tokens
243
+ if (!installed)
244
+ throw brokerFailure()
245
+ if (params.refreshToken === true || codexAuthTokensExpireSoon(installed)) {
246
+ this.abort.signal.throwIfAborted()
247
+ const refreshed = await this.input.auth.refreshAuthTokens(toAuthRefreshInput({ reason: 'unauthorized' }, installed))
248
+ await this.installTokens(refreshed)
249
+ }
250
+ }
251
+
252
+ private async installTokens(tokens: CodexAuthTokens): Promise<void> {
253
+ this.abort.signal.throwIfAborted()
254
+ if (this.tokens?.chatgptAccountId === tokens.chatgptAccountId
255
+ && this.tokens.accessTokenGeneration === tokens.accessTokenGeneration
256
+ && this.tokens.accessTokenFingerprint === tokens.accessTokenFingerprint
257
+ && (this.tokens.chatgptPlanType ?? null) === (tokens.chatgptPlanType ?? null)) {
258
+ return
259
+ }
260
+ this.tokens = tokens
261
+ await this.client.request('account/login/start', toCodexLoginParams(tokens))
262
+ }
263
+
264
+ private async writeResponse(message: object): Promise<void> {
265
+ this.abort.signal.throwIfAborted()
266
+ const response = `${JSON.stringify(message)}\n`
267
+ if (Buffer.byteLength(response) > MAX_BYTES)
268
+ throw brokerFailure()
269
+ await new Promise<void>((resolve, reject) => this.socket.write(response, error => error ? reject(brokerFailure()) : resolve()))
270
+ }
271
+
272
+ private async deadline<T>(operation: Promise<T>): Promise<T> {
273
+ this.abort.signal.throwIfAborted()
274
+ let timer: ReturnType<typeof setTimeout> | undefined
275
+ let rejectAbort: (() => void) | undefined
276
+ try {
277
+ return await Promise.race([
278
+ operation,
279
+ new Promise<never>((_, reject) => {
280
+ rejectAbort = () => reject(brokerFailure())
281
+ this.abort.signal.addEventListener('abort', rejectAbort, { once: true })
282
+ }),
283
+ new Promise<never>((_, reject) => { timer = setTimeout(() => reject(brokerFailure()), DEADLINE_MS) }),
284
+ ])
285
+ }
286
+ finally {
287
+ clearTimeout(timer)
288
+ if (rejectAbort)
289
+ this.abort.signal.removeEventListener('abort', rejectAbort)
290
+ }
291
+ }
292
+ }
293
+
294
+ function isInitializedNotification(raw: unknown): boolean {
295
+ return typeof raw === 'object' && raw !== null && 'method' in raw && raw.method === 'initialized' && !('id' in raw)
296
+ }
297
+
298
+ function parseRequest(raw: unknown): BrowserRequest {
299
+ if (typeof raw !== 'object' || raw === null || !('id' in raw) || !('method' in raw)
300
+ || !(typeof raw.id === 'string' || (typeof raw.id === 'number' && Number.isSafeInteger(raw.id)))
301
+ || typeof raw.method !== 'string'
302
+ || !['initialize', 'getAuthStatus', 'configRequirements/read', 'config/read', 'config/value/write', 'config/batchWrite'].includes(raw.method)) {
303
+ throw brokerFailure()
304
+ }
305
+ if (raw.method === 'configRequirements/read') {
306
+ if ('params' in raw && raw.params !== null)
307
+ throw brokerFailure()
308
+ // Both native wire forms denote no payload; the generated client uses undefined.
309
+ return { id: raw.id, method: raw.method, params: undefined }
310
+ }
311
+ if (!('params' in raw) || typeof raw.params !== 'object' || raw.params === null || Array.isArray(raw.params))
312
+ throw brokerFailure()
313
+ // Raw Unix transport boundary only: Codex validates the generated native params;
314
+ // this broker validates framing, correlation and its narrow method policy.
315
+ return raw as BrowserRequest
316
+ }
317
+
318
+ function brokerFailure(): AgentProviderProtocolError {
319
+ return new AgentProviderProtocolError({ message: 'Browser authentication connection failed.' })
320
+ }
@@ -0,0 +1,55 @@
1
+ import type { CodexAuthTokens } from './codex-auth'
2
+ import type { ClientRequestParamsByMethod, ClientRequestResponsesByMethod } from './codex-protocol'
3
+ import { Buffer } from 'node:buffer'
4
+ import { z } from 'zod'
5
+ import { AgentProviderProtocolError } from '../agent-runtime-error'
6
+
7
+ const externalAuthStatusSchema = z.object({
8
+ authMethod: z.literal('chatgptAuthTokens'),
9
+ authToken: z.string().nullable(),
10
+ requiresOpenaiAuth: z.boolean().nullable(),
11
+ }).passthrough() satisfies z.ZodType<ClientRequestResponsesByMethod['getAuthStatus']>
12
+
13
+ const accountClaimsNamespace = 'https://api.openai.com/auth'
14
+ const browserAccountClaimsSchema = z.object({
15
+ [accountClaimsNamespace]: z.object({
16
+ chatgpt_account_id: z.string().refine(value => value.trim().length > 0),
17
+ }),
18
+ })
19
+
20
+ /** Pinned Browser Use accepts the legacy label only; native auth and refresh stay external. */
21
+ export function projectCodexBrowserAuthStatus(input: {
22
+ response: ClientRequestResponsesByMethod['getAuthStatus']
23
+ params: ClientRequestParamsByMethod['getAuthStatus']
24
+ installedTokens: CodexAuthTokens
25
+ }): ClientRequestResponsesByMethod['getAuthStatus'] {
26
+ // The generated client types the RPC result; validate the actual native wire value here.
27
+ const validated = externalAuthStatusSchema.safeParse(input.response)
28
+ if (!validated.success)
29
+ throw invalidBrowserAuthStatus()
30
+ const expectedToken = input.params.includeToken === true ? input.installedTokens.accessToken : null
31
+ if (validated.data.authToken !== expectedToken || !installedAccountMatchesToken(input.installedTokens))
32
+ throw invalidBrowserAuthStatus()
33
+ // Preserve the exact native token, auth requirement, and extension fields. This is not
34
+ // a claim that Codex owns refresh, nor a change to JWT claims or remote authorization.
35
+ return { ...input.response, authMethod: 'chatgpt' }
36
+ }
37
+
38
+ function installedAccountMatchesToken(tokens: CodexAuthTokens): boolean {
39
+ const parts = tokens.accessToken.split('.')
40
+ if (parts.length !== 3 || parts.some(part => part.length === 0 || /[^\w-]/.test(part)))
41
+ return false
42
+ try {
43
+ // Account consistency only, not signature verification. The native consumer uses
44
+ // this original claim for its account header; never supply a fallback or rewrite it.
45
+ const claims = browserAccountClaimsSchema.safeParse(JSON.parse(Buffer.from(parts[1]!, 'base64url').toString('utf8')))
46
+ return claims.success && claims.data[accountClaimsNamespace].chatgpt_account_id === tokens.chatgptAccountId
47
+ }
48
+ catch {
49
+ return false
50
+ }
51
+ }
52
+
53
+ function invalidBrowserAuthStatus(): AgentProviderProtocolError {
54
+ return new AgentProviderProtocolError({ message: 'Browser authentication status is invalid.' })
55
+ }
@@ -11,13 +11,18 @@ interface CodexUserInputMapperInput {
11
11
  origin?: 'user' | 'supervisor'
12
12
  author?: AgentAuthor
13
13
  senderContext?: AgentSenderContext
14
+ authorContext?: string
15
+ // The session's one-time context block, carried by its first prompt only.
16
+ sessionContext?: string
14
17
  }
15
18
 
16
19
  export function toCodexUserInput(input: CodexUserInputMapperInput): CodexUserInput[] {
17
- const { prompt, origin = 'user', author, senderContext } = input
18
- const blocks = prompt.blocks.map(toCodexInputBlock)
20
+ const { prompt, origin = 'user', author, senderContext, authorContext, sessionContext } = input
21
+ const blocks: CodexUserInput[] = prompt.blocks.map(toCodexInputBlock)
22
+ if (sessionContext)
23
+ blocks.unshift({ type: 'text', text: sessionContext, text_elements: [] })
19
24
  return origin === 'user' && author
20
- ? [{ type: 'text', text: renderAuthorContext(author, senderContext), text_elements: [] }, ...blocks]
25
+ ? [{ type: 'text', text: authorContext ?? renderAuthorContext(author, senderContext), text_elements: [] }, ...blocks]
21
26
  : blocks
22
27
  }
23
28
 
@@ -65,6 +70,36 @@ export function codexEffort(effort: AgentReasoningEffort | undefined): CodexTurn
65
70
  return effort
66
71
  }
67
72
 
73
+ export const codexReasoningSummary: CodexTurnStartParams['summary'] = 'auto'
74
+
75
+ // A type alias: the generated thread config is an index-signature record, which interfaces don't satisfy.
76
+ // eslint-disable-next-line ts/consistent-type-definitions
77
+ type CodexThreadConfig = {
78
+ 'tools.update_plan.enabled': true
79
+ 'features.step_model_switching'?: true
80
+ 'model_reasoning_effort'?: CodexTurnStartParams['effort']
81
+ 'model_reasoning_summary': CodexTurnStartParams['summary']
82
+ }
83
+
84
+ // Codex prewarms each thread by sending a baseline request over its WebSocket right after
85
+ // thread/start, using the thread's configured effort and summary. The first turn/start reuses that
86
+ // baseline incrementally (previous_response_id + delta items) only when its reasoning settings match;
87
+ // otherwise Codex logs "incremental request failed, websocket reuse properties didn't match" and
88
+ // resends the whole prompt. Configure the thread with exactly what every turn sends.
89
+ //
90
+ // `features.step_model_switching` enables `turn/settings/update`, which a steer uses to change the
91
+ // running turn's model/effort; without it the app-server answers -32600. The feature is experimental
92
+ // upstream and pinned by the sandbox image's Codex version: re-verify it on every Codex bump.
93
+ export function codexThreadConfig(effort: AgentReasoningEffort | undefined, stepModelSwitching = false): CodexThreadConfig {
94
+ const reasoningEffort = codexEffort(effort)
95
+ return {
96
+ 'tools.update_plan.enabled': true,
97
+ ...(stepModelSwitching ? { 'features.step_model_switching': true as const } : {}),
98
+ ...(reasoningEffort === undefined ? {} : { model_reasoning_effort: reasoningEffort }),
99
+ 'model_reasoning_summary': codexReasoningSummary,
100
+ }
101
+ }
102
+
68
103
  function toCodexInputBlock(block: AgentInputBlock): CodexUserInput {
69
104
  if (block.type === 'text') {
70
105
  return { type: 'text', text: block.text, text_elements: [] }