@love-moon/ai-sdk 0.3.1 → 0.4.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 (59) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/client.d.ts +15 -0
  5. package/dist/client.js +103 -1
  6. package/dist/external-provider-registry.js +4 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +3 -0
  9. package/dist/manager/account.d.ts +6 -0
  10. package/dist/manager/account.js +121 -0
  11. package/dist/manager/auth-parser.d.ts +27 -0
  12. package/dist/manager/auth-parser.js +54 -0
  13. package/dist/manager/config.d.ts +6 -0
  14. package/dist/manager/config.js +32 -0
  15. package/dist/manager/index.d.ts +12 -0
  16. package/dist/manager/index.js +11 -0
  17. package/dist/manager/install.d.ts +9 -0
  18. package/dist/manager/install.js +117 -0
  19. package/dist/manager/manager.d.ts +51 -0
  20. package/dist/manager/manager.js +105 -0
  21. package/dist/manager/network.d.ts +8 -0
  22. package/dist/manager/network.js +46 -0
  23. package/dist/manager/paths.d.ts +6 -0
  24. package/dist/manager/paths.js +16 -0
  25. package/dist/manager/quota/cache.d.ts +9 -0
  26. package/dist/manager/quota/cache.js +33 -0
  27. package/dist/manager/quota/claude.d.ts +19 -0
  28. package/dist/manager/quota/claude.js +193 -0
  29. package/dist/manager/quota/codex.d.ts +27 -0
  30. package/dist/manager/quota/codex.js +182 -0
  31. package/dist/manager/quota/copilot.d.ts +64 -0
  32. package/dist/manager/quota/copilot.js +718 -0
  33. package/dist/manager/quota/external.d.ts +29 -0
  34. package/dist/manager/quota/external.js +176 -0
  35. package/dist/manager/quota/headers.d.ts +5 -0
  36. package/dist/manager/quota/headers.js +29 -0
  37. package/dist/manager/quota/kimi.d.ts +24 -0
  38. package/dist/manager/quota/kimi.js +230 -0
  39. package/dist/manager/types.d.ts +166 -0
  40. package/dist/manager/types.js +1 -0
  41. package/dist/providers/chat-web-session.d.ts +218 -0
  42. package/dist/providers/chat-web-session.js +584 -0
  43. package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
  44. package/dist/providers/claude-agent-sdk-session.js +109 -1
  45. package/dist/providers/codex-app-server-session.d.ts +107 -0
  46. package/dist/providers/codex-app-server-session.js +479 -9
  47. package/dist/providers/copilot-sdk-session.d.ts +9 -1
  48. package/dist/providers/copilot-sdk-session.js +48 -0
  49. package/dist/resume/chat-web.d.ts +20 -0
  50. package/dist/resume/chat-web.js +44 -0
  51. package/dist/resume/index.js +2 -0
  52. package/dist/session-factory.d.ts +3 -1
  53. package/dist/session-factory.js +17 -4
  54. package/dist/shared.d.ts +159 -0
  55. package/dist/shared.js +111 -0
  56. package/dist/transports/codex-app-server-transport.d.ts +1 -0
  57. package/dist/transports/codex-app-server-transport.js +45 -1
  58. package/dist/worker.js +19 -5
  59. package/package.json +10 -3
@@ -1,7 +1,11 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { CODEX_APP_SERVER_VARIANT } from "../built-in-backends.js";
3
3
  import { CodexAppServerTransport } from "../transports/codex-app-server-transport.js";
