@nextclaw/nextclaw-ncp-runtime-codex-sdk 0.1.29 → 0.1.30

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/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CodexLiveOutputChannel, CodexLiveOutputEvent, CodexLiveOutputStream } from "./services/codex-live-output-stream.service.js";
1
2
  import { CodexAssetContentPathResolver, CodexThreadInput, buildCodexInputBuilder } from "./codex-input.utils.js";
2
3
  import { NcpAgentConversationStateManager, NcpAgentRunInput, NcpAgentRunOptions, NcpAgentRuntime, NcpEndpointEvent } from "@nextclaw/ncp";
3
4
  import { CodexOptions, ThreadOptions } from "@openai/codex-sdk";
@@ -16,12 +17,15 @@ type CodexSdkNcpAgentRuntimeConfig = {
16
17
  sessionMetadata?: Record<string, unknown>;
17
18
  setSessionMetadata?: (nextMetadata: Record<string, unknown>) => void;
18
19
  inputBuilder?: (input: NcpAgentRunInput) => Promise<CodexThreadInput> | CodexThreadInput;
20
+ liveOutputStream?: CodexLiveOutputStream;
19
21
  resolveAssetContentPath?: CodexAssetContentPathResolver;
20
22
  stateManager?: NcpAgentConversationStateManager;
21
23
  };
