@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.
@@ -1,36 +1,26 @@
1
+ //#region src/codex-cli-env.ts
1
2
  function readString(value) {
2
- if (typeof value !== "string") {
3
- return void 0;
4
- }
5
- return value;
3
+ if (typeof value !== "string") return;
4
+ return value;
6
5
  }
7
6
  function copyProcessEnv() {
8
- const env = {};
9
- for (const [key, value] of Object.entries(process.env)) {
10
- const normalized = readString(value);
11
- if (normalized === void 0) {
12
- continue;
13
- }
14
- env[key] = normalized;
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
- const env = copyProcessEnv();
20
- for (const [key, value] of Object.entries(config.env ?? {})) {
21
- if (typeof value !== "string") {
22
- continue;
23
- }
24
- env[key] = value;
25
- }
26
- if (config.apiKey.trim()) {
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
- export {
35
- buildCodexCliEnv
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
- switch (item.type) {
4
- case "mcp_tool_call":
5
- return {
6
- toolName: item.server ? `mcp:${item.server}.${item.tool}` : `mcp:${item.tool}`,
7
- args: item.arguments
8
- };
9
- case "command_execution":
10
- return {
11
- toolName: "command_execution",
12
- args: {
13
- command: item.command
14
- }
15
- };
16
- case "web_search":
17
- return {
18
- toolName: "web_search",
19
- args: {
20
- query: item.query
21
- }
22
- };
23
- case "file_change":
24
- return {
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
- switch (item.type) {
41
- case "mcp_tool_call":
42
- return item.status === "failed" ? { ok: false, error: item.error ?? { message: "MCP tool call failed." } } : {
43
- ok: item.status === "completed",
44
- status: item.status,
45
- result: item.result ?? null
46
- };
47
- case "command_execution":
48
- return {
49
- status: item.status,
50
- command: item.command,
51
- aggregated_output: item.aggregated_output,
52
- ...typeof item.exit_code === "number" ? { exit_code: item.exit_code } : {}
53
- };
54
- case "web_search":
55
- return {
56
- status: "completed",
57
- query: item.query
58
- };
59
- case "file_change":
60
- return {
61
- status: item.status,
62
- changes: item.changes
63
- };
64
- case "todo_list":
65
- return {
66
- status: "completed",
67
- items: item.items
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
- try {
73
- return JSON.stringify(args ?? {});
74
- } catch {
75
- return JSON.stringify({
76
- __serialization_error__: "tool arguments are not JSON serializable"
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
- return item.type === "mcp_tool_call" || item.type === "command_execution" || item.type === "web_search" || item.type === "file_change" || item.type === "todo_list";
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
- const { sessionId, messageId, event, itemTextById, toolStateById } = params;
85
- const { item } = event;
86
- if (item.type === "agent_message") {
87
- const currentText = item.text ?? "";
88
- const previous2 = itemTextById.get(item.id) ?? { text: "", started: false };
89
- if (!previous2.started) {
90
- yield {
91
- type: NcpEventType.MessageTextStart,
92
- payload: {
93
- sessionId,
94
- messageId
95
- }
96
- };
97
- }
98
- if (currentText.length > previous2.text.length) {
99
- const delta = currentText.slice(previous2.text.length);
100
- yield {
101
- type: NcpEventType.MessageTextDelta,
102
- payload: {
103
- sessionId,
104
- messageId,
105
- delta
106
- }
107
- };
108
- }
109
- itemTextById.set(item.id, {
110
- text: currentText,
111
- started: true
112
- });
113
- if (event.type === "item.completed") {
114
- yield {
115
- type: NcpEventType.MessageTextEnd,
116
- payload: {
117
- sessionId,
118
- messageId
119
- }
120
- };
121
- }
122
- return;
123
- }
124
- if (item.type === "reasoning") {
125
- const currentText = item.text ?? "";
126
- const previous2 = itemTextById.get(item.id) ?? { text: "", started: false };
127
- if (!previous2.started) {
128
- yield {
129
- type: NcpEventType.MessageReasoningStart,
130
- payload: {
131
- sessionId,
132
- messageId
133
- }
134
- };
135
- }
136
- if (currentText.length > previous2.text.length) {
137
- const delta = currentText.slice(previous2.text.length);
138
- yield {
139
- type: NcpEventType.MessageReasoningDelta,
140
- payload: {
141
- sessionId,
142
- messageId,
143
- delta
144
- }
145
- };
146
- }
147
- itemTextById.set(item.id, {
148
- text: currentText,
149
- started: true
150
- });
151
- if (event.type === "item.completed") {
152
- yield {
153
- type: NcpEventType.MessageReasoningEnd,
154
- payload: {
155
- sessionId,
156
- messageId
157
- }
158
- };
159
- }
160
- return;
161
- }
162
- if (!isToolLikeItem(item)) {
163
- return;
164
- }
165
- const previous = toolStateById.get(item.id) ?? {
166
- started: false,
167
- argsEmitted: false,
168
- ended: false
169
- };
170
- const descriptor = buildToolDescriptor(item);
171
- if (!previous.started) {
172
- yield {
173
- type: NcpEventType.MessageToolCallStart,
174
- payload: {
175
- sessionId,
176
- messageId,
177
- toolCallId: item.id,
178
- toolName: descriptor.toolName
179
- }
180
- };
181
- previous.started = true;
182
- }
183
- if (!previous.argsEmitted) {
184
- yield {
185
- type: NcpEventType.MessageToolCallArgs,
186
- payload: {
187
- sessionId,
188
- toolCallId: item.id,
189
- args: stringifyToolArgs(descriptor.args)
190
- }
191
- };
192
- previous.argsEmitted = true;
193
- }
194
- if (!previous.ended) {
195
- yield {
196
- type: NcpEventType.MessageToolCallEnd,
197
- payload: {
198
- sessionId,
199
- toolCallId: item.id
200
- }
201
- };
202
- previous.ended = true;
203
- }
204
- if (event.type === "item.updated" || event.type === "item.completed") {
205
- yield {
206
- type: NcpEventType.MessageToolCallResult,
207
- payload: {
208
- sessionId,
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
- export {
217
- mapCodexItemEvent
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 { CodexOptions, ThreadOptions } from '@openai/codex-sdk';
2
- import { NcpAgentRunInput, NcpAgentConversationStateManager, NcpAgentRuntime, NcpAgentRunOptions, NcpEndpointEvent } from '@nextclaw/ncp';
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
- sessionId: string;
6
- apiKey: string;
7
- apiBase?: string;
8
- model?: string;
9
- threadId?: string | null;
10
- codexPathOverride?: string;
11
- env?: Record<string, string>;
12
- cliConfig?: CodexOptions["config"];
13
- threadOptions?: ThreadOptions;
14
- sessionMetadata?: Record<string, unknown>;
15
- setSessionMetadata?: (nextMetadata: Record<string, unknown>) => void;
16
- inputBuilder?: (input: NcpAgentRunInput) => Promise<string> | string;
17
- stateManager?: NcpAgentConversationStateManager;
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
- private readonly config;
21
- private codexPromise;
22
- private thread;
23
- private threadId;
24
- private readonly sessionMetadata;
25
- constructor(config: CodexSdkNcpAgentRuntimeConfig);
26
- run(input: NcpAgentRunInput, options?: NcpAgentRunOptions): AsyncGenerator<NcpEndpointEvent>;
27
- private getCodex;
28
- private resolveThread;
29
- private buildTurnInput;
30
- private emitEvent;
31
- private updateThreadId;
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, type CodexSdkNcpAgentRuntimeConfig };
42
+ //#endregion
43
+ export { type CodexAssetContentPathResolver, CodexSdkNcpAgentRuntime, CodexSdkNcpAgentRuntimeConfig, buildCodexInputBuilder };
package/dist/index.js CHANGED
@@ -1,241 +1,223 @@
1
- import { createRequire } from "node:module";
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
- const require2 = createRequire(import.meta.url);
10
- const codexLoader = require2("../codex-sdk-loader.cjs");
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
- return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
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
- if (reason instanceof Error) {
29
- return reason;
30
- }
31
- const message = typeof reason === "string" && reason.trim() ? reason.trim() : "operation aborted";
32
- const error = new Error(message);
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
- return {
38
- ...options,
39
- skipGitRepoCheck: options?.skipGitRepoCheck ?? true,
40
- ...model ? { model } : {}
41
- };
20
+ return {
21
+ ...options,
22
+ skipGitRepoCheck: options?.skipGitRepoCheck ?? true,
23
+ ...model ? { model } : {}
24
+ };
42
25
  }
43
- class CodexSdkNcpAgentRuntime {
44
- constructor(config) {
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
- export {
240
- CodexSdkNcpAgentRuntime
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.14",
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.4.6"
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": "tsup --config tsup.config.ts",
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
  }
@@ -1,7 +0,0 @@
1
- import { CodexSdkNcpAgentRuntimeConfig } from './index.js';
2
- import '@openai/codex-sdk';
3
- import '@nextclaw/ncp';
4
-
5
- declare function buildCodexCliEnv(config: CodexSdkNcpAgentRuntimeConfig): Record<string, string> | undefined;
6
-
7
- export { buildCodexCliEnv };
@@ -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 };