@janole/ai-sdk-provider-codex-asp 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@janole/ai-sdk-provider-codex-asp` is a [Vercel AI SDK](https://ai-sdk.dev/) v6 custom provider for the Codex App Server Protocol.
4
4
 
5
- Status: POC feature-complete for language model usage. Currently tested with [codex-cli](https://github.com/openai/codex/releases/tag/rust-v0.105.0) 0.105.0.
5
+ Status: POC feature-complete for language model usage. Currently tested with [codex-cli](https://github.com/openai/codex/releases/tag/rust-v0.106.0) 0.106.0.
6
6
 
7
7
  - `LanguageModelV3` provider implementation
8
8
  - Streaming (`streamText`) and non-streaming (`generateText`)
package/dist/index.cjs CHANGED
@@ -274,6 +274,7 @@ var AppServerClient = class {
274
274
  // src/client/transport-persistent.ts
275
275
  var PersistentTransport = class {
276
276
  pool;
277
+ signal;
277
278
  worker = null;
278
279
  pendingInitializeId = null;
279
280
  initializeIntercepted = false;
@@ -282,9 +283,10 @@ var PersistentTransport = class {
282
283
  closeListeners = /* @__PURE__ */ new Set();
283
284
  constructor(settings) {
284
285
  this.pool = settings.pool;
286
+ this.signal = settings.signal;
285
287
  }
286
288
  async connect() {
287
- this.worker = this.pool.acquire();
289
+ this.worker = await this.pool.acquire(stripUndefined({ signal: this.signal }));
288
290
  await this.worker.ensureConnected();
289
291
  }
290
292
  disconnect() {
@@ -724,6 +726,7 @@ var CodexWorker = class {
724
726
  var CodexWorkerPool = class {
725
727
  workers;
726
728
  shutdownCalled = false;
729
+ waiters = [];
727
730
  constructor(settings) {
728
731
  const size = settings.poolSize ?? 1;
729
732
  const idleTimeoutMs = settings.idleTimeoutMs ?? 3e5;
@@ -735,7 +738,7 @@ var CodexWorkerPool = class {
735
738
  })
736
739
  );
737
740
  }
738
- acquire() {
741
+ async acquire(options) {
739
742
  if (this.shutdownCalled) {
740
743
  throw new CodexProviderError("Worker pool has been shut down.");
741
744
  }
@@ -743,20 +746,61 @@ var CodexWorkerPool = class {
743
746
  (w) => w.state === "idle" || w.state === "disconnected"
744
747
  );
745
748
  if (!worker) {
746
- throw new CodexProviderError(
747
- "All workers are busy. Try again later or increase poolSize."
748
- );
749
+ if (options?.signal?.aborted) {
750
+ throw new CodexProviderError("Worker acquisition aborted while waiting.");
751
+ }
752
+ return new Promise((resolve, reject) => {
753
+ const waiter = {
754
+ resolve,
755
+ reject,
756
+ signal: options?.signal,
757
+ abortHandler: void 0
758
+ };
759
+ if (waiter.signal) {
760
+ waiter.abortHandler = () => {
761
+ this.removeWaiter(waiter);
762
+ waiter.reject(new CodexProviderError("Worker acquisition aborted while waiting."));
763
+ };
764
+ waiter.signal.addEventListener("abort", waiter.abortHandler, { once: true });
765
+ }
766
+ this.waiters.push(waiter);
767
+ });
749
768
  }
750
769
  worker.acquire();
751
770
  return worker;
752
771
  }
753
772
  release(worker) {
754
- worker.release();
773
+ const waiter = this.waiters.shift();
774
+ if (waiter) {
775
+ this.clearWaiterAbortHandler(waiter);
776
+ waiter.resolve(worker);
777
+ } else {
778
+ worker.release();
779
+ }
755
780
  }
756
781
  async shutdown() {
757
782
  this.shutdownCalled = true;
783
+ while (this.waiters.length > 0) {
784
+ const waiter = this.waiters.shift();
785
+ this.clearWaiterAbortHandler(waiter);
786
+ waiter.reject(new CodexProviderError("Worker pool has been shut down."));
787
+ }
758
788
  await Promise.all(this.workers.map((w) => w.shutdown()));
759
789
  }
790
+ removeWaiter(target) {
791
+ const index = this.waiters.indexOf(target);
792
+ if (index >= 0) {
793
+ this.waiters.splice(index, 1);
794
+ }
795
+ }
796
+ /** Remove the abort listener so it doesn't fire after the waiter is already served. */
797
+ clearWaiterAbortHandler(waiter) {
798
+ if (!waiter.signal || !waiter.abortHandler) {
799
+ return;
800
+ }
801
+ waiter.signal.removeEventListener("abort", waiter.abortHandler);
802
+ waiter.abortHandler = void 0;
803
+ }
760
804
  };
761
805
 
762
806
  // src/dynamic-tools.ts
@@ -886,7 +930,7 @@ var DynamicToolsDispatcher = class {
886
930
  // package.json
887
931
  var package_default = {
888
932
  name: "@janole/ai-sdk-provider-codex-asp",
889
- version: "0.2.1"};
933
+ version: "0.2.3"};
890
934
 
891
935
  // src/package-info.ts
892
936
  var PACKAGE_NAME = package_default.name;
@@ -932,15 +976,28 @@ function toFinishReason(status) {
932
976
  }
933
977
  }
934
978
  var CodexEventMapper = class {
979
+ options;
935
980
  streamStarted = false;
936
981
  openTextParts = /* @__PURE__ */ new Set();
982
+ textDeltaReceived = /* @__PURE__ */ new Set();
937
983
  openReasoningParts = /* @__PURE__ */ new Set();
938
984
  openToolCalls = /* @__PURE__ */ new Map();
985
+ planSequenceByTurnId = /* @__PURE__ */ new Map();
939
986
  threadId;
940
987
  latestUsage;
988
+ constructor(options) {
989
+ this.options = {
990
+ emitPlanUpdates: options?.emitPlanUpdates ?? true
991
+ };
992
+ }
941
993
  setThreadId(threadId) {
942
994
  this.threadId = threadId;
943
995
  }
996
+ nextPlanSequence(turnId) {
997
+ const next = (this.planSequenceByTurnId.get(turnId) ?? 0) + 1;
998
+ this.planSequenceByTurnId.set(turnId, next);
999
+ return next;
1000
+ }
944
1001
  map(event) {
945
1002
  const parts = [];
946
1003
  const withMeta = (part) => withProviderMetadata(part, this.threadId);
@@ -1020,6 +1077,7 @@ var CodexEventMapper = class {
1020
1077
  parts.push(withMeta({ type: "text-start", id: delta.itemId }));
1021
1078
  }
1022
1079
  parts.push(withMeta({ type: "text-delta", id: delta.itemId, delta: delta.delta }));
1080
+ this.textDeltaReceived.add(delta.itemId);
1023
1081
  break;
1024
1082
  }
1025
1083
  case "item/completed": {
@@ -1028,9 +1086,19 @@ var CodexEventMapper = class {
1028
1086
  if (!item?.id) {
1029
1087
  break;
1030
1088
  }
1031
- if (item.type === "agentMessage" && this.openTextParts.has(item.id)) {
1032
- parts.push(withMeta({ type: "text-end", id: item.id }));
1033
- this.openTextParts.delete(item.id);
1089
+ if (item.type === "agentMessage") {
1090
+ if (!this.textDeltaReceived.has(item.id) && item.text) {
1091
+ pushStreamStart();
1092
+ if (!this.openTextParts.has(item.id)) {
1093
+ this.openTextParts.add(item.id);
1094
+ parts.push(withMeta({ type: "text-start", id: item.id }));
1095
+ }
1096
+ parts.push(withMeta({ type: "text-delta", id: item.id, delta: item.text }));
1097
+ }
1098
+ if (this.openTextParts.has(item.id)) {
1099
+ parts.push(withMeta({ type: "text-end", id: item.id }));
1100
+ this.openTextParts.delete(item.id);
1101
+ }
1034
1102
  } else if (item.type === "commandExecution" && this.openToolCalls.has(item.id)) {
1035
1103
  const tracked = this.openToolCalls.get(item.id);
1036
1104
  const output = item.aggregatedOutput ?? tracked.output;
@@ -1059,6 +1127,64 @@ var CodexEventMapper = class {
1059
1127
  }
1060
1128
  break;
1061
1129
  }
1130
+ case "item/reasoning/summaryPartAdded": {
1131
+ const params = event.params ?? {};
1132
+ if (params.itemId) {
1133
+ pushReasoningDelta(params.itemId, "\n\n");
1134
+ }
1135
+ break;
1136
+ }
1137
+ // codex/event/agent_reasoning mirrors canonical reasoning summary
1138
+ // stream events in current logs. Ignore wrapper to avoid duplicate
1139
+ // reasoning text in consumers.
1140
+ case "codex/event/agent_reasoning":
1141
+ break;
1142
+ // codex/event/agent_reasoning_section_break is the wrapper form of
1143
+ // item/reasoning/summaryPartAdded (identical 1:1). Handled by the
1144
+ // canonical event above — skip the wrapper to avoid double "\n\n".
1145
+ case "codex/event/agent_reasoning_section_break":
1146
+ break;
1147
+ case "turn/plan/updated": {
1148
+ if (!this.options.emitPlanUpdates) {
1149
+ break;
1150
+ }
1151
+ const params = event.params ?? {};
1152
+ const turnId = params.turnId;
1153
+ const plan = params.plan;
1154
+ if (turnId && plan) {
1155
+ pushStreamStart();
1156
+ const planSequence = this.nextPlanSequence(turnId);
1157
+ const toolCallId = `plan:${turnId}:${planSequence}`;
1158
+ const toolName = "codex_plan_update";
1159
+ parts.push(withMeta({
1160
+ type: "tool-call",
1161
+ toolCallId,
1162
+ toolName,
1163
+ input: JSON.stringify({}),
1164
+ providerExecuted: true,
1165
+ dynamic: true
1166
+ }));
1167
+ parts.push(withMeta({
1168
+ type: "tool-result",
1169
+ toolCallId,
1170
+ toolName,
1171
+ result: { plan, explanation: params.explanation ?? void 0 }
1172
+ }));
1173
+ }
1174
+ break;
1175
+ }
1176
+ // codex/event/plan_update is the wrapper form of turn/plan/updated (1:1).
1177
+ case "codex/event/plan_update":
1178
+ break;
1179
+ // NOTE: turn/diff/updated and codex/event/turn_diff are intentionally
1180
+ // NOT mapped. They carry full unified diffs (often 50-100 KB) which,
1181
+ // when emitted as reasoning deltas, crash or freeze the frontend
1182
+ // markdown renderer. If these need to surface in the UI, they should
1183
+ // use a dedicated part type with lazy/collapsed rendering — not
1184
+ // reasoning text.
1185
+ case "turn/diff/updated":
1186
+ case "codex/event/turn_diff":
1187
+ break;
1062
1188
  case "item/commandExecution/outputDelta": {
1063
1189
  const delta = event.params ?? {};
1064
1190
  if (delta.itemId && delta.delta && this.openToolCalls.has(delta.itemId)) {
@@ -1074,6 +1200,43 @@ var CodexEventMapper = class {
1074
1200
  }
1075
1201
  break;
1076
1202
  }
1203
+ case "codex/event/mcp_tool_call_begin": {
1204
+ const params = event.params ?? {};
1205
+ const callId = params.msg?.call_id;
1206
+ const inv = params.msg?.invocation;
1207
+ if (callId && inv) {
1208
+ pushStreamStart();
1209
+ const toolName = `mcp:${inv.server}/${inv.tool}`;
1210
+ this.openToolCalls.set(callId, { toolName, output: "" });
1211
+ parts.push(withMeta({
1212
+ type: "tool-call",
1213
+ toolCallId: callId,
1214
+ toolName,
1215
+ input: JSON.stringify(inv.arguments ?? {}),
1216
+ providerExecuted: true,
1217
+ dynamic: true
1218
+ }));
1219
+ }
1220
+ break;
1221
+ }
1222
+ case "codex/event/mcp_tool_call_end": {
1223
+ const params = event.params ?? {};
1224
+ const callId = params.msg?.call_id;
1225
+ if (callId && this.openToolCalls.has(callId)) {
1226
+ const tracked = this.openToolCalls.get(callId);
1227
+ const result = params.msg?.result;
1228
+ const textParts = result?.Ok?.content?.filter((c) => c.type === "text").map((c) => c.text) ?? [];
1229
+ const output = textParts.join("\n") || (result?.Err ? JSON.stringify(result.Err) : "");
1230
+ parts.push(withMeta({
1231
+ type: "tool-result",
1232
+ toolCallId: callId,
1233
+ toolName: tracked.toolName,
1234
+ result: { output }
1235
+ }));
1236
+ this.openToolCalls.delete(callId);
1237
+ }
1238
+ break;
1239
+ }
1077
1240
  case "item/mcpToolCall/progress": {
1078
1241
  const params = event.params ?? {};
1079
1242
  if (params.itemId && params.message) {
@@ -1143,6 +1306,9 @@ var CodexEventMapper = class {
1143
1306
  }
1144
1307
  this.openToolCalls.clear();
1145
1308
  const completed = event.params ?? {};
1309
+ if (completed.turn?.id) {
1310
+ this.planSequenceByTurnId.delete(completed.turn.id);
1311
+ }
1146
1312
  const usage = this.latestUsage ?? EMPTY_USAGE;
1147
1313
  parts.push(withMeta({ type: "finish", finishReason: toFinishReason(completed.turn?.status), usage }));
1148
1314
  break;
@@ -1433,7 +1599,7 @@ var CodexLanguageModel = class {
1433
1599
  });
1434
1600
  }
1435
1601
  doStream(options) {
1436
- const transport = this.config.providerSettings.transportFactory ? this.config.providerSettings.transportFactory() : this.config.providerSettings.transport?.type === "websocket" ? new WebSocketTransport(this.config.providerSettings.transport.websocket) : new StdioTransport(this.config.providerSettings.transport?.stdio);
1602
+ const transport = this.config.providerSettings.transportFactory ? this.config.providerSettings.transportFactory(options.abortSignal) : this.config.providerSettings.transport?.type === "websocket" ? new WebSocketTransport(this.config.providerSettings.transport.websocket) : new StdioTransport(this.config.providerSettings.transport?.stdio);
1437
1603
  const packetLogger = this.config.providerSettings.debug?.logPackets === true ? this.config.providerSettings.debug.logger ?? ((packet) => {
1438
1604
  if (packet.direction === "inbound") {
1439
1605
  console.debug("[codex packet]", packet.message);
@@ -1448,7 +1614,9 @@ var CodexLanguageModel = class {
1448
1614
  const client = new AppServerClient(transport, stripUndefined({
1449
1615
  onPacket: packetLogger
1450
1616
  }));
1451
- const mapper = new CodexEventMapper();
1617
+ const mapper = new CodexEventMapper(stripUndefined({
1618
+ emitPlanUpdates: this.config.providerSettings.emitPlanUpdates
1619
+ }));
1452
1620
  let activeThreadId;
1453
1621
  let activeTurnId;
1454
1622
  const interruptTimeoutMs = this.config.providerSettings.interruptTimeoutMs ?? 1e4;
@@ -1845,7 +2013,7 @@ function createCodexAppServer(settings = {}) {
1845
2013
  });
1846
2014
  }
1847
2015
  const persistentPool = persistentPoolHandle?.pool ?? null;
1848
- const effectiveTransportFactory = persistentPool ? () => new PersistentTransport({ pool: persistentPool }) : baseTransportFactory;
2016
+ const effectiveTransportFactory = persistentPool ? (signal) => new PersistentTransport(stripUndefined({ pool: persistentPool, signal })) : baseTransportFactory;
1849
2017
  const resolvedSettings = Object.freeze(stripUndefined({
1850
2018
  defaultModel: settings.defaultModel,
1851
2019
  experimentalApi: settings.experimentalApi,