@meistrari/agent-core 0.0.0 → 0.1.0
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/README.md +24 -3
- package/bin/supervisor.ts +116 -0
- package/package.json +42 -3
- package/scripts/build-supervisor-executable.ts +32 -0
- package/src/agents/agent-error-serializer.ts +23 -0
- package/src/agents/agent-event-stream.ts +81 -0
- package/src/agents/agent-id.ts +17 -0
- package/src/agents/agent-operation.ts +11 -0
- package/src/agents/agent-provider.ts +37 -0
- package/src/agents/agent-run.ts +32 -0
- package/src/agents/agent-runtime-error.ts +161 -0
- package/src/agents/agent-session-events.ts +32 -0
- package/src/agents/agent-tool-runner.ts +83 -0
- package/src/agents/agent-tool.ts +111 -0
- package/src/agents/author-context.ts +36 -0
- package/src/agents/claude/claude-command-mapper.ts +234 -0
- package/src/agents/claude/claude-event-mapper.ts +736 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +464 -0
- package/src/agents/claude/claude-tool-mapper.ts +186 -0
- package/src/agents/claude/index.ts +1 -0
- package/src/agents/codex/codex-auth.ts +34 -0
- package/src/agents/codex/codex-command-mapper.ts +78 -0
- package/src/agents/codex/codex-event-mapper.ts +708 -0
- package/src/agents/codex/codex-json-rpc-client.ts +326 -0
- package/src/agents/codex/codex-protocol.ts +36 -0
- package/src/agents/codex/codex-provider.ts +1050 -0
- package/src/agents/codex/codex-run.ts +404 -0
- package/src/agents/codex/codex-skill-catalog.ts +158 -0
- package/src/agents/codex/codex-skill-roots.ts +19 -0
- package/src/agents/codex/codex-tool-mapper.ts +55 -0
- package/src/agents/codex/codex.errors.ts +68 -0
- package/src/agents/codex/generated/meta.gen.ts +606 -0
- package/src/agents/codex/generated/namespaces.gen.ts +311 -0
- package/src/agents/codex/generated/schema.gen.ts +34883 -0
- package/src/agents/codex/index.ts +2 -0
- package/src/agents/index.ts +14 -0
- package/src/agents/input-attachment-preparation.ts +86 -0
- package/src/agents/input-attachment.errors.ts +16 -0
- package/src/agents/instructions.ts +8 -0
- package/src/agents/materialized-input-attachment.ts +26 -0
- package/src/agents/message-id.ts +23 -0
- package/src/agents/normalize.ts +8 -0
- package/src/agents/sandbox-environment.ts +1 -0
- package/src/agents/tools/ping.tool.ts +13 -0
- package/src/agents/user-input-request.ts +470 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +125 -0
- package/src/supervisor/config.ts +49 -0
- package/src/supervisor/control-authority-verifier.ts +135 -0
- package/src/supervisor/create-supervisor-runtime.ts +25 -0
- package/src/supervisor/errors.ts +24 -0
- package/src/supervisor/index.ts +34 -0
- package/src/supervisor/persistence/json.ts +21 -0
- package/src/supervisor/persistence/state-discovery.ts +56 -0
- package/src/supervisor/persistence/supervisor-store.ts +364 -0
- package/src/supervisor/ports/index.ts +109 -0
- package/src/supervisor/provider-factory.ts +37 -0
- package/src/supervisor/resident.ts +143 -0
- package/src/supervisor/rpc-client.ts +120 -0
- package/src/supervisor/runtime-handler.ts +309 -0
- package/src/supervisor/websocket-server.ts +434 -0
- package/src/supervisor-protocol/bootstrap.ts +1 -1
- package/src/template-onboarding.ts +47 -0
- package/src/testing/es256-test-keys.ts +73 -0
- package/src/testing/in-memory-runtime-control-plane.ts +205 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/loopback-supervisor-connection.ts +71 -0
- package/src/testing/scripted-provider.ts +64 -0
- package/src/worker-runtime-client/command-pump.ts +132 -0
- package/src/worker-runtime-client/connection-attempt.ts +340 -0
- package/src/worker-runtime-client/control-authority-signer.ts +100 -0
- package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
- package/src/worker-runtime-client/frame-processor.ts +178 -0
- package/src/worker-runtime-client/index.ts +27 -0
- package/src/worker-runtime-client/lease-reconciler.ts +14 -0
- package/src/worker-runtime-client/ports.ts +137 -0
- package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
- package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
- package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +300 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentAuthor,
|
|
3
|
+
AgentPrompt,
|
|
4
|
+
AgentReasoningEffort,
|
|
5
|
+
AgentRunMetadata,
|
|
6
|
+
InterruptAgentCommand,
|
|
7
|
+
RespondUserInputAgentCommand,
|
|
8
|
+
SendPromptCommand,
|
|
9
|
+
StopAgentCommand,
|
|
10
|
+
} from '../../protocol'
|
|
11
|
+
import type { AgentEventDraft } from '../agent-event-stream'
|
|
12
|
+
import type { AgentOperationOptions } from '../agent-operation'
|
|
13
|
+
import type { AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
14
|
+
import type { UserInputRequestInput, UserInputRequestStore } from '../user-input-request'
|
|
15
|
+
import type { CodexEventMapperState } from './codex-event-mapper'
|
|
16
|
+
import type { CodexJsonRpcClient } from './codex-json-rpc-client'
|
|
17
|
+
import type { ClientRequestResponsesByMethod, CodexTurnStartParams, CodexUserInput } from './codex-protocol'
|
|
18
|
+
import { AgentEventStream } from '../agent-event-stream'
|
|
19
|
+
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from '../agent-runtime-error'
|
|
20
|
+
import { deriveMessageId } from '../message-id'
|
|
21
|
+
import { codexEffort, prepareCodexUserInput, toCodexUserInput } from './codex-command-mapper'
|
|
22
|
+
import { CodexJsonRpcResponseError } from './codex-json-rpc-client'
|
|
23
|
+
import { CodexSkillCatalog } from './codex-skill-catalog'
|
|
24
|
+
import { catalogRepositorySkillRoots, workspaceSkillRoots } from './codex-skill-roots'
|
|
25
|
+
import { CodexJsonRpcTransportClosedError } from './codex.errors'
|
|
26
|
+
|
|
27
|
+
const codexReasoningSummary: CodexTurnStartParams['summary'] = 'auto'
|
|
28
|
+
|
|
29
|
+
interface CodexRunAuthFailureInput {
|
|
30
|
+
reason: 'codex_auth_refresh_failed'
|
|
31
|
+
error: { message: string, name?: string, stack?: string }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PreparedCodexTurn {
|
|
35
|
+
input: CodexUserInput[]
|
|
36
|
+
commandId: string
|
|
37
|
+
origin: 'user' | 'supervisor'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CodexRunInput {
|
|
41
|
+
metadata: AgentRunMetadata
|
|
42
|
+
reasoningEffort?: AgentReasoningEffort
|
|
43
|
+
client: CodexJsonRpcClient
|
|
44
|
+
requestedCwd: string
|
|
45
|
+
workspaceRepositoryRoots: readonly string[]
|
|
46
|
+
registeredSkillRoots: readonly string[]
|
|
47
|
+
repositorySkillRoots: readonly string[]
|
|
48
|
+
reconcileInstructionBridge: (repositoryRoots: readonly string[]) => Promise<void>
|
|
49
|
+
userInputRequests: UserInputRequestStore
|
|
50
|
+
eventMapperState: CodexEventMapperState
|
|
51
|
+
abortSignal: AbortSignal
|
|
52
|
+
onStop: () => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class CodexRun implements AgentRun {
|
|
56
|
+
readonly events: AgentEventStream
|
|
57
|
+
readonly metadata: AgentRunMetadata
|
|
58
|
+
private readonly client: CodexJsonRpcClient
|
|
59
|
+
private readonly reasoningEffort: AgentReasoningEffort | undefined
|
|
60
|
+
private readonly onStop: () => void
|
|
61
|
+
private readonly userInputRequests: UserInputRequestStore
|
|
62
|
+
private readonly eventMapperState: CodexEventMapperState
|
|
63
|
+
private readonly skillCatalog: CodexSkillCatalog
|
|
64
|
+
private readonly requestedCwd: string
|
|
65
|
+
private readonly reconcileInstructionBridge: (repositoryRoots: readonly string[]) => Promise<void>
|
|
66
|
+
private workspaceRepositoryRoots: readonly string[]
|
|
67
|
+
private registeredSkillRoots: readonly string[]
|
|
68
|
+
private workspacePrepared = false
|
|
69
|
+
private stopped = false
|
|
70
|
+
private stoppingTask: Promise<void> | undefined
|
|
71
|
+
private activeTurnId: string | undefined
|
|
72
|
+
private readonly interruptingTurnIds = new Set<string>()
|
|
73
|
+
|
|
74
|
+
constructor(input: CodexRunInput) {
|
|
75
|
+
this.metadata = input.metadata
|
|
76
|
+
this.client = input.client
|
|
77
|
+
this.reasoningEffort = input.reasoningEffort
|
|
78
|
+
this.onStop = input.onStop
|
|
79
|
+
this.userInputRequests = input.userInputRequests
|
|
80
|
+
this.eventMapperState = input.eventMapperState
|
|
81
|
+
this.requestedCwd = input.requestedCwd
|
|
82
|
+
this.workspaceRepositoryRoots = [...input.workspaceRepositoryRoots]
|
|
83
|
+
this.registeredSkillRoots = input.registeredSkillRoots
|
|
84
|
+
this.reconcileInstructionBridge = input.reconcileInstructionBridge
|
|
85
|
+
this.events = new AgentEventStream({
|
|
86
|
+
provider: 'codex',
|
|
87
|
+
sessionId: input.metadata.sessionId,
|
|
88
|
+
providerSessionId: input.metadata.providerSessionId,
|
|
89
|
+
})
|
|
90
|
+
this.skillCatalog = new CodexSkillCatalog({
|
|
91
|
+
client: input.client,
|
|
92
|
+
cwd: input.metadata.cwd,
|
|
93
|
+
repositorySkillRoots: input.repositorySkillRoots,
|
|
94
|
+
emitSnapshot: draft => this.pushEvent(draft),
|
|
95
|
+
emitError: draft => this.pushEvent(draft),
|
|
96
|
+
})
|
|
97
|
+
input.abortSignal.addEventListener('abort', () => void this.stop({
|
|
98
|
+
type: 'agent.stop',
|
|
99
|
+
provider: 'codex',
|
|
100
|
+
sessionId: this.metadata.sessionId,
|
|
101
|
+
providerSessionId: this.metadata.providerSessionId,
|
|
102
|
+
reason: 'aborted',
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pushEvent(draft: AgentEventDraft): void {
|
|
107
|
+
if (this.stopped)
|
|
108
|
+
return
|
|
109
|
+
const decorated = this.userInputRequests.decorateTurnEnded(draft)
|
|
110
|
+
if (decorated.type === 'session.state.changed' && decorated.payload.state === 'idle' && this.userInputRequests.shouldHoldIdle()) {
|
|
111
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'waiting_for_user_input' } })
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
this.events.push(decorated)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async prepareWorkspace(options: AgentOperationOptions = {}): Promise<void> {
|
|
118
|
+
this.assertRunning()
|
|
119
|
+
if (this.workspacePrepared)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
const abort = () => void this.stop({
|
|
123
|
+
type: 'agent.stop',
|
|
124
|
+
provider: 'codex',
|
|
125
|
+
sessionId: this.metadata.sessionId,
|
|
126
|
+
providerSessionId: this.metadata.providerSessionId,
|
|
127
|
+
reason: 'workspace_preparation_aborted',
|
|
128
|
+
}).catch(() => undefined)
|
|
129
|
+
if (options.signal?.aborted)
|
|
130
|
+
abort()
|
|
131
|
+
else
|
|
132
|
+
options.signal?.addEventListener('abort', abort, { once: true })
|
|
133
|
+
try {
|
|
134
|
+
options.signal?.throwIfAborted()
|
|
135
|
+
await this.client.request('skills/extraRoots/set', {
|
|
136
|
+
extraRoots: [...this.registeredSkillRoots],
|
|
137
|
+
})
|
|
138
|
+
options.signal?.throwIfAborted()
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
await this.stop({
|
|
142
|
+
type: 'agent.stop',
|
|
143
|
+
provider: 'codex',
|
|
144
|
+
sessionId: this.metadata.sessionId,
|
|
145
|
+
providerSessionId: this.metadata.providerSessionId,
|
|
146
|
+
reason: 'workspace_preparation_failed',
|
|
147
|
+
}).catch(() => undefined)
|
|
148
|
+
options.signal?.throwIfAborted()
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
options.signal?.removeEventListener('abort', abort)
|
|
153
|
+
}
|
|
154
|
+
this.workspacePrepared = true
|
|
155
|
+
this.skillCatalog.requestRefresh()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async addWorkspaceRepositoryRoot(root: string, options: AgentOperationOptions = {}): Promise<void> {
|
|
159
|
+
this.assertRunning()
|
|
160
|
+
options.signal?.throwIfAborted()
|
|
161
|
+
if (this.workspaceRepositoryRoots.includes(root))
|
|
162
|
+
return
|
|
163
|
+
const repositoryRoots = [...this.workspaceRepositoryRoots, root]
|
|
164
|
+
await this.reconcileInstructionBridge(repositoryRoots)
|
|
165
|
+
options.signal?.throwIfAborted()
|
|
166
|
+
const registeredSkillRoots = workspaceSkillRoots(repositoryRoots)
|
|
167
|
+
await this.client.request('skills/extraRoots/set', { extraRoots: registeredSkillRoots })
|
|
168
|
+
this.workspaceRepositoryRoots = repositoryRoots
|
|
169
|
+
this.registeredSkillRoots = registeredSkillRoots
|
|
170
|
+
this.skillCatalog.setRepositorySkillRoots(catalogRepositorySkillRoots({
|
|
171
|
+
repositoryRoots,
|
|
172
|
+
requestedCwd: this.requestedCwd,
|
|
173
|
+
runtimeCwd: this.metadata.cwd,
|
|
174
|
+
}))
|
|
175
|
+
this.skillCatalog.requestRefresh()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
requestSkillsRefresh(): void {
|
|
179
|
+
if (!this.stopped && this.workspacePrepared)
|
|
180
|
+
this.skillCatalog.requestRefresh()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async requestUserInput(input: UserInputRequestInput): Promise<{ requestId: string }> {
|
|
184
|
+
this.assertRunning()
|
|
185
|
+
const request = this.userInputRequests.create(input)
|
|
186
|
+
this.pushEvent(request.event)
|
|
187
|
+
return { requestId: request.requestId }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setActiveTurn(turnId: string | undefined): void {
|
|
191
|
+
this.activeTurnId = turnId
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
clearActiveTurn(): void {
|
|
195
|
+
this.activeTurnId = undefined
|
|
196
|
+
this.interruptingTurnIds.clear()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async failActiveTurnAndEndRun(input: CodexRunAuthFailureInput): Promise<void> {
|
|
200
|
+
if (this.stopped)
|
|
201
|
+
return
|
|
202
|
+
this.pushEvent({ type: 'error', payload: { message: 'Codex auth refresh failed.', fatal: false, source: 'provider', error: input.error } })
|
|
203
|
+
if (this.activeTurnId) {
|
|
204
|
+
this.pushEvent({
|
|
205
|
+
type: 'turn.ended',
|
|
206
|
+
turnId: this.activeTurnId,
|
|
207
|
+
actor: { type: 'main', actorId: 'main' },
|
|
208
|
+
payload: { status: 'failed', reason: input.reason, error: input.error },
|
|
209
|
+
})
|
|
210
|
+
this.activeTurnId = undefined
|
|
211
|
+
this.interruptingTurnIds.clear()
|
|
212
|
+
}
|
|
213
|
+
this.pushEvent({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
214
|
+
this.stopped = true
|
|
215
|
+
// Pinned Codex forwards preceding stdin lines before EOF, preserving the refresh error ordering.
|
|
216
|
+
await this.client.closeInputAndStop()
|
|
217
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: input.error } })
|
|
218
|
+
this.events.end()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async sendPrompt(command: SendPromptCommand, options: AgentOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
222
|
+
this.assertCommandTargetsRun(command)
|
|
223
|
+
this.assertRunning()
|
|
224
|
+
// Released 0.145.0 accepts turn/start during an active turn by silently steering the input
|
|
225
|
+
// and answering a phantom turn id, so the next-vs-active race must be refused from adapter
|
|
226
|
+
// state before any request — nothing is prepared or retained.
|
|
227
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
228
|
+
const prepared = await this.prepareTurn({
|
|
229
|
+
prompt: command.prompt,
|
|
230
|
+
commandId: command.commandId,
|
|
231
|
+
origin: command.origin,
|
|
232
|
+
author: command.author,
|
|
233
|
+
options,
|
|
234
|
+
})
|
|
235
|
+
// A turn/started fact may have reduced during attachment preparation.
|
|
236
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
237
|
+
|
|
238
|
+
const activeTurnId = this.activeTurnId
|
|
239
|
+
if (command.mode === 'next' || !activeTurnId)
|
|
240
|
+
return await this.startPreparedTurn(prepared)
|
|
241
|
+
|
|
242
|
+
const clientUserMessageId = this.rememberPromptCommand(prepared.commandId, prepared.origin)
|
|
243
|
+
let result: ClientRequestResponsesByMethod['turn/steer']
|
|
244
|
+
try {
|
|
245
|
+
result = await this.client.request('turn/steer', {
|
|
246
|
+
threadId: this.metadata.providerSessionId,
|
|
247
|
+
expectedTurnId: activeTurnId,
|
|
248
|
+
input: prepared.input,
|
|
249
|
+
clientUserMessageId,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (isSteerNotAcceptedResponse(error)) {
|
|
254
|
+
throw new AgentPromptDeferredError({
|
|
255
|
+
message: 'Codex rejected the steer at a turn boundary race.',
|
|
256
|
+
cause: error,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
throw mapTurnTransportLoss(error)
|
|
260
|
+
}
|
|
261
|
+
return { kind: 'turn', turnId: result.turnId, placement: 'steered' }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private deferNextWhileTurnActive(mode: SendPromptCommand['mode']): void {
|
|
265
|
+
if (mode !== 'next' || !this.activeTurnId)
|
|
266
|
+
return
|
|
267
|
+
throw new AgentPromptDeferredError({
|
|
268
|
+
message: 'Codex turn is active; a next prompt cannot start a turn.',
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async respondUserInput(command: RespondUserInputAgentCommand, options: AgentOperationOptions = {}): Promise<AgentUserInputSubmission> {
|
|
273
|
+
this.assertCommandTargetsRun(command)
|
|
274
|
+
this.assertRunning()
|
|
275
|
+
options.signal?.throwIfAborted()
|
|
276
|
+
const resolved = this.userInputRequests.resolve(command)
|
|
277
|
+
if (!resolved)
|
|
278
|
+
return 'already_delivered'
|
|
279
|
+
if (resolved.event)
|
|
280
|
+
this.pushEvent(resolved.event)
|
|
281
|
+
const prepared = await this.prepareTurn({ prompt: resolved.prompt, commandId: command.commandId, options })
|
|
282
|
+
await this.startPreparedTurn(prepared)
|
|
283
|
+
this.userInputRequests.markDelivered(command.requestId)
|
|
284
|
+
return 'submitted'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async interrupt(command: InterruptAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
288
|
+
this.assertCommandTargetsRun(command)
|
|
289
|
+
this.assertRunning()
|
|
290
|
+
options.signal?.throwIfAborted()
|
|
291
|
+
const turnId = command.turnId ?? this.activeTurnId
|
|
292
|
+
if (!turnId)
|
|
293
|
+
return
|
|
294
|
+
// codex never answers a second turn/interrupt for a turn already being interrupted; the duplicate await would hang forever.
|
|
295
|
+
if (this.interruptingTurnIds.has(turnId))
|
|
296
|
+
return
|
|
297
|
+
this.interruptingTurnIds.add(turnId)
|
|
298
|
+
try {
|
|
299
|
+
await this.client.request('turn/interrupt', { threadId: this.metadata.providerSessionId, turnId })
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
this.interruptingTurnIds.delete(turnId)
|
|
303
|
+
throw mapTurnTransportLoss(error)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async stop(command: StopAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
308
|
+
this.assertCommandTargetsRun(command)
|
|
309
|
+
if (this.stoppingTask) {
|
|
310
|
+
await this.stoppingTask
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
if (this.stopped)
|
|
314
|
+
return
|
|
315
|
+
options.signal?.throwIfAborted()
|
|
316
|
+
this.stopped = true
|
|
317
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'stopping' } })
|
|
318
|
+
this.onStop()
|
|
319
|
+
this.stoppingTask = (async () => {
|
|
320
|
+
await this.client.stop()
|
|
321
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'stopped' } })
|
|
322
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'stopped' } })
|
|
323
|
+
this.events.end()
|
|
324
|
+
})()
|
|
325
|
+
await this.stoppingTask
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async prepareTurn({ prompt, commandId, origin = 'user', author, options }: { prompt: AgentPrompt, commandId: string, origin?: 'user' | 'supervisor', author?: AgentAuthor, options: AgentOperationOptions }): Promise<PreparedCodexTurn> {
|
|
329
|
+
options.signal?.throwIfAborted()
|
|
330
|
+
const mapperInput = { prompt, origin, author, senderContext: options.senderContext }
|
|
331
|
+
const input = options.inputAttachmentPreparation
|
|
332
|
+
? await prepareCodexUserInput({ ...mapperInput, preparation: options.inputAttachmentPreparation })
|
|
333
|
+
: toCodexUserInput(mapperInput)
|
|
334
|
+
options.signal?.throwIfAborted()
|
|
335
|
+
return { input, commandId, origin }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private async startPreparedTurn(prepared: PreparedCodexTurn): Promise<AgentPromptAcceptance> {
|
|
339
|
+
const clientUserMessageId = this.rememberPromptCommand(prepared.commandId, prepared.origin)
|
|
340
|
+
let result: ClientRequestResponsesByMethod['turn/start']
|
|
341
|
+
try {
|
|
342
|
+
result = await this.client.request('turn/start', {
|
|
343
|
+
threadId: this.metadata.providerSessionId,
|
|
344
|
+
input: prepared.input,
|
|
345
|
+
cwd: this.metadata.cwd,
|
|
346
|
+
model: this.metadata.model,
|
|
347
|
+
effort: codexEffort(this.reasoningEffort),
|
|
348
|
+
summary: codexReasoningSummary,
|
|
349
|
+
approvalPolicy: 'never',
|
|
350
|
+
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
351
|
+
clientUserMessageId,
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
throw mapTurnTransportLoss(error)
|
|
356
|
+
}
|
|
357
|
+
this.activeTurnId = result.turn.id
|
|
358
|
+
return { kind: 'turn', turnId: result.turn.id, placement: 'started' }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private rememberPromptCommand(commandId: string, origin: 'user' | 'supervisor'): string {
|
|
362
|
+
const clientUserMessageId = deriveMessageId(commandId)
|
|
363
|
+
this.eventMapperState.promptCommandByClientUserMessageId.set(clientUserMessageId, { commandId, origin })
|
|
364
|
+
return clientUserMessageId
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private assertRunning(): void {
|
|
368
|
+
if (this.stopped) {
|
|
369
|
+
throw new AgentRunStateError({ message: 'Codex run has stopped.' })
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private assertCommandTargetsRun(command: { provider: string, sessionId: string, providerSessionId: string }): void {
|
|
374
|
+
if (command.provider !== 'codex' || command.sessionId !== this.metadata.sessionId || command.providerSessionId !== this.metadata.providerSessionId) {
|
|
375
|
+
throw new AgentRunStateError({ message: 'Command does not target this Codex run.' })
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Only the proven not-accepted fact may defer a steer: on the pinned runtime every turn/steer
|
|
381
|
+
// -32600 means the input was not accepted. Other codes (-32001 included) are not blanket
|
|
382
|
+
// overload/deferral facts and keep their honest provider error.
|
|
383
|
+
function isSteerNotAcceptedResponse(error: unknown): error is CodexJsonRpcResponseError {
|
|
384
|
+
return error instanceof CodexJsonRpcResponseError && error.method === 'turn/steer' && error.responseErrorCode === -32600
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Transport loss on an input-carrying request maps to write-state truth: possible_write means the
|
|
388
|
+
// input may already be in the provider thread (delivery unknown, never blind-retryable);
|
|
389
|
+
// before_write is proven non-delivery and safe to retry. Anything else passes through unchanged.
|
|
390
|
+
function mapTurnTransportLoss(error: unknown): unknown {
|
|
391
|
+
if (!(error instanceof CodexJsonRpcTransportClosedError))
|
|
392
|
+
return error
|
|
393
|
+
if (error.writeState === 'possible_write') {
|
|
394
|
+
return new AgentDeliveryUnknownError({
|
|
395
|
+
message: 'Codex app-server connection was lost after the input may have been written.',
|
|
396
|
+
cause: error,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
return new AgentNotAcceptedError({
|
|
400
|
+
message: 'Codex app-server connection was closed before the input was written.',
|
|
401
|
+
recovery: { kind: 'retry', reason: 'provider_connection_lost' },
|
|
402
|
+
cause: error,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { AgentSkill } from '../../protocol'
|
|
2
|
+
import type { AgentEventDraft } from '../agent-event-stream'
|
|
3
|
+
import type { CodexJsonRpcClient } from './codex-json-rpc-client'
|
|
4
|
+
import type { ClientRequestResponsesByMethod } from './codex-protocol'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { realpath } from 'node:fs/promises'
|
|
7
|
+
import { isAbsolute, relative, sep } from 'node:path'
|
|
8
|
+
|
|
9
|
+
type SessionSkillsUpdatedDraft = Extract<AgentEventDraft, { type: 'session.skills.updated' }>
|
|
10
|
+
type ErrorDraft = Extract<AgentEventDraft, { type: 'error' }>
|
|
11
|
+
type SkillsListResponse = ClientRequestResponsesByMethod['skills/list']
|
|
12
|
+
|
|
13
|
+
interface CodexSkillCatalogInput {
|
|
14
|
+
client: CodexJsonRpcClient
|
|
15
|
+
cwd: string
|
|
16
|
+
repositorySkillRoots: readonly string[]
|
|
17
|
+
emitSnapshot: (draft: SessionSkillsUpdatedDraft) => void
|
|
18
|
+
emitError: (draft: ErrorDraft) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CodexSkillCatalog {
|
|
22
|
+
private readonly client: CodexJsonRpcClient
|
|
23
|
+
private readonly cwd: string
|
|
24
|
+
private repositorySkillRoots: readonly string[]
|
|
25
|
+
private readonly emitSnapshot: CodexSkillCatalogInput['emitSnapshot']
|
|
26
|
+
private readonly emitError: CodexSkillCatalogInput['emitError']
|
|
27
|
+
private refreshInFlight = false
|
|
28
|
+
private refreshRequested = false
|
|
29
|
+
private refreshScheduled = false
|
|
30
|
+
private lastSnapshot: string | undefined
|
|
31
|
+
|
|
32
|
+
constructor({ client, cwd, repositorySkillRoots, emitSnapshot, emitError }: CodexSkillCatalogInput) {
|
|
33
|
+
this.client = client
|
|
34
|
+
this.cwd = cwd
|
|
35
|
+
this.repositorySkillRoots = repositorySkillRoots
|
|
36
|
+
this.emitSnapshot = emitSnapshot
|
|
37
|
+
this.emitError = emitError
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setRepositorySkillRoots(repositorySkillRoots: readonly string[]): void {
|
|
41
|
+
this.repositorySkillRoots = repositorySkillRoots
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
requestRefresh(): void {
|
|
45
|
+
this.refreshRequested = true
|
|
46
|
+
if (this.refreshInFlight || this.refreshScheduled)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
this.refreshScheduled = true
|
|
50
|
+
const immediate = setImmediate(() => {
|
|
51
|
+
this.refreshScheduled = false
|
|
52
|
+
void this.refreshUntilCurrent()
|
|
53
|
+
})
|
|
54
|
+
immediate.unref?.()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async refreshUntilCurrent(): Promise<void> {
|
|
58
|
+
if (this.refreshInFlight)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
this.refreshInFlight = true
|
|
62
|
+
try {
|
|
63
|
+
while (this.refreshRequested) {
|
|
64
|
+
this.refreshRequested = false
|
|
65
|
+
await this.refreshWithOneRetry()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
this.refreshInFlight = false
|
|
70
|
+
if (this.refreshRequested)
|
|
71
|
+
this.requestRefresh()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async refreshWithOneRetry(): Promise<void> {
|
|
76
|
+
if (await this.refreshOnce())
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
// The force-reloaded retry also satisfies invalidations received during the failed request.
|
|
80
|
+
this.refreshRequested = false
|
|
81
|
+
await this.refreshOnce()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async refreshOnce(): Promise<boolean> {
|
|
85
|
+
try {
|
|
86
|
+
const repositorySkillRoots = await repositorySkillRootAliases(this.repositorySkillRoots)
|
|
87
|
+
const response = await this.client.request('skills/list', {
|
|
88
|
+
cwds: [this.cwd],
|
|
89
|
+
forceReload: true,
|
|
90
|
+
})
|
|
91
|
+
const skills = availableSkills(response, repositorySkillRoots)
|
|
92
|
+
const snapshot = JSON.stringify(skills)
|
|
93
|
+
if (snapshot === this.lastSnapshot)
|
|
94
|
+
return true
|
|
95
|
+
|
|
96
|
+
this.lastSnapshot = snapshot
|
|
97
|
+
this.emitSnapshot({ type: 'session.skills.updated', payload: { skills } })
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
this.emitError({
|
|
102
|
+
type: 'error',
|
|
103
|
+
payload: {
|
|
104
|
+
message: 'Codex skills catalog refresh failed.',
|
|
105
|
+
fatal: false,
|
|
106
|
+
source: 'provider',
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function repositorySkillRootAliases(roots: readonly string[]): Promise<string[]> {
|
|
115
|
+
const physicalRoots = (await Promise.allSettled(roots.map(async root => await realpath(root))))
|
|
116
|
+
.flatMap(result => result.status === 'fulfilled' ? [result.value] : [])
|
|
117
|
+
return [...new Set([...roots, ...physicalRoots])]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function availableSkills(response: SkillsListResponse, repositorySkillRoots: readonly string[]): AgentSkill[] {
|
|
121
|
+
return response.data.flatMap(entry => entry.skills
|
|
122
|
+
.filter(skill => skill.enabled)
|
|
123
|
+
.map(skill => ({
|
|
124
|
+
id: skillId(skill.path),
|
|
125
|
+
name: skill.name,
|
|
126
|
+
description: skill.description,
|
|
127
|
+
scope: canonicalSkillScope({ providerScope: skill.scope, path: skill.path, repositorySkillRoots }),
|
|
128
|
+
})))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function canonicalSkillScope(input: {
|
|
132
|
+
providerScope: SkillsListResponse['data'][number]['skills'][number]['scope']
|
|
133
|
+
path: string
|
|
134
|
+
repositorySkillRoots: readonly string[]
|
|
135
|
+
}): AgentSkill['scope'] {
|
|
136
|
+
// Codex 0.145 labels runtime extra roots as user-scoped even when we registered them for selected repositories.
|
|
137
|
+
if (input.providerScope === 'user' && input.repositorySkillRoots.some(root => isPathInside(root, input.path)))
|
|
138
|
+
return 'repository'
|
|
139
|
+
|
|
140
|
+
switch (input.providerScope) {
|
|
141
|
+
case 'user':
|
|
142
|
+
case 'system':
|
|
143
|
+
case 'admin':
|
|
144
|
+
return input.providerScope
|
|
145
|
+
case 'repo':
|
|
146
|
+
return 'repository'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isPathInside(root: string, candidate: string): boolean {
|
|
151
|
+
const pathFromRoot = relative(root, candidate)
|
|
152
|
+
return pathFromRoot === '' || (!isAbsolute(pathFromRoot) && pathFromRoot !== '..' && !pathFromRoot.startsWith(`..${sep}`))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function skillId(path: string): string {
|
|
156
|
+
const digest = createHash('sha256').update(`codex-skill\0${path}`).digest('hex')
|
|
157
|
+
return `skill_${digest}`
|
|
158
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { join, relative, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
export function workspaceSkillRoots(repositoryRoots: readonly string[]): string[] {
|
|
4
|
+
return stableRepositoryRoots(repositoryRoots).map(root => join(root, '.agents', 'skills'))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function catalogRepositorySkillRoots(input: {
|
|
8
|
+
repositoryRoots: readonly string[]
|
|
9
|
+
requestedCwd: string
|
|
10
|
+
runtimeCwd: string
|
|
11
|
+
}): string[] {
|
|
12
|
+
const requestedRoots = workspaceSkillRoots(input.repositoryRoots)
|
|
13
|
+
const runtimeRepositoryRoots = input.repositoryRoots.map(root => resolve(input.runtimeCwd, relative(input.requestedCwd, root)))
|
|
14
|
+
return [...new Set([...requestedRoots, ...workspaceSkillRoots(runtimeRepositoryRoots)])]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function stableRepositoryRoots(repositoryRoots: readonly string[]): string[] {
|
|
18
|
+
return [...new Set(repositoryRoots)].sort()
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JsonValue } from '../../protocol'
|
|
2
|
+
import type { AgentTool, AgentToolContext } from '../agent-tool'
|
|
3
|
+
import type {
|
|
4
|
+
CodexDynamicToolCallParams,
|
|
5
|
+
CodexDynamicToolCallResponse,
|
|
6
|
+
CodexDynamicToolOutputContentItem,
|
|
7
|
+
CodexDynamicToolSpec,
|
|
8
|
+
} from './codex-protocol'
|
|
9
|
+
import { isAppError } from '../../errors'
|
|
10
|
+
import { runAgentTool } from '../agent-tool-runner'
|
|
11
|
+
|
|
12
|
+
const UNEXPECTED_TOOL_FAILURE_MESSAGE = 'The tool could not be executed.'
|
|
13
|
+
|
|
14
|
+
export function toCodexDynamicTools(tools: readonly AgentTool[]): CodexDynamicToolSpec[] {
|
|
15
|
+
return tools.map(runtimeTool => ({
|
|
16
|
+
type: 'function',
|
|
17
|
+
name: runtimeTool.name,
|
|
18
|
+
description: runtimeTool.description,
|
|
19
|
+
inputSchema: runtimeTool.definition().inputSchema,
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runCodexDynamicTool(input: {
|
|
24
|
+
tool: AgentTool
|
|
25
|
+
params: CodexDynamicToolCallParams
|
|
26
|
+
context: AgentToolContext
|
|
27
|
+
}): Promise<CodexDynamicToolCallResponse> {
|
|
28
|
+
try {
|
|
29
|
+
const result = await runAgentTool({
|
|
30
|
+
tool: input.tool,
|
|
31
|
+
actor: input.params.threadId === input.context.providerSessionId ? 'main' : 'subagent',
|
|
32
|
+
rawInput: input.params.arguments as JsonValue,
|
|
33
|
+
context: input.context,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
success: result.success,
|
|
38
|
+
contentItems: result.content.map(toCodexToolContent),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (isAppError(error))
|
|
43
|
+
throw error
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
contentItems: [{ type: 'inputText', text: UNEXPECTED_TOOL_FAILURE_MESSAGE }],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toCodexToolContent(content: { type: 'text', text: string } | { type: 'image', data: string, mimeType: string }): CodexDynamicToolOutputContentItem {
|
|
52
|
+
if (content.type === 'text')
|
|
53
|
+
return { type: 'inputText', text: content.text }
|
|
54
|
+
return { type: 'inputImage', imageUrl: `data:${content.mimeType};base64,${content.data}` }
|
|
55
|
+
}
|