@nextclaw/nextclaw-ncp-runtime-codex-sdk 0.1.14 → 0.1.16
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/dist/codex-cli-env.js +20 -30
- package/dist/codex-input.utils.d.ts +23 -0
- package/dist/codex-input.utils.js +124 -0
- package/dist/codex-sdk-ncp-event-mapper.js +184 -207
- package/dist/completed-assistant-message.utils.js +25 -0
- package/dist/index.d.ts +38 -29
- package/dist/index.js +214 -232
- package/package.json +3 -4
- package/dist/codex-cli-env.d.ts +0 -7
- package/dist/codex-sdk-ncp-event-mapper.d.ts +0 -23
package/dist/codex-cli-env.js
CHANGED
|
@@ -1,36 +1,26 @@
|
|
|
1
|
+
//#region src/codex-cli-env.ts
|
|
1
2
|
function readString(value) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
return value;
|
|
3
|
+
if (typeof value !== "string") return;
|
|
4
|
+
return value;
|
|
6
5
|
}
|
|
7
6
|
function copyProcessEnv() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
return env;
|
|
7
|
+
const env = {};
|
|
8
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
9
|
+
const normalized = readString(value);
|
|
10
|
+
if (normalized === void 0) continue;
|
|
11
|
+
env[key] = normalized;
|
|
12
|
+
}
|
|
13
|
+
return env;
|
|
17
14
|
}
|
|
18
15
|
function buildCodexCliEnv(config) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
env.OPENAI_API_KEY = config.apiKey;
|
|
28
|
-
}
|
|
29
|
-
if (config.apiBase?.trim()) {
|
|
30
|
-
env.OPENAI_BASE_URL = config.apiBase.trim();
|
|
31
|
-
}
|
|
32
|
-
return Object.keys(env).length > 0 ? env : void 0;
|
|
16
|
+
const env = copyProcessEnv();
|
|
17
|
+
for (const [key, value] of Object.entries(config.env ?? {})) {
|
|
18
|
+
if (typeof value !== "string") continue;
|
|
19
|
+
env[key] = value;
|
|
20
|
+
}
|
|
21
|
+
if (config.apiKey.trim()) env.OPENAI_API_KEY = config.apiKey;
|
|
22
|
+
if (config.apiBase?.trim()) env.OPENAI_BASE_URL = config.apiBase.trim();
|
|
23
|
+
return Object.keys(env).length > 0 ? env : void 0;
|
|
33
24
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { buildCodexCliEnv };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NcpAgentRunInput } from "@nextclaw/ncp";
|
|
2
|
+
import { Thread } from "@openai/codex-sdk";
|
|
3
|
+
|
|
4
|
+
//#region src/codex-input.utils.d.ts
|
|
5
|
+
type CodexThreadInput = Parameters<Thread["runStreamed"]>[0];
|
|
6
|
+
type CodexAssetContentPathResolver = (assetUri: string) => Promise<string | null> | string | null;
|
|
7
|
+
type RuntimeAgentPromptBuilder = {
|
|
8
|
+
buildRuntimeUserPrompt: (params: {
|
|
9
|
+
workspace?: string;
|
|
10
|
+
hostWorkspace?: string;
|
|
11
|
+
sessionKey?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
userMessage: string;
|
|
14
|
+
}) => string;
|
|
15
|
+
};
|
|
16
|
+
declare function buildCodexInputBuilder(runtimeAgent: RuntimeAgentPromptBuilder, params: {
|
|
17
|
+
workspace: string;
|
|
18
|
+
hostWorkspace?: string;
|
|
19
|
+
sessionMetadata?: Record<string, unknown>;
|
|
20
|
+
resolveAssetContentPath?: CodexAssetContentPathResolver;
|
|
21
|
+
}): (input: NcpAgentRunInput) => Promise<CodexThreadInput>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CodexAssetContentPathResolver, CodexThreadInput, buildCodexInputBuilder };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
//#region src/codex-input.utils.ts
|
|
4
|
+
function readOptionalString(value) {
|
|
5
|
+
if (typeof value !== "string") return null;
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
8
|
+
}
|
|
9
|
+
function isImageMimeType(value) {
|
|
10
|
+
return value?.startsWith("image/") ?? false;
|
|
11
|
+
}
|
|
12
|
+
function readLatestUserMessageParts(input) {
|
|
13
|
+
for (let index = input.messages.length - 1; index >= 0; index -= 1) {
|
|
14
|
+
const message = input.messages[index];
|
|
15
|
+
if (message?.role !== "user") continue;
|
|
16
|
+
if (message.parts.length > 0) return message.parts;
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
function formatAssetReferenceBlock(part) {
|
|
21
|
+
const fileName = readOptionalString(part.name) ?? "asset";
|
|
22
|
+
const mimeType = readOptionalString(part.mimeType) ?? "application/octet-stream";
|
|
23
|
+
const assetUri = readOptionalString(part.assetUri);
|
|
24
|
+
const url = readOptionalString(part.url);
|
|
25
|
+
const sizeText = typeof part.sizeBytes === "number" && Number.isFinite(part.sizeBytes) ? String(part.sizeBytes) : null;
|
|
26
|
+
return [
|
|
27
|
+
`[Asset: ${fileName}]`,
|
|
28
|
+
`[MIME: ${mimeType}]`,
|
|
29
|
+
...assetUri ? [`[Asset URI: ${assetUri}]`] : [],
|
|
30
|
+
...sizeText ? [`[Size Bytes: ${sizeText}]`] : [],
|
|
31
|
+
...url ? [`[Preview URL: ${url}]`] : [],
|
|
32
|
+
"[Instruction: This file is not embedded in the prompt. If you need to inspect or transform it, use asset_export to copy it to a normal file path first.]"
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
function formatImageAttachmentHint(part) {
|
|
36
|
+
const fileName = readOptionalString(part.name) ?? "asset";
|
|
37
|
+
const mimeType = readOptionalString(part.mimeType) ?? "application/octet-stream";
|
|
38
|
+
const assetUri = readOptionalString(part.assetUri);
|
|
39
|
+
const sizeText = typeof part.sizeBytes === "number" && Number.isFinite(part.sizeBytes) ? String(part.sizeBytes) : null;
|
|
40
|
+
return [
|
|
41
|
+
`[Attached Image: ${fileName}]`,
|
|
42
|
+
`[MIME: ${mimeType}]`,
|
|
43
|
+
...assetUri ? [`[Asset URI: ${assetUri}]`] : [],
|
|
44
|
+
...sizeText ? [`[Size Bytes: ${sizeText}]`] : [],
|
|
45
|
+
assetUri ? "[Instruction: This image is embedded in the prompt. If you need to transform or process the original file with tools, use the asset URI.]" : "[Instruction: This image is embedded in the prompt.]"
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
function resolveFileUrlToPath(value) {
|
|
49
|
+
if (!value) return null;
|
|
50
|
+
if (value.startsWith("file://")) try {
|
|
51
|
+
return fileURLToPath(value);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return isAbsolute(value) ? value : null;
|
|
56
|
+
}
|
|
57
|
+
async function resolveLocalImagePath(part, resolveAssetContentPath) {
|
|
58
|
+
if (!isImageMimeType(readOptionalString(part.mimeType))) return null;
|
|
59
|
+
const assetUri = readOptionalString(part.assetUri);
|
|
60
|
+
if (assetUri && resolveAssetContentPath) {
|
|
61
|
+
const resolvedPath = readOptionalString(await resolveAssetContentPath(assetUri));
|
|
62
|
+
if (resolvedPath) return resolvedPath;
|
|
63
|
+
}
|
|
64
|
+
return resolveFileUrlToPath(readOptionalString(part.url));
|
|
65
|
+
}
|
|
66
|
+
async function buildCodexPromptContent(parts, resolveAssetContentPath) {
|
|
67
|
+
const promptBlocks = [];
|
|
68
|
+
const userInputs = [];
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
if ((part.type === "text" || part.type === "rich-text") && part.text.trim().length > 0) {
|
|
71
|
+
promptBlocks.push(part.text);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (part.type !== "file") continue;
|
|
75
|
+
const localImagePath = await resolveLocalImagePath(part, resolveAssetContentPath);
|
|
76
|
+
if (localImagePath) {
|
|
77
|
+
promptBlocks.push(formatImageAttachmentHint(part));
|
|
78
|
+
userInputs.push({
|
|
79
|
+
type: "local_image",
|
|
80
|
+
path: localImagePath
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
promptBlocks.push(formatAssetReferenceBlock(part));
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
promptText: promptBlocks.join("\n\n").trim(),
|
|
88
|
+
userInputs
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function buildCodexThreadInput(promptText, userInputs) {
|
|
92
|
+
const normalizedPrompt = promptText.trim();
|
|
93
|
+
if (userInputs.length === 0) return normalizedPrompt;
|
|
94
|
+
return [...normalizedPrompt ? [{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: normalizedPrompt
|
|
97
|
+
}] : [], ...userInputs];
|
|
98
|
+
}
|
|
99
|
+
function readMetadata(value) {
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function buildCodexInputBuilder(runtimeAgent, params) {
|
|
104
|
+
return async (input) => {
|
|
105
|
+
const { promptText, userInputs } = await buildCodexPromptContent(readLatestUserMessageParts(input), params.resolveAssetContentPath);
|
|
106
|
+
const metadata = {
|
|
107
|
+
...readMetadata(params.sessionMetadata),
|
|
108
|
+
...readMetadata(input.metadata)
|
|
109
|
+
};
|
|
110
|
+
return buildCodexThreadInput(runtimeAgent.buildRuntimeUserPrompt({
|
|
111
|
+
workspace: params.workspace,
|
|
112
|
+
hostWorkspace: params.hostWorkspace,
|
|
113
|
+
sessionKey: input.sessionId,
|
|
114
|
+
metadata,
|
|
115
|
+
userMessage: promptText
|
|
116
|
+
}), userInputs);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function buildCodexTurnInputFromRunInput(input, options = {}) {
|
|
120
|
+
const { promptText, userInputs } = await buildCodexPromptContent(readLatestUserMessageParts(input), options.resolveAssetContentPath);
|
|
121
|
+
return buildCodexThreadInput(promptText, userInputs);
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { buildCodexInputBuilder, buildCodexTurnInputFromRunInput };
|
|
@@ -1,218 +1,195 @@
|
|
|
1
1
|
import { NcpEventType } from "@nextclaw/ncp";
|
|
2
|
+
//#region src/codex-sdk-ncp-event-mapper.ts
|
|
2
3
|
function buildToolDescriptor(item) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
toolName: "file_change",
|
|
26
|
-
args: {
|
|
27
|
-
changes: item.changes
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
case "todo_list":
|
|
31
|
-
return {
|
|
32
|
-
toolName: "todo_list",
|
|
33
|
-
args: {
|
|
34
|
-
items: item.items
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
4
|
+
switch (item.type) {
|
|
5
|
+
case "mcp_tool_call": return {
|
|
6
|
+
toolName: item.server ? `mcp:${item.server}.${item.tool}` : `mcp:${item.tool}`,
|
|
7
|
+
args: item.arguments
|
|
8
|
+
};
|
|
9
|
+
case "command_execution": return {
|
|
10
|
+
toolName: "command_execution",
|
|
11
|
+
args: { command: item.command }
|
|
12
|
+
};
|
|
13
|
+
case "web_search": return {
|
|
14
|
+
toolName: "web_search",
|
|
15
|
+
args: { query: item.query }
|
|
16
|
+
};
|
|
17
|
+
case "file_change": return {
|
|
18
|
+
toolName: "file_change",
|
|
19
|
+
args: { changes: item.changes }
|
|
20
|
+
};
|
|
21
|
+
case "todo_list": return {
|
|
22
|
+
toolName: "todo_list",
|
|
23
|
+
args: { items: item.items }
|
|
24
|
+
};
|
|
25
|
+
}
|
|
38
26
|
}
|
|
39
27
|
function buildToolResult(item) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
69
|
-
}
|
|
28
|
+
switch (item.type) {
|
|
29
|
+
case "mcp_tool_call": return item.status === "failed" ? {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: item.error ?? { message: "MCP tool call failed." }
|
|
32
|
+
} : {
|
|
33
|
+
ok: item.status === "completed",
|
|
34
|
+
status: item.status,
|
|
35
|
+
result: item.result ?? null
|
|
36
|
+
};
|
|
37
|
+
case "command_execution": return {
|
|
38
|
+
status: item.status,
|
|
39
|
+
command: item.command,
|
|
40
|
+
aggregated_output: item.aggregated_output,
|
|
41
|
+
...typeof item.exit_code === "number" ? { exit_code: item.exit_code } : {}
|
|
42
|
+
};
|
|
43
|
+
case "web_search": return {
|
|
44
|
+
status: "completed",
|
|
45
|
+
query: item.query
|
|
46
|
+
};
|
|
47
|
+
case "file_change": return {
|
|
48
|
+
status: item.status,
|
|
49
|
+
changes: item.changes
|
|
50
|
+
};
|
|
51
|
+
case "todo_list": return {
|
|
52
|
+
status: "completed",
|
|
53
|
+
items: item.items
|
|
54
|
+
};
|
|
55
|
+
}
|
|
70
56
|
}
|
|
71
57
|
function stringifyToolArgs(args) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
}
|
|
58
|
+
try {
|
|
59
|
+
return JSON.stringify(args ?? {});
|
|
60
|
+
} catch {
|
|
61
|
+
return JSON.stringify({ __serialization_error__: "tool arguments are not JSON serializable" });
|
|
62
|
+
}
|
|
79
63
|
}
|
|
80
64
|
function isToolLikeItem(item) {
|
|
81
|
-
|
|
65
|
+
return item.type === "mcp_tool_call" || item.type === "command_execution" || item.type === "web_search" || item.type === "file_change" || item.type === "todo_list";
|
|
82
66
|
}
|
|
83
67
|
async function* mapCodexItemEvent(params) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
toolCallId: item.id,
|
|
210
|
-
content: buildToolResult(item)
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
toolStateById.set(item.id, previous);
|
|
68
|
+
const { sessionId, messageId, event, itemTextById, toolStateById } = params;
|
|
69
|
+
const { item } = event;
|
|
70
|
+
if (item.type === "agent_message") {
|
|
71
|
+
const currentText = item.text ?? "";
|
|
72
|
+
const previous = itemTextById.get(item.id) ?? {
|
|
73
|
+
text: "",
|
|
74
|
+
started: false
|
|
75
|
+
};
|
|
76
|
+
if (!previous.started) yield {
|
|
77
|
+
type: NcpEventType.MessageTextStart,
|
|
78
|
+
payload: {
|
|
79
|
+
sessionId,
|
|
80
|
+
messageId
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
if (currentText.length > previous.text.length) {
|
|
84
|
+
const delta = currentText.slice(previous.text.length);
|
|
85
|
+
yield {
|
|
86
|
+
type: NcpEventType.MessageTextDelta,
|
|
87
|
+
payload: {
|
|
88
|
+
sessionId,
|
|
89
|
+
messageId,
|
|
90
|
+
delta
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
itemTextById.set(item.id, {
|
|
95
|
+
text: currentText,
|
|
96
|
+
started: true
|
|
97
|
+
});
|
|
98
|
+
if (event.type === "item.completed") yield {
|
|
99
|
+
type: NcpEventType.MessageTextEnd,
|
|
100
|
+
payload: {
|
|
101
|
+
sessionId,
|
|
102
|
+
messageId
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (item.type === "reasoning") {
|
|
108
|
+
const currentText = item.text ?? "";
|
|
109
|
+
const previous = itemTextById.get(item.id) ?? {
|
|
110
|
+
text: "",
|
|
111
|
+
started: false
|
|
112
|
+
};
|
|
113
|
+
if (!previous.started) yield {
|
|
114
|
+
type: NcpEventType.MessageReasoningStart,
|
|
115
|
+
payload: {
|
|
116
|
+
sessionId,
|
|
117
|
+
messageId
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (currentText.length > previous.text.length) {
|
|
121
|
+
const delta = currentText.slice(previous.text.length);
|
|
122
|
+
yield {
|
|
123
|
+
type: NcpEventType.MessageReasoningDelta,
|
|
124
|
+
payload: {
|
|
125
|
+
sessionId,
|
|
126
|
+
messageId,
|
|
127
|
+
delta
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
itemTextById.set(item.id, {
|
|
132
|
+
text: currentText,
|
|
133
|
+
started: true
|
|
134
|
+
});
|
|
135
|
+
if (event.type === "item.completed") yield {
|
|
136
|
+
type: NcpEventType.MessageReasoningEnd,
|
|
137
|
+
payload: {
|
|
138
|
+
sessionId,
|
|
139
|
+
messageId
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isToolLikeItem(item)) return;
|
|
145
|
+
const previous = toolStateById.get(item.id) ?? {
|
|
146
|
+
started: false,
|
|
147
|
+
argsEmitted: false,
|
|
148
|
+
ended: false
|
|
149
|
+
};
|
|
150
|
+
const descriptor = buildToolDescriptor(item);
|
|
151
|
+
if (!previous.started) {
|
|
152
|
+
yield {
|
|
153
|
+
type: NcpEventType.MessageToolCallStart,
|
|
154
|
+
payload: {
|
|
155
|
+
sessionId,
|
|
156
|
+
messageId,
|
|
157
|
+
toolCallId: item.id,
|
|
158
|
+
toolName: descriptor.toolName
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
previous.started = true;
|
|
162
|
+
}
|
|
163
|
+
if (!previous.argsEmitted) {
|
|
164
|
+
yield {
|
|
165
|
+
type: NcpEventType.MessageToolCallArgs,
|
|
166
|
+
payload: {
|
|
167
|
+
sessionId,
|
|
168
|
+
toolCallId: item.id,
|
|
169
|
+
args: stringifyToolArgs(descriptor.args)
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
previous.argsEmitted = true;
|
|
173
|
+
}
|
|
174
|
+
if (!previous.ended) {
|
|
175
|
+
yield {
|
|
176
|
+
type: NcpEventType.MessageToolCallEnd,
|
|
177
|
+
payload: {
|
|
178
|
+
sessionId,
|
|
179
|
+
toolCallId: item.id
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
previous.ended = true;
|
|
183
|
+
}
|
|
184
|
+
if (event.type === "item.updated" || event.type === "item.completed") yield {
|
|
185
|
+
type: NcpEventType.MessageToolCallResult,
|
|
186
|
+
payload: {
|
|
187
|
+
sessionId,
|
|
188
|
+
toolCallId: item.id,
|
|
189
|
+
content: buildToolResult(item)
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
toolStateById.set(item.id, previous);
|
|
215
193
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
};
|
|
194
|
+
//#endregion
|
|
195
|
+
export { mapCodexItemEvent };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/completed-assistant-message.utils.ts
|
|
2
|
+
function buildCompletedAssistantMessage(params) {
|
|
3
|
+
const { stateManager, sessionId, messageId } = params;
|
|
4
|
+
const snapshot = stateManager?.getSnapshot();
|
|
5
|
+
const existingMessage = snapshot?.messages.find((message) => message.id === messageId && message.role === "assistant");
|
|
6
|
+
if (existingMessage) return {
|
|
7
|
+
...structuredClone(existingMessage),
|
|
8
|
+
status: "final"
|
|
9
|
+
};
|
|
10
|
+
const streamingMessage = snapshot?.streamingMessage;
|
|
11
|
+
if (streamingMessage?.id === messageId && streamingMessage.role === "assistant") return {
|
|
12
|
+
...structuredClone(streamingMessage),
|
|
13
|
+
status: "final"
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
id: messageId,
|
|
17
|
+
sessionId,
|
|
18
|
+
role: "assistant",
|
|
19
|
+
status: "final",
|
|
20
|
+
parts: [],
|
|
21
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { buildCompletedAssistantMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { NcpAgentRunInput,
|
|
1
|
+
import { CodexAssetContentPathResolver, CodexThreadInput, buildCodexInputBuilder } from "./codex-input.utils.js";
|
|
2
|
+
import { NcpAgentConversationStateManager, NcpAgentRunInput, NcpAgentRunOptions, NcpAgentRuntime, NcpEndpointEvent } from "@nextclaw/ncp";
|
|
3
|
+
import { CodexOptions, ThreadOptions } from "@openai/codex-sdk";
|
|
3
4
|
|
|
5
|
+
//#region src/index.d.ts
|
|
4
6
|
type CodexSdkNcpAgentRuntimeConfig = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
sessionId: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
apiBase?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
threadId?: string | null;
|
|
12
|
+
codexPathOverride?: string;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
cliConfig?: CodexOptions["config"];
|
|
15
|
+
threadOptions?: ThreadOptions;
|
|
16
|
+
sessionMetadata?: Record<string, unknown>;
|
|
17
|
+
setSessionMetadata?: (nextMetadata: Record<string, unknown>) => void;
|
|
18
|
+
inputBuilder?: (input: NcpAgentRunInput) => Promise<CodexThreadInput> | CodexThreadInput;
|
|
19
|
+
resolveAssetContentPath?: CodexAssetContentPathResolver;
|
|
20
|
+
stateManager?: NcpAgentConversationStateManager;
|
|
18
21
|
};
|
|
19
22
|
declare class CodexSdkNcpAgentRuntime implements NcpAgentRuntime {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
private readonly config;
|
|
24
|
+
private codexPromise;
|
|
25
|
+
private thread;
|
|
26
|
+
private threadId;
|
|
27
|
+
private readonly sessionMetadata;
|
|
28
|
+
constructor(config: CodexSdkNcpAgentRuntimeConfig);
|
|
29
|
+
run: (this: CodexSdkNcpAgentRuntime, input: NcpAgentRunInput, options?: NcpAgentRunOptions) => AsyncGenerator<NcpEndpointEvent>;
|
|
30
|
+
private getCodex;
|
|
31
|
+
private resolveThread;
|
|
32
|
+
private buildTurnInput;
|
|
33
|
+
private emitEvent;
|
|
34
|
+
private emitRunStarted;
|
|
35
|
+
private emitReadyMetadata;
|
|
36
|
+
private emitRunError;
|
|
37
|
+
private emitRunCompleted;
|
|
38
|
+
private streamTurnEvents;
|
|
39
|
+
private handleThreadEvent;
|
|
40
|
+
private updateThreadId;
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
export { CodexSdkNcpAgentRuntime,
|
|
42
|
+
//#endregion
|
|
43
|
+
export { type CodexAssetContentPathResolver, CodexSdkNcpAgentRuntime, CodexSdkNcpAgentRuntimeConfig, buildCodexInputBuilder };
|
package/dist/index.js
CHANGED
|
@@ -1,241 +1,223 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
NcpEventType
|
|
4
|
-
} from "@nextclaw/ncp";
|
|
5
|
-
import {
|
|
6
|
-
mapCodexItemEvent
|
|
7
|
-
} from "./codex-sdk-ncp-event-mapper.js";
|
|
1
|
+
import { buildCompletedAssistantMessage } from "./completed-assistant-message.utils.js";
|
|
2
|
+
import { mapCodexItemEvent } from "./codex-sdk-ncp-event-mapper.js";
|
|
8
3
|
import { buildCodexCliEnv } from "./codex-cli-env.js";
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import { buildCodexInputBuilder, buildCodexTurnInputFromRunInput } from "./codex-input.utils.js";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { NcpEventType } from "@nextclaw/ncp";
|
|
7
|
+
//#region src/index.ts
|
|
8
|
+
const codexLoader = createRequire(import.meta.url)("../codex-sdk-loader.cjs");
|
|
11
9
|
function createId(prefix) {
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
function readUserText(input) {
|
|
15
|
-
for (let index = input.messages.length - 1; index >= 0; index -= 1) {
|
|
16
|
-
const message = input.messages[index];
|
|
17
|
-
if (message?.role !== "user") {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const text = message.parts.filter((part) => part.type === "text").map((part) => part.text).join("").trim();
|
|
21
|
-
if (text) {
|
|
22
|
-
return text;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return "";
|
|
10
|
+
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
26
11
|
}
|
|
27
12
|
function toAbortError(reason) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
error.name = "AbortError";
|
|
34
|
-
return error;
|
|
13
|
+
if (reason instanceof Error) return reason;
|
|
14
|
+
const message = typeof reason === "string" && reason.trim() ? reason.trim() : "operation aborted";
|
|
15
|
+
const error = new Error(message);
|
|
16
|
+
error.name = "AbortError";
|
|
17
|
+
return error;
|
|
35
18
|
}
|
|
36
19
|
function normalizeThreadOptions(options, model) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
20
|
+
return {
|
|
21
|
+
...options,
|
|
22
|
+
skipGitRepoCheck: options?.skipGitRepoCheck ?? true,
|
|
23
|
+
...model ? { model } : {}
|
|
24
|
+
};
|
|
42
25
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.config = config;
|
|
46
|
-
this.threadId = config.threadId?.trim() || null;
|
|
47
|
-
this.sessionMetadata = {
|
|
48
|
-
...config.sessionMetadata ? structuredClone(config.sessionMetadata) : {}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
codexPromise = null;
|
|
52
|
-
thread = null;
|
|
53
|
-
threadId;
|
|
54
|
-
sessionMetadata;
|
|
55
|
-
async *run(input, options) {
|
|
56
|
-
const messageId = createId("codex-message");
|
|
57
|
-
const runId = createId("codex-run");
|
|
58
|
-
const itemTextById = /* @__PURE__ */ new Map();
|
|
59
|
-
const toolStateById = /* @__PURE__ */ new Map();
|
|
60
|
-
let finished = false;
|
|
61
|
-
yield* this.emitEvent({
|
|
62
|
-
type: NcpEventType.RunStarted,
|
|
63
|
-
payload: {
|
|
64
|
-
sessionId: input.sessionId,
|
|
65
|
-
messageId,
|
|
66
|
-
runId
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
yield* this.emitEvent({
|
|
70
|
-
type: NcpEventType.RunMetadata,
|
|
71
|
-
payload: {
|
|
72
|
-
sessionId: input.sessionId,
|
|
73
|
-
messageId,
|
|
74
|
-
runId,
|
|
75
|
-
metadata: {
|
|
76
|
-
kind: "ready",
|
|
77
|
-
runId,
|
|
78
|
-
sessionId: input.sessionId,
|
|
79
|
-
supportsAbort: true
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
const thread = await this.resolveThread();
|
|
84
|
-
const turnInput = await this.buildTurnInput(input);
|
|
85
|
-
const streamed = await thread.runStreamed(turnInput, {
|
|
86
|
-
...options?.signal ? { signal: options.signal } : {}
|
|
87
|
-
});
|
|
88
|
-
try {
|
|
89
|
-
for await (const event of streamed.events) {
|
|
90
|
-
if (options?.signal?.aborted) {
|
|
91
|
-
throw toAbortError(options.signal.reason);
|
|
92
|
-
}
|
|
93
|
-
if (event.type === "thread.started") {
|
|
94
|
-
this.updateThreadId(event.thread_id);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (event.type === "turn.failed") {
|
|
98
|
-
yield* this.emitEvent({
|
|
99
|
-
type: NcpEventType.RunError,
|
|
100
|
-
payload: {
|
|
101
|
-
sessionId: input.sessionId,
|
|
102
|
-
messageId,
|
|
103
|
-
runId,
|
|
104
|
-
error: event.error.message
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
finished = true;
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (event.type === "error") {
|
|
111
|
-
yield* this.emitEvent({
|
|
112
|
-
type: NcpEventType.RunError,
|
|
113
|
-
payload: {
|
|
114
|
-
sessionId: input.sessionId,
|
|
115
|
-
messageId,
|
|
116
|
-
runId,
|
|
117
|
-
error: event.message
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
finished = true;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
|
|
124
|
-
for await (const mappedEvent of mapCodexItemEvent({
|
|
125
|
-
sessionId: input.sessionId,
|
|
126
|
-
messageId,
|
|
127
|
-
event,
|
|
128
|
-
itemTextById,
|
|
129
|
-
toolStateById
|
|
130
|
-
})) {
|
|
131
|
-
yield* this.emitEvent(mappedEvent);
|
|
132
|
-
}
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (event.type === "turn.completed") {
|
|
136
|
-
yield* this.emitEvent({
|
|
137
|
-
type: NcpEventType.RunMetadata,
|
|
138
|
-
payload: {
|
|
139
|
-
sessionId: input.sessionId,
|
|
140
|
-
messageId,
|
|
141
|
-
runId,
|
|
142
|
-
metadata: {
|
|
143
|
-
kind: "final",
|
|
144
|
-
sessionId: input.sessionId
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
yield* this.emitEvent({
|
|
149
|
-
type: NcpEventType.RunFinished,
|
|
150
|
-
payload: {
|
|
151
|
-
sessionId: input.sessionId,
|
|
152
|
-
messageId,
|
|
153
|
-
runId
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
finished = true;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (!finished) {
|
|
161
|
-
yield* this.emitEvent({
|
|
162
|
-
type: NcpEventType.RunMetadata,
|
|
163
|
-
payload: {
|
|
164
|
-
sessionId: input.sessionId,
|
|
165
|
-
messageId,
|
|
166
|
-
runId,
|
|
167
|
-
metadata: {
|
|
168
|
-
kind: "final",
|
|
169
|
-
sessionId: input.sessionId
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
yield* this.emitEvent({
|
|
174
|
-
type: NcpEventType.RunFinished,
|
|
175
|
-
payload: {
|
|
176
|
-
sessionId: input.sessionId,
|
|
177
|
-
messageId,
|
|
178
|
-
runId
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
} catch (error) {
|
|
183
|
-
if (options?.signal?.aborted) {
|
|
184
|
-
throw toAbortError(options.signal.reason);
|
|
185
|
-
}
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async getCodex() {
|
|
190
|
-
if (!this.codexPromise) {
|
|
191
|
-
const env = buildCodexCliEnv(this.config);
|
|
192
|
-
this.codexPromise = codexLoader.loadCodexConstructor().then(
|
|
193
|
-
(Ctor) => new Ctor({
|
|
194
|
-
apiKey: this.config.apiKey,
|
|
195
|
-
baseUrl: this.config.apiBase,
|
|
196
|
-
...this.config.codexPathOverride ? { codexPathOverride: this.config.codexPathOverride } : {},
|
|
197
|
-
...env ? { env } : {},
|
|
198
|
-
...this.config.cliConfig ? { config: this.config.cliConfig } : {}
|
|
199
|
-
})
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
return this.codexPromise;
|
|
203
|
-
}
|
|
204
|
-
async resolveThread() {
|
|
205
|
-
if (this.thread) {
|
|
206
|
-
return this.thread;
|
|
207
|
-
}
|
|
208
|
-
const codex = await this.getCodex();
|
|
209
|
-
const threadOptions = normalizeThreadOptions(this.config.threadOptions, this.config.model);
|
|
210
|
-
this.thread = this.threadId ? codex.resumeThread(this.threadId, threadOptions) : codex.startThread(threadOptions);
|
|
211
|
-
return this.thread;
|
|
212
|
-
}
|
|
213
|
-
async buildTurnInput(input) {
|
|
214
|
-
if (this.config.inputBuilder) {
|
|
215
|
-
return await this.config.inputBuilder(input);
|
|
216
|
-
}
|
|
217
|
-
return readUserText(input);
|
|
218
|
-
}
|
|
219
|
-
async *emitEvent(event) {
|
|
220
|
-
await this.config.stateManager?.dispatch(event);
|
|
221
|
-
yield event;
|
|
222
|
-
}
|
|
223
|
-
updateThreadId(nextThreadId) {
|
|
224
|
-
const normalizedThreadId = nextThreadId.trim();
|
|
225
|
-
if (!normalizedThreadId || normalizedThreadId === this.threadId) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
this.threadId = normalizedThreadId;
|
|
229
|
-
const nextMetadata = {
|
|
230
|
-
...this.sessionMetadata,
|
|
231
|
-
session_type: "codex",
|
|
232
|
-
codex_thread_id: normalizedThreadId
|
|
233
|
-
};
|
|
234
|
-
this.sessionMetadata.codex_thread_id = normalizedThreadId;
|
|
235
|
-
this.sessionMetadata.session_type = "codex";
|
|
236
|
-
this.config.setSessionMetadata?.(nextMetadata);
|
|
237
|
-
}
|
|
26
|
+
function isItemLifecycleEvent(event) {
|
|
27
|
+
return event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed";
|
|
238
28
|
}
|
|
239
|
-
|
|
240
|
-
|
|
29
|
+
var CodexSdkNcpAgentRuntime = class {
|
|
30
|
+
codexPromise = null;
|
|
31
|
+
thread = null;
|
|
32
|
+
threadId;
|
|
33
|
+
sessionMetadata;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.threadId = config.threadId?.trim() || null;
|
|
37
|
+
this.sessionMetadata = { ...config.sessionMetadata ? structuredClone(config.sessionMetadata) : {} };
|
|
38
|
+
}
|
|
39
|
+
run = async function* (input, options) {
|
|
40
|
+
const messageId = createId("codex-message");
|
|
41
|
+
const runId = createId("codex-run");
|
|
42
|
+
const itemTextById = /* @__PURE__ */ new Map();
|
|
43
|
+
const toolStateById = /* @__PURE__ */ new Map();
|
|
44
|
+
yield* this.emitRunStarted(input.sessionId, messageId, runId);
|
|
45
|
+
yield* this.emitReadyMetadata(input.sessionId, messageId, runId);
|
|
46
|
+
const thread = await this.resolveThread();
|
|
47
|
+
const turnInput = await this.buildTurnInput(input);
|
|
48
|
+
const streamed = await thread.runStreamed(turnInput, { ...options?.signal ? { signal: options.signal } : {} });
|
|
49
|
+
try {
|
|
50
|
+
yield* this.streamTurnEvents({
|
|
51
|
+
sessionId: input.sessionId,
|
|
52
|
+
messageId,
|
|
53
|
+
runId,
|
|
54
|
+
streamed,
|
|
55
|
+
signal: options?.signal,
|
|
56
|
+
itemTextById,
|
|
57
|
+
toolStateById
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (options?.signal?.aborted) throw toAbortError(options.signal.reason);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
getCodex = async () => {
|
|
65
|
+
if (!this.codexPromise) {
|
|
66
|
+
const env = buildCodexCliEnv(this.config);
|
|
67
|
+
this.codexPromise = codexLoader.loadCodexConstructor().then((Ctor) => new Ctor({
|
|
68
|
+
apiKey: this.config.apiKey,
|
|
69
|
+
baseUrl: this.config.apiBase,
|
|
70
|
+
...this.config.codexPathOverride ? { codexPathOverride: this.config.codexPathOverride } : {},
|
|
71
|
+
...env ? { env } : {},
|
|
72
|
+
...this.config.cliConfig ? { config: this.config.cliConfig } : {}
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
return this.codexPromise;
|
|
76
|
+
};
|
|
77
|
+
resolveThread = async () => {
|
|
78
|
+
if (this.thread) return this.thread;
|
|
79
|
+
const codex = await this.getCodex();
|
|
80
|
+
const threadOptions = normalizeThreadOptions(this.config.threadOptions, this.config.model);
|
|
81
|
+
this.thread = this.threadId ? codex.resumeThread(this.threadId, threadOptions) : codex.startThread(threadOptions);
|
|
82
|
+
return this.thread;
|
|
83
|
+
};
|
|
84
|
+
buildTurnInput = async (input) => {
|
|
85
|
+
if (this.config.inputBuilder) return await this.config.inputBuilder(input);
|
|
86
|
+
return await buildCodexTurnInputFromRunInput(input, { resolveAssetContentPath: this.config.resolveAssetContentPath });
|
|
87
|
+
};
|
|
88
|
+
emitEvent = async function* (event) {
|
|
89
|
+
await this.config.stateManager?.dispatch(event);
|
|
90
|
+
yield event;
|
|
91
|
+
};
|
|
92
|
+
emitRunStarted = async function* (sessionId, messageId, runId) {
|
|
93
|
+
yield* this.emitEvent({
|
|
94
|
+
type: NcpEventType.RunStarted,
|
|
95
|
+
payload: {
|
|
96
|
+
sessionId,
|
|
97
|
+
messageId,
|
|
98
|
+
runId
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
emitReadyMetadata = async function* (sessionId, messageId, runId) {
|
|
103
|
+
yield* this.emitEvent({
|
|
104
|
+
type: NcpEventType.RunMetadata,
|
|
105
|
+
payload: {
|
|
106
|
+
sessionId,
|
|
107
|
+
messageId,
|
|
108
|
+
runId,
|
|
109
|
+
metadata: {
|
|
110
|
+
kind: "ready",
|
|
111
|
+
runId,
|
|
112
|
+
sessionId,
|
|
113
|
+
supportsAbort: true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
emitRunError = async function* (sessionId, messageId, runId, error) {
|
|
119
|
+
yield* this.emitEvent({
|
|
120
|
+
type: NcpEventType.RunError,
|
|
121
|
+
payload: {
|
|
122
|
+
sessionId,
|
|
123
|
+
messageId,
|
|
124
|
+
runId,
|
|
125
|
+
error
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
emitRunCompleted = async function* (sessionId, messageId, runId) {
|
|
130
|
+
yield* this.emitEvent({
|
|
131
|
+
type: NcpEventType.RunMetadata,
|
|
132
|
+
payload: {
|
|
133
|
+
sessionId,
|
|
134
|
+
messageId,
|
|
135
|
+
runId,
|
|
136
|
+
metadata: {
|
|
137
|
+
kind: "final",
|
|
138
|
+
sessionId
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
yield* this.emitEvent({
|
|
143
|
+
type: NcpEventType.MessageCompleted,
|
|
144
|
+
payload: {
|
|
145
|
+
sessionId,
|
|
146
|
+
message: buildCompletedAssistantMessage({
|
|
147
|
+
stateManager: this.config.stateManager,
|
|
148
|
+
sessionId,
|
|
149
|
+
messageId
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
yield* this.emitEvent({
|
|
154
|
+
type: NcpEventType.RunFinished,
|
|
155
|
+
payload: {
|
|
156
|
+
sessionId,
|
|
157
|
+
messageId,
|
|
158
|
+
runId
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
streamTurnEvents = async function* (params) {
|
|
163
|
+
let finished = false;
|
|
164
|
+
for await (const event of params.streamed.events) if (yield* this.handleThreadEvent({
|
|
165
|
+
sessionId: params.sessionId,
|
|
166
|
+
messageId: params.messageId,
|
|
167
|
+
runId: params.runId,
|
|
168
|
+
event,
|
|
169
|
+
signal: params.signal,
|
|
170
|
+
itemTextById: params.itemTextById,
|
|
171
|
+
toolStateById: params.toolStateById
|
|
172
|
+
})) {
|
|
173
|
+
finished = true;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!finished) yield* this.emitRunCompleted(params.sessionId, params.messageId, params.runId);
|
|
177
|
+
};
|
|
178
|
+
handleThreadEvent = async function* (params) {
|
|
179
|
+
if (params.signal?.aborted) throw toAbortError(params.signal.reason);
|
|
180
|
+
if (params.event.type === "thread.started") {
|
|
181
|
+
this.updateThreadId(params.event.thread_id);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (params.event.type === "turn.failed") {
|
|
185
|
+
yield* this.emitRunError(params.sessionId, params.messageId, params.runId, params.event.error.message);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (params.event.type === "error") {
|
|
189
|
+
yield* this.emitRunError(params.sessionId, params.messageId, params.runId, params.event.message);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (isItemLifecycleEvent(params.event)) {
|
|
193
|
+
for await (const mappedEvent of mapCodexItemEvent({
|
|
194
|
+
sessionId: params.sessionId,
|
|
195
|
+
messageId: params.messageId,
|
|
196
|
+
event: params.event,
|
|
197
|
+
itemTextById: params.itemTextById,
|
|
198
|
+
toolStateById: params.toolStateById
|
|
199
|
+
})) yield* this.emitEvent(mappedEvent);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (params.event.type === "turn.completed") {
|
|
203
|
+
yield* this.emitRunCompleted(params.sessionId, params.messageId, params.runId);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
};
|
|
208
|
+
updateThreadId = (nextThreadId) => {
|
|
209
|
+
const normalizedThreadId = nextThreadId.trim();
|
|
210
|
+
if (!normalizedThreadId || normalizedThreadId === this.threadId) return;
|
|
211
|
+
this.threadId = normalizedThreadId;
|
|
212
|
+
const nextMetadata = {
|
|
213
|
+
...this.sessionMetadata,
|
|
214
|
+
session_type: "codex",
|
|
215
|
+
codex_thread_id: normalizedThreadId
|
|
216
|
+
};
|
|
217
|
+
this.sessionMetadata.codex_thread_id = normalizedThreadId;
|
|
218
|
+
this.sessionMetadata.session_type = "codex";
|
|
219
|
+
this.config.setSessionMetadata?.(nextMetadata);
|
|
220
|
+
};
|
|
241
221
|
};
|
|
222
|
+
//#endregion
|
|
223
|
+
export { CodexSdkNcpAgentRuntime, buildCodexInputBuilder };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/nextclaw-ncp-runtime-codex-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Optional NCP runtime adapter backed by OpenAI Codex SDK.",
|
|
6
6
|
"type": "module",
|
|
@@ -17,15 +17,14 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@openai/codex-sdk": "^0.107.0",
|
|
20
|
-
"@nextclaw/ncp": "0.
|
|
20
|
+
"@nextclaw/ncp": "0.5.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^20.17.6",
|
|
24
|
-
"tsup": "^8.3.5",
|
|
25
24
|
"typescript": "^5.6.3"
|
|
26
25
|
},
|
|
27
26
|
"scripts": {
|
|
28
|
-
"build": "
|
|
27
|
+
"build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
|
|
29
28
|
"lint": "eslint .",
|
|
30
29
|
"tsc": "tsc -p tsconfig.json"
|
|
31
30
|
}
|
package/dist/codex-cli-env.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { ThreadEvent } from '@openai/codex-sdk';
|
|
2
|
-
import { NcpEndpointEvent } from '@nextclaw/ncp';
|
|
3
|
-
|
|
4
|
-
type ItemTextSnapshot = {
|
|
5
|
-
text: string;
|
|
6
|
-
started: boolean;
|
|
7
|
-
};
|
|
8
|
-
type ToolSnapshot = {
|
|
9
|
-
started: boolean;
|
|
10
|
-
argsEmitted: boolean;
|
|
11
|
-
ended: boolean;
|
|
12
|
-
};
|
|
13
|
-
declare function mapCodexItemEvent(params: {
|
|
14
|
-
sessionId: string;
|
|
15
|
-
messageId: string;
|
|
16
|
-
event: Extract<ThreadEvent, {
|
|
17
|
-
type: "item.started" | "item.updated" | "item.completed";
|
|
18
|
-
}>;
|
|
19
|
-
itemTextById: Map<string, ItemTextSnapshot>;
|
|
20
|
-
toolStateById: Map<string, ToolSnapshot>;
|
|
21
|
-
}): AsyncGenerator<NcpEndpointEvent>;
|
|
22
|
-
|
|
23
|
-
export { type ItemTextSnapshot, type ToolSnapshot, mapCodexItemEvent };
|