4
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
4
+ import { TERMINAL_GOAL_STATUSES, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
5
+ const TERMINAL_GOAL_STATUSES_LOCAL = new Set(TERMINAL_GOAL_STATUSES);
6
+ function isTerminalGoalStatusLocal(status) {
7
+ return typeof status === "string" && TERMINAL_GOAL_STATUSES_LOCAL.has(status);
8
+ }
5
9
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
6
10
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
7
11
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -170,6 +174,18 @@ function createTurnError(message, extras = {}) {
170
174
  return error;
171
175
  }
172
176
  export class CodexAppServerSession extends EventEmitter {
177
+ /**
178
+ * Optional feature flags surfaced to callers via {@link getSnapshot}.
179
+ * Treat as read-only.
180
+ * @type {Readonly<import("../shared.js").SessionCapabilities>}
181
+ */
182
+ static capabilities = Object.freeze({ goal: true });
183
+ /**
184
+ * @returns {import("../shared.js").SessionCapabilities}
185
+ */
186
+ getCapabilities() {
187
+ return { ...CodexAppServerSession.capabilities };
188
+ }
173
189
  constructor(backend, options = {}) {
174
190
  super();
175
191
  this.backend = normalizeCodexBackend(backend);
@@ -203,6 +219,12 @@ export class CodexAppServerSession extends EventEmitter {
203
219
  this.currentTurn = null;
204
220
  this.bootPromise = null;
205
221
  this.booted = false;
222
+ this.goalMode = options.goalMode === true;
223
+ this.currentGoal = null;
224
+ // Goal-mode lifecycle is decoupled from `currentTurn` so that goal events
225
+ // that arrive after a per-turn `turn/completed` still route correctly.
226
+ // See `runGoal` for the shape; `null` means no goal is currently running.
227
+ this.currentGoalRun = null;
206
228
  const envConfig = loadEnvConfig(options.configFile);
207
229
  const proxyEnv = proxyToEnv(envConfig);
208
230
  const extraEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
@@ -219,6 +241,7 @@ export class CodexAppServerSession extends EventEmitter {
219
241
  },
220
242
  },
221
243
  commandLine: options.commandLine,
244
+ enableGoals: this.goalMode || options.enableGoals === true,
222
245
  });
223
246
  this.transport.on("notification", ({ method, params }) => {
224
247
  void this.handleNotification(method, params);
@@ -262,6 +285,7 @@ export class CodexAppServerSession extends EventEmitter {
262
285
  }
263
286
  : null,
264
287
  currentTurnStatus: this.getCurrentTurnStatus(),
288
+ capabilities: this.getCapabilities(),
265
289
  pid: this.transport.pid || undefined,
266
290
  };
267
291
  }
@@ -638,9 +662,26 @@ export class CodexAppServerSession extends EventEmitter {
638
662
  return null;
639
663
  }
640
664
  const turnId = typeof params?.turnId === "string" ? params.turnId.trim() : "";
641
- if (turnId && currentTurn.turnId && currentTurn.turnId !== turnId) {
665
+ if (!turnId) {
666
+ return currentTurn;
667
+ }
668
+ if (currentTurn.turnId && currentTurn.turnId !== turnId) {
669
+ // In goal mode, the second turn's delta may arrive before
670
+ // `turn/started` for that turn. Adopt the new turnId in that case so
671
+ // the delta isn't dropped (the caller is presumed to be in goal mode
672
+ // because non-goal turns spawn a fresh `currentTurn` per call).
673
+ if (currentTurn.goalMode) {
674
+ currentTurn.turnId = turnId;
675
+ return currentTurn;
676
+ }
642
677
  return null;
643
678
  }
679
+ if (!currentTurn.turnId) {
680
+ // First event we've seen for this turn — bind the id eagerly so later
681
+ // events match. Particularly important for goal mode where multiple
682
+ // turns share the same `currentTurn`.
683
+ currentTurn.turnId = turnId;
684
+ }
644
685
  return currentTurn;
645
686
  }
646
687
  async queueAssistantDelta(delta, { messageId = "" } = {}) {
@@ -687,6 +728,18 @@ export class CodexAppServerSession extends EventEmitter {
687
728
  if (turnId) {
688
729
  currentTurn.turnId = turnId;
689
730
  }
731
+ // Flush any deltas queued for this turn before `turn/started`.
732
+ // This races between the server emitting a delta and the
733
+ // `turn/started` notification (common for goal mode's 2nd+ turn).
734
+ if (turnId && this.currentGoalRun && this.currentGoalRun.pendingDeltasByTurnId) {
735
+ const queue = this.currentGoalRun.pendingDeltasByTurnId.get(turnId);
736
+ if (queue && queue.length > 0) {
737
+ this.currentGoalRun.pendingDeltasByTurnId.delete(turnId);
738
+ for (const queued of queue) {
739
+ await this.queueAssistantDelta(queued.delta, { messageId: queued.messageId });
740
+ }
741
+ }
742
+ }
690
743
  await this.emitWorkingStatus({
691
744
  phase: "turn_started",
692
745
  reply_in_progress: true,
@@ -761,15 +814,23 @@ export class CodexAppServerSession extends EventEmitter {
761
814
  });
762
815
  return;
763
816
  case "item/agentMessage/delta": {
764
- const currentTurn = this.ensureCurrentTurn(params);
765
- if (!currentTurn) {
766
- return;
767
- }
768
817
  const delta = typeof params?.delta === "string" ? params.delta : "";
769
818
  const messageId = normalizeItemId(params?.itemId || params?.item_id);
770
819
  if (!delta) {
771
820
  return;
772
821
  }
822
+ const currentTurn = this.ensureCurrentTurn(params);
823
+ if (!currentTurn) {
824
+ // Goal mode race: delta arrived for a turn we haven't seen
825
+ // `turn/started` for yet. Queue it; we'll flush in `turn/started`.
826
+ const deltaTurnId = typeof params?.turnId === "string" ? params.turnId.trim() : "";
827
+ if (this.currentGoalRun && deltaTurnId) {
828
+ const queue = this.currentGoalRun.pendingDeltasByTurnId.get(deltaTurnId) || [];
829
+ queue.push({ delta, messageId });
830
+ this.currentGoalRun.pendingDeltasByTurnId.set(deltaTurnId, queue);
831
+ }
832
+ return;
833
+ }
773
834
  await this.emitWorkingStatus({
774
835
  phase: "message_aggregation",
775
836
  reply_in_progress: true,
@@ -784,6 +845,29 @@ export class CodexAppServerSession extends EventEmitter {
784
845
  case "account/rateLimits/updated":
785
846
  this.rateLimits = params?.rateLimits && typeof params.rateLimits === "object" ? { ...params.rateLimits } : null;
786
847
  return;
848
+ case "thread/goal/updated": {
849
+ const goalPayload = this.normalizeGoalPayload(params?.goal ?? params);
850
+ if (!goalPayload) {
851
+ return;
852
+ }
853
+ this.currentGoal = goalPayload;
854
+ const goalRun = this.currentGoalRun;
855
+ if (goalRun && typeof goalRun.handleGoalUpdate === "function") {
856
+ goalRun.handleGoalUpdate(goalPayload);
857
+ }
858
+ this.emit("goal_update", { ...goalPayload });
859
+ return;
860
+ }
861
+ case "thread/goal/cleared": {
862
+ const previous = this.currentGoal;
863
+ this.currentGoal = null;
864
+ const goalRun = this.currentGoalRun;
865
+ if (goalRun && typeof goalRun.handleGoalCleared === "function") {
866
+ goalRun.handleGoalCleared(previous);
867
+ }
868
+ this.emit("goal_cleared", previous ? { ...previous } : null);
869
+ return;
870
+ }
787
871
  case "turn/completed": {
788
872
  const currentTurn = this.ensureCurrentTurn(params);
789
873
  if (!currentTurn) {
@@ -808,8 +892,39 @@ export class CodexAppServerSession extends EventEmitter {
808
892
  reply_in_progress: false,
809
893
  status_done_line: `codex failed (${status})`,
810
894
  });
811
- currentTurn.reject(error);
812
- this.currentTurn = null;
895
+ if (currentTurn.goalMode) {
896
+ // Propagate to the goal-run completion so callers see the failure.
897
+ const goalRun = this.currentGoalRun;
898
+ if (goalRun && typeof goalRun.handleTurnFailed === "function") {
899
+ goalRun.handleTurnFailed(error);
900
+ }
901
+ }
902
+ else {
903
+ currentTurn.reject(error);
904
+ this.currentTurn = null;
905
+ }
906
+ return;
907
+ }
908
+ // In goal mode, do NOT resolve/clear currentTurn on per-turn completion.
909
+ // The goal may span multiple turns and resolves on a terminal
910
+ // `thread/goal/updated` status (or `thread/goal/cleared`). The
911
+ // per-turn text is folded into `currentGoalRun.fullText` so it
912
+ // survives the per-turn reset.
913
+ if (currentTurn.goalMode) {
914
+ const goalRun = this.currentGoalRun;
915
+ if (goalRun) {
916
+ const turnText = currentTurn.fullText || "";
917
+ if (turnText) {
918
+ goalRun.fullText = `${goalRun.fullText || ""}${turnText}`;
919
+ goalRun.lastTurnText = turnText;
920
+ }
921
+ }
922
+ // Reset per-turn accumulator state but keep the goal-scoped turn alive
923
+ // so subsequent `turn/started` / `item/*` events continue to be handled.
924
+ currentTurn.turnId = "";
925
+ currentTurn.fullText = "";
926
+ currentTurn.activeAssistantMessageId = "";
927
+ currentTurn.activeAssistantMessageText = "";
813
928
  return;
814
929
  }
815
930
  await this.emitWorkingStatus({
@@ -829,6 +944,14 @@ export class CodexAppServerSession extends EventEmitter {
829
944
  }
830
945
  }
831
946
  handleTransportFailure(error) {
947
+ if (this.currentGoalRun && typeof this.currentGoalRun.handleTurnFailed === "function") {
948
+ try {
949
+ this.currentGoalRun.handleTurnFailed(error);
950
+ }
951
+ catch {
952
+ // best effort
953
+ }
954
+ }
832
955
  if (this.currentTurn) {
833
956
  this.currentTurn.reject(error);
834
957
  this.currentTurn = null;
@@ -842,9 +965,13 @@ export class CodexAppServerSession extends EventEmitter {
842
965
  stderr: payload?.stderr,
843
966
  });
844
967
  this.closed = true;
968
+ // Reject in-flight goal-runs / per-turn callers with the precise
969
+ // `transport_exited` error BEFORE we flip `closeRequested` / flush close
970
+ // waiters. Otherwise the closeGuard would win the Promise.race and the
971
+ // caller would see a generic `session_closed` instead of the real cause.
972
+ this.handleTransportFailure(exitError);
845
973
  this.closeRequested = true;
846
974
  this.flushCloseWaiters();
847
- this.handleTransportFailure(exitError);
848
975
  this.emit("process.exited", {
849
976
  pid: this.transport.pid || null,
850
977
  code: payload?.code,
@@ -987,6 +1114,349 @@ export class CodexAppServerSession extends EventEmitter {
987
1114
  turnTimeoutGuard.cleanup();
988
1115
  }
989
1116
  }
1117
+ /**
1118
+ * Normalize a goal payload from a codex `thread/goal/*` notification into
1119
+ * the cross-layer {@link GoalState} shape.
1120
+ *
1121
+ * @param {unknown} payload
1122
+ * @returns {(import("../shared.js").GoalState & { raw?: unknown }) | null}
1123
+ */
1124
+ normalizeGoalPayload(payload) {
1125
+ if (!payload || typeof payload !== "object") {
1126
+ return null;
1127
+ }
1128
+ const raw = /** @type {Record<string, unknown>} */ (payload);
1129
+ const objective = typeof raw.objective === "string"
1130
+ ? raw.objective
1131
+ : typeof raw.goal === "string"
1132
+ ? raw.goal
1133
+ : "";
1134
+ const status = typeof raw.status === "string" && raw.status.trim()
1135
+ ? /** @type {import("../shared.js").GoalStatus} */ (raw.status.trim())
1136
+ : "active";
1137
+ const id = typeof raw.id === "string"
1138
+ ? raw.id
1139
+ : typeof raw.goalId === "string"
1140
+ ? raw.goalId
1141
+ : undefined;
1142
+ const threadId = typeof raw.threadId === "string"
1143
+ ? raw.threadId
1144
+ : this.sessionId || undefined;
1145
+ const tokenBudget = raw.tokenBudget === null
1146
+ ? null
1147
+ : Number.isFinite(Number(raw.tokenBudget))
1148
+ ? Number(raw.tokenBudget)
1149
+ : undefined;
1150
+ return {
1151
+ id,
1152
+ threadId,
1153
+ objective,
1154
+ status,
1155
+ tokenBudget,
1156
+ };
1157
+ }
1158
+ /**
1159
+ * Trigger Codex's experimental `thread/goal/set` JSON-RPC. Requires the
1160
+ * app-server to have been booted with `--enable goals`; dynamic enablement
1161
+ * is NOT supported. Pass `{ goalMode: true }` to the session factory so the
1162
+ * transport spawns with the right flag.
1163
+ *
1164
+ * Resolves when the goal reaches a terminal status (`complete`, `blocked`,
1165
+ * `usageLimited`, `budgetLimited`) or when a `thread/goal/cleared`
1166
+ * notification arrives.
1167
+ *
1168
+ * @param {import("../shared.js").GoalRequest} goal
1169
+ * @param {object} [options]
1170
+ * @returns {Promise<import("../shared.js").GoalResult>}
1171
+ */
1172
+ async runGoal(goal, options = {}) {
1173
+ if (this.closeRequested) {
1174
+ throw this.createSessionClosedError();
1175
+ }
1176
+ const objective = typeof goal?.objective === "string" ? goal.objective.trim() : "";
1177
+ if (!objective) {
1178
+ throw createTurnError("runGoal requires a non-empty objective", {
1179
+ reason: "invalid_goal",
1180
+ });
1181
+ }
1182
+ if (this.currentTurn || this.currentGoalRun) {
1183
+ throw createTurnError("Codex app-server turn already running", {
1184
+ reason: "turn_already_running",
1185
+ });
1186
+ }
1187
+ this.markTurnStartedStatus();
1188
+ try {
1189
+ await this.boot();
1190
+ }
1191
+ catch (error) {
1192
+ await this.failPendingTurnStart(error);
1193
+ throw error;
1194
+ }
1195
+ const tokenBudget = goal?.tokenBudget === null || Number.isFinite(Number(goal?.tokenBudget))
1196
+ ? goal.tokenBudget ?? null
1197
+ : null;
1198
+ const closeGuard = this.createCloseGuard();
1199
+ const turnTimeoutGuard = this.createTurnTimeoutGuard();
1200
+ let resolveGoal = null;
1201
+ let rejectGoal = null;
1202
+ const completion = new Promise((resolve, reject) => {
1203
+ resolveGoal = resolve;
1204
+ rejectGoal = reject;
1205
+ });
1206
+ // Goal-scoped state. Tracks accumulated text across multiple turns and
1207
+ // serves as the routing target for `thread/goal/*` notifications so that
1208
+ // goal lifecycle is decoupled from per-turn (`currentTurn`) lifecycle.
1209
+ const goalRun = {
1210
+ id: undefined,
1211
+ threadId: this.sessionId || undefined,
1212
+ objective,
1213
+ status: "active",
1214
+ tokenBudget,
1215
+ fullText: "",
1216
+ lastTurnText: "",
1217
+ latestGoal: null,
1218
+ completion,
1219
+ pendingDeltasByTurnId: new Map(),
1220
+ handleGoalUpdate: (payload) => {
1221
+ if (!payload) {
1222
+ return;
1223
+ }
1224
+ goalRun.latestGoal = { ...payload, objective: payload.objective || objective };
1225
+ goalRun.status = payload.status || goalRun.status;
1226
+ if (isTerminalGoalStatusLocal(payload.status)) {
1227
+ resolveGoal?.({
1228
+ goal: { ...goalRun.latestGoal },
1229
+ usage: this.tokenUsage ? { ...this.tokenUsage } : null,
1230
+ cleared: false,
1231
+ });
1232
+ }
1233
+ },
1234
+ handleGoalCleared: (previous) => {
1235
+ const finalGoal = goalRun.latestGoal || previous || {
1236
+ objective,
1237
+ status: "complete",
1238
+ threadId: this.sessionId || undefined,
1239
+ tokenBudget,
1240
+ };
1241
+ resolveGoal?.({
1242
+ goal: { ...finalGoal, status: finalGoal.status || "complete" },
1243
+ usage: this.tokenUsage ? { ...this.tokenUsage } : null,
1244
+ cleared: true,
1245
+ });
1246
+ },
1247
+ handleTurnFailed: (error) => {
1248
+ rejectGoal?.(error);
1249
+ },
1250
+ };
1251
+ this.currentGoalRun = goalRun;
1252
+ // `currentTurn` is still installed so that turn-scoped notification
1253
+ // handling (turn/started, item/started, item/agentMessage/delta, etc.)
1254
+ // continues to work. Its `resolve`/`reject` are stubs; goal completion
1255
+ // is signaled via the `completion` promise above.
1256
+ const currentTurn = {
1257
+ turnId: "",
1258
+ fullText: "",
1259
+ activeAssistantMessageId: "",
1260
+ activeAssistantMessageText: "",
1261
+ resolve: () => { },
1262
+ reject: (error) => {
1263
+ rejectGoal?.(error);
1264
+ },
1265
+ goalMode: true,
1266
+ };
1267
+ this.currentTurn = currentTurn;
1268
+ try {
1269
+ let setResult;
1270
+ try {
1271
+ setResult = await this.transport.request("thread/goal/set", {
1272
+ threadId: this.sessionId,
1273
+ objective,
1274
+ status: "active",
1275
+ tokenBudget,
1276
+ });
1277
+ }
1278
+ catch (error) {
1279
+ const message = String(error?.message || "");
1280
+ if (/goals?\s+feature\s+is\s+disabled/i.test(message) || /unsupported.*goal/i.test(message)) {
1281
+ const hint = new Error(`Codex goals feature is disabled. Re-create the session with { goalMode: true } so the app-server spawns with "--enable goals". (${message})`);
1282
+ hint.reason = "goals_feature_disabled";
1283
+ hint.cause = error;
1284
+ throw hint;
1285
+ }
1286
+ throw error;
1287
+ }
1288
+ const initialGoalPayload = setResult && Object.prototype.hasOwnProperty.call(setResult, "goal")
1289
+ ? setResult.goal
1290
+ : setResult;
1291
+ const initialGoal = this.normalizeGoalPayload(initialGoalPayload);
1292
+ if (initialGoal) {
1293
+ this.currentGoal = initialGoal;
1294
+ goalRun.latestGoal = initialGoal;
1295
+ goalRun.id = initialGoal.id ?? goalRun.id;
1296
+ goalRun.threadId = initialGoal.threadId ?? goalRun.threadId;
1297
+ goalRun.status = initialGoal.status || goalRun.status;
1298
+ if (isTerminalGoalStatusLocal(initialGoal.status)) {
1299
+ resolveGoal?.({
1300
+ goal: { ...initialGoal },
1301
+ usage: this.tokenUsage ? { ...this.tokenUsage } : null,
1302
+ cleared: false,
1303
+ });
1304
+ }
1305
+ }
1306
+ const goalResult = await Promise.race([
1307
+ completion,
1308
+ closeGuard.promise,
1309
+ turnTimeoutGuard.promise,
1310
+ ]);
1311
+ // Fold any in-flight per-turn text into the goal's aggregate text.
1312
+ await this.finalizeActiveAssistantMessage(currentTurn, {
1313
+ messageId: currentTurn.activeAssistantMessageId,
1314
+ });
1315
+ if (currentTurn.fullText) {
1316
+ goalRun.fullText = `${goalRun.fullText || ""}${currentTurn.fullText}`;
1317
+ }
1318
+ const goalState = goalResult?.goal || {
1319
+ objective,
1320
+ status: "complete",
1321
+ threadId: this.sessionId || undefined,
1322
+ tokenBudget,
1323
+ };
1324
+ await this.emitWorkingStatus({
1325
+ phase: "turn_completed",
1326
+ reply_in_progress: false,
1327
+ status_done_line: `codex goal ${goalState.status}`,
1328
+ });
1329
+ if (goalRun.fullText) {
1330
+ this.history.push({ role: "assistant", content: goalRun.fullText });
1331
+ }
1332
+ return {
1333
+ text: goalRun.fullText,
1334
+ goal: {
1335
+ id: goalState.id,
1336
+ threadId: goalState.threadId || this.sessionId || undefined,
1337
+ objective: goalState.objective || objective,
1338
+ status: goalState.status,
1339
+ tokenBudget: goalState.tokenBudget !== undefined ? goalState.tokenBudget : tokenBudget,
1340
+ },
1341
+ usage: goalResult?.usage || (this.tokenUsage ? { ...this.tokenUsage } : null),
1342
+ metadata: {
1343
+ source: CODEX_APP_SERVER_VARIANT,
1344
+ threadId: this.sessionId,
1345
+ threadPath: this.threadPath || undefined,
1346
+ nativeSessionId: this.nativeSessionId || undefined,
1347
+ goalCleared: Boolean(goalResult?.cleared),
1348
+ },
1349
+ };
1350
+ }
1351
+ catch (error) {
1352
+ if (error?.reason === "turn_timeout") {
1353
+ await this.interruptCurrentTurn();
1354
+ }
1355
+ if (!this.closeRequested && error?.reason !== "session_closed") {
1356
+ await this.emitWorkingStatus({
1357
+ phase: "turn_failed",
1358
+ reply_in_progress: false,
1359
+ status_done_line: error instanceof Error ? error.message : String(error),
1360
+ });
1361
+ }
1362
+ this.maybeEmitAuthRequired(error);
1363
+ throw error;
1364
+ }
1365
+ finally {
1366
+ // Always clear goal-run state (NOT currentTurn — that's done by per-turn
1367
+ // lifecycle). currentTurn is also cleared here because the goal owns it.
1368
+ if (this.currentGoalRun === goalRun) {
1369
+ this.currentGoalRun = null;
1370
+ }
1371
+ if (this.currentTurn === currentTurn) {
1372
+ this.currentTurn = null;
1373
+ }
1374
+ closeGuard.cleanup();
1375
+ turnTimeoutGuard.cleanup();
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Fetch the current goal via `thread/goal/get`.
1380
+ *
1381
+ * @returns {Promise<import("../shared.js").GoalState | null>}
1382
+ */
1383
+ async getGoal() {
1384
+ await this.boot();
1385
+ try {
1386
+ const result = await this.transport.request("thread/goal/get", {
1387
+ threadId: this.sessionId,
1388
+ });
1389
+ const hasGoalField = result && Object.prototype.hasOwnProperty.call(result, "goal");
1390
+ if (hasGoalField) {
1391
+ // Server explicitly told us about the goal (including null for
1392
+ // "no active goal"). Update in-memory state to match.
1393
+ const normalized = this.normalizeGoalPayload(result.goal);
1394
+ this.currentGoal = normalized;
1395
+ return normalized;
1396
+ }
1397
+ if (result === null || result === undefined) {
1398
+ // Transport hiccup (e.g. error mapped to a null result). Do NOT
1399
+ // wipe a previously known goal — return the cached state instead.
1400
+ return this.currentGoal ? { ...this.currentGoal } : null;
1401
+ }
1402
+ // Older codex builds returned the goal payload as the root result.
1403
+ const normalized = this.normalizeGoalPayload(result);
1404
+ if (normalized) {
1405
+ this.currentGoal = normalized;
1406
+ }
1407
+ return normalized;
1408
+ }
1409
+ catch (error) {
1410
+ const message = String(error?.message || "");
1411
+ if (/goals?\s+feature\s+is\s+disabled/i.test(message)) {
1412
+ return null;
1413
+ }
1414
+ throw error;
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Clear the current goal via `thread/goal/clear`.
1419
+ *
1420
+ * If a {@link runGoal} call is in flight when this is invoked externally,
1421
+ * the pending `currentGoalRun.completion` is unblocked with a synthetic
1422
+ * "cleared" payload so the caller's `await runGoal(...)` resolves promptly
1423
+ * instead of waiting for the turn-timeout. The server-initiated
1424
+ * `thread/goal/clear` does not emit a `thread/goal/cleared` notification
1425
+ * (it's the response to OUR own RPC), so we have to wake the goal-run
1426
+ * ourselves here.
1427
+ *
1428
+ * @returns {Promise<boolean>} true if a goal was cleared, false if there was nothing to clear.
1429
+ */
1430
+ async clearGoal() {
1431
+ await this.boot();
1432
+ const goalRun = this.currentGoalRun;
1433
+ try {
1434
+ const result = await this.transport.request("thread/goal/clear", {
1435
+ threadId: this.sessionId,
1436
+ });
1437
+ const cleared = result === undefined || result === null
1438
+ ? Boolean(this.currentGoal)
1439
+ : result?.cleared !== false;
1440
+ const previous = this.currentGoal;
1441
+ this.currentGoal = null;
1442
+ if (goalRun && typeof goalRun.handleGoalCleared === "function") {
1443
+ try {
1444
+ goalRun.handleGoalCleared(previous);
1445
+ }
1446
+ catch {
1447
+ // best effort — never let the wake-up throw past clearGoal
1448
+ }
1449
+ }
1450
+ return cleared;
1451
+ }
1452
+ catch (error) {
1453
+ const message = String(error?.message || "");
1454
+ if (/no\s+goal/i.test(message) || /not\s+set/i.test(message)) {
1455
+ return false;
1456
+ }
1457
+ throw error;
1458
+ }
1459
+ }
990
1460
  async close() {
991
1461
  if (this.closed) {
992
1462
  return;
@@ -1,3 +1,10 @@
1
+ export function resolveBundledCopilotCliPath({ platform, arch, resolvePackage, resolvePackagePaths, existsSyncFn, }?: {
2
+ platform?: NodeJS.Platform | undefined;
3
+ arch?: NodeJS.Architecture | undefined;
4
+ resolvePackage?: ((packageName: any) => string) | undefined;
5
+ resolvePackagePaths?: ((packageName: any) => string[]) | undefined;
6
+ existsSyncFn?: typeof existsSync | undefined;
7
+ }): string | null;
1
8
  export class CopilotSdkSession extends EventEmitter<[never]> {
2
9
  constructor(backend: any, options?: {});
3
10
  backend: string;
@@ -46,7 +53,7 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
46
53
  session: any;
47
54
  booted: boolean;
48
55
  bootPromise: Promise<void> | null;
49
- sdkModulePromise: Promise<any> | Promise<typeof import("@github/copilot-sdk")> | null;
56
+ sdkModulePromise: Promise<typeof import("@github/copilot-sdk")> | Promise<any> | null;
50
57
  sessionSubscriptions: any[];
51
58
  lastAuthRequiredSignature: string;
52
59
  env: any;
@@ -190,4 +197,5 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
190
197
  }>;
191
198
  close(): Promise<void>;
192
199
  }
200
+ import { existsSync } from "node:fs";
193
201
  import { EventEmitter } from "node:events";
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import path from "node:path";
4
5
  import { COPILOT_SDK_VARIANT as COPILOT_PROVIDER_VARIANT } from "../built-in-backends.js";
5
6
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, } from "../shared.js";
@@ -9,6 +10,7 @@ const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
9
10
  const DEFAULT_CLOSE_TIMEOUT_MS = 5 * 1000;
10
11
  const SDK_SEND_AND_WAIT_TIMEOUT_GRACE_MS = 5 * 1000;
11
12
  const LEGACY_COPILOT_CLI_ARGS = new Set(["--allow-all-paths", "--allow-all-tools"]);
13
+ const moduleRequire = createRequire(import.meta.url);
12
14
  function waitForever() {
13
15
  return new Promise(() => { });
14
16
  }
@@ -192,6 +194,44 @@ function unwrapEnvironmentCommand(command, args) {
192
194
  function hasOwnEnumerableKeys(value) {
193
195
  return value && typeof value === "object" && Object.keys(value).length > 0;
194
196
  }
197
+ function resolveCopilotPlatformPackageName(platform = process.platform, arch = process.arch) {
198
+ if (!["darwin", "linux", "win32"].includes(platform)) {
199
+ return null;
200
+ }
201
+ if (!["arm64", "x64"].includes(arch)) {
202
+ return null;
203
+ }
204
+ return `@github/copilot-${platform}-${arch}`;
205
+ }
206
+ function resolvePackageFileFromSearchPaths(packageName, relativePath, resolvePackagePaths, existsSyncFn) {
207
+ const searchPaths = resolvePackagePaths(packageName) || [];
208
+ const packageParts = packageName.split("/");
209
+ for (const basePath of searchPaths) {
210
+ const candidate = path.join(basePath, ...packageParts, relativePath);
211
+ if (existsSyncFn(candidate)) {
212
+ return candidate;
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ export function resolveBundledCopilotCliPath({ platform = process.platform, arch = process.arch, resolvePackage = (packageName) => moduleRequire.resolve(packageName), resolvePackagePaths = (packageName) => moduleRequire.resolve.paths(packageName) || [], existsSyncFn = existsSync, } = {}) {
218
+ const platformPackageName = resolveCopilotPlatformPackageName(platform, arch);
219
+ if (platformPackageName) {
220
+ try {
221
+ const platformExecutablePath = resolvePackage(platformPackageName);
222
+ if (platformExecutablePath && existsSyncFn(platformExecutablePath)) {
223
+ return platformExecutablePath;
224
+ }
225
+ }
226
+ catch {
227
+ // Optional platform packages may be absent when optional dependencies are disabled.
228
+ }
229
+ }
230
+ return resolvePackageFileFromSearchPaths("@github/copilot", "npm-loader.js", resolvePackagePaths, existsSyncFn);
231
+ }
232
+ function hasExplicitCopilotCliPathEnv(env) {
233
+ return typeof env?.COPILOT_CLI_PATH === "string" && env.COPILOT_CLI_PATH.trim();
234
+ }
195
235
  function resolveCopilotCliLaunch(commandLine, env = process.env) {
196
236
  const normalized = typeof commandLine === "string" ? commandLine.trim() : "";
197
237
  if (!normalized) {
@@ -395,6 +435,14 @@ function buildCopilotClientOptions(options, cwd, env) {
395
435
  clientOptions.env = hasExplicitGithubToken
396
436
  ? resolvedEnv
397
437
  : withoutCopilotGithubTokenEnv(resolvedEnv);
438
+ if (clientOptions.cliPath === undefined &&
439
+ clientOptions.cliUrl === undefined &&
440
+ !hasExplicitCopilotCliPathEnv(clientOptions.env)) {
441
+ const bundledCliPath = resolveBundledCopilotCliPath();
442
+ if (bundledCliPath) {
443
+ clientOptions.cliPath = bundledCliPath;
444
+ }
445
+ }
398
446
  if (!hasExplicitGithubToken && clientOptions.useLoggedInUser === undefined) {
399
447
  clientOptions.useLoggedInUser = true;
400
448
  }