22
24
  declare class CodexSdkNcpAgentRuntime implements NcpAgentRuntime {
23
25
  private readonly config;
24
26
  private codexPromise;
27
+ private readonly eventEmitter;
28
+ private readonly liveOutputEventMergeService;
25
29
  private thread;
26
30
  private threadId;
27
31
  private readonly sessionMetadata;
@@ -30,14 +34,9 @@ declare class CodexSdkNcpAgentRuntime implements NcpAgentRuntime {
30
34
  private getCodex;
31
35
  private resolveThread;
32
36
  private buildTurnInput;
33
- private emitEvent;
34
- private emitRunStarted;
35
- private emitReadyMetadata;
36
- private emitRunError;
37
- private emitRunCompleted;
38
37
  private streamTurnEvents;
39
38
  private handleThreadEvent;
40
39
  private updateThreadId;
41
40
  }
42
41
  //#endregion
43
- export { type CodexAssetContentPathResolver, CodexSdkNcpAgentRuntime, CodexSdkNcpAgentRuntimeConfig, buildCodexInputBuilder };
42
+ export { type CodexAssetContentPathResolver, type CodexLiveOutputChannel, type CodexLiveOutputEvent, CodexLiveOutputStream, CodexSdkNcpAgentRuntime, CodexSdkNcpAgentRuntimeConfig, buildCodexInputBuilder };
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
- import { buildCompletedAssistantMessage } from "./completed-assistant-message.utils.js";
1
+ import { CodexLiveOutputEventMergeService } from "./services/codex-live-output-event-merge.service.js";
2
+ import { CodexNcpRunEventEmitter } from "./services/codex-ncp-run-event-emitter.service.js";
2
3
  import { mapCodexItemEvent } from "./utils/codex-sdk-ncp-event-mapper.utils.js";
3
4
  import { buildCodexCliEnv } from "./codex-cli-env.js";
4
5
  import { buildCodexInputBuilder, buildCodexTurnInputFromRunInput } from "./codex-input.utils.js";
6
+ import { CodexLiveOutputStream } from "./services/codex-live-output-stream.service.js";
5
7
  import { createRequire } from "node:module";
6
- import { NcpEventType } from "@nextclaw/ncp";
8
+ import "@nextclaw/ncp";
7
9
  //#region src/index.ts
8
10
  const codexLoader = createRequire(import.meta.url)("../codex-sdk-loader.cjs");
9
11
  function createId(prefix) {
@@ -28,36 +30,41 @@ function isItemLifecycleEvent(event) {
28
30
  }
29
31
  var CodexSdkNcpAgentRuntime = class {
30
32
  codexPromise = null;
33
+ eventEmitter;
34
+ liveOutputEventMergeService = new CodexLiveOutputEventMergeService();
31
35
  thread = null;
32
36
  threadId;
33
37
  sessionMetadata;
34
38
  constructor(config) {
35
39
  this.config = config;
40
+ this.eventEmitter = new CodexNcpRunEventEmitter(config.stateManager);
36
41
  this.threadId = config.threadId?.trim() || null;
37
42
  this.sessionMetadata = { ...config.sessionMetadata ? structuredClone(config.sessionMetadata) : {} };
38
43
  }
39
44
  run = async function* (input, options) {
45
+ const signal = options?.signal;
40
46
  const messageId = createId("codex-message");
41
47
  const runId = createId("codex-run");
42
48
  const itemTextById = /* @__PURE__ */ new Map();
43
49
  const toolStateById = /* @__PURE__ */ new Map();
44
- yield* this.emitRunStarted(input.sessionId, messageId, runId);
45
- yield* this.emitReadyMetadata(input.sessionId, messageId, runId);
50
+ yield* this.eventEmitter.emitRunStarted(input.sessionId, messageId, runId);
51
+ yield* this.eventEmitter.emitReadyMetadata(input.sessionId, messageId, runId);
46
52
  const thread = await this.resolveThread();
47
53
  const turnInput = await this.buildTurnInput(input);
48
- const streamed = await thread.runStreamed(turnInput, { ...options?.signal ? { signal: options.signal } : {} });
54
+ this.config.liveOutputStream?.reset();
55
+ const streamed = await thread.runStreamed(turnInput, { ...signal ? { signal } : {} });
49
56
  try {
50
57
  yield* this.streamTurnEvents({
51
58
  sessionId: input.sessionId,
52
59
  messageId,
53
60
  runId,
54
61
  streamed,
55
- signal: options?.signal,
62
+ signal,
56
63
  itemTextById,
57
64
  toolStateById
58
65
  });
59
66
  } catch (error) {
60
- if (options?.signal?.aborted) throw toAbortError(options.signal.reason);
67
+ if (signal?.aborted) throw toAbortError(signal.reason);
61
68
  throw error;
62
69
  }
63
70
  };
@@ -85,122 +92,63 @@ var CodexSdkNcpAgentRuntime = class {
85
92
  if (this.config.inputBuilder) return await this.config.inputBuilder(input);
86
93
  return await buildCodexTurnInputFromRunInput(input, { resolveAssetContentPath: this.config.resolveAssetContentPath });
87
94
  };
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
95
  streamTurnEvents = async function* (params) {
96
+ const { itemTextById, messageId, runId, sessionId, signal, streamed, toolStateById } = params;
97
+ const liveOutputStream = this.config.liveOutputStream;
98
+ if (liveOutputStream) {
99
+ yield* this.liveOutputEventMergeService.stream({
100
+ ...params,
101
+ liveOutputStream,
102
+ emitEvent: (event) => this.eventEmitter.emitEvent(event),
103
+ emitRunCompleted: (sessionId, messageId, runId) => this.eventEmitter.emitRunCompleted(sessionId, messageId, runId),
104
+ handleThreadEvent: (handlerParams) => this.handleThreadEvent(handlerParams)
105
+ });
106
+ return;
107
+ }
163
108
  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,
109
+ for await (const event of streamed.events) if (yield* this.handleThreadEvent({
110
+ sessionId,
111
+ messageId,
112
+ runId,
168
113
  event,
169
- signal: params.signal,
170
- itemTextById: params.itemTextById,
171
- toolStateById: params.toolStateById
114
+ signal,
115
+ itemTextById,
116
+ toolStateById
172
117
  })) {
173
118
  finished = true;
174
119
  return;
175
120
  }
176
- if (!finished) yield* this.emitRunCompleted(params.sessionId, params.messageId, params.runId);
121
+ if (!finished) yield* this.eventEmitter.emitRunCompleted(sessionId, messageId, runId);
177
122
  };
178
123
  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);
124
+ const { event, itemTextById, messageId, runId, sessionId, signal, suppressLiveChannels, toolStateById } = params;
125
+ if (signal?.aborted) throw toAbortError(signal.reason);
126
+ if (event.type === "thread.started") {
127
+ this.updateThreadId(event.thread_id);
182
128
  return false;
183
129
  }
184
- if (params.event.type === "turn.failed") {
185
- yield* this.emitRunError(params.sessionId, params.messageId, params.runId, params.event.error.message);
130
+ if (event.type === "turn.failed") {
131
+ yield* this.eventEmitter.emitRunError(sessionId, messageId, runId, event.error.message);
186
132
  return true;
187
133
  }
188
- if (params.event.type === "error") {
189
- yield* this.emitRunError(params.sessionId, params.messageId, params.runId, params.event.message);
134
+ if (event.type === "error") {
135
+ yield* this.eventEmitter.emitRunError(sessionId, messageId, runId, event.message);
190
136
  return true;
191
137
  }
192
- if (isItemLifecycleEvent(params.event)) {
138
+ if (isItemLifecycleEvent(event)) {
139
+ const suppressedChannel = event.item.type === "agent_message" ? "text" : event.item.type === "reasoning" ? "reasoning" : null;
140
+ if (suppressedChannel && suppressLiveChannels?.has(suppressedChannel)) return false;
193
141
  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);
142
+ sessionId,
143
+ messageId,
144
+ event,
145
+ itemTextById,
146
+ toolStateById
147
+ })) yield* this.eventEmitter.emitEvent(mappedEvent);
200
148
  return false;
201
149
  }
202
- if (params.event.type === "turn.completed") {
203
- yield* this.emitRunCompleted(params.sessionId, params.messageId, params.runId);
150
+ if (event.type === "turn.completed") {
151
+ yield* this.eventEmitter.emitRunCompleted(sessionId, messageId, runId);
204
152
  return true;
205
153
  }
206
154
  return false;
@@ -220,4 +168,4 @@ var CodexSdkNcpAgentRuntime = class {
220
168
  };
221
169
  };
222
170
  //#endregion
223
- export { CodexSdkNcpAgentRuntime, buildCodexInputBuilder };
171
+ export { CodexLiveOutputStream, CodexSdkNcpAgentRuntime, buildCodexInputBuilder };
@@ -0,0 +1,101 @@
1
+ import { NcpEventType } from "@nextclaw/ncp";
2
+ //#region src/services/codex-live-output-event-merge.service.ts
3
+ var CodexLiveOutputEventMergeService = class {
4
+ stream = async function* (params) {
5
+ const { emitEvent, emitRunCompleted, handleThreadEvent, itemTextById, liveOutputStream, messageId, runId, sessionId, signal, streamed, toolStateById } = params;
6
+ const liveChannels = /* @__PURE__ */ new Set();
7
+ const codexIterator = streamed.events[Symbol.asyncIterator]();
8
+ const liveIterator = this.streamLiveOutputEvents({
9
+ emitEvent,
10
+ liveChannels,
11
+ liveOutputStream,
12
+ messageId,
13
+ sessionId,
14
+ signal
15
+ })[Symbol.asyncIterator]();
16
+ let finished = false;
17
+ let codexDone = false;
18
+ let liveDone = false;
19
+ let codexNext = codexIterator.next();
20
+ let liveNext = liveIterator.next();
21
+ while (!codexDone || !liveDone) {
22
+ const pending = [...!codexDone ? [codexNext.then((next) => ({
23
+ source: "codex",
24
+ next
25
+ }))] : [], ...!liveDone ? [liveNext.then((next) => ({
26
+ source: "live",
27
+ next
28
+ }))] : []];
29
+ const result = await Promise.race(pending);
30
+ if (result.source === "live") {
31
+ if (result.next.done) liveDone = true;
32
+ else {
33
+ yield result.next.value;
34
+ liveNext = liveIterator.next();
35
+ }
36
+ continue;
37
+ }
38
+ if (result.next.done) {
39
+ codexDone = true;
40
+ continue;
41
+ }
42
+ if (yield* handleThreadEvent({
43
+ event: result.next.value,
44
+ itemTextById,
45
+ messageId,
46
+ runId,
47
+ sessionId,
48
+ signal,
49
+ suppressLiveChannels: liveChannels,
50
+ toolStateById
51
+ })) {
52
+ finished = true;
53
+ break;
54
+ }
55
+ codexNext = codexIterator.next();
56
+ }
57
+ await liveIterator.return?.(void 0);
58
+ if (!finished) yield* emitRunCompleted(sessionId, messageId, runId);
59
+ };
60
+ streamLiveOutputEvents = async function* (params) {
61
+ const { emitEvent, liveChannels, liveOutputStream, messageId, sessionId, signal } = params;
62
+ const startedChannels = /* @__PURE__ */ new Set();
63
+ for await (const event of liveOutputStream.events(signal)) {
64
+ if (event.type === "delta") {
65
+ liveChannels.add(event.channel);
66
+ if (!startedChannels.has(event.channel)) {
67
+ startedChannels.add(event.channel);
68
+ yield* emitEvent({
69
+ type: event.channel === "reasoning" ? NcpEventType.MessageReasoningStart : NcpEventType.MessageTextStart,
70
+ payload: {
71
+ sessionId,
72
+ messageId
73
+ }
74
+ });
75
+ }
76
+ yield* emitEvent({
77
+ type: event.channel === "reasoning" ? NcpEventType.MessageReasoningDelta : NcpEventType.MessageTextDelta,
78
+ payload: {
79
+ sessionId,
80
+ messageId,
81
+ delta: event.delta
82
+ }
83
+ });
84
+ continue;
85
+ }
86
+ if (event.type === "end") {
87
+ if (startedChannels.has(event.channel)) yield* emitEvent({
88
+ type: event.channel === "reasoning" ? NcpEventType.MessageReasoningEnd : NcpEventType.MessageTextEnd,
89
+ payload: {
90
+ sessionId,
91
+ messageId
92
+ }
93
+ });
94
+ continue;
95
+ }
96
+ return;
97
+ }
98
+ };
99
+ };
100
+ //#endregion
101
+ export { CodexLiveOutputEventMergeService };
@@ -0,0 +1,31 @@
1
+ //#region src/services/codex-live-output-stream.service.d.ts
2
+ type CodexLiveOutputChannel = "reasoning" | "text";
3
+ type CodexLiveOutputEvent = {
4
+ type: "delta";
5
+ channel: CodexLiveOutputChannel;
6
+ delta: string;
7
+ } | {
8
+ type: "end";
9
+ channel: CodexLiveOutputChannel;
10
+ } | {
11
+ type: "done";
12
+ };
13
+ declare class CodexLiveOutputStream {
14
+ private eventsQueue;
15
+ private waiters;
16
+ private done;
17
+ private reasoningEnded;
18
+ private textEnded;
19
+ reset: () => void;
20
+ onReasoningDelta: (delta: string) => void;
21
+ onReasoningDone: () => void;
22
+ onTextDelta: (delta: string) => void;
23
+ onTextDone: () => void;
24
+ onDone: () => void;
25
+ events: (this: CodexLiveOutputStream, signal?: AbortSignal) => AsyncGenerator<CodexLiveOutputEvent>;
26
+ private pushDelta;
27
+ private pushEnd;
28
+ private push;
29
+ }
30
+ //#endregion
31
+ export { CodexLiveOutputChannel, CodexLiveOutputEvent, CodexLiveOutputStream };
@@ -0,0 +1,81 @@
1
+ //#region src/services/codex-live-output-stream.service.ts
2
+ var CodexLiveOutputStream = class {
3
+ eventsQueue = [];
4
+ waiters = [];
5
+ done = false;
6
+ reasoningEnded = false;
7
+ textEnded = false;
8
+ reset = () => {
9
+ this.eventsQueue = [];
10
+ this.done = false;
11
+ this.reasoningEnded = false;
12
+ this.textEnded = false;
13
+ };
14
+ onReasoningDelta = (delta) => {
15
+ this.pushDelta("reasoning", delta);
16
+ };
17
+ onReasoningDone = () => {
18
+ this.pushEnd("reasoning");
19
+ };
20
+ onTextDelta = (delta) => {
21
+ this.pushDelta("text", delta);
22
+ };
23
+ onTextDone = () => {
24
+ this.pushEnd("text");
25
+ };
26
+ onDone = () => {
27
+ if (this.done) return;
28
+ this.done = true;
29
+ this.push({ type: "done" });
30
+ };
31
+ events = async function* (signal) {
32
+ while (!signal?.aborted) {
33
+ const event = this.eventsQueue.shift();
34
+ if (event) {
35
+ yield event;
36
+ if (event.type === "done") return;
37
+ continue;
38
+ }
39
+ await new Promise((resolve) => {
40
+ const abort = () => {
41
+ signal?.removeEventListener("abort", abort);
42
+ resolve();
43
+ };
44
+ this.waiters.push(() => {
45
+ signal?.removeEventListener("abort", abort);
46
+ resolve();
47
+ });
48
+ signal?.addEventListener("abort", abort, { once: true });
49
+ });
50
+ }
51
+ };
52
+ pushDelta = (channel, delta) => {
53
+ if (!delta || this.done) return;
54
+ this.push({
55
+ type: "delta",
56
+ channel,
57
+ delta
58
+ });
59
+ };
60
+ pushEnd = (channel) => {
61
+ if (this.done) return;
62
+ if (channel === "reasoning") {
63
+ if (this.reasoningEnded) return;
64
+ this.reasoningEnded = true;
65
+ } else {
66
+ if (this.textEnded) return;
67
+ this.textEnded = true;
68
+ }
69
+ this.push({
70
+ type: "end",
71
+ channel
72
+ });
73
+ };
74
+ push = (event) => {
75
+ this.eventsQueue.push(event);
76
+ const waiters = this.waiters.splice(0);
77
+ for (const waiter of waiters) waiter();
78
+ };
79
+ };
80
+ //#endregion
81
+ export { CodexLiveOutputStream };
@@ -0,0 +1,84 @@
1
+ import { buildCompletedAssistantMessage } from "../completed-assistant-message.utils.js";
2
+ import { NcpEventType } from "@nextclaw/ncp";
3
+ //#region src/services/codex-ncp-run-event-emitter.service.ts
4
+ var CodexNcpRunEventEmitter = class {
5
+ constructor(stateManager) {
6
+ this.stateManager = stateManager;
7
+ }
8
+ emitEvent = async function* (event) {
9
+ await this.stateManager?.dispatch(event);
10
+ yield event;
11
+ };
12
+ emitRunStarted = async function* (sessionId, messageId, runId) {
13
+ yield* this.emitEvent({
14
+ type: NcpEventType.RunStarted,
15
+ payload: {
16
+ sessionId,
17
+ messageId,
18
+ runId
19
+ }
20
+ });
21
+ };
22
+ emitReadyMetadata = async function* (sessionId, messageId, runId) {
23
+ yield* this.emitEvent({
24
+ type: NcpEventType.RunMetadata,
25
+ payload: {
26
+ sessionId,
27
+ messageId,
28
+ runId,
29
+ metadata: {
30
+ kind: "ready",
31
+ runId,
32
+ sessionId,
33
+ supportsAbort: true
34
+ }
35
+ }
36
+ });
37
+ };
38
+ emitRunError = async function* (sessionId, messageId, runId, error) {
39
+ yield* this.emitEvent({
40
+ type: NcpEventType.RunError,
41
+ payload: {
42
+ sessionId,
43
+ messageId,
44
+ runId,
45
+ error
46
+ }
47
+ });
48
+ };
49
+ emitRunCompleted = async function* (sessionId, messageId, runId) {
50
+ yield* this.emitEvent({
51
+ type: NcpEventType.RunMetadata,
52
+ payload: {
53
+ sessionId,
54
+ messageId,
55
+ runId,
56
+ metadata: {
57
+ kind: "final",
58
+ sessionId
59
+ }
60
+ }
61
+ });
62
+ yield* this.emitEvent({
63
+ type: NcpEventType.MessageCompleted,
64
+ payload: {
65
+ sessionId,
66
+ message: buildCompletedAssistantMessage({
67
+ stateManager: this.stateManager,
68
+ sessionId,
69
+ messageId
70
+ })
71
+ }
72
+ });
73
+ yield* this.emitEvent({
74
+ type: NcpEventType.RunFinished,
75
+ payload: {
76
+ sessionId,
77
+ messageId,
78
+ runId
79
+ }
80
+ });
81
+ };
82
+ };
83
+ //#endregion
84
+ export { CodexNcpRunEventEmitter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/nextclaw-ncp-runtime-codex-sdk",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "private": false,
5
5
  "description": "Optional NCP runtime adapter backed by OpenAI Codex SDK.",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "scripts": {
28
28
  "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
29
29
  "lint": "eslint .",
30
- "test": "vitest run src/utils/codex-sdk-ncp-event-mapper.utils.test.ts",
30
+ "test": "vitest run src/utils/codex-sdk-ncp-event-mapper.utils.test.ts src/services/codex-live-output-stream.service.test.ts",
31
31
  "tsc": "tsc -p tsconfig.json"
32
32
  }
33
33
  }