@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.
- package/CHANGELOG.md +18 -0
- package/dist/built-in-backends.d.ts +1 -0
- package/dist/built-in-backends.js +6 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +103 -1
- package/dist/external-provider-registry.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/manager/account.d.ts +6 -0
- package/dist/manager/account.js +121 -0
- package/dist/manager/auth-parser.d.ts +27 -0
- package/dist/manager/auth-parser.js +54 -0
- package/dist/manager/config.d.ts +6 -0
- package/dist/manager/config.js +32 -0
- package/dist/manager/index.d.ts +12 -0
- package/dist/manager/index.js +11 -0
- package/dist/manager/install.d.ts +9 -0
- package/dist/manager/install.js +117 -0
- package/dist/manager/manager.d.ts +51 -0
- package/dist/manager/manager.js +105 -0
- package/dist/manager/network.d.ts +8 -0
- package/dist/manager/network.js +46 -0
- package/dist/manager/paths.d.ts +6 -0
- package/dist/manager/paths.js +16 -0
- package/dist/manager/quota/cache.d.ts +9 -0
- package/dist/manager/quota/cache.js +33 -0
- package/dist/manager/quota/claude.d.ts +19 -0
- package/dist/manager/quota/claude.js +193 -0
- package/dist/manager/quota/codex.d.ts +27 -0
- package/dist/manager/quota/codex.js +182 -0
- package/dist/manager/quota/copilot.d.ts +64 -0
- package/dist/manager/quota/copilot.js +718 -0
- package/dist/manager/quota/external.d.ts +29 -0
- package/dist/manager/quota/external.js +176 -0
- package/dist/manager/quota/headers.d.ts +5 -0
- package/dist/manager/quota/headers.js +29 -0
- package/dist/manager/quota/kimi.d.ts +24 -0
- package/dist/manager/quota/kimi.js +230 -0
- package/dist/manager/types.d.ts +166 -0
- package/dist/manager/types.js +1 -0
- package/dist/providers/chat-web-session.d.ts +218 -0
- package/dist/providers/chat-web-session.js +584 -0
- package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
- package/dist/providers/claude-agent-sdk-session.js +109 -1
- package/dist/providers/codex-app-server-session.d.ts +107 -0
- package/dist/providers/codex-app-server-session.js +479 -9
- package/dist/providers/copilot-sdk-session.d.ts +9 -1
- package/dist/providers/copilot-sdk-session.js +48 -0
- package/dist/resume/chat-web.d.ts +20 -0
- package/dist/resume/chat-web.js +44 -0
- package/dist/resume/index.js +2 -0
- package/dist/session-factory.d.ts +3 -1
- package/dist/session-factory.js +17 -4
- package/dist/shared.d.ts +159 -0
- package/dist/shared.js +111 -0
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +45 -1
- package/dist/worker.js +19 -5
- 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
|
|
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.
|
|
812
|
-
|
|
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<
|
|
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
|
}
|