@nyssance/codex-acp-v2 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +410 -117
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -3190,13 +3190,13 @@ var require_ril = __commonJS(function(exports) {
3190
3190
  var util_1 = __require("util");
3191
3191
  var api_1 = require_api();
3192
3192
 
3193
- class MessageBuffer extends api_1.AbstractMessageBuffer {
3193
+ class MessageBuffer2 extends api_1.AbstractMessageBuffer {
3194
3194
  static emptyBuffer = Buffer.allocUnsafe(0);
3195
3195
  constructor(encoding = "utf-8") {
3196
3196
  super(encoding);
3197
3197
  }
3198
3198
  emptyBuffer() {
3199
- return MessageBuffer.emptyBuffer;
3199
+ return MessageBuffer2.emptyBuffer;
3200
3200
  }
3201
3201
  fromString(value, encoding) {
3202
3202
  return Buffer.from(value, encoding);
@@ -3282,7 +3282,7 @@ var require_ril = __commonJS(function(exports) {
3282
3282
  }
3283
3283
  var _ril = Object.freeze({
3284
3284
  messageBuffer: Object.freeze({
3285
- create: (encoding) => new MessageBuffer(encoding)
3285
+ create: (encoding) => new MessageBuffer2(encoding)
3286
3286
  }),
3287
3287
  applicationJson: Object.freeze({
3288
3288
  encoder: Object.freeze({
@@ -15886,7 +15886,9 @@ var zSetSessionConfigOptionResponse = object({
15886
15886
  return;
15887
15887
  })
15888
15888
  });
15889
+ var zMessageId = string2();
15889
15890
  var zPromptResponse = object({
15891
+ messageId: zMessageId,
15890
15892
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
15891
15893
  return;
15892
15894
  })
@@ -16044,7 +16046,6 @@ var zAgentResponse = union([
16044
16046
  error: zError
16045
16047
  })
16046
16048
  ]);
16047
- var zMessageId = string2();
16048
16049
  var zContentChunk = object({
16049
16050
  messageId: zMessageId,
16050
16051
  content: zContentBlock,
@@ -16324,6 +16325,22 @@ var zUsageUpdate = object({
16324
16325
  return;
16325
16326
  })
16326
16327
  });
16328
+ var zNoticeSeverity = union([
16329
+ literal("info"),
16330
+ literal("warning"),
16331
+ literal("error"),
16332
+ string2()
16333
+ ]);
16334
+ var zNotice = object({
16335
+ severity: zNoticeSeverity,
16336
+ title: string2().min(1),
16337
+ description: defaultOnError(string2().nullish(), () => {
16338
+ return;
16339
+ }),
16340
+ _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
16341
+ return;
16342
+ })
16343
+ });
16327
16344
  var zCompactionId = string2();
16328
16345
  var zCompactionStatus = union([
16329
16346
  literal("in_progress"),
@@ -16404,6 +16421,9 @@ var zSessionUpdate = preserveCustomPayload(union([
16404
16421
  zUsageUpdate.and(object({
16405
16422
  sessionUpdate: literal("usage_update")
16406
16423
  })),
16424
+ zNotice.and(object({
16425
+ sessionUpdate: literal("notice")
16426
+ })),
16407
16427
  zCompactionUpdate.and(object({
16408
16428
  sessionUpdate: literal("compaction_update")
16409
16429
  })),
@@ -16421,6 +16441,7 @@ var zSessionUpdate = preserveCustomPayload(union([
16421
16441
  "compaction_summary_chunk",
16422
16442
  "compaction_update",
16423
16443
  "config_option_update",
16444
+ "notice",
16424
16445
  "plan_removed",
16425
16446
  "plan_update",
16426
16447
  "session_info_update",
@@ -16442,6 +16463,7 @@ var zSessionUpdate = preserveCustomPayload(union([
16442
16463
  "compaction_summary_chunk",
16443
16464
  "compaction_update",
16444
16465
  "config_option_update",
16466
+ "notice",
16445
16467
  "plan_removed",
16446
16468
  "plan_update",
16447
16469
  "session_info_update",
@@ -17148,6 +17170,7 @@ var zGuardSessionUpdateAvailableCommandsUpdate = zAvailableCommandsUpdate.and(ob
17148
17170
  var zGuardSessionUpdateConfigOptionUpdate = zConfigOptionUpdate.and(object({ sessionUpdate: literal("config_option_update") }));
17149
17171
  var zGuardSessionUpdateSessionInfoUpdate = zSessionInfoUpdate.and(object({ sessionUpdate: literal("session_info_update") }));
17150
17172
  var zGuardSessionUpdateUsageUpdate = zUsageUpdate.and(object({ sessionUpdate: literal("usage_update") }));
17173
+ var zGuardSessionUpdateNotice = zNotice.and(object({ sessionUpdate: literal("notice") }));
17151
17174
  var zGuardSessionUpdateCompactionUpdate = zCompactionUpdate.and(object({ sessionUpdate: literal("compaction_update") }));
17152
17175
  var zGuardSessionUpdateCompactionSummaryChunk = zCompactionSummaryChunk.and(object({ sessionUpdate: literal("compaction_summary_chunk") }));
17153
17176
  var zGuardStateUpdateRunning = zRunningStateUpdate.and(object({ state: literal("running") }));
@@ -17258,6 +17281,9 @@ var SessionUpdate = {
17258
17281
  isUsageUpdate(value) {
17259
17282
  return tagOf(value, "sessionUpdate") === "usage_update" && zGuardSessionUpdateUsageUpdate.safeParse(value).success;
17260
17283
  },
17284
+ isNotice(value) {
17285
+ return tagOf(value, "sessionUpdate") === "notice" && zGuardSessionUpdateNotice.safeParse(value).success;
17286
+ },
17261
17287
  isCompactionUpdate(value) {
17262
17288
  return tagOf(value, "sessionUpdate") === "compaction_update" && zGuardSessionUpdateCompactionUpdate.safeParse(value).success;
17263
17289
  },
@@ -17275,6 +17301,7 @@ var SessionUpdate = {
17275
17301
  "compaction_summary_chunk",
17276
17302
  "compaction_update",
17277
17303
  "config_option_update",
17304
+ "notice",
17278
17305
  "plan_removed",
17279
17306
  "plan_update",
17280
17307
  "session_info_update",
@@ -18106,53 +18133,124 @@ function protocolErrorResponse(error40) {
18106
18133
  };
18107
18134
  }
18108
18135
 
18136
+ // node_modules/@agentclientprotocol/sdk/dist/stream-limits.js
18137
+ var DEFAULT_MAX_MESSAGE_BYTES = 32 * 1024 * 1024;
18138
+
18139
+ class MessageTooLargeError extends Error {
18140
+ maxMessageBytes;
18141
+ constructor(maxMessageBytes) {
18142
+ super(`Incoming ACP data exceeds the configured ${maxMessageBytes} byte limit`);
18143
+ this.maxMessageBytes = maxMessageBytes;
18144
+ this.name = "MessageTooLargeError";
18145
+ }
18146
+ }
18147
+ function resolveMaxMessageBytes(value) {
18148
+ const maxMessageBytes = value ?? DEFAULT_MAX_MESSAGE_BYTES;
18149
+ if (!Number.isSafeInteger(maxMessageBytes) || maxMessageBytes <= 0) {
18150
+ throw new RangeError("maxMessageBytes must be a positive safe integer");
18151
+ }
18152
+ return maxMessageBytes;
18153
+ }
18154
+
18155
+ class MessageBuffer {
18156
+ #buffer = new Uint8Array(0);
18157
+ #length = 0;
18158
+ #maxMessageBytes;
18159
+ constructor(maxMessageBytes) {
18160
+ this.#maxMessageBytes = resolveMaxMessageBytes(maxMessageBytes);
18161
+ }
18162
+ get byteLength() {
18163
+ return this.#length;
18164
+ }
18165
+ get lastByte() {
18166
+ return this.#buffer[this.#length - 1];
18167
+ }
18168
+ #checkAppend(byteLength) {
18169
+ if (byteLength > this.#maxMessageBytes - this.#length) {
18170
+ this.clear();
18171
+ throw new MessageTooLargeError(this.#maxMessageBytes);
18172
+ }
18173
+ }
18174
+ append(bytes) {
18175
+ this.#checkAppend(bytes.byteLength);
18176
+ const length = this.#length + bytes.byteLength;
18177
+ if (length > this.#buffer.byteLength) {
18178
+ const capacity = Math.min(this.#maxMessageBytes, Math.max(length, this.#buffer.byteLength * 2, 1024));
18179
+ const buffer = new Uint8Array(capacity);
18180
+ buffer.set(this.#buffer.subarray(0, this.#length));
18181
+ this.#buffer = buffer;
18182
+ }
18183
+ this.#buffer.set(bytes, this.#length);
18184
+ this.#length = length;
18185
+ }
18186
+ take() {
18187
+ const bytes = this.#buffer.subarray(0, this.#length);
18188
+ this.clear();
18189
+ return bytes;
18190
+ }
18191
+ clear() {
18192
+ this.#buffer = new Uint8Array(0);
18193
+ this.#length = 0;
18194
+ }
18195
+ }
18196
+
18109
18197
  // node_modules/@agentclientprotocol/sdk/dist/line-buffer.js
18110
18198
  var newline = 10;
18111
18199
 
18112
18200
  class LineBuffer {
18113
- #pending = [];
18114
- push(chunk) {
18115
- const lines = [];
18201
+ #pending;
18202
+ #maxMessageBytes;
18203
+ constructor(maxMessageBytes) {
18204
+ this.#maxMessageBytes = resolveMaxMessageBytes(maxMessageBytes);
18205
+ this.#pending = new MessageBuffer(Math.min(Number.MAX_SAFE_INTEGER, this.#maxMessageBytes + 1));
18206
+ }
18207
+ *push(chunk) {
18116
18208
  let start = 0;
18117
18209
  let newlineIndex = chunk.indexOf(newline, start);
18118
18210
  while (newlineIndex !== -1) {
18119
- lines.push(this.#takeLine(chunk.subarray(start, newlineIndex)));
18211
+ yield this.#takeLine(chunk.subarray(start, newlineIndex));
18120
18212
  start = newlineIndex + 1;
18121
18213
  newlineIndex = chunk.indexOf(newline, start);
18122
18214
  }
18123
18215
  if (start < chunk.byteLength) {
18124
- this.#pending.push(start === 0 ? chunk : new Uint8Array(chunk.subarray(start)));
18216
+ const tail = chunk.subarray(start);
18217
+ this.#checkLine(tail);
18218
+ this.#pending.append(tail);
18125
18219
  }
18126
- return lines;
18127
18220
  }
18128
18221
  flush() {
18129
- if (this.#pending.length === 0) {
18222
+ if (this.#pending.byteLength === 0) {
18130
18223
  return;
18131
18224
  }
18132
- return this.#takeLine(new Uint8Array(0));
18225
+ return stripCarriageReturn(this.#pending.take());
18226
+ }
18227
+ clear() {
18228
+ this.#pending.clear();
18133
18229
  }
18134
18230
  #takeLine(tail) {
18135
- if (this.#pending.length === 0) {
18136
- return tail;
18137
- }
18138
- let total = tail.byteLength;
18139
- for (const part of this.#pending) {
18140
- total += part.byteLength;
18231
+ this.#checkLine(tail);
18232
+ if (this.#pending.byteLength === 0) {
18233
+ return stripCarriageReturn(tail);
18141
18234
  }
18142
- const line = new Uint8Array(total);
18143
- let offset = 0;
18144
- for (const part of this.#pending) {
18145
- line.set(part, offset);
18146
- offset += part.byteLength;
18235
+ this.#pending.append(tail);
18236
+ return stripCarriageReturn(this.#pending.take());
18237
+ }
18238
+ #checkLine(tail) {
18239
+ const lastByte = tail.byteLength > 0 ? tail[tail.byteLength - 1] : this.#pending.lastByte;
18240
+ const byteLength = this.#pending.byteLength + tail.byteLength - (lastByte === 13 ? 1 : 0);
18241
+ if (byteLength > this.#maxMessageBytes) {
18242
+ this.clear();
18243
+ throw new MessageTooLargeError(this.#maxMessageBytes);
18147
18244
  }
18148
- line.set(tail, offset);
18149
- this.#pending = [];
18150
- return line;
18151
18245
  }
18152
18246
  }
18247
+ function stripCarriageReturn(line) {
18248
+ return line[line.byteLength - 1] === 13 ? line.subarray(0, -1) : line;
18249
+ }
18153
18250
 
18154
18251
  // node_modules/@agentclientprotocol/sdk/dist/stream.js
18155
- function ndJsonStream(output, input) {
18252
+ function ndJsonStream(output, input, options = {}) {
18253
+ const maxMessageBytes = resolveMaxMessageBytes(options.maxMessageBytes);
18156
18254
  const textEncoder = new TextEncoder;
18157
18255
  const textDecoder = new TextDecoder;
18158
18256
  let cancelled = false;
@@ -18174,7 +18272,7 @@ function ndJsonStream(output, input) {
18174
18272
  };
18175
18273
  const readable = new ReadableStream({
18176
18274
  async start(controller) {
18177
- const lines = new LineBuffer;
18275
+ const lines = new LineBuffer(maxMessageBytes);
18178
18276
  const enqueueLine = async (lineBytes) => {
18179
18277
  const trimmedLine = textDecoder.decode(lineBytes).trim();
18180
18278
  if (!trimmedLine) {
@@ -18226,8 +18324,10 @@ function ndJsonStream(output, input) {
18226
18324
  return;
18227
18325
  }
18228
18326
  controller.error(err);
18327
+ reader.cancel(err).catch(() => {});
18229
18328
  return;
18230
18329
  } finally {
18330
+ lines.clear();
18231
18331
  if (inputReader === reader) {
18232
18332
  inputReader = undefined;
18233
18333
  }
@@ -19711,6 +19811,22 @@ var zUsageUpdate2 = object({
19711
19811
  return;
19712
19812
  })
19713
19813
  });
19814
+ var zNoticeSeverity2 = union([
19815
+ literal("info"),
19816
+ literal("warning"),
19817
+ literal("error"),
19818
+ string2()
19819
+ ]);
19820
+ var zNotice2 = object({
19821
+ severity: zNoticeSeverity2,
19822
+ title: string2().min(1),
19823
+ description: defaultOnError(string2().nullish(), () => {
19824
+ return;
19825
+ }),
19826
+ _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
19827
+ return;
19828
+ })
19829
+ });
19714
19830
  var zCompactionId2 = string2();
19715
19831
  var zCompactionStatus2 = union([
19716
19832
  literal("in_progress"),
@@ -19779,6 +19895,9 @@ var zSessionUpdate2 = union([
19779
19895
  zUsageUpdate2.and(object({
19780
19896
  sessionUpdate: literal("usage_update")
19781
19897
  })),
19898
+ zNotice2.and(object({
19899
+ sessionUpdate: literal("notice")
19900
+ })),
19782
19901
  zCompactionUpdate2.and(object({
19783
19902
  sessionUpdate: literal("compaction_update")
19784
19903
  })),
@@ -19840,6 +19959,7 @@ var zSessionConfigOptionsCapabilities = object({
19840
19959
  return;
19841
19960
  })
19842
19961
  });
19962
+ var zNoticeCapabilities = record(string2(), unknown());
19843
19963
  var zClientSessionCapabilities = object({
19844
19964
  compaction: defaultOnError(zCompactionCapabilities.nullish(), () => {
19845
19965
  return;
@@ -19847,6 +19967,9 @@ var zClientSessionCapabilities = object({
19847
19967
  configOptions: defaultOnError(zSessionConfigOptionsCapabilities.nullish(), () => {
19848
19968
  return;
19849
19969
  }),
19970
+ notices: defaultOnError(zNoticeCapabilities.nullish(), () => {
19971
+ return;
19972
+ }),
19850
19973
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
19851
19974
  return;
19852
19975
  })
@@ -20522,8 +20645,8 @@ var zCancelRequestNotification2 = object({
20522
20645
  })
20523
20646
  });
20524
20647
  // node_modules/@agentclientprotocol/sdk/dist/v2/acp.js
20525
- function ndJsonStream2(output, input) {
20526
- return ndJsonStream(output, input);
20648
+ function ndJsonStream2(output, input, options) {
20649
+ return ndJsonStream(output, input, options);
20527
20650
  }
20528
20651
  function emptyObjectResponse(response) {
20529
20652
  return response ?? {};
@@ -21367,7 +21490,7 @@ var agentRequestSpecs = {
21367
21490
  unstable_disableProvider: requestSpec(AGENT_METHODS.providers_disable, zDisableProviderRequest, zDisableProviderResponse, emptyObjectResponse),
21368
21491
  newSession: requestSpec(AGENT_METHODS.session_new, zNewSessionRequest, zNewSessionResponse),
21369
21492
  setSessionConfigOption: requestSpec(AGENT_METHODS.session_set_config_option, zSetSessionConfigOptionRequest, zSetSessionConfigOptionResponse),
21370
- prompt: requestSpec(AGENT_METHODS.session_prompt, zPromptRequest, zPromptResponse, emptyObjectResponse),
21493
+ prompt: requestSpec(AGENT_METHODS.session_prompt, zPromptRequest, zPromptResponse),
21371
21494
  unstable_messageMcp: requestSpec(AGENT_METHODS.mcp_message, zMessageMcpRequest, zMessageMcpResponse),
21372
21495
  listSessions: requestSpec(AGENT_METHODS.session_list, zListSessionsRequest, zListSessionsResponse),
21373
21496
  deleteSession: requestSpec(AGENT_METHODS.session_delete, zDeleteSessionRequest, zDeleteSessionResponse, emptyObjectResponse),
@@ -21721,7 +21844,7 @@ var package_default = {
21721
21844
  publishConfig: {
21722
21845
  access: "public"
21723
21846
  },
21724
- version: "0.6.0",
21847
+ version: "0.7.0",
21725
21848
  description: "Native ACP v2 agent for the OpenAI Codex app-server",
21726
21849
  packageManager: "bun@1.4.0",
21727
21850
  engines: {
@@ -21755,7 +21878,7 @@ var package_default = {
21755
21878
  "package:win-x64": "cd dist/bin && zip codex-acp-v2-x64-windows.zip codex-acp-v2-x64-windows.exe",
21756
21879
  "package:win-arm64": "cd dist/bin && zip codex-acp-v2-arm64-windows.zip codex-acp-v2-arm64-windows.exe",
21757
21880
  start: "bun src/index.ts",
21758
- "generate-types": "codex app-server generate-ts --out src/app-server",
21881
+ "generate-types": "codex app-server generate-ts --experimental --out src/app-server",
21759
21882
  test: "vitest run --exclude src/__tests__/e2e.test.ts",
21760
21883
  "test:watch": "vitest",
21761
21884
  typecheck: "tsc --noEmit",
@@ -21787,7 +21910,7 @@ var package_default = {
21787
21910
  vitest: "^4.1.11"
21788
21911
  },
21789
21912
  dependencies: {
21790
- "@agentclientprotocol/sdk": "^1.4.0",
21913
+ "@agentclientprotocol/sdk": "^1.5.1",
21791
21914
  diff: "^9.0.0",
21792
21915
  open: "^11.0.2",
21793
21916
  "vscode-jsonrpc": "^9.0.2"
@@ -21797,43 +21920,182 @@ var package_default = {
21797
21920
  }
21798
21921
  };
21799
21922
 
21923
+ // src/util/abort.ts
21924
+ function abortable(operation, signal) {
21925
+ if (signal.aborted) {
21926
+ operation.catch(() => {});
21927
+ return Promise.reject(signal.reason);
21928
+ }
21929
+ return new Promise((resolve, reject) => {
21930
+ const abort = () => {
21931
+ cleanup();
21932
+ reject(signal.reason);
21933
+ };
21934
+ const cleanup = () => signal.removeEventListener("abort", abort);
21935
+ signal.addEventListener("abort", abort, { once: true });
21936
+ operation.then((value) => {
21937
+ cleanup();
21938
+ resolve(value);
21939
+ }, (error40) => {
21940
+ cleanup();
21941
+ reject(error40);
21942
+ });
21943
+ });
21944
+ }
21945
+
21946
+ // src/agent/clientTools.ts
21947
+ var PREFIX = "alwith_client_";
21948
+ var MAX_RESULT_BYTES = 16 * 1024 * 1024;
21949
+ function record2(value) {
21950
+ return value !== null && typeof value === "object" && !Array.isArray(value);
21951
+ }
21952
+ function clientToolsOf(meta) {
21953
+ if (!record2(meta) || !record2(meta["alwith"]) || meta["alwith"]["tools"] === undefined)
21954
+ return;
21955
+ const value = meta["alwith"]["tools"];
21956
+ if (!record2(value) || value["version"] !== 1 || typeof value["revision"] !== "string" || !value["revision"].trim() || !Array.isArray(value["definitions"]) || value["definitions"].length > 128) {
21957
+ throw RequestError.invalidParams(undefined, "Expected a version 1 client tool set with revision and definitions");
21958
+ }
21959
+ const names = new Set;
21960
+ const definitions = value["definitions"].map((tool) => {
21961
+ if (!record2(tool) || typeof tool["name"] !== "string" || !/^[a-zA-Z][a-zA-Z0-9_]{0,49}$/.test(tool["name"]) || names.has(tool["name"]) || typeof tool["description"] !== "string" || !record2(tool["inputSchema"]) || tool["inputSchema"]["type"] !== "object") {
21962
+ throw RequestError.invalidParams(undefined, "Client tools require unique names, descriptions and object input schemas");
21963
+ }
21964
+ names.add(tool["name"]);
21965
+ return { name: tool["name"], description: tool["description"], inputSchema: structuredClone(tool["inputSchema"]) };
21966
+ });
21967
+ return { version: 1, revision: value["revision"], definitions };
21968
+ }
21969
+ function dynamicTools(set2) {
21970
+ return set2.definitions.map((tool) => ({ type: "function", ...tool, name: PREFIX + tool["name"], deferLoading: false }));
21971
+ }
21972
+ function failure(message) {
21973
+ return { success: false, contentItems: [{ type: "inputText", text: message }] };
21974
+ }
21975
+ function toolResponse(value) {
21976
+ if (!record2(value) || typeof value["success"] !== "boolean" || !Array.isArray(value["contentItems"]) || Buffer.byteLength(JSON.stringify(value)) > MAX_RESULT_BYTES) {
21977
+ throw new Error("Invalid or oversized client tool result");
21978
+ }
21979
+ const contentItems = value["contentItems"].map((item) => {
21980
+ if (!record2(item))
21981
+ throw new Error("Invalid client tool content");
21982
+ if (item["type"] === "text" && typeof item["text"] === "string")
21983
+ return { type: "inputText", text: item["text"] };
21984
+ if ((item["type"] === "image" || item["type"] === "audio") && typeof item["data"] === "string" && typeof item["mimeType"] === "string" && /^[A-Za-z0-9+/]*={0,2}$/.test(item["data"])) {
21985
+ const supported = item["type"] === "image" ? /^image\/(png|jpeg|webp|gif)$/ : /^audio\/(wav|mpeg|mp3|ogg|flac|mp4)$/;
21986
+ if (!supported.test(item["mimeType"]))
21987
+ throw new Error("Unsupported client tool media type");
21988
+ const url2 = `data:${item["mimeType"]};base64,${item["data"]}`;
21989
+ return item["type"] === "image" ? { type: "inputImage", imageUrl: url2 } : { type: "inputAudio", audioUrl: url2 };
21990
+ }
21991
+ throw new Error("Client tool content must be text or bounded inline media");
21992
+ });
21993
+ return { success: value["success"], contentItems };
21994
+ }
21995
+
21996
+ class ClientTools {
21997
+ session;
21998
+ client;
21999
+ lifetime;
22000
+ calls = new WeakMap;
22001
+ constructor(session, client, lifetime) {
22002
+ this.session = session;
22003
+ this.client = client;
22004
+ this.lifetime = lifetime;
22005
+ }
22006
+ call(params) {
22007
+ const turn = this.session.activeTurn;
22008
+ const set2 = this.session.clientTools;
22009
+ if (!turn || !set2 || this.session.closed || this.lifetime.aborted || turn.abort.signal.aborted || params.threadId !== this.session.id || params.namespace !== null || turn.turnId !== null && params.turnId !== turn.turnId)
22010
+ return Promise.resolve(failure("Client tool turn is no longer active"));
22011
+ const tool = set2.definitions.find((tool2) => PREFIX + tool2["name"] === params.tool);
22012
+ if (!tool)
22013
+ return Promise.resolve(failure("Client tool was not declared for this session"));
22014
+ let calls = this.calls.get(turn);
22015
+ if (!calls) {
22016
+ calls = new Map;
22017
+ this.calls.set(turn, calls);
22018
+ }
22019
+ const fingerprint = JSON.stringify([params.tool, params.arguments, set2.revision]);
22020
+ const previous = calls.get(params.callId);
22021
+ if (previous)
22022
+ return previous.fingerprint === fingerprint ? previous.response : Promise.resolve(failure("Client tool call ID was reused with different arguments"));
22023
+ const response = this.execute(turn, set2, tool, params);
22024
+ calls.set(params.callId, { fingerprint, response });
22025
+ return response;
22026
+ }
22027
+ async execute(turn, set2, tool, params) {
22028
+ const signal = AbortSignal.any([this.lifetime, this.client.signal, turn.abort.signal, turn.stop.signal]);
22029
+ try {
22030
+ signal.throwIfAborted();
22031
+ const turnId = turn.turnId ?? await abortable(turn.started, signal);
22032
+ if (turnId !== params.turnId || this.session.activeTurn !== turn)
22033
+ return failure("Client tool turn is no longer active");
22034
+ if (this.session.mode.approvalPolicy !== "never") {
22035
+ const permission = await this.client.requestPermission({
22036
+ title: `Run ${tool["name"]}?`,
22037
+ subject: { type: "tool_call", toolCall: { toolCallId: params.callId, name: tool["name"], title: tool["description"], kind: "other", status: "pending" } },
22038
+ options: [{ optionId: "allow_once", name: "Allow", kind: "allow_once" }, { optionId: "reject_once", name: "Reject", kind: "reject_once" }]
22039
+ }, signal);
22040
+ if (permission.outcome.outcome !== "selected" || permission.outcome.optionId !== "allow_once")
22041
+ return failure("Client tool permission was denied");
22042
+ }
22043
+ signal.throwIfAborted();
22044
+ const value = await abortable(this.client.requestTool({
22045
+ sessionId: this.session.id,
22046
+ turnId: params.turnId,
22047
+ toolCallId: params.callId,
22048
+ name: tool["name"],
22049
+ arguments: params.arguments,
22050
+ toolSetRevision: set2.revision
22051
+ }, signal), signal);
22052
+ if (this.session.activeTurn !== turn)
22053
+ return failure("Client tool turn is no longer active");
22054
+ signal.throwIfAborted();
22055
+ return toolResponse(value);
22056
+ } catch {
22057
+ return failure(signal.aborted ? "Client tool call was cancelled" : "Client tool call failed");
22058
+ }
22059
+ }
22060
+ }
22061
+
21800
22062
  // src/agent/turnErrors.ts
21801
22063
  function classifyTurnError(info) {
21802
- const failure = (category, retryable = false, stopReason = "_error") => ({ category, retryable, stopReason });
22064
+ const failure2 = (category, retryable = false, stopReason = "_error") => ({ category, retryable, stopReason });
21803
22065
  if (info === null)
21804
- return failure("unknown");
22066
+ return failure2("unknown");
21805
22067
  if (typeof info === "object") {
21806
22068
  if ("activeTurnNotSteerable" in info)
21807
- return failure("turn_not_steerable");
22069
+ return failure2("turn_not_steerable");
21808
22070
  const status = "httpConnectionFailed" in info ? info.httpConnectionFailed.httpStatusCode : ("responseStreamConnectionFailed" in info) ? info.responseStreamConnectionFailed.httpStatusCode : ("responseStreamDisconnected" in info) ? info.responseStreamDisconnected.httpStatusCode : info.responseTooManyFailedAttempts.httpStatusCode;
21809
- return failure("connection", status === null || status === 408 || status === 429 || status >= 500);
22071
+ return failure2("connection", status === null || status === 408 || status === 429 || status >= 500);
21810
22072
  }
21811
22073
  switch (info) {
21812
22074
  case "contextWindowExceeded":
21813
- return failure("context_window", false, "max_tokens");
22075
+ return failure2("context_window", false, "max_tokens");
21814
22076
  case "cyberPolicy":
21815
22077
  case "misalignmentPolicyViolation":
21816
- return failure("policy", false, "refusal");
22078
+ return failure2("policy", false, "refusal");
21817
22079
  case "sessionBudgetExceeded":
21818
- return failure("budget");
22080
+ return failure2("budget");
21819
22081
  case "usageLimitExceeded":
21820
- return failure("usage_limit");
22082
+ return failure2("usage_limit");
21821
22083
  case "rateLimitExceeded":
21822
- return failure("rate_limit", true);
22084
+ return failure2("rate_limit", true);
21823
22085
  case "serverOverloaded":
21824
- return failure("overloaded", true);
22086
+ return failure2("overloaded", true);
21825
22087
  case "internalServerError":
21826
- return failure("internal", true);
22088
+ return failure2("internal", true);
21827
22089
  case "unauthorized":
21828
- return failure("authentication");
22090
+ return failure2("authentication");
21829
22091
  case "badRequest":
21830
- return failure("invalid_request");
22092
+ return failure2("invalid_request");
21831
22093
  case "threadRollbackFailed":
21832
- return failure("rollback");
22094
+ return failure2("rollback");
21833
22095
  case "sandboxError":
21834
- return failure("sandbox");
22096
+ return failure2("sandbox");
21835
22097
  case "other":
21836
- return failure("unknown");
22098
+ return failure2("unknown");
21837
22099
  }
21838
22100
  }
21839
22101
 
@@ -23344,7 +23606,7 @@ function itemSnapshot(item) {
23344
23606
  switch (item.type) {
23345
23607
  case "userMessage": {
23346
23608
  const content = item.content.flatMap(fromUserInput);
23347
- return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
23609
+ return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.clientId ?? item.id, content }] : [];
23348
23610
  }
23349
23611
  case "agentMessage":
23350
23612
  return item.text.length > 0 ? [{
@@ -23701,7 +23963,7 @@ ${details}` : summary)];
23701
23963
  return [compactionUpdate(item.id, "in_progress")];
23702
23964
  case "userMessage": {
23703
23965
  const content = item.content.flatMap(fromUserInput);
23704
- return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
23966
+ return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.clientId ?? item.id, content }] : [];
23705
23967
  }
23706
23968
  case "hookPrompt":
23707
23969
  case "functionCallOutput":
@@ -24902,29 +25164,6 @@ class TurnContext {
24902
25164
  }
24903
25165
  }
24904
25166
 
24905
- // src/util/abort.ts
24906
- function abortable(operation, signal) {
24907
- if (signal.aborted) {
24908
- operation.catch(() => {});
24909
- return Promise.reject(signal.reason);
24910
- }
24911
- return new Promise((resolve, reject) => {
24912
- const abort = () => {
24913
- cleanup();
24914
- reject(signal.reason);
24915
- };
24916
- const cleanup = () => signal.removeEventListener("abort", abort);
24917
- signal.addEventListener("abort", abort, { once: true });
24918
- operation.then((value) => {
24919
- cleanup();
24920
- resolve(value);
24921
- }, (error40) => {
24922
- cleanup();
24923
- reject(error40);
24924
- });
24925
- });
24926
- }
24927
-
24928
25167
  // node_modules/open/index.js
24929
25168
  import process10 from "process";
24930
25169
  import path5 from "path";
@@ -25742,6 +25981,9 @@ class ClientSession {
25742
25981
  return signal ? abortable(pending, signal) : pending;
25743
25982
  }, signal);
25744
25983
  }
25984
+ async requestTool(params, signal) {
25985
+ return await this.link.request("_alwith/tool/call", params, { cancellationSignal: signal });
25986
+ }
25745
25987
  async createElicitation(request, signal) {
25746
25988
  signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
25747
25989
  signal.throwIfAborted();
@@ -26021,7 +26263,7 @@ async function applyConfigOption(session, codex, request) {
26021
26263
  threadId: session.id,
26022
26264
  collaborationMode: {
26023
26265
  mode,
26024
- settings: { model: session.model.model, reasoning_effort: session.model.effort, developer_instructions: null }
26266
+ settings: { model: findModel(session.catalog, session.model.model)?.model ?? session.model.model, reasoning_effort: session.model.effort, developer_instructions: null }
26025
26267
  }
26026
26268
  });
26027
26269
  session.collaborationMode = mode;
@@ -26077,6 +26319,9 @@ var OPENAI_PROVIDER_ID = "openai";
26077
26319
  var GATEWAY_MODEL_PROVIDER = "custom-gateway";
26078
26320
  var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
26079
26321
  var GATEWAY_ID_PATTERN = /^[a-z0-9-]+$/;
26322
+ function gatewayModelId(connectionId, modelId) {
26323
+ return `gateway:${encodeURIComponent(connectionId)}:${encodeURIComponent(modelId)}`;
26324
+ }
26080
26325
 
26081
26326
  class ProviderRouting {
26082
26327
  baseConfig;
@@ -26131,14 +26376,6 @@ class ProviderRouting {
26131
26376
  }
26132
26377
  if (hints.mode === "route" || this.mode !== hints.mode)
26133
26378
  this.gateways.clear();
26134
- for (const other of this.gateways.values()) {
26135
- if (other.id === hints.id)
26136
- continue;
26137
- const duplicate = hints.models.find((model) => other.models.some((candidate) => candidate.id === model.id));
26138
- if (duplicate) {
26139
- throw RequestError.invalidParams({ id: hints.id, model: duplicate.id }, `Model "${duplicate.id}" is already served by gateway "${other.id}"; model ids must be unique across gateways`);
26140
- }
26141
- }
26142
26379
  this.gateways.set(hints.id, {
26143
26380
  id: hints.id,
26144
26381
  baseUrl,
@@ -26157,7 +26394,7 @@ class ProviderRouting {
26157
26394
  }
26158
26395
  gatewayFor(modelId) {
26159
26396
  for (const gateway of this.gateways.values()) {
26160
- if (gateway.models.some((model) => model.id === modelId))
26397
+ if (gateway.models.some((model) => (this.mode === "catalog" ? gatewayModelId(gateway.id, model.id) : model.id) === modelId))
26161
26398
  return gateway;
26162
26399
  }
26163
26400
  return null;
@@ -26171,7 +26408,19 @@ class ProviderRouting {
26171
26408
  gatewayGroups() {
26172
26409
  if (this.mode !== "catalog")
26173
26410
  return [];
26174
- return [...this.gateways.values()].filter((gateway) => gateway.models.length > 0).map((gateway) => ({ id: gateway.id, name: gateway.name, modelIds: gateway.models.map((model) => model.id) }));
26411
+ return [...this.gateways.values()].filter((gateway) => gateway.models.length > 0).map((gateway) => ({ id: gateway.id, name: gateway.name, modelIds: gateway.models.map((model) => gatewayModelId(gateway.id, model.id)) }));
26412
+ }
26413
+ selectionId(modelId, provider) {
26414
+ return modelId !== null && this.mode === "catalog" && this.hasGateway(provider) ? gatewayModelId(provider, modelId) : modelId;
26415
+ }
26416
+ upstreamModel(selectionId) {
26417
+ if (selectionId === null || !selectionId.startsWith("gateway:"))
26418
+ return selectionId;
26419
+ const gateway = this.gatewayFor(selectionId);
26420
+ const model = gateway?.models.find((model2) => gatewayModelId(gateway.id, model2.id) === selectionId);
26421
+ if (!model)
26422
+ throw RequestError.invalidParams({ model: selectionId }, "The selected provider connection or model is unavailable");
26423
+ return model.id;
26175
26424
  }
26176
26425
  disable(request) {
26177
26426
  if (request.providerId !== OPENAI_PROVIDER_ID)
@@ -26250,7 +26499,7 @@ class ProviderRouting {
26250
26499
  const own = [];
26251
26500
  for (const gateway of this.gateways.values()) {
26252
26501
  for (const model of gateway.models)
26253
- own.push(gatewayModel(model, false, codexCatalog[0], gateway.catalogEntries.get(model.id)));
26502
+ own.push({ ...gatewayModel(model, false, codexCatalog[0], gateway.catalogEntries.get(model.id)), id: gatewayModelId(gateway.id, model.id) });
26254
26503
  }
26255
26504
  return [...codexCatalog, ...own];
26256
26505
  }
@@ -26379,7 +26628,7 @@ function gatewayModel(model, isDefault, template, entry) {
26379
26628
  }
26380
26629
 
26381
26630
  // src/agent/session.ts
26382
- function createActiveTurn(threadId) {
26631
+ function createActiveTurn(threadId, clientUserMessageId = null) {
26383
26632
  let resolveStarted = () => {};
26384
26633
  let resolveFinished = () => {};
26385
26634
  let started = new Promise((resolve) => {
@@ -26393,6 +26642,7 @@ function createActiveTurn(threadId) {
26393
26642
  return {
26394
26643
  turnId: null,
26395
26644
  threadId,
26645
+ clientUserMessageId,
26396
26646
  abort: new AbortController,
26397
26647
  stop: new AbortController,
26398
26648
  interrupts: new Map,
@@ -26517,7 +26767,7 @@ class CodexAgent {
26517
26767
  additionalDirectories: {}
26518
26768
  },
26519
26769
  providers: {},
26520
- _meta: { codex: { archive: true, seedHistory: true, skills: true, plugins: true, providerCatalog: true, rename: true, account: true, fuzzyFileSearch: true } }
26770
+ _meta: { alwith: { tools: { version: 1 } }, codex: { archive: true, seedHistory: true, skills: true, plugins: true, providerCatalog: true, rename: true, account: true, fuzzyFileSearch: true } }
26521
26771
  },
26522
26772
  authMethods: authMethods(this.capabilities, this.env)
26523
26773
  };
@@ -26604,7 +26854,8 @@ class CodexAgent {
26604
26854
  const catalog = await this.withCodex(() => this.codex.allModels(true));
26605
26855
  const nativeProvider = await this.resolveModelProvider(candidate);
26606
26856
  const wholesale = this.providers.active !== null && this.providers.mode === "route" || candidate.active !== null && candidate.mode === "route";
26607
- const moving = [...this.sessions.values()].filter((runtime) => wholesale || this.providers.hasGateway(runtime.modelProvider) && candidate.gateway(runtime.modelProvider) !== this.providers.gateway(runtime.modelProvider));
26857
+ const unavailable = new Set([...this.sessions.values()].filter((runtime) => this.providers.mode === "catalog" && this.providers.hasGateway(runtime.modelProvider) && !candidate.hasGateway(runtime.modelProvider)));
26858
+ const moving = [...this.sessions.values()].filter((runtime) => !unavailable.has(runtime) && (wholesale || this.providers.hasGateway(runtime.modelProvider) && candidate.gateway(runtime.modelProvider) !== this.providers.gateway(runtime.modelProvider)));
26608
26859
  const targets = new Map(moving.map((runtime) => {
26609
26860
  if (candidate.active === null)
26610
26861
  return [runtime, null];
@@ -26637,11 +26888,13 @@ class CodexAgent {
26637
26888
  cwd: runtime.session.cwd,
26638
26889
  config: config2,
26639
26890
  modelProvider,
26640
- model: selection.model,
26891
+ model: candidate.upstreamModel(selection.model),
26641
26892
  excludeTurns: true
26642
26893
  }));
26643
- staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, requestedModel ?? thread.model, thread.reasoningEffort) });
26894
+ staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, requestedModel ?? candidate.selectionId(thread.model, modelProvider), thread.reasoningEffort) });
26644
26895
  }
26896
+ for (const runtime of unavailable)
26897
+ runtime.config = candidate.rebind(runtime.config, this.providers, null);
26645
26898
  this.providers = candidate;
26646
26899
  for (const runtime of this.sessions.values()) {
26647
26900
  const next = staged.get(runtime);
@@ -26653,7 +26906,7 @@ class CodexAgent {
26653
26906
  }
26654
26907
  runtime.session.catalog = nextCatalog;
26655
26908
  runtime.session.gatewayGroups = candidate.gatewayGroups();
26656
- if (!next)
26909
+ if (!next && !unavailable.has(runtime))
26657
26910
  runtime.session.model = resolveModelSelection(nextCatalog, findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null, runtime.session.model.effort);
26658
26911
  runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: false } } } }).catch((error40) => logger.error("Provider config notification failed", error40));
26659
26912
  }
@@ -26669,7 +26922,7 @@ class CodexAgent {
26669
26922
  cwd: runtime.session.cwd,
26670
26923
  config: runtime.config,
26671
26924
  modelProvider: runtime.modelProvider,
26672
- model: runtime.session.model.model,
26925
+ model: this.providers.upstreamModel(runtime.session.model.model),
26673
26926
  excludeTurns: true
26674
26927
  });
26675
26928
  } catch (rollbackError) {
@@ -26743,6 +26996,13 @@ class CodexAgent {
26743
26996
  this.assertUnsubscribeSettled(open2.request.sessionId);
26744
26997
  }
26745
26998
  const mcpServers = request.mcpServers ?? [];
26999
+ const clientTools = clientToolsOf(request._meta);
27000
+ const host = hostSessionOptions(request._meta);
27001
+ const mode = host.mode ?? initialAgentMode(this.env);
27002
+ const hostThreadOptions = {
27003
+ ...host.instructions === undefined ? {} : { developerInstructions: host.instructions },
27004
+ ...host.mode ? { approvalPolicy: mode.approvalPolicy, approvalsReviewer: mode.approvalsReviewer, sandbox: mode.sandboxMode } : {}
27005
+ };
26746
27006
  const requestedModel = requestedModelOf(request._meta);
26747
27007
  let gatewayId = this.providers.defaultGatewayId();
26748
27008
  if (this.providers.active !== null && this.providers.mode === "catalog") {
@@ -26763,17 +27023,24 @@ class CodexAgent {
26763
27023
  const config2 = buildThreadConfig(this.providers.threadConfig(gatewayId), request.cwd, additionalDirectories, mcpServers, existingMcp);
26764
27024
  const mcpStartupGeneration = this.codex.mcpStartupGeneration;
26765
27025
  const modelProvider = gatewayId ?? await this.withCodex(() => this.resolveModelProvider());
26766
- const startModel = onGateway ? requestedModel ?? this.providers.gateway(gatewayId)?.model ?? null : requestedModel;
27026
+ const startModel = this.providers.upstreamModel(onGateway ? requestedModel ?? this.providers.gateway(gatewayId)?.model ?? null : requestedModel);
26767
27027
  let readOnly = false;
26768
27028
  const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
26769
27029
  if (signal?.aborted)
26770
27030
  throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26771
27031
  switch (open2.kind) {
26772
27032
  case "new":
26773
- return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider, model: startModel });
27033
+ return await this.codex.threadStart({
27034
+ ...hostThreadOptions,
27035
+ config: config2,
27036
+ cwd: request.cwd,
27037
+ modelProvider,
27038
+ model: startModel,
27039
+ ...clientTools ? { dynamicTools: dynamicTools(clientTools) } : {}
27040
+ });
26774
27041
  case "resume":
26775
27042
  try {
26776
- return await this.codex.threadResume({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
27043
+ return await this.codex.threadResume({ ...hostThreadOptions, threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
26777
27044
  } catch (error40) {
26778
27045
  if (!errorMessage(error40).includes("already has an active writer"))
26779
27046
  throw error40;
@@ -26781,7 +27048,7 @@ class CodexAgent {
26781
27048
  return readOnlyThread((await this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false })).thread, request.cwd);
26782
27049
  }
26783
27050
  case "fork":
26784
- return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
27051
+ return await this.codex.threadFork({ ...hostThreadOptions, threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
26785
27052
  }
26786
27053
  }) }));
26787
27054
  const sessionId = thread.thread.id;
@@ -26810,8 +27077,9 @@ class CodexAgent {
26810
27077
  openedAccount = (await this.withCodex(() => this.codex.accountRead({ refreshToken: false }))).account;
26811
27078
  }
26812
27079
  const catalog = this.providers.catalog(codexCatalog);
26813
- const model = resolveModelSelection(catalog, startModel ?? thread.model, thread.reasoningEffort);
27080
+ const model = resolveModelSelection(catalog, this.providers.selectionId(startModel ?? thread.model, gatewayId ?? thread.modelProvider), thread.reasoningEffort);
26814
27081
  const session = {
27082
+ ...clientTools ? { clientTools } : {},
26815
27083
  id: sessionId,
26816
27084
  cwd: request.cwd,
26817
27085
  additionalDirectories,
@@ -26819,7 +27087,7 @@ class CodexAgent {
26819
27087
  catalog,
26820
27088
  gatewayGroups: this.providers.gatewayGroups(),
26821
27089
  model,
26822
- mode: initialAgentMode(this.env),
27090
+ mode,
26823
27091
  collaborationMode: DEFAULT_COLLABORATION_MODE,
26824
27092
  fastMode: thread.serviceTier === FAST_SERVICE_TIER,
26825
27093
  account: openedAccount,
@@ -26888,11 +27156,13 @@ class CodexAgent {
26888
27156
  const approval = new CodexApprovalHandler(client, turnContext, signal);
26889
27157
  const elicitation = new CodexElicitationHandler(client, turnContext, signal);
26890
27158
  const runtime = { config: config2, modelProvider, stale: false, readOnly, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
27159
+ const tools = new ClientTools(session, client, AbortSignal.any([runtime.lifetime.signal, this.codex.disconnectSignal]));
26891
27160
  const drained = (operation) => async (params) => {
26892
27161
  await abortable(this.drain(runtime), this.codex.disconnectSignal);
26893
27162
  return await operation(params);
26894
27163
  };
26895
27164
  this.codex.attachThread(session.id, {
27165
+ tool: (params) => tools.call(params),
26896
27166
  notification: (notification) => this.enqueue(runtime, notification),
26897
27167
  approval: {
26898
27168
  handleCommandExecution: drained((params) => approval.handleCommandExecution(params)),
@@ -26996,8 +27266,8 @@ class CodexAgent {
26996
27266
  const result = await this.codex.awaitMcpStartup(runtime.session.mcpServerNames, afterGeneration, runtime.lifetime.signal, runtime.session.id);
26997
27267
  if (runtime.session.closed)
26998
27268
  return;
26999
- for (const failure of result.failed) {
27000
- await runtime.client.update(mcpStartupFailed(failure.server, `MCP server "${failure.server}" failed to start: ${failure.error}`));
27269
+ for (const failure2 of result.failed) {
27270
+ await runtime.client.update(mcpStartupFailed(failure2.server, `MCP server "${failure2.server}" failed to start: ${failure2.error}`));
27001
27271
  }
27002
27272
  for (const server of result.cancelled) {
27003
27273
  await runtime.client.update(mcpStartupFailed(server, `MCP server "${server}" startup was cancelled.`));
@@ -27205,10 +27475,11 @@ class CodexAgent {
27205
27475
  async setSessionConfigOptionInternal(params) {
27206
27476
  const runtime = this.runtime(params.sessionId, "session/set_config_option");
27207
27477
  assertWritable(runtime);
27208
- if (params.configId === MODEL_CONFIG_ID && this.providers.mode === "catalog" && typeof params.value === "string") {
27478
+ if (params.configId === MODEL_CONFIG_ID && (this.providers.mode === "catalog" || runtime.session.model.model.startsWith("gateway:")) && typeof params.value === "string") {
27479
+ this.providers.upstreamModel(params.value);
27209
27480
  const target = this.providers.gatewayIdFor(params.value);
27210
- const current = this.providers.hasGateway(runtime.modelProvider) ? runtime.modelProvider : null;
27211
- if (target !== current)
27481
+ const destination = target ?? await this.withCodex(() => this.resolveModelProvider());
27482
+ if (destination !== runtime.modelProvider)
27212
27483
  await this.rerouteSession(runtime, target, params.value);
27213
27484
  }
27214
27485
  await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
@@ -27231,10 +27502,10 @@ class CodexAgent {
27231
27502
  const modelProvider = gatewayId ?? await this.withCodex(() => this.resolveModelProvider());
27232
27503
  await this.codex.threadUnsubscribe({ threadId: session.id });
27233
27504
  try {
27234
- await this.withCodex(() => this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: config2, modelProvider, model: modelId, excludeTurns: true }));
27505
+ await this.withCodex(() => this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: config2, modelProvider, model: this.providers.upstreamModel(modelId), excludeTurns: true }));
27235
27506
  } catch (error40) {
27236
27507
  try {
27237
- await this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: runtime.config, modelProvider: runtime.modelProvider, model: session.model.model, excludeTurns: true });
27508
+ await this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: runtime.config, modelProvider: runtime.modelProvider, model: this.providers.upstreamModel(session.model.model), excludeTurns: true });
27238
27509
  } catch (rollbackError) {
27239
27510
  runtime.stale = true;
27240
27511
  logger.error("Provider rollback failed", rollbackError, { sessionId: session.id });
@@ -27253,9 +27524,9 @@ class CodexAgent {
27253
27524
  return runtime;
27254
27525
  }
27255
27526
  async prompt(params) {
27256
- return await this.promptInternal(params);
27527
+ return await this.promptInternal(params, false, crypto.randomUUID());
27257
27528
  }
27258
- async promptInternal(params, allowReentry = false) {
27529
+ async promptInternal(params, allowReentry, messageId) {
27259
27530
  this.assertRoutingAvailable();
27260
27531
  if (!allowReentry && this.sessionMutations.has(params.sessionId))
27261
27532
  throw RequestError.invalidRequest({ sessionId: params.sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
@@ -27267,31 +27538,32 @@ class CodexAgent {
27267
27538
  throw RequestError.invalidParams(undefined, "prompt must contain at least one content block");
27268
27539
  }
27269
27540
  const { session } = runtime;
27541
+ this.providers.upstreamModel(session.model.model);
27270
27542
  const model = findModel(session.catalog, session.model.model);
27271
27543
  if (!modelSupportsImages(model) && params.prompt.some((block) => block.type === "image")) {
27272
27544
  throw RequestError.invalidParams({ model: session.model.model }, "The current model does not support image input");
27273
27545
  }
27274
27546
  if (session.activeTurn) {
27275
- return await this.steerActiveTurn(runtime, session.activeTurn, params);
27547
+ return await this.steerActiveTurn(runtime, session.activeTurn, params, messageId);
27276
27548
  }
27277
- const turn = createActiveTurn(session.id);
27549
+ const turn = createActiveTurn(session.id, messageId);
27278
27550
  session.activeTurn = turn;
27279
27551
  runtime.client.reportRunning();
27280
27552
  this.runPrompt(runtime, turn, params);
27281
- return {};
27553
+ return { messageId };
27282
27554
  }
27283
- async steerActiveTurn(runtime, turn, params) {
27555
+ async steerActiveTurn(runtime, turn, params, messageId) {
27284
27556
  const turnId = turn.turnId ?? await turn.started;
27285
27557
  if (turnId === null || turn.threadId !== runtime.session.id) {
27286
27558
  throw RequestError.invalidRequest({ sessionId: params.sessionId }, "A turn is already running; wait for it to finish or cancel it");
27287
27559
  }
27288
27560
  try {
27289
- await this.codex.turnSteer({ threadId: runtime.session.id, expectedTurnId: turnId, input: toUserInput(params.prompt) });
27290
- return { _meta: { codex: { steered: turnId } } };
27561
+ await this.codex.turnSteer({ threadId: runtime.session.id, expectedTurnId: turnId, clientUserMessageId: messageId, input: toUserInput(params.prompt) });
27562
+ return { messageId, _meta: { codex: { steered: turnId } } };
27291
27563
  } catch (error40) {
27292
27564
  if (this.completedTurns.get(turn) === turnId || runtime.session.activeTurn !== turn) {
27293
27565
  await turn.finished;
27294
- return await this.promptInternal(params, true);
27566
+ return await this.promptInternal(params, true, messageId);
27295
27567
  }
27296
27568
  throw RequestError.invalidRequest({ sessionId: params.sessionId, turnId }, `Could not steer the running turn: ${errorMessage(error40)}`);
27297
27569
  }
@@ -27396,11 +27668,12 @@ class CodexAgent {
27396
27668
  startSent = true;
27397
27669
  const completed = this.withCodex(() => this.codex.runTurn({
27398
27670
  threadId: session.id,
27671
+ clientUserMessageId: turn.clientUserMessageId,
27399
27672
  input: toUserInput(prompt),
27400
27673
  approvalPolicy: session.mode.approvalPolicy,
27401
27674
  approvalsReviewer: session.mode.approvalsReviewer,
27402
27675
  sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
27403
- model: session.model.model,
27676
+ model: this.providers.upstreamModel(session.model.model),
27404
27677
  effort: session.model.effort,
27405
27678
  summary: disableSummary ? "none" : "auto",
27406
27679
  serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
@@ -27763,6 +28036,17 @@ function readOnlyThread(thread, cwd) {
27763
28036
  itemsBackwardsCursor: null
27764
28037
  };
27765
28038
  }
28039
+ function hostSessionOptions(meta) {
28040
+ const instructions = meta?.alwith?.appendSystemPrompt;
28041
+ if (instructions !== undefined && typeof instructions !== "string") {
28042
+ throw RequestError.invalidParams(undefined, "appendSystemPrompt must be a string");
28043
+ }
28044
+ const modeId = meta?.codex?.mode;
28045
+ const mode = typeof modeId === "string" ? findAgentMode(modeId) : undefined;
28046
+ if (modeId !== undefined && !mode)
28047
+ throw RequestError.invalidParams(undefined, "Unknown Codex session mode");
28048
+ return { instructions, mode };
28049
+ }
27766
28050
  function requestedModelOf(meta) {
27767
28051
  const raw = meta?.alwith?.model;
27768
28052
  return typeof raw === "string" && raw.length > 0 ? raw : null;
@@ -27823,12 +28107,14 @@ var FileChangeApprovalRequest = new import_node.RequestType("item/fileChange/req
27823
28107
  var PermissionsApprovalRequest = new import_node.RequestType("item/permissions/requestApproval");
27824
28108
  var McpServerElicitationRequest = new import_node.RequestType("mcpServer/elicitation/request");
27825
28109
  var ToolRequestUserInputRequest = new import_node.RequestType("item/tool/requestUserInput");
28110
+ var DynamicToolCallRequest = new import_node.RequestType("item/tool/call");
27826
28111
 
27827
28112
  class AppServerClient {
27828
28113
  connection;
27829
28114
  threadHandlers = new Map;
27830
28115
  approvalHandlers = new Map;
27831
28116
  elicitationHandlers = new Map;
28117
+ dynamicToolHandlers = new Map;
27832
28118
  turnCompletionWaiters = new Map;
27833
28119
  earlyTurnCompletions = new Map;
27834
28120
  startingThreads = new Map;
@@ -27841,6 +28127,10 @@ class AppServerClient {
27841
28127
  disconnected = new AbortController;
27842
28128
  constructor(connection) {
27843
28129
  this.connection = connection;
28130
+ connection.onRequest(DynamicToolCallRequest, async (params) => {
28131
+ const handler = this.dynamicToolHandlers.get(params.threadId);
28132
+ return handler ? await handler(params) : { success: false, contentItems: [{ type: "inputText", text: "Client tools are unavailable for this thread" }] };
28133
+ });
27844
28134
  connection.onClose(() => this.disconnected.abort(new Error("Connection to Codex was lost")));
27845
28135
  connection.onDispose(() => this.disconnected.abort(new Error("Connection to Codex was lost")));
27846
28136
  connection.onUnhandledNotification((message) => {
@@ -27874,11 +28164,14 @@ class AppServerClient {
27874
28164
  this.threadHandlers.set(threadId, handlers.notification);
27875
28165
  this.approvalHandlers.set(threadId, handlers.approval);
27876
28166
  this.elicitationHandlers.set(threadId, handlers.elicitation);
28167
+ if (handlers.tool)
28168
+ this.dynamicToolHandlers.set(threadId, handlers.tool);
27877
28169
  }
27878
28170
  detachThread(threadId) {
27879
28171
  this.threadHandlers.delete(threadId);
27880
28172
  this.approvalHandlers.delete(threadId);
27881
28173
  this.elicitationHandlers.delete(threadId);
28174
+ this.dynamicToolHandlers.delete(threadId);
27882
28175
  for (const key of this.mcpStartupStates.keys()) {
27883
28176
  if (key.startsWith(`${threadId}\x00`))
27884
28177
  this.mcpStartupStates.delete(key);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.0",
6
+ "version": "0.7.0",
7
7
  "description": "Native ACP v2 agent for the OpenAI Codex app-server",
8
8
  "packageManager": "bun@1.4.0",
9
9
  "engines": {
@@ -37,7 +37,7 @@
37
37
  "package:win-x64": "cd dist/bin && zip codex-acp-v2-x64-windows.zip codex-acp-v2-x64-windows.exe",
38
38
  "package:win-arm64": "cd dist/bin && zip codex-acp-v2-arm64-windows.zip codex-acp-v2-arm64-windows.exe",
39
39
  "start": "bun src/index.ts",
40
- "generate-types": "codex app-server generate-ts --out src/app-server",
40
+ "generate-types": "codex app-server generate-ts --experimental --out src/app-server",
41
41
  "test": "vitest run --exclude src/__tests__/e2e.test.ts",
42
42
  "test:watch": "vitest",
43
43
  "typecheck": "tsc --noEmit",
@@ -69,7 +69,7 @@
69
69
  "vitest": "^4.1.11"
70
70
  },
71
71
  "dependencies": {
72
- "@agentclientprotocol/sdk": "^1.4.0",
72
+ "@agentclientprotocol/sdk": "^1.5.1",
73
73
  "diff": "^9.0.0",
74
74
  "open": "^11.0.2",
75
75
  "vscode-jsonrpc": "^9.0.2"