@gakr-gakr/codex 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/autobot.plugin.json +374 -0
- package/doctor-contract-api.ts +68 -0
- package/harness.ts +72 -0
- package/index.ts +124 -0
- package/media-understanding-provider.ts +521 -0
- package/package.json +40 -0
- package/prompt-overlay.ts +21 -0
- package/provider-catalog.ts +83 -0
- package/provider-discovery.ts +45 -0
- package/provider.ts +243 -0
- package/src/app-server/app-inventory-cache.ts +324 -0
- package/src/app-server/approval-bridge.ts +1211 -0
- package/src/app-server/auth-bridge.ts +614 -0
- package/src/app-server/capabilities.ts +27 -0
- package/src/app-server/client-factory.ts +24 -0
- package/src/app-server/client.ts +715 -0
- package/src/app-server/compact.ts +512 -0
- package/src/app-server/computer-use.ts +683 -0
- package/src/app-server/config.ts +1038 -0
- package/src/app-server/context-engine-projection.ts +403 -0
- package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
- package/src/app-server/dynamic-tool-profile.ts +70 -0
- package/src/app-server/dynamic-tools.ts +623 -0
- package/src/app-server/elicitation-bridge.ts +783 -0
- package/src/app-server/event-projector.ts +2065 -0
- package/src/app-server/image-payload-sanitizer.ts +167 -0
- package/src/app-server/local-runtime-attribution.ts +39 -0
- package/src/app-server/managed-binary.ts +193 -0
- package/src/app-server/models.ts +172 -0
- package/src/app-server/native-hook-relay.ts +150 -0
- package/src/app-server/native-subagent-task-mirror.ts +497 -0
- package/src/app-server/plugin-activation.ts +283 -0
- package/src/app-server/plugin-app-cache-key.ts +74 -0
- package/src/app-server/plugin-approval-roundtrip.ts +122 -0
- package/src/app-server/plugin-inventory.ts +357 -0
- package/src/app-server/plugin-thread-config.ts +455 -0
- package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
- package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
- package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
- package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
- package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
- package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
- package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
- package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
- package/src/app-server/protocol-validators.ts +203 -0
- package/src/app-server/protocol.ts +520 -0
- package/src/app-server/rate-limit-cache.ts +48 -0
- package/src/app-server/rate-limits.ts +583 -0
- package/src/app-server/request.ts +73 -0
- package/src/app-server/run-attempt.ts +4862 -0
- package/src/app-server/session-binding.ts +398 -0
- package/src/app-server/session-history.ts +44 -0
- package/src/app-server/shared-client.ts +289 -0
- package/src/app-server/side-question.ts +1009 -0
- package/src/app-server/test-support.ts +48 -0
- package/src/app-server/thread-lifecycle.ts +959 -0
- package/src/app-server/timeout.ts +9 -0
- package/src/app-server/tool-progress-normalization.ts +77 -0
- package/src/app-server/trajectory.ts +368 -0
- package/src/app-server/transcript-mirror.ts +208 -0
- package/src/app-server/transport-stdio.ts +107 -0
- package/src/app-server/transport-websocket.ts +90 -0
- package/src/app-server/transport.ts +117 -0
- package/src/app-server/user-input-bridge.ts +316 -0
- package/src/app-server/version.ts +4 -0
- package/src/app-server/vision-tools.ts +12 -0
- package/src/command-account.ts +544 -0
- package/src/command-formatters.ts +426 -0
- package/src/command-handlers.ts +2021 -0
- package/src/command-plugins-management.ts +137 -0
- package/src/command-rpc.ts +142 -0
- package/src/commands.ts +65 -0
- package/src/conversation-binding-data.ts +124 -0
- package/src/conversation-binding.ts +561 -0
- package/src/conversation-control.ts +303 -0
- package/src/conversation-turn-collector.ts +186 -0
- package/src/conversation-turn-input.ts +106 -0
- package/src/migration/apply.ts +501 -0
- package/src/migration/helpers.ts +55 -0
- package/src/migration/plan.ts +461 -0
- package/src/migration/provider.ts +41 -0
- package/src/migration/source.ts +643 -0
- package/src/migration/targets.ts +25 -0
- package/src/node-cli-sessions.ts +711 -0
- package/test-api.ts +95 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
|
|
2
|
+
import {
|
|
3
|
+
isCodexFastServiceTier,
|
|
4
|
+
resolveCodexAppServerRuntimeOptions,
|
|
5
|
+
type CodexAppServerApprovalPolicy,
|
|
6
|
+
type CodexAppServerSandboxMode,
|
|
7
|
+
} from "./app-server/config.js";
|
|
8
|
+
import type { CodexServiceTier, CodexThreadResumeResponse } from "./app-server/protocol.js";
|
|
9
|
+
import {
|
|
10
|
+
readCodexAppServerBinding,
|
|
11
|
+
writeCodexAppServerBinding,
|
|
12
|
+
} from "./app-server/session-binding.js";
|
|
13
|
+
import { getSharedCodexAppServerClient } from "./app-server/shared-client.js";
|
|
14
|
+
import { formatCodexDisplayText } from "./command-formatters.js";
|
|
15
|
+
|
|
16
|
+
type ActiveTurn = {
|
|
17
|
+
sessionFile: string;
|
|
18
|
+
threadId: string;
|
|
19
|
+
turnId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type CodexAppServerBindingLookup = NonNullable<Parameters<typeof readCodexAppServerBinding>[1]>;
|
|
23
|
+
|
|
24
|
+
type PermissionsMode = "default" | "yolo";
|
|
25
|
+
|
|
26
|
+
const CODEX_CONVERSATION_CONTROL_STATE = Symbol.for("autobot.codex.conversationControl");
|
|
27
|
+
|
|
28
|
+
function getActiveTurns(): Map<string, ActiveTurn> {
|
|
29
|
+
const globalState = globalThis as typeof globalThis & {
|
|
30
|
+
[CODEX_CONVERSATION_CONTROL_STATE]?: Map<string, ActiveTurn>;
|
|
31
|
+
};
|
|
32
|
+
globalState[CODEX_CONVERSATION_CONTROL_STATE] ??= new Map();
|
|
33
|
+
return globalState[CODEX_CONVERSATION_CONTROL_STATE];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function trackCodexConversationActiveTurn(active: ActiveTurn): () => void {
|
|
37
|
+
const activeTurns = getActiveTurns();
|
|
38
|
+
activeTurns.set(active.sessionFile, active);
|
|
39
|
+
return () => {
|
|
40
|
+
const current = activeTurns.get(active.sessionFile);
|
|
41
|
+
if (current?.turnId === active.turnId) {
|
|
42
|
+
activeTurns.delete(active.sessionFile);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function readCodexConversationActiveTurn(sessionFile: string): ActiveTurn | undefined {
|
|
48
|
+
return getActiveTurns().get(sessionFile);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function stopCodexConversationTurn(params: {
|
|
52
|
+
sessionFile: string;
|
|
53
|
+
pluginConfig?: unknown;
|
|
54
|
+
agentDir?: string;
|
|
55
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
56
|
+
}): Promise<{ stopped: boolean; message: string }> {
|
|
57
|
+
const active = readCodexConversationActiveTurn(params.sessionFile);
|
|
58
|
+
if (!active) {
|
|
59
|
+
return { stopped: false, message: "No active Codex run to stop." };
|
|
60
|
+
}
|
|
61
|
+
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
|
|
62
|
+
const lookup = buildBindingLookup(params);
|
|
63
|
+
const binding = await readCodexAppServerBinding(params.sessionFile, lookup);
|
|
64
|
+
const client = await getSharedCodexAppServerClient({
|
|
65
|
+
startOptions: runtime.start,
|
|
66
|
+
timeoutMs: runtime.requestTimeoutMs,
|
|
67
|
+
authProfileId: binding?.authProfileId,
|
|
68
|
+
...lookup,
|
|
69
|
+
});
|
|
70
|
+
await client.request(
|
|
71
|
+
"turn/interrupt",
|
|
72
|
+
{
|
|
73
|
+
threadId: active.threadId,
|
|
74
|
+
turnId: active.turnId,
|
|
75
|
+
},
|
|
76
|
+
{ timeoutMs: runtime.requestTimeoutMs },
|
|
77
|
+
);
|
|
78
|
+
return { stopped: true, message: "Codex stop requested." };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function steerCodexConversationTurn(params: {
|
|
82
|
+
sessionFile: string;
|
|
83
|
+
message: string;
|
|
84
|
+
pluginConfig?: unknown;
|
|
85
|
+
agentDir?: string;
|
|
86
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
87
|
+
}): Promise<{ steered: boolean; message: string }> {
|
|
88
|
+
const active = readCodexConversationActiveTurn(params.sessionFile);
|
|
89
|
+
const text = params.message.trim();
|
|
90
|
+
if (!text) {
|
|
91
|
+
return { steered: false, message: "Usage: /codex steer <message>" };
|
|
92
|
+
}
|
|
93
|
+
if (!active) {
|
|
94
|
+
return { steered: false, message: "No active Codex run to steer." };
|
|
95
|
+
}
|
|
96
|
+
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
|
|
97
|
+
const lookup = buildBindingLookup(params);
|
|
98
|
+
const binding = await readCodexAppServerBinding(params.sessionFile, lookup);
|
|
99
|
+
const client = await getSharedCodexAppServerClient({
|
|
100
|
+
startOptions: runtime.start,
|
|
101
|
+
timeoutMs: runtime.requestTimeoutMs,
|
|
102
|
+
authProfileId: binding?.authProfileId,
|
|
103
|
+
...lookup,
|
|
104
|
+
});
|
|
105
|
+
await client.request(
|
|
106
|
+
"turn/steer",
|
|
107
|
+
{
|
|
108
|
+
threadId: active.threadId,
|
|
109
|
+
expectedTurnId: active.turnId,
|
|
110
|
+
input: [{ type: "text", text, text_elements: [] }],
|
|
111
|
+
},
|
|
112
|
+
{ timeoutMs: runtime.requestTimeoutMs },
|
|
113
|
+
);
|
|
114
|
+
return { steered: true, message: "Sent steer message to Codex." };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function setCodexConversationModel(params: {
|
|
118
|
+
sessionFile: string;
|
|
119
|
+
model: string;
|
|
120
|
+
pluginConfig?: unknown;
|
|
121
|
+
agentDir?: string;
|
|
122
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
123
|
+
}): Promise<string> {
|
|
124
|
+
const model = params.model.trim();
|
|
125
|
+
if (!model) {
|
|
126
|
+
return "Usage: /codex model <model>";
|
|
127
|
+
}
|
|
128
|
+
const lookup = buildBindingLookup(params);
|
|
129
|
+
const binding = await requireThreadBinding(params.sessionFile, lookup);
|
|
130
|
+
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
|
|
131
|
+
const response = await resumeThreadWithOverrides({
|
|
132
|
+
pluginConfig: params.pluginConfig,
|
|
133
|
+
threadId: binding.threadId,
|
|
134
|
+
authProfileId: binding.authProfileId,
|
|
135
|
+
...lookup,
|
|
136
|
+
model,
|
|
137
|
+
});
|
|
138
|
+
await writeCodexAppServerBinding(
|
|
139
|
+
params.sessionFile,
|
|
140
|
+
{
|
|
141
|
+
...binding,
|
|
142
|
+
cwd: response.thread.cwd ?? binding.cwd,
|
|
143
|
+
model: response.model ?? model,
|
|
144
|
+
modelProvider: response.modelProvider ?? binding.modelProvider,
|
|
145
|
+
approvalPolicy: binding.approvalPolicy,
|
|
146
|
+
sandbox: binding.sandbox,
|
|
147
|
+
serviceTier: binding.serviceTier ?? runtime.serviceTier,
|
|
148
|
+
},
|
|
149
|
+
lookup,
|
|
150
|
+
);
|
|
151
|
+
return `Codex model set to ${formatCodexDisplayText(response.model ?? model)}.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function setCodexConversationFastMode(params: {
|
|
155
|
+
sessionFile: string;
|
|
156
|
+
enabled?: boolean;
|
|
157
|
+
pluginConfig?: unknown;
|
|
158
|
+
agentDir?: string;
|
|
159
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
160
|
+
}): Promise<string> {
|
|
161
|
+
const lookup = buildBindingLookup(params);
|
|
162
|
+
const binding = await requireThreadBinding(params.sessionFile, lookup);
|
|
163
|
+
if (params.enabled == null) {
|
|
164
|
+
return `Codex fast mode: ${isCodexFastServiceTier(binding.serviceTier) ? "on" : "off"}.`;
|
|
165
|
+
}
|
|
166
|
+
const serviceTier: CodexServiceTier = params.enabled ? "priority" : "flex";
|
|
167
|
+
// Fast mode is sent on each later turn; do not require Codex to accept an
|
|
168
|
+
// immediate thread/resume control request just to persist the preference.
|
|
169
|
+
await writeCodexAppServerBinding(
|
|
170
|
+
params.sessionFile,
|
|
171
|
+
{
|
|
172
|
+
...binding,
|
|
173
|
+
serviceTier,
|
|
174
|
+
},
|
|
175
|
+
lookup,
|
|
176
|
+
);
|
|
177
|
+
return `Codex fast mode ${params.enabled ? "enabled" : "disabled"}.`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function setCodexConversationPermissions(params: {
|
|
181
|
+
sessionFile: string;
|
|
182
|
+
mode?: PermissionsMode;
|
|
183
|
+
pluginConfig?: unknown;
|
|
184
|
+
agentDir?: string;
|
|
185
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
186
|
+
}): Promise<string> {
|
|
187
|
+
const lookup = buildBindingLookup(params);
|
|
188
|
+
const binding = await requireThreadBinding(params.sessionFile, lookup);
|
|
189
|
+
if (!params.mode) {
|
|
190
|
+
return `Codex permissions: ${formatPermissionsMode(binding)}.`;
|
|
191
|
+
}
|
|
192
|
+
const policy = permissionsForMode(params.mode);
|
|
193
|
+
// Native bound turns pass these settings at turn/start time, so this command
|
|
194
|
+
// can update the local binding even when app-server resume overrides fail.
|
|
195
|
+
await writeCodexAppServerBinding(
|
|
196
|
+
params.sessionFile,
|
|
197
|
+
{
|
|
198
|
+
...binding,
|
|
199
|
+
approvalPolicy: policy.approvalPolicy,
|
|
200
|
+
sandbox: policy.sandbox,
|
|
201
|
+
},
|
|
202
|
+
lookup,
|
|
203
|
+
);
|
|
204
|
+
return `Codex permissions set to ${params.mode === "yolo" ? "full access" : "default"}.`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function parseCodexFastModeArg(arg: string | undefined): boolean | undefined {
|
|
208
|
+
const normalized = arg?.trim().toLowerCase();
|
|
209
|
+
if (!normalized || normalized === "status") {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (normalized === "on" || normalized === "true" || normalized === "fast") {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (normalized === "off" || normalized === "false" || normalized === "flex") {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function parseCodexPermissionsModeArg(arg: string | undefined): PermissionsMode | undefined {
|
|
222
|
+
const normalized = arg?.trim().toLowerCase();
|
|
223
|
+
if (!normalized || normalized === "status") {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
if (normalized === "yolo" || normalized === "full" || normalized === "full-access") {
|
|
227
|
+
return "yolo";
|
|
228
|
+
}
|
|
229
|
+
if (normalized === "default" || normalized === "guardian") {
|
|
230
|
+
return "default";
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function formatPermissionsMode(binding: {
|
|
236
|
+
approvalPolicy?: CodexAppServerApprovalPolicy;
|
|
237
|
+
sandbox?: CodexAppServerSandboxMode;
|
|
238
|
+
}): string {
|
|
239
|
+
return binding.approvalPolicy === "never" && binding.sandbox === "danger-full-access"
|
|
240
|
+
? "full access"
|
|
241
|
+
: "default";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function requireThreadBinding(sessionFile: string, lookup: CodexAppServerBindingLookup = {}) {
|
|
245
|
+
const binding = await readCodexAppServerBinding(sessionFile, lookup);
|
|
246
|
+
if (!binding?.threadId) {
|
|
247
|
+
throw new Error("No Codex thread is attached to this AutoBot session yet.");
|
|
248
|
+
}
|
|
249
|
+
return binding;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function resumeThreadWithOverrides(params: {
|
|
253
|
+
pluginConfig?: unknown;
|
|
254
|
+
threadId: string;
|
|
255
|
+
authProfileId?: string;
|
|
256
|
+
agentDir?: string;
|
|
257
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
258
|
+
model?: string;
|
|
259
|
+
approvalPolicy?: CodexAppServerApprovalPolicy;
|
|
260
|
+
sandbox?: CodexAppServerSandboxMode;
|
|
261
|
+
serviceTier?: CodexServiceTier;
|
|
262
|
+
}): Promise<CodexThreadResumeResponse> {
|
|
263
|
+
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
|
|
264
|
+
const client = await getSharedCodexAppServerClient({
|
|
265
|
+
startOptions: runtime.start,
|
|
266
|
+
timeoutMs: runtime.requestTimeoutMs,
|
|
267
|
+
authProfileId: params.authProfileId,
|
|
268
|
+
...buildBindingLookup(params),
|
|
269
|
+
});
|
|
270
|
+
return await client.request(
|
|
271
|
+
CODEX_CONTROL_METHODS.resumeThread,
|
|
272
|
+
{
|
|
273
|
+
threadId: params.threadId,
|
|
274
|
+
...(params.model ? { model: params.model } : {}),
|
|
275
|
+
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
|
|
276
|
+
sandbox: params.sandbox ?? runtime.sandbox,
|
|
277
|
+
approvalsReviewer: runtime.approvalsReviewer,
|
|
278
|
+
...(params.serviceTier ? { serviceTier: params.serviceTier } : {}),
|
|
279
|
+
persistExtendedHistory: true,
|
|
280
|
+
},
|
|
281
|
+
{ timeoutMs: runtime.requestTimeoutMs },
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildBindingLookup(params: {
|
|
286
|
+
agentDir?: string;
|
|
287
|
+
config?: CodexAppServerBindingLookup["config"];
|
|
288
|
+
}): CodexAppServerBindingLookup {
|
|
289
|
+
const agentDir = params.agentDir?.trim();
|
|
290
|
+
return {
|
|
291
|
+
...(agentDir ? { agentDir } : {}),
|
|
292
|
+
...(params.config ? { config: params.config } : {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function permissionsForMode(mode: PermissionsMode): {
|
|
297
|
+
approvalPolicy: CodexAppServerApprovalPolicy;
|
|
298
|
+
sandbox: CodexAppServerSandboxMode;
|
|
299
|
+
} {
|
|
300
|
+
return mode === "yolo"
|
|
301
|
+
? { approvalPolicy: "never", sandbox: "danger-full-access" }
|
|
302
|
+
: { approvalPolicy: "on-request", sandbox: "workspace-write" };
|
|
303
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isJsonObject,
|
|
3
|
+
type CodexServerNotification,
|
|
4
|
+
type JsonObject,
|
|
5
|
+
} from "./app-server/protocol.js";
|
|
6
|
+
|
|
7
|
+
const MAX_PENDING_NOTIFICATIONS_PER_TURN = 100;
|
|
8
|
+
|
|
9
|
+
export function createCodexConversationTurnCollector(threadId: string) {
|
|
10
|
+
let turnId: string | undefined;
|
|
11
|
+
let completed = false;
|
|
12
|
+
let failedError: string | undefined;
|
|
13
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
14
|
+
const assistantTextByItem = new Map<string, string>();
|
|
15
|
+
const assistantOrder: string[] = [];
|
|
16
|
+
const pendingNotificationsByTurnId = new Map<string, CodexServerNotification[]>();
|
|
17
|
+
let resolveCompletion: ((value: { replyText: string }) => void) | undefined;
|
|
18
|
+
let rejectCompletion: ((error: Error) => void) | undefined;
|
|
19
|
+
|
|
20
|
+
const rememberItem = (itemId: string) => {
|
|
21
|
+
if (!assistantOrder.includes(itemId)) {
|
|
22
|
+
assistantOrder.push(itemId);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const collectReplyText = (): string => {
|
|
26
|
+
const texts = assistantOrder
|
|
27
|
+
.map((itemId) => assistantTextByItem.get(itemId)?.trim())
|
|
28
|
+
.filter((text): text is string => Boolean(text));
|
|
29
|
+
return texts.at(-1) ?? "";
|
|
30
|
+
};
|
|
31
|
+
const clearWaitState = () => {
|
|
32
|
+
if (timeout) {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
timeout = undefined;
|
|
35
|
+
}
|
|
36
|
+
resolveCompletion = undefined;
|
|
37
|
+
rejectCompletion = undefined;
|
|
38
|
+
};
|
|
39
|
+
const finish = () => {
|
|
40
|
+
if (completed) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
completed = true;
|
|
44
|
+
if (failedError) {
|
|
45
|
+
rejectCompletion?.(new Error(failedError));
|
|
46
|
+
} else {
|
|
47
|
+
resolveCompletion?.({ replyText: collectReplyText() });
|
|
48
|
+
}
|
|
49
|
+
clearWaitState();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleNotification = (notification: CodexServerNotification) => {
|
|
53
|
+
const params = isJsonObject(notification.params) ? notification.params : undefined;
|
|
54
|
+
if (!params || readString(params, "threadId") !== threadId) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!turnId) {
|
|
58
|
+
const pendingTurnId = readNotificationTurnId(params);
|
|
59
|
+
if (pendingTurnId) {
|
|
60
|
+
const pending = pendingNotificationsByTurnId.get(pendingTurnId) ?? [];
|
|
61
|
+
if (pending.length < MAX_PENDING_NOTIFICATIONS_PER_TURN) {
|
|
62
|
+
pending.push(notification);
|
|
63
|
+
pendingNotificationsByTurnId.set(pendingTurnId, pending);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!isNotificationForTurn(params, threadId, turnId)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
72
|
+
const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "assistant";
|
|
73
|
+
const delta = readTextString(params, "delta");
|
|
74
|
+
if (!delta) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
rememberItem(itemId);
|
|
78
|
+
assistantTextByItem.set(itemId, `${assistantTextByItem.get(itemId) ?? ""}${delta}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (notification.method === "item/completed") {
|
|
82
|
+
const item = isJsonObject(params.item) ? params.item : undefined;
|
|
83
|
+
if (item?.type === "agentMessage") {
|
|
84
|
+
const itemId = readString(item, "id") ?? readString(params, "itemId") ?? "assistant";
|
|
85
|
+
const text = readTextString(item, "text");
|
|
86
|
+
if (text) {
|
|
87
|
+
rememberItem(itemId);
|
|
88
|
+
assistantTextByItem.set(itemId, text);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (notification.method === "turn/completed") {
|
|
94
|
+
const turn = isJsonObject(params.turn) ? params.turn : undefined;
|
|
95
|
+
const status = readString(turn, "status");
|
|
96
|
+
if (status === "failed") {
|
|
97
|
+
failedError =
|
|
98
|
+
readString(readRecord(turn?.error), "message") ?? "codex app-server turn failed";
|
|
99
|
+
}
|
|
100
|
+
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
if (!isJsonObject(item) || item.type !== "agentMessage") {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const itemId = readString(item, "id") ?? `assistant-${assistantOrder.length + 1}`;
|
|
106
|
+
const text = readTextString(item, "text");
|
|
107
|
+
if (text) {
|
|
108
|
+
rememberItem(itemId);
|
|
109
|
+
assistantTextByItem.set(itemId, text);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finish();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
setTurnId(nextTurnId: string) {
|
|
118
|
+
turnId = nextTurnId;
|
|
119
|
+
const pending = pendingNotificationsByTurnId.get(nextTurnId) ?? [];
|
|
120
|
+
pendingNotificationsByTurnId.clear();
|
|
121
|
+
for (const notification of pending) {
|
|
122
|
+
handleNotification(notification);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
handleNotification,
|
|
126
|
+
wait(params: { timeoutMs: number }): Promise<{ replyText: string }> {
|
|
127
|
+
if (completed) {
|
|
128
|
+
return failedError
|
|
129
|
+
? Promise.reject(new Error(failedError))
|
|
130
|
+
: Promise.resolve({ replyText: collectReplyText() });
|
|
131
|
+
}
|
|
132
|
+
return new Promise<{ replyText: string }>((resolve, reject) => {
|
|
133
|
+
resolveCompletion = resolve;
|
|
134
|
+
rejectCompletion = reject;
|
|
135
|
+
timeout = setTimeout(
|
|
136
|
+
() => {
|
|
137
|
+
completed = true;
|
|
138
|
+
reject(new Error("codex app-server bound turn timed out"));
|
|
139
|
+
clearWaitState();
|
|
140
|
+
},
|
|
141
|
+
Math.max(100, params.timeoutMs),
|
|
142
|
+
);
|
|
143
|
+
timeout.unref?.();
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isNotificationForTurn(
|
|
150
|
+
params: JsonObject,
|
|
151
|
+
threadId: string,
|
|
152
|
+
turnId: string | undefined,
|
|
153
|
+
): boolean {
|
|
154
|
+
if (readString(params, "threadId") !== threadId) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (!turnId) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const directTurnId = readString(params, "turnId");
|
|
161
|
+
if (directTurnId) {
|
|
162
|
+
return directTurnId === turnId;
|
|
163
|
+
}
|
|
164
|
+
const turn = isJsonObject(params.turn) ? params.turn : undefined;
|
|
165
|
+
return readString(turn, "id") === turnId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readNotificationTurnId(params: JsonObject): string | undefined {
|
|
169
|
+
return readString(params, "turnId") ?? readString(readRecord(params.turn), "id");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
173
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
174
|
+
? (value as Record<string, unknown>)
|
|
175
|
+
: undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readString(record: Record<string, unknown> | JsonObject | undefined, key: string) {
|
|
179
|
+
const value = record?.[key];
|
|
180
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readTextString(record: Record<string, unknown> | JsonObject | undefined, key: string) {
|
|
184
|
+
const value = record?.[key];
|
|
185
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
186
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { PluginHookInboundClaimEvent } from "autobot/plugin-sdk/plugin-entry";
|
|
4
|
+
import type { CodexUserInput } from "./app-server/protocol.js";
|
|
5
|
+
|
|
6
|
+
type InboundMedia = {
|
|
7
|
+
path?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const IMAGE_EXTENSIONS = new Set([".avif", ".gif", ".jpeg", ".jpg", ".png", ".webp"]);
|
|
13
|
+
|
|
14
|
+
export function buildCodexConversationTurnInput(params: {
|
|
15
|
+
prompt: string;
|
|
16
|
+
event: PluginHookInboundClaimEvent;
|
|
17
|
+
}): CodexUserInput[] {
|
|
18
|
+
return [
|
|
19
|
+
{ type: "text", text: params.prompt, text_elements: [] },
|
|
20
|
+
...extractInboundMedia(params.event)
|
|
21
|
+
.map(toCodexImageInput)
|
|
22
|
+
.filter((item): item is CodexUserInput => item !== undefined),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractInboundMedia(event: PluginHookInboundClaimEvent): InboundMedia[] {
|
|
27
|
+
const metadata = event.metadata ?? {};
|
|
28
|
+
// AutoBot channels expose either local staged files or remote URLs. Keep
|
|
29
|
+
// them separate so Codex can receive the cheaper localImage input when a file
|
|
30
|
+
// is already present, while still supporting remote-only transports.
|
|
31
|
+
const paths = readStringArray(metadata.mediaPaths).concat(readStringArray(metadata.mediaPath));
|
|
32
|
+
const urls = readStringArray(metadata.mediaUrls).concat(readStringArray(metadata.mediaUrl));
|
|
33
|
+
const mimeTypes = readStringArray(metadata.mediaTypes).concat(
|
|
34
|
+
readStringArray(metadata.mediaType),
|
|
35
|
+
);
|
|
36
|
+
const count = Math.max(paths.length, urls.length, mimeTypes.length);
|
|
37
|
+
const media: InboundMedia[] = [];
|
|
38
|
+
for (let index = 0; index < count; index += 1) {
|
|
39
|
+
media.push({
|
|
40
|
+
path: paths[index],
|
|
41
|
+
url: urls[index],
|
|
42
|
+
mimeType: mimeTypes[index] ?? mimeTypes[0],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return media;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toCodexImageInput(media: InboundMedia): CodexUserInput | undefined {
|
|
49
|
+
if (!isImageMedia(media)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const localPath = media.path ?? readLocalMediaPath(media.url);
|
|
53
|
+
if (localPath) {
|
|
54
|
+
const normalized = normalizeFileUrl(localPath);
|
|
55
|
+
return normalized ? { type: "localImage", path: normalized } : undefined;
|
|
56
|
+
}
|
|
57
|
+
return media.url ? { type: "image", url: media.url } : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isImageMedia(media: InboundMedia): boolean {
|
|
61
|
+
if (media.mimeType?.toLowerCase().startsWith("image/")) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const candidate = media.path ?? media.url;
|
|
65
|
+
if (!candidate) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return IMAGE_EXTENSIONS.has(path.extname(candidate.split(/[?#]/, 1)[0] ?? "").toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeFileUrl(value: string): string | undefined {
|
|
72
|
+
if (!value.startsWith("file://")) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return fileURLToPath(value);
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readLocalMediaPath(value: string | undefined): string | undefined {
|
|
83
|
+
if (!value) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (value.startsWith("file://")) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
if (value.startsWith("//")) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(value) ? undefined : value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readStringArray(value: unknown): string[] {
|
|
99
|
+
if (typeof value === "string" && value.trim()) {
|
|
100
|
+
return [value.trim()];
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(value)) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
|
106
|
+
}
|