@navels/neal 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/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import { Codex } from '@openai/codex-sdk';
|
|
2
|
+
import { runStructuredJsonProtocol } from '../agents/structured-json.js';
|
|
3
|
+
import { NealProviderError } from './types.js';
|
|
4
|
+
const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
5
|
+
class CodexInactivityTimeoutError extends Error {
|
|
6
|
+
constructor(timeoutMs) {
|
|
7
|
+
super(`Codex timed out after ${Math.round(timeoutMs / 1000)}s without progress`);
|
|
8
|
+
this.name = 'CodexInactivityTimeoutError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// In-round transient (`api_error`) retries back off exponentially: 500 ms
|
|
12
|
+
// base, doubling per retry, capped at 5 s — the same schedule as the
|
|
13
|
+
// generic-agentic adapter. `sleep` is injectable through the adapter options
|
|
14
|
+
// so tests observe the delays without wall-clock waiting.
|
|
15
|
+
const API_RETRY_BASE_DELAY_MS = 500;
|
|
16
|
+
const API_RETRY_MAX_DELAY_MS = 5_000;
|
|
17
|
+
function defaultSleep(ms) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
setTimeout(resolve, ms);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function getApiRetryDelayMs(retryCount) {
|
|
23
|
+
return Math.min(API_RETRY_BASE_DELAY_MS * 2 ** (retryCount - 1), API_RETRY_MAX_DELAY_MS);
|
|
24
|
+
}
|
|
25
|
+
async function nextWithTimeout(promise, timeoutMs, onTimeout) {
|
|
26
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
onTimeout?.();
|
|
29
|
+
rejectPromise(new CodexInactivityTimeoutError(timeoutMs));
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
promise.then((value) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolvePromise(value);
|
|
34
|
+
}, (error) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
rejectPromise(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Propagates an external caller-owned abort signal into the adapter's
|
|
41
|
+
// internal per-turn AbortController so orchestration-level supervision can
|
|
42
|
+
// cancel an in-flight Codex turn. Repair threads use their own controllers
|
|
43
|
+
// and are intentionally not linked; repairs are short, prompt-only turns.
|
|
44
|
+
function linkExternalAbortSignal(signal, abortController) {
|
|
45
|
+
if (!signal) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
abortController.abort(signal.reason);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
signal.addEventListener('abort', () => abortController.abort(signal.reason), { once: true });
|
|
53
|
+
}
|
|
54
|
+
function isTransientCodexApiFailure(message) {
|
|
55
|
+
const text = message.toLowerCase();
|
|
56
|
+
return (text.includes('api_error') ||
|
|
57
|
+
text.includes('api error') ||
|
|
58
|
+
text.includes('internal server error') ||
|
|
59
|
+
text.includes('overloaded') ||
|
|
60
|
+
text.includes('at capacity') ||
|
|
61
|
+
text.includes('rate limit') ||
|
|
62
|
+
text.includes('temporar') ||
|
|
63
|
+
text.includes('try again'));
|
|
64
|
+
}
|
|
65
|
+
function inferCodexErrorKind(error, message, fallback) {
|
|
66
|
+
const text = message.toLowerCase();
|
|
67
|
+
if (error instanceof CodexInactivityTimeoutError || /\btimed out after\b/i.test(message)) {
|
|
68
|
+
return 'timeout';
|
|
69
|
+
}
|
|
70
|
+
if (text.includes('permission') || text.includes('denied') || text.includes('forbidden') || text.includes('not authorized')) {
|
|
71
|
+
return 'permission_denied';
|
|
72
|
+
}
|
|
73
|
+
if (text.includes('property_name_above_max_length') ||
|
|
74
|
+
text.includes('orphan function call output') ||
|
|
75
|
+
text.includes('failed to record rollout items') ||
|
|
76
|
+
text.includes('session unavailable') ||
|
|
77
|
+
text.includes('session not found') ||
|
|
78
|
+
text.includes('thread not found')) {
|
|
79
|
+
return 'session_unavailable';
|
|
80
|
+
}
|
|
81
|
+
if (isTransientCodexApiFailure(message)) {
|
|
82
|
+
return 'api_error';
|
|
83
|
+
}
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
function isRetryableCodexError(kind, message) {
|
|
87
|
+
return kind === 'timeout' || kind === 'session_unavailable' || (kind === 'api_error' && isTransientCodexApiFailure(message));
|
|
88
|
+
}
|
|
89
|
+
function isCodexReconnectProgressMessage(message) {
|
|
90
|
+
return /^reconnecting\.\.\.\s*\d+\/\d+\b/i.test(message.trim());
|
|
91
|
+
}
|
|
92
|
+
function createCodexProviderError(args) {
|
|
93
|
+
const kind = args.kind ?? inferCodexErrorKind(args.cause, args.message, 'provider_failed');
|
|
94
|
+
return new NealProviderError({
|
|
95
|
+
message: args.message,
|
|
96
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
97
|
+
role: args.role,
|
|
98
|
+
sessionHandle: args.sessionHandle,
|
|
99
|
+
kind,
|
|
100
|
+
retryable: args.retryable ?? isRetryableCodexError(kind, args.message),
|
|
101
|
+
cause: args.cause,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function observeCodexFailure(args) {
|
|
105
|
+
return {
|
|
106
|
+
message: args.message,
|
|
107
|
+
kind: args.kind ?? inferCodexErrorKind(args.cause, args.message, 'provider_failed'),
|
|
108
|
+
source: args.source,
|
|
109
|
+
cause: args.cause,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function isGenericCodexProcessFailure(message) {
|
|
113
|
+
const text = message.toLowerCase();
|
|
114
|
+
return text.includes('codex exec exited') || text.includes('reading prompt from stdin') || text.includes('exited with code');
|
|
115
|
+
}
|
|
116
|
+
function shouldReplaceCodexFailure(current, next) {
|
|
117
|
+
if (!current) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return isGenericCodexProcessFailure(current.message) || !isGenericCodexProcessFailure(next.message);
|
|
121
|
+
}
|
|
122
|
+
function createCodexProviderErrorFromObservedFailure(failure, args) {
|
|
123
|
+
const cause = args.cause === undefined
|
|
124
|
+
? failure.cause
|
|
125
|
+
: {
|
|
126
|
+
source: failure.source,
|
|
127
|
+
providerCause: failure.cause,
|
|
128
|
+
iteratorCause: args.cause,
|
|
129
|
+
};
|
|
130
|
+
return createCodexProviderError({
|
|
131
|
+
message: failure.message,
|
|
132
|
+
role: args.role,
|
|
133
|
+
sessionHandle: args.sessionHandle,
|
|
134
|
+
kind: failure.kind,
|
|
135
|
+
cause,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function normalizeCodexProviderError(error, args) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
if (error instanceof NealProviderError) {
|
|
141
|
+
if (error.sessionHandle || !args.sessionHandle) {
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
return createCodexProviderError({
|
|
145
|
+
message: error.message,
|
|
146
|
+
role: error.role,
|
|
147
|
+
sessionHandle: args.sessionHandle,
|
|
148
|
+
kind: error.kind,
|
|
149
|
+
retryable: error.retryable,
|
|
150
|
+
cause: error,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return createCodexProviderError({
|
|
154
|
+
message,
|
|
155
|
+
role: args.role,
|
|
156
|
+
sessionHandle: args.sessionHandle,
|
|
157
|
+
kind: args.kind,
|
|
158
|
+
cause: error,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function summarizeItem(item) {
|
|
162
|
+
switch (item.type) {
|
|
163
|
+
case 'command_execution':
|
|
164
|
+
return {
|
|
165
|
+
itemId: item.id,
|
|
166
|
+
itemType: item.type,
|
|
167
|
+
status: item.status,
|
|
168
|
+
command: item.command,
|
|
169
|
+
outputLength: item.aggregated_output.length,
|
|
170
|
+
exitCode: item.exit_code,
|
|
171
|
+
};
|
|
172
|
+
case 'file_change':
|
|
173
|
+
return {
|
|
174
|
+
itemId: item.id,
|
|
175
|
+
itemType: item.type,
|
|
176
|
+
status: item.status,
|
|
177
|
+
files: item.changes.map((change) => change.path),
|
|
178
|
+
};
|
|
179
|
+
case 'mcp_tool_call':
|
|
180
|
+
return {
|
|
181
|
+
itemId: item.id,
|
|
182
|
+
itemType: item.type,
|
|
183
|
+
status: item.status,
|
|
184
|
+
server: item.server,
|
|
185
|
+
tool: item.tool,
|
|
186
|
+
};
|
|
187
|
+
case 'web_search':
|
|
188
|
+
return {
|
|
189
|
+
itemId: item.id,
|
|
190
|
+
itemType: item.type,
|
|
191
|
+
query: item.query,
|
|
192
|
+
};
|
|
193
|
+
case 'todo_list':
|
|
194
|
+
return {
|
|
195
|
+
itemId: item.id,
|
|
196
|
+
itemType: item.type,
|
|
197
|
+
completed: item.items.filter((todo) => todo.completed).length,
|
|
198
|
+
total: item.items.length,
|
|
199
|
+
};
|
|
200
|
+
case 'reasoning':
|
|
201
|
+
return {
|
|
202
|
+
itemId: item.id,
|
|
203
|
+
itemType: item.type,
|
|
204
|
+
textLength: item.text.length,
|
|
205
|
+
};
|
|
206
|
+
case 'agent_message':
|
|
207
|
+
return {
|
|
208
|
+
itemId: item.id,
|
|
209
|
+
itemType: item.type,
|
|
210
|
+
textLength: item.text.length,
|
|
211
|
+
};
|
|
212
|
+
case 'error':
|
|
213
|
+
return {
|
|
214
|
+
itemId: item.id,
|
|
215
|
+
itemType: item.type,
|
|
216
|
+
message: item.message,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function buildCodexThreadOptions(args) {
|
|
221
|
+
return {
|
|
222
|
+
...(args.model ? { model: args.model } : {}),
|
|
223
|
+
...(args.effort ? { modelReasoningEffort: args.effort } : {}),
|
|
224
|
+
approvalPolicy: 'never',
|
|
225
|
+
sandboxMode: args.sandboxMode ?? 'danger-full-access',
|
|
226
|
+
workingDirectory: args.cwd,
|
|
227
|
+
skipGitRepoCheck: args.skipGitRepoCheck ?? false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function createCodexThread(args) {
|
|
231
|
+
const codex = new Codex();
|
|
232
|
+
const threadOptions = buildCodexThreadOptions({
|
|
233
|
+
cwd: args.cwd,
|
|
234
|
+
model: args.model,
|
|
235
|
+
effort: args.effort,
|
|
236
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
237
|
+
sandboxMode: args.sandboxMode,
|
|
238
|
+
});
|
|
239
|
+
return args.sessionHandle
|
|
240
|
+
? codex.resumeThread(args.sessionHandle, threadOptions)
|
|
241
|
+
: codex.startThread(threadOptions);
|
|
242
|
+
}
|
|
243
|
+
async function emitProviderEvent(events, event) {
|
|
244
|
+
await events?.(event);
|
|
245
|
+
}
|
|
246
|
+
async function emitCodexStreamError(args) {
|
|
247
|
+
if (isCodexReconnectProgressMessage(args.message)) {
|
|
248
|
+
await emitProviderEvent(args.events, {
|
|
249
|
+
type: 'tool_progress',
|
|
250
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
251
|
+
role: args.role,
|
|
252
|
+
label: args.label,
|
|
253
|
+
sessionHandle: args.sessionHandle,
|
|
254
|
+
toolName: 'codex_stream',
|
|
255
|
+
message: args.message,
|
|
256
|
+
isError: true,
|
|
257
|
+
providerData: { sdkEventType: 'error', nonFatal: true },
|
|
258
|
+
});
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
await emitProviderEvent(args.events, {
|
|
262
|
+
type: 'provider_error',
|
|
263
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
264
|
+
role: args.role,
|
|
265
|
+
label: args.label,
|
|
266
|
+
sessionHandle: args.sessionHandle,
|
|
267
|
+
message: args.message,
|
|
268
|
+
providerData: { sdkEventType: 'error' },
|
|
269
|
+
});
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessionStarted, abortController) {
|
|
273
|
+
let finalResponse = '';
|
|
274
|
+
let observedFailure = null;
|
|
275
|
+
let sessionHandle = null;
|
|
276
|
+
const iterator = turn.events[Symbol.asyncIterator]();
|
|
277
|
+
while (true) {
|
|
278
|
+
let next;
|
|
279
|
+
try {
|
|
280
|
+
next = await nextWithTimeout(iterator.next(), inactivityTimeoutMs, () => abortController?.abort());
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (observedFailure) {
|
|
284
|
+
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
285
|
+
role: 'coder',
|
|
286
|
+
sessionHandle,
|
|
287
|
+
cause: error,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
throw normalizeCodexProviderError(error, {
|
|
291
|
+
role: 'coder',
|
|
292
|
+
sessionHandle,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (next.done) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
const event = next.value;
|
|
299
|
+
switch (event.type) {
|
|
300
|
+
case 'thread.started':
|
|
301
|
+
sessionHandle = event.thread_id;
|
|
302
|
+
await emitProviderEvent(events, {
|
|
303
|
+
type: 'session_started',
|
|
304
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
305
|
+
role: 'coder',
|
|
306
|
+
sessionHandle: event.thread_id,
|
|
307
|
+
providerData: { sdkEventType: event.type },
|
|
308
|
+
});
|
|
309
|
+
await onSessionStarted?.(event.thread_id);
|
|
310
|
+
break;
|
|
311
|
+
case 'turn.started':
|
|
312
|
+
await emitProviderEvent(events, {
|
|
313
|
+
type: 'turn_started',
|
|
314
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
315
|
+
role: 'coder',
|
|
316
|
+
sessionHandle,
|
|
317
|
+
providerData: { sdkEventType: event.type },
|
|
318
|
+
});
|
|
319
|
+
break;
|
|
320
|
+
case 'item.started':
|
|
321
|
+
await emitProviderEvent(events, {
|
|
322
|
+
type: 'tool_started',
|
|
323
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
324
|
+
role: 'coder',
|
|
325
|
+
sessionHandle,
|
|
326
|
+
itemId: summarizeItem(event.item).itemId,
|
|
327
|
+
toolName: event.item.type,
|
|
328
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
case 'item.updated':
|
|
332
|
+
await emitProviderEvent(events, {
|
|
333
|
+
type: 'tool_progress',
|
|
334
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
335
|
+
role: 'coder',
|
|
336
|
+
sessionHandle,
|
|
337
|
+
itemId: summarizeItem(event.item).itemId,
|
|
338
|
+
toolName: event.item.type,
|
|
339
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
340
|
+
});
|
|
341
|
+
break;
|
|
342
|
+
case 'item.completed':
|
|
343
|
+
if (event.item.type === 'command_execution') {
|
|
344
|
+
await emitProviderEvent(events, {
|
|
345
|
+
type: 'command_completed',
|
|
346
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
347
|
+
role: 'coder',
|
|
348
|
+
sessionHandle,
|
|
349
|
+
itemId: event.item.id,
|
|
350
|
+
command: event.item.command,
|
|
351
|
+
status: event.item.status,
|
|
352
|
+
exitCode: event.item.exit_code ?? null,
|
|
353
|
+
output: event.item.aggregated_output,
|
|
354
|
+
outputLength: event.item.aggregated_output.length,
|
|
355
|
+
cwd,
|
|
356
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (event.item.type === 'file_change' && event.item.changes.length > 0) {
|
|
360
|
+
const files = event.item.changes.map((change) => change.path);
|
|
361
|
+
await emitProviderEvent(events, {
|
|
362
|
+
type: 'file_changed',
|
|
363
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
364
|
+
role: 'coder',
|
|
365
|
+
sessionHandle,
|
|
366
|
+
files,
|
|
367
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
else if (event.item.type === 'agent_message') {
|
|
371
|
+
finalResponse = event.item.text;
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
case 'turn.completed':
|
|
375
|
+
await emitProviderEvent(events, {
|
|
376
|
+
type: 'turn_completed',
|
|
377
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
378
|
+
role: 'coder',
|
|
379
|
+
sessionHandle,
|
|
380
|
+
usage: event.usage,
|
|
381
|
+
providerData: { sdkEventType: event.type },
|
|
382
|
+
});
|
|
383
|
+
if (event.usage !== undefined) {
|
|
384
|
+
await emitProviderEvent(events, {
|
|
385
|
+
type: 'usage_reported',
|
|
386
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
387
|
+
role: 'coder',
|
|
388
|
+
sessionHandle,
|
|
389
|
+
usage: event.usage,
|
|
390
|
+
providerData: { sdkEventType: event.type },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
case 'turn.failed':
|
|
395
|
+
{
|
|
396
|
+
const nextFailure = observeCodexFailure({
|
|
397
|
+
message: event.error.message,
|
|
398
|
+
source: event.type,
|
|
399
|
+
cause: event.error,
|
|
400
|
+
});
|
|
401
|
+
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
402
|
+
observedFailure = nextFailure;
|
|
403
|
+
await emitProviderEvent(events, {
|
|
404
|
+
type: 'provider_error',
|
|
405
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
406
|
+
role: 'coder',
|
|
407
|
+
sessionHandle,
|
|
408
|
+
message: event.error.message,
|
|
409
|
+
providerData: { sdkEventType: event.type },
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
case 'error':
|
|
415
|
+
{
|
|
416
|
+
const nextFailure = observeCodexFailure({
|
|
417
|
+
message: event.message,
|
|
418
|
+
source: event.type,
|
|
419
|
+
cause: event,
|
|
420
|
+
});
|
|
421
|
+
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
422
|
+
if (await emitCodexStreamError({
|
|
423
|
+
events,
|
|
424
|
+
role: 'coder',
|
|
425
|
+
sessionHandle,
|
|
426
|
+
message: event.message,
|
|
427
|
+
})) {
|
|
428
|
+
observedFailure = nextFailure;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
default:
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (observedFailure) {
|
|
438
|
+
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
439
|
+
role: 'coder',
|
|
440
|
+
sessionHandle,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return { finalResponse, sessionHandle };
|
|
444
|
+
}
|
|
445
|
+
async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, events, abortController) {
|
|
446
|
+
let finalResponse = '';
|
|
447
|
+
let observedFailure = null;
|
|
448
|
+
let sessionHandle = null;
|
|
449
|
+
const iterator = turn.events[Symbol.asyncIterator]();
|
|
450
|
+
while (true) {
|
|
451
|
+
let next;
|
|
452
|
+
try {
|
|
453
|
+
next = await nextWithTimeout(iterator.next(), inactivityTimeoutMs, () => abortController?.abort());
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (observedFailure) {
|
|
457
|
+
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
458
|
+
role: 'structured-advisor',
|
|
459
|
+
sessionHandle,
|
|
460
|
+
cause: error,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
throw normalizeCodexProviderError(error, {
|
|
464
|
+
role: 'structured-advisor',
|
|
465
|
+
sessionHandle,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (next.done) {
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
const event = next.value;
|
|
472
|
+
switch (event.type) {
|
|
473
|
+
case 'thread.started':
|
|
474
|
+
sessionHandle = event.thread_id;
|
|
475
|
+
await emitProviderEvent(events, {
|
|
476
|
+
type: 'session_started',
|
|
477
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
478
|
+
role: 'structured-advisor',
|
|
479
|
+
label,
|
|
480
|
+
sessionHandle: event.thread_id,
|
|
481
|
+
providerData: { sdkEventType: event.type },
|
|
482
|
+
});
|
|
483
|
+
break;
|
|
484
|
+
case 'turn.started':
|
|
485
|
+
await emitProviderEvent(events, {
|
|
486
|
+
type: 'turn_started',
|
|
487
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
488
|
+
role: 'structured-advisor',
|
|
489
|
+
label,
|
|
490
|
+
sessionHandle,
|
|
491
|
+
providerData: { sdkEventType: event.type },
|
|
492
|
+
});
|
|
493
|
+
break;
|
|
494
|
+
case 'item.started':
|
|
495
|
+
await emitProviderEvent(events, {
|
|
496
|
+
type: 'tool_started',
|
|
497
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
498
|
+
role: 'structured-advisor',
|
|
499
|
+
label,
|
|
500
|
+
sessionHandle,
|
|
501
|
+
itemId: summarizeItem(event.item).itemId,
|
|
502
|
+
toolName: event.item.type,
|
|
503
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
504
|
+
});
|
|
505
|
+
break;
|
|
506
|
+
case 'item.updated':
|
|
507
|
+
await emitProviderEvent(events, {
|
|
508
|
+
type: 'tool_progress',
|
|
509
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
510
|
+
role: 'structured-advisor',
|
|
511
|
+
label,
|
|
512
|
+
sessionHandle,
|
|
513
|
+
itemId: summarizeItem(event.item).itemId,
|
|
514
|
+
toolName: event.item.type,
|
|
515
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
case 'item.completed':
|
|
519
|
+
if (event.item.type === 'command_execution') {
|
|
520
|
+
await emitProviderEvent(events, {
|
|
521
|
+
type: 'command_completed',
|
|
522
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
523
|
+
role: 'structured-advisor',
|
|
524
|
+
label,
|
|
525
|
+
sessionHandle,
|
|
526
|
+
itemId: event.item.id,
|
|
527
|
+
command: event.item.command,
|
|
528
|
+
status: event.item.status,
|
|
529
|
+
exitCode: event.item.exit_code ?? null,
|
|
530
|
+
output: event.item.aggregated_output,
|
|
531
|
+
outputLength: event.item.aggregated_output.length,
|
|
532
|
+
cwd,
|
|
533
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
else if (event.item.type === 'file_change' && event.item.changes.length > 0) {
|
|
537
|
+
const files = event.item.changes.map((change) => change.path);
|
|
538
|
+
await emitProviderEvent(events, {
|
|
539
|
+
type: 'file_changed',
|
|
540
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
541
|
+
role: 'structured-advisor',
|
|
542
|
+
label,
|
|
543
|
+
sessionHandle,
|
|
544
|
+
files,
|
|
545
|
+
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
else if (event.item.type === 'agent_message') {
|
|
549
|
+
finalResponse = event.item.text;
|
|
550
|
+
}
|
|
551
|
+
break;
|
|
552
|
+
case 'turn.completed':
|
|
553
|
+
await emitProviderEvent(events, {
|
|
554
|
+
type: 'turn_completed',
|
|
555
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
556
|
+
role: 'structured-advisor',
|
|
557
|
+
label,
|
|
558
|
+
sessionHandle,
|
|
559
|
+
usage: event.usage,
|
|
560
|
+
providerData: { sdkEventType: event.type },
|
|
561
|
+
});
|
|
562
|
+
if (event.usage !== undefined) {
|
|
563
|
+
await emitProviderEvent(events, {
|
|
564
|
+
type: 'usage_reported',
|
|
565
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
566
|
+
role: 'structured-advisor',
|
|
567
|
+
label,
|
|
568
|
+
sessionHandle,
|
|
569
|
+
usage: event.usage,
|
|
570
|
+
providerData: { sdkEventType: event.type },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
case 'turn.failed':
|
|
575
|
+
{
|
|
576
|
+
const nextFailure = observeCodexFailure({
|
|
577
|
+
message: event.error.message,
|
|
578
|
+
source: event.type,
|
|
579
|
+
cause: event.error,
|
|
580
|
+
});
|
|
581
|
+
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
582
|
+
observedFailure = nextFailure;
|
|
583
|
+
await emitProviderEvent(events, {
|
|
584
|
+
type: 'provider_error',
|
|
585
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
586
|
+
role: 'structured-advisor',
|
|
587
|
+
label,
|
|
588
|
+
sessionHandle,
|
|
589
|
+
message: event.error.message,
|
|
590
|
+
providerData: { sdkEventType: event.type },
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
case 'error':
|
|
596
|
+
{
|
|
597
|
+
const nextFailure = observeCodexFailure({
|
|
598
|
+
message: event.message,
|
|
599
|
+
source: event.type,
|
|
600
|
+
cause: event,
|
|
601
|
+
});
|
|
602
|
+
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
603
|
+
if (await emitCodexStreamError({
|
|
604
|
+
events,
|
|
605
|
+
role: 'structured-advisor',
|
|
606
|
+
label,
|
|
607
|
+
sessionHandle,
|
|
608
|
+
message: event.message,
|
|
609
|
+
})) {
|
|
610
|
+
observedFailure = nextFailure;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
default:
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (observedFailure) {
|
|
620
|
+
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
621
|
+
role: 'structured-advisor',
|
|
622
|
+
sessionHandle,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return { finalResponse, sessionHandle };
|
|
626
|
+
}
|
|
627
|
+
function parseCodexStructuredOutput(finalResponse, label, sessionHandle) {
|
|
628
|
+
try {
|
|
629
|
+
return JSON.parse(finalResponse);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
+
throw createCodexProviderError({
|
|
634
|
+
message: `Codex ${label} returned invalid structured output: ${message}`,
|
|
635
|
+
role: 'structured-advisor',
|
|
636
|
+
sessionHandle,
|
|
637
|
+
kind: 'structured_output_invalid',
|
|
638
|
+
cause: error,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function buildCodexStructuredAdvisorRunOptions(args, signal) {
|
|
643
|
+
if (args.structuredJsonProtocol?.protocol === 'neal-json-block-v1') {
|
|
644
|
+
return { signal };
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
outputSchema: args.schema,
|
|
648
|
+
signal,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
// Best-effort `toolPolicy` enforcement for Codex coder turns. The Codex SDK
|
|
652
|
+
// has no per-tool-call hook, so `allowedWritePaths` cannot be enforced at
|
|
653
|
+
// path granularity; sandbox modes are the strongest mechanical lever it
|
|
654
|
+
// offers. A jailed plan-authoring turn (allowRun false + allowedWritePaths)
|
|
655
|
+
// runs under 'workspace-write' so writes are confined to the workspace
|
|
656
|
+
// sandbox. Residual gap: shell still runs inside that sandbox and can write
|
|
657
|
+
// any workspace path, not just the allowlisted plan document — documented in
|
|
658
|
+
// docs/providers.md. Policy-free coder rounds keep 'danger-full-access'
|
|
659
|
+
// because scope verification legitimately runs arbitrary commands.
|
|
660
|
+
function getCodexCoderSandboxMode(toolPolicy) {
|
|
661
|
+
return toolPolicy?.allowRun === false && toolPolicy.allowedWritePaths
|
|
662
|
+
? 'workspace-write'
|
|
663
|
+
: 'danger-full-access';
|
|
664
|
+
}
|
|
665
|
+
class OpenAICodexCoderAdapter {
|
|
666
|
+
options;
|
|
667
|
+
constructor(options = {}) {
|
|
668
|
+
this.options = options;
|
|
669
|
+
}
|
|
670
|
+
async runPrompt(args) {
|
|
671
|
+
let thread = null;
|
|
672
|
+
try {
|
|
673
|
+
const createThread = this.options.createThread ?? createCodexThread;
|
|
674
|
+
thread = createThread({
|
|
675
|
+
cwd: args.cwd,
|
|
676
|
+
sessionHandle: args.resumeHandle,
|
|
677
|
+
model: this.options.model ?? undefined,
|
|
678
|
+
effort: this.options.effort ?? undefined,
|
|
679
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
680
|
+
sandboxMode: getCodexCoderSandboxMode(args.toolPolicy),
|
|
681
|
+
});
|
|
682
|
+
const abortController = new AbortController();
|
|
683
|
+
linkExternalAbortSignal(args.signal, abortController);
|
|
684
|
+
const streamedTurn = await thread.runStreamed(args.prompt, {
|
|
685
|
+
...(args.outputSchema ? { outputSchema: args.outputSchema } : {}),
|
|
686
|
+
signal: abortController.signal,
|
|
687
|
+
});
|
|
688
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController);
|
|
689
|
+
if (args.outputSchema) {
|
|
690
|
+
await emitProviderEvent(args.events, {
|
|
691
|
+
type: 'structured_output_received',
|
|
692
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
693
|
+
role: 'coder',
|
|
694
|
+
sessionHandle: thread.id,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
sessionHandle: thread.id,
|
|
699
|
+
finalResponse: result.finalResponse,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
const providerError = normalizeCodexProviderError(error, {
|
|
704
|
+
role: 'coder',
|
|
705
|
+
sessionHandle: thread?.id ?? args.resumeHandle ?? null,
|
|
706
|
+
});
|
|
707
|
+
await emitProviderEvent(args.events, {
|
|
708
|
+
type: 'provider_error',
|
|
709
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
710
|
+
role: 'coder',
|
|
711
|
+
sessionHandle: providerError.sessionHandle,
|
|
712
|
+
message: providerError.message,
|
|
713
|
+
errorKind: providerError.kind,
|
|
714
|
+
});
|
|
715
|
+
throw providerError;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async runStructuredPrompt(args) {
|
|
719
|
+
const sleep = this.options.sleep ?? defaultSleep;
|
|
720
|
+
let apiRetryCount = 0;
|
|
721
|
+
const apiRetryLimit = args.apiRetryLimit ?? 0;
|
|
722
|
+
while (true) {
|
|
723
|
+
let thread = null;
|
|
724
|
+
try {
|
|
725
|
+
const createThread = this.options.createThread ?? createCodexThread;
|
|
726
|
+
thread = createThread({
|
|
727
|
+
cwd: args.cwd,
|
|
728
|
+
sessionHandle: args.resumeHandle,
|
|
729
|
+
model: this.options.model ?? undefined,
|
|
730
|
+
effort: this.options.effort ?? undefined,
|
|
731
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
732
|
+
sandboxMode: getCodexCoderSandboxMode(args.toolPolicy),
|
|
733
|
+
});
|
|
734
|
+
const abortController = new AbortController();
|
|
735
|
+
linkExternalAbortSignal(args.signal, abortController);
|
|
736
|
+
return await runStructuredJsonProtocol({
|
|
737
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
738
|
+
role: 'coder',
|
|
739
|
+
label: args.label,
|
|
740
|
+
protocol: args.structuredJsonProtocol,
|
|
741
|
+
prompt: args.prompt,
|
|
742
|
+
events: args.events,
|
|
743
|
+
initialSessionHandle: args.resumeHandle ?? null,
|
|
744
|
+
runInitial: async (prompt) => {
|
|
745
|
+
const streamedTurn = await thread.runStreamed(prompt, {
|
|
746
|
+
signal: abortController.signal,
|
|
747
|
+
});
|
|
748
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController);
|
|
749
|
+
return {
|
|
750
|
+
assistantText: result.finalResponse,
|
|
751
|
+
sessionHandle: thread.id,
|
|
752
|
+
};
|
|
753
|
+
},
|
|
754
|
+
runRepair: async (prompt) => {
|
|
755
|
+
// Repair runs on a fresh prompt-only thread whose prompt forbids
|
|
756
|
+
// tool use entirely, so nothing in the repair flow needs write
|
|
757
|
+
// access; the read-only sandbox enforces that mechanically
|
|
758
|
+
// (mirroring the structured-advisor repair thread).
|
|
759
|
+
const repairThread = createThread({
|
|
760
|
+
cwd: args.cwd,
|
|
761
|
+
model: this.options.model ?? undefined,
|
|
762
|
+
effort: this.options.effort ?? undefined,
|
|
763
|
+
skipGitRepoCheck: true,
|
|
764
|
+
sandboxMode: 'read-only',
|
|
765
|
+
});
|
|
766
|
+
const repairAbortController = new AbortController();
|
|
767
|
+
const streamedTurn = await repairThread.runStreamed(prompt, {
|
|
768
|
+
signal: repairAbortController.signal,
|
|
769
|
+
});
|
|
770
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, undefined, repairAbortController);
|
|
771
|
+
return {
|
|
772
|
+
assistantText: result.finalResponse,
|
|
773
|
+
sessionHandle: repairThread.id,
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
createProviderError: (errorArgs) => createCodexProviderError({
|
|
777
|
+
message: errorArgs.message,
|
|
778
|
+
role: 'coder',
|
|
779
|
+
sessionHandle: errorArgs.sessionHandle,
|
|
780
|
+
kind: errorArgs.kind,
|
|
781
|
+
retryable: false,
|
|
782
|
+
cause: errorArgs.cause,
|
|
783
|
+
}),
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
const providerError = normalizeCodexProviderError(error, {
|
|
788
|
+
role: 'coder',
|
|
789
|
+
sessionHandle: thread?.id ?? args.resumeHandle ?? null,
|
|
790
|
+
});
|
|
791
|
+
// Bounded in-round retry for transient provider failures, mirroring
|
|
792
|
+
// the Claude structured-coder loop: only retryable `api_error` kinds
|
|
793
|
+
// burn budget and a caller abort never does. Each retry starts a
|
|
794
|
+
// fresh thread; per the adapter contract resumed sessions pass 0.
|
|
795
|
+
if (providerError.kind === 'api_error' &&
|
|
796
|
+
providerError.retryable &&
|
|
797
|
+
apiRetryCount < apiRetryLimit &&
|
|
798
|
+
!args.signal?.aborted) {
|
|
799
|
+
apiRetryCount += 1;
|
|
800
|
+
await emitProviderEvent(args.events, {
|
|
801
|
+
type: 'tool_progress',
|
|
802
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
803
|
+
role: 'coder',
|
|
804
|
+
label: args.label,
|
|
805
|
+
sessionHandle: providerError.sessionHandle,
|
|
806
|
+
toolName: 'api_retry',
|
|
807
|
+
message: `transient Codex failure; retrying ${args.label} (${apiRetryCount}/${apiRetryLimit})`,
|
|
808
|
+
isError: true,
|
|
809
|
+
providerData: {
|
|
810
|
+
retryCount: apiRetryCount,
|
|
811
|
+
retryLimit: apiRetryLimit,
|
|
812
|
+
message: providerError.message,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
await sleep(getApiRetryDelayMs(apiRetryCount));
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
await emitProviderEvent(args.events, {
|
|
819
|
+
type: 'provider_error',
|
|
820
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
821
|
+
role: 'coder',
|
|
822
|
+
label: args.label,
|
|
823
|
+
sessionHandle: providerError.sessionHandle,
|
|
824
|
+
message: providerError.message,
|
|
825
|
+
errorKind: providerError.kind,
|
|
826
|
+
});
|
|
827
|
+
throw providerError;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
class OpenAICodexStructuredAdvisorAdapter {
|
|
833
|
+
options;
|
|
834
|
+
constructor(options = {}) {
|
|
835
|
+
this.options = options;
|
|
836
|
+
}
|
|
837
|
+
async runStructuredRound(args) {
|
|
838
|
+
const createThread = this.options.createThread ?? createCodexThread;
|
|
839
|
+
const sleep = this.options.sleep ?? defaultSleep;
|
|
840
|
+
let apiRetryCount = 0;
|
|
841
|
+
const apiRetryLimit = args.apiRetryLimit;
|
|
842
|
+
while (true) {
|
|
843
|
+
let thread = null;
|
|
844
|
+
try {
|
|
845
|
+
thread = createThread({
|
|
846
|
+
cwd: args.cwd,
|
|
847
|
+
sessionHandle: args.resumeHandle,
|
|
848
|
+
model: this.options.model ?? undefined,
|
|
849
|
+
effort: this.options.effort ?? undefined,
|
|
850
|
+
skipGitRepoCheck: args.skipGitRepoCheck,
|
|
851
|
+
sandboxMode: 'read-only',
|
|
852
|
+
});
|
|
853
|
+
const abortController = new AbortController();
|
|
854
|
+
linkExternalAbortSignal(args.signal, abortController);
|
|
855
|
+
if (args.structuredJsonProtocol?.protocol === 'neal-json-block-v1') {
|
|
856
|
+
return await runStructuredJsonProtocol({
|
|
857
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
858
|
+
role: 'structured-advisor',
|
|
859
|
+
label: args.label,
|
|
860
|
+
protocol: args.structuredJsonProtocol,
|
|
861
|
+
prompt: args.prompt,
|
|
862
|
+
events: args.events,
|
|
863
|
+
initialSessionHandle: args.resumeHandle ?? null,
|
|
864
|
+
runInitial: async (prompt) => {
|
|
865
|
+
const streamedTurn = await thread.runStreamed(prompt, {
|
|
866
|
+
signal: abortController.signal,
|
|
867
|
+
});
|
|
868
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController);
|
|
869
|
+
return {
|
|
870
|
+
assistantText: result.finalResponse,
|
|
871
|
+
sessionHandle: thread.id ?? result.sessionHandle ?? null,
|
|
872
|
+
};
|
|
873
|
+
},
|
|
874
|
+
runRepair: async (prompt) => {
|
|
875
|
+
const repairThread = createThread({
|
|
876
|
+
cwd: args.cwd,
|
|
877
|
+
model: this.options.model ?? undefined,
|
|
878
|
+
effort: this.options.effort ?? undefined,
|
|
879
|
+
skipGitRepoCheck: true,
|
|
880
|
+
sandboxMode: 'read-only',
|
|
881
|
+
});
|
|
882
|
+
const repairAbortController = new AbortController();
|
|
883
|
+
const streamedTurn = await repairThread.runStreamed(prompt, {
|
|
884
|
+
signal: repairAbortController.signal,
|
|
885
|
+
});
|
|
886
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, repairAbortController);
|
|
887
|
+
return {
|
|
888
|
+
assistantText: result.finalResponse,
|
|
889
|
+
sessionHandle: repairThread.id ?? result.sessionHandle ?? null,
|
|
890
|
+
};
|
|
891
|
+
},
|
|
892
|
+
createProviderError: (errorArgs) => createCodexProviderError({
|
|
893
|
+
message: errorArgs.message,
|
|
894
|
+
role: 'structured-advisor',
|
|
895
|
+
sessionHandle: errorArgs.sessionHandle,
|
|
896
|
+
kind: errorArgs.kind,
|
|
897
|
+
retryable: false,
|
|
898
|
+
cause: errorArgs.cause,
|
|
899
|
+
}),
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const streamedTurn = await thread.runStreamed(args.prompt, buildCodexStructuredAdvisorRunOptions(args, abortController.signal));
|
|
903
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController);
|
|
904
|
+
const structured = parseCodexStructuredOutput(result.finalResponse, args.label, thread.id);
|
|
905
|
+
await emitProviderEvent(args.events, {
|
|
906
|
+
type: 'structured_output_received',
|
|
907
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
908
|
+
role: 'structured-advisor',
|
|
909
|
+
label: args.label,
|
|
910
|
+
sessionHandle: thread.id,
|
|
911
|
+
});
|
|
912
|
+
return {
|
|
913
|
+
sessionHandle: thread.id,
|
|
914
|
+
structured,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
const providerError = normalizeCodexProviderError(error, {
|
|
919
|
+
role: 'structured-advisor',
|
|
920
|
+
sessionHandle: thread?.id ?? args.resumeHandle ?? null,
|
|
921
|
+
});
|
|
922
|
+
// Bounded in-round retry for transient provider failures, mirroring
|
|
923
|
+
// the Claude advisor rounds: only retryable `api_error` kinds burn
|
|
924
|
+
// budget and a caller abort never does. Each retry starts a fresh
|
|
925
|
+
// thread; per the adapter contract resumed sessions pass 0.
|
|
926
|
+
if (providerError.kind === 'api_error' &&
|
|
927
|
+
providerError.retryable &&
|
|
928
|
+
apiRetryCount < apiRetryLimit &&
|
|
929
|
+
!args.signal?.aborted) {
|
|
930
|
+
apiRetryCount += 1;
|
|
931
|
+
await emitProviderEvent(args.events, {
|
|
932
|
+
type: 'tool_progress',
|
|
933
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
934
|
+
role: 'structured-advisor',
|
|
935
|
+
label: args.label,
|
|
936
|
+
sessionHandle: providerError.sessionHandle,
|
|
937
|
+
toolName: 'api_retry',
|
|
938
|
+
message: `transient Codex failure; retrying ${args.label} (${apiRetryCount}/${apiRetryLimit})`,
|
|
939
|
+
isError: true,
|
|
940
|
+
providerData: {
|
|
941
|
+
retryCount: apiRetryCount,
|
|
942
|
+
retryLimit: apiRetryLimit,
|
|
943
|
+
message: providerError.message,
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
await sleep(getApiRetryDelayMs(apiRetryCount));
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
await emitProviderEvent(args.events, {
|
|
950
|
+
type: 'provider_error',
|
|
951
|
+
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
952
|
+
role: 'structured-advisor',
|
|
953
|
+
label: args.label,
|
|
954
|
+
sessionHandle: providerError.sessionHandle,
|
|
955
|
+
message: providerError.message,
|
|
956
|
+
errorKind: providerError.kind,
|
|
957
|
+
});
|
|
958
|
+
throw providerError;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
export function createOpenAICodexCoderAdapter(options = {}) {
|
|
964
|
+
return new OpenAICodexCoderAdapter(options);
|
|
965
|
+
}
|
|
966
|
+
export function createOpenAICodexStructuredAdvisorAdapter(options = {}) {
|
|
967
|
+
return new OpenAICodexStructuredAdvisorAdapter(options);
|
|
968
|
+
}
|
|
969
|
+
export const openAICodexProviderDefinition = {
|
|
970
|
+
id: 'openai-codex',
|
|
971
|
+
displayName: 'OpenAI Codex',
|
|
972
|
+
capabilities: {
|
|
973
|
+
coder: {
|
|
974
|
+
supported: true,
|
|
975
|
+
toolAccess: {
|
|
976
|
+
read: true,
|
|
977
|
+
write: true,
|
|
978
|
+
shell: true,
|
|
979
|
+
},
|
|
980
|
+
supportsSessionResume: true,
|
|
981
|
+
supportsModelOverride: true,
|
|
982
|
+
supportsStructuredOutput: true,
|
|
983
|
+
usageReporting: 'opportunistic',
|
|
984
|
+
supportedEfforts: ['minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
985
|
+
},
|
|
986
|
+
'structured-advisor': {
|
|
987
|
+
supported: true,
|
|
988
|
+
// Reviewers run read-only: no write, no shell. The structured-advisor
|
|
989
|
+
// thread (primary and repair) runs under the SDK read-only sandbox so a
|
|
990
|
+
// reviewer can inspect the checkout but never mutate it or run commands.
|
|
991
|
+
toolAccess: {
|
|
992
|
+
read: true,
|
|
993
|
+
write: false,
|
|
994
|
+
shell: false,
|
|
995
|
+
},
|
|
996
|
+
supportsSessionResume: true,
|
|
997
|
+
supportsModelOverride: true,
|
|
998
|
+
supportsStructuredOutput: true,
|
|
999
|
+
usageReporting: 'opportunistic',
|
|
1000
|
+
supportedEfforts: ['minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
createCoderAdapter: createOpenAICodexCoderAdapter,
|
|
1004
|
+
createStructuredAdvisorAdapter: createOpenAICodexStructuredAdvisorAdapter,
|
|
1005
|
+
};
|
|
1006
|
+
export const openAICodexProviderTestHooks = {
|
|
1007
|
+
buildCodexStructuredAdvisorRunOptions,
|
|
1008
|
+
buildCodexThreadOptions,
|
|
1009
|
+
createCoderAdapterWithThreadFactory: (createThread, options) => new OpenAICodexCoderAdapter({ ...options, createThread }),
|
|
1010
|
+
createStructuredAdvisorAdapterWithThreadFactory: (createThread, options) => new OpenAICodexStructuredAdvisorAdapter({ ...options, createThread }),
|
|
1011
|
+
consumeCodexTurn,
|
|
1012
|
+
consumeCodexAdvisorTurn,
|
|
1013
|
+
parseCodexStructuredOutput,
|
|
1014
|
+
};
|