@love-moon/ai-sdk 0.7.7 → 0.9.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 +8 -0
- package/dist/built-in-backends.d.ts +1 -0
- package/dist/built-in-backends.js +6 -0
- package/dist/context-files.d.ts +16 -0
- package/dist/context-files.js +86 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/manager/index.d.ts +1 -0
- package/dist/manager/index.js +1 -0
- package/dist/manager/install.js +29 -2
- package/dist/manager/manager.d.ts +5 -1
- package/dist/manager/manager.js +8 -0
- package/dist/manager/network.js +4 -2
- package/dist/manager/quota/dsh.d.ts +55 -0
- package/dist/manager/quota/dsh.js +146 -0
- package/dist/manager/types.d.ts +26 -1
- package/dist/media-adapters.d.ts +34 -0
- package/dist/media-adapters.js +57 -0
- package/dist/media-input.d.ts +21 -0
- package/dist/media-input.js +197 -0
- package/dist/providers/chat-web-session.d.ts +7 -1
- package/dist/providers/chat-web-session.js +12 -1
- package/dist/providers/claude-agent-sdk-session.d.ts +10 -1
- package/dist/providers/claude-agent-sdk-session.js +24 -4
- package/dist/providers/codex-app-server-session.d.ts +12 -1
- package/dist/providers/codex-app-server-session.js +49 -20
- package/dist/providers/codex-exec-session.d.ts +8 -3
- package/dist/providers/codex-exec-session.js +13 -13
- package/dist/providers/copilot-sdk-session.d.ts +6 -1
- package/dist/providers/copilot-sdk-session.js +10 -3
- package/dist/providers/dsh-sdk-session.d.ts +194 -0
- package/dist/providers/dsh-sdk-session.js +951 -0
- package/dist/providers/kimi-cli-session.d.ts +11 -1
- package/dist/providers/kimi-cli-session.js +58 -4
- package/dist/providers/kimi-print-session.d.ts +10 -3
- package/dist/providers/kimi-print-session.js +23 -53
- package/dist/providers/opencode-sdk-session.d.ts +6 -1
- package/dist/providers/opencode-sdk-session.js +11 -9
- package/dist/resume/dsh.d.ts +23 -0
- package/dist/resume/dsh.js +122 -0
- package/dist/resume/index.js +2 -0
- package/dist/session-factory.d.ts +3 -1
- package/dist/session-factory.js +5 -1
- package/dsh/cordis.yml +100 -0
- package/package.json +24 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function normalizeMediaInputs(value: any): {
|
|
2
|
+
kind: any;
|
|
3
|
+
path: any;
|
|
4
|
+
mimeType: any;
|
|
5
|
+
name: string;
|
|
6
|
+
sizeBytes: number;
|
|
7
|
+
}[];
|
|
8
|
+
export function resolveTurnMedia(sessionOptions: any, turnOptions?: {}): {
|
|
9
|
+
kind: any;
|
|
10
|
+
path: any;
|
|
11
|
+
mimeType: any;
|
|
12
|
+
name: string;
|
|
13
|
+
sizeBytes: number;
|
|
14
|
+
}[];
|
|
15
|
+
export function assertMediaCapabilities(media: any, backend: any, capabilities: any): void;
|
|
16
|
+
export function fileMediaToDataUri(media: any): string;
|
|
17
|
+
export function fileMediaToBase64(media: any): string;
|
|
18
|
+
export function createMediaInputError(message: any, extras?: {}): Error;
|
|
19
|
+
export function defaultPromptForMedia(media: any): string;
|
|
20
|
+
export const MEDIA_CAPABILITY_NATIVE: "native";
|
|
21
|
+
export const MEDIA_CAPABILITY_UNSUPPORTED: "unsupported";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const SUPPORTED_KINDS = new Set(["image"]);
|
|
4
|
+
const SUPPORTED_MIME_TYPES = new Set([
|
|
5
|
+
"image/gif",
|
|
6
|
+
"image/jpeg",
|
|
7
|
+
"image/png",
|
|
8
|
+
"image/webp",
|
|
9
|
+
]);
|
|
10
|
+
const MAX_MEDIA_ITEMS = 20;
|
|
11
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
12
|
+
const MAX_TOTAL_MEDIA_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
export const MEDIA_CAPABILITY_NATIVE = "native";
|
|
14
|
+
export const MEDIA_CAPABILITY_UNSUPPORTED = "unsupported";
|
|
15
|
+
function inferKind(mimeType) {
|
|
16
|
+
if (mimeType.startsWith("image/"))
|
|
17
|
+
return "image";
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
function inferMimeType(filePath, kind) {
|
|
21
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
22
|
+
const known = {
|
|
23
|
+
".bmp": "image/bmp",
|
|
24
|
+
".gif": "image/gif",
|
|
25
|
+
".heic": "image/heic",
|
|
26
|
+
".jpeg": "image/jpeg",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".png": "image/png",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
};
|
|
31
|
+
return known[extension] || `${kind}/unknown`;
|
|
32
|
+
}
|
|
33
|
+
function hasExpectedSignature(filePath, mimeType) {
|
|
34
|
+
const handle = fs.openSync(filePath, "r");
|
|
35
|
+
try {
|
|
36
|
+
const header = Buffer.alloc(12);
|
|
37
|
+
const bytesRead = fs.readSync(handle, header, 0, header.length, 0);
|
|
38
|
+
if (bytesRead < 4)
|
|
39
|
+
return false;
|
|
40
|
+
if (mimeType === "image/png")
|
|
41
|
+
return header.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"));
|
|
42
|
+
if (mimeType === "image/jpeg")
|
|
43
|
+
return header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff;
|
|
44
|
+
if (mimeType === "image/gif")
|
|
45
|
+
return header.subarray(0, 6).toString("ascii") === "GIF87a" || header.subarray(0, 6).toString("ascii") === "GIF89a";
|
|
46
|
+
if (mimeType === "image/webp")
|
|
47
|
+
return header.subarray(0, 4).toString("ascii") === "RIFF" && header.subarray(8, 12).toString("ascii") === "WEBP";
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
fs.closeSync(handle);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function normalizeMediaInputs(value) {
|
|
55
|
+
if (value === undefined || value === null)
|
|
56
|
+
return [];
|
|
57
|
+
if (!Array.isArray(value)) {
|
|
58
|
+
throw createMediaInputError("media must be an array", { reason: "invalid_media_input" });
|
|
59
|
+
}
|
|
60
|
+
if (value.length > MAX_MEDIA_ITEMS) {
|
|
61
|
+
throw createMediaInputError(`media cannot contain more than ${MAX_MEDIA_ITEMS} files`, {
|
|
62
|
+
reason: "media_limit_exceeded",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
let totalBytes = 0;
|
|
66
|
+
return value.map((item, index) => {
|
|
67
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
68
|
+
throw createMediaInputError(`media[${index}] must be an object`, {
|
|
69
|
+
reason: "invalid_media_input",
|
|
70
|
+
mediaIndex: index,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const filePath = typeof item.path === "string" ? item.path.trim() : "";
|
|
74
|
+
const mimeType = typeof item.mimeType === "string" ? item.mimeType.trim().toLowerCase() : "";
|
|
75
|
+
const explicitKind = typeof item.kind === "string" ? item.kind.trim().toLowerCase() : "";
|
|
76
|
+
const kind = explicitKind || inferKind(mimeType);
|
|
77
|
+
if (!SUPPORTED_KINDS.has(kind)) {
|
|
78
|
+
throw createMediaInputError(`media[${index}] kind must be image`, {
|
|
79
|
+
reason: "invalid_media_input",
|
|
80
|
+
mediaIndex: index,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!filePath || !path.isAbsolute(filePath)) {
|
|
84
|
+
throw createMediaInputError(`media[${index}] path must be absolute`, {
|
|
85
|
+
reason: "invalid_media_input",
|
|
86
|
+
mediaIndex: index,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
let stat;
|
|
90
|
+
try {
|
|
91
|
+
const linkStat = fs.lstatSync(filePath);
|
|
92
|
+
if (linkStat.isSymbolicLink()) {
|
|
93
|
+
throw new Error("symbolic links are not allowed");
|
|
94
|
+
}
|
|
95
|
+
stat = fs.statSync(filePath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw createMediaInputError(`media[${index}] file does not exist`, {
|
|
99
|
+
reason: "invalid_media_input",
|
|
100
|
+
mediaIndex: index,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!stat.isFile()) {
|
|
104
|
+
throw createMediaInputError(`media[${index}] path must reference a file`, {
|
|
105
|
+
reason: "invalid_media_input",
|
|
106
|
+
mediaIndex: index,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const normalizedMimeType = mimeType || inferMimeType(filePath, kind);
|
|
110
|
+
if (!SUPPORTED_MIME_TYPES.has(normalizedMimeType) || inferKind(normalizedMimeType) !== kind) {
|
|
111
|
+
throw createMediaInputError(`media[${index}] kind does not match mimeType`, {
|
|
112
|
+
reason: "invalid_media_input",
|
|
113
|
+
mediaIndex: index,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!hasExpectedSignature(filePath, normalizedMimeType)) {
|
|
117
|
+
throw createMediaInputError(`media[${index}] content does not match mimeType`, {
|
|
118
|
+
reason: "invalid_media_input",
|
|
119
|
+
mediaIndex: index,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const itemLimit = MAX_IMAGE_BYTES;
|
|
123
|
+
if (stat.size > itemLimit) {
|
|
124
|
+
throw createMediaInputError(`media[${index}] exceeds the ${kind} size limit`, {
|
|
125
|
+
reason: "media_limit_exceeded",
|
|
126
|
+
mediaIndex: index,
|
|
127
|
+
maxBytes: itemLimit,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
totalBytes += stat.size;
|
|
131
|
+
if (totalBytes > MAX_TOTAL_MEDIA_BYTES) {
|
|
132
|
+
throw createMediaInputError("media exceeds the total size limit", {
|
|
133
|
+
reason: "media_limit_exceeded",
|
|
134
|
+
maxBytes: MAX_TOTAL_MEDIA_BYTES,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind,
|
|
139
|
+
path: filePath,
|
|
140
|
+
mimeType: normalizedMimeType,
|
|
141
|
+
name: typeof item.name === "string" && item.name.trim()
|
|
142
|
+
? path.basename(item.name.trim())
|
|
143
|
+
: path.basename(filePath),
|
|
144
|
+
sizeBytes: stat.size,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export function resolveTurnMedia(sessionOptions, turnOptions = {}) {
|
|
149
|
+
const media = normalizeMediaInputs(turnOptions.media);
|
|
150
|
+
if (!turnOptions.useInitialImages || !Array.isArray(sessionOptions?.initialImages)) {
|
|
151
|
+
return media;
|
|
152
|
+
}
|
|
153
|
+
const legacy = sessionOptions.initialImages
|
|
154
|
+
.filter((item) => typeof item === "string" && item.trim())
|
|
155
|
+
.map((filePath) => ({
|
|
156
|
+
kind: "image",
|
|
157
|
+
path: path.resolve(sessionOptions?.cwd || process.cwd(), filePath.trim()),
|
|
158
|
+
mimeType: inferMimeType(filePath.trim(), "image"),
|
|
159
|
+
}));
|
|
160
|
+
const combined = [...normalizeMediaInputs(legacy), ...media];
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
return combined.filter((item) => {
|
|
163
|
+
const key = `${item.kind}:${item.path}`;
|
|
164
|
+
if (seen.has(key))
|
|
165
|
+
return false;
|
|
166
|
+
seen.add(key);
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export function assertMediaCapabilities(media, backend, capabilities) {
|
|
171
|
+
for (const [index, item] of media.entries()) {
|
|
172
|
+
if (capabilities?.[item.kind] !== MEDIA_CAPABILITY_NATIVE) {
|
|
173
|
+
throw createMediaInputError(`${backend} does not support ${item.kind} input`, {
|
|
174
|
+
reason: "unsupported_media",
|
|
175
|
+
backend,
|
|
176
|
+
mediaKind: item.kind,
|
|
177
|
+
mediaIndex: index,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function fileMediaToDataUri(media) {
|
|
183
|
+
return `data:${media.mimeType};base64,${fs.readFileSync(media.path).toString("base64")}`;
|
|
184
|
+
}
|
|
185
|
+
export function fileMediaToBase64(media) {
|
|
186
|
+
return fs.readFileSync(media.path).toString("base64");
|
|
187
|
+
}
|
|
188
|
+
export function createMediaInputError(message, extras = {}) {
|
|
189
|
+
const error = new Error(message);
|
|
190
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
191
|
+
error[key] = value;
|
|
192
|
+
}
|
|
193
|
+
return error;
|
|
194
|
+
}
|
|
195
|
+
export function defaultPromptForMedia(media) {
|
|
196
|
+
return "Analyze the attached images.";
|
|
197
|
+
}
|
|
@@ -109,6 +109,11 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
109
109
|
session_file_path: undefined;
|
|
110
110
|
updated_at: string;
|
|
111
111
|
} | null;
|
|
112
|
+
capabilities: {
|
|
113
|
+
media: Readonly<{
|
|
114
|
+
image: "unsupported";
|
|
115
|
+
}>;
|
|
116
|
+
};
|
|
112
117
|
chatWebProvider: string;
|
|
113
118
|
providerConversationId: any;
|
|
114
119
|
providerUrl: string | undefined;
|
|
@@ -173,7 +178,8 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
173
178
|
* render "open in ChatGPT" / "open in AI Studio" links.
|
|
174
179
|
*/
|
|
175
180
|
providerConversationUrl(): string | undefined;
|
|
176
|
-
runTurn(promptText: any, { onProgress }?: {
|
|
181
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress }?: {
|
|
182
|
+
useInitialImages?: boolean | undefined;
|
|
177
183
|
onProgress?: null | undefined;
|
|
178
184
|
}): Promise<{
|
|
179
185
|
text: string;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { CHAT_WEB_SESSION_VARIANT } from "../built-in-backends.js";
|
|
3
|
+
import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
|
|
4
|
+
import { assertMediaCapabilities, resolveTurnMedia } from "../media-input.js";
|
|
5
|
+
import { normalizeContextFiles } from "../context-files.js";
|
|
3
6
|
import { emitLog, normalizeLogger } from "../shared.js";
|
|
4
7
|
const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini"]);
|
|
5
8
|
const DEFAULT_CHAT_WEB_PROVIDER = "chatgpt";
|
|
@@ -209,6 +212,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
209
212
|
? { ready: true, command: this.providerConversationUrl() }
|
|
210
213
|
: null,
|
|
211
214
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
215
|
+
capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
|
|
212
216
|
chatWebProvider: this.chatWebProvider,
|
|
213
217
|
providerConversationId: this.providerConversationId,
|
|
214
218
|
providerUrl: this.providerConversationUrl(),
|
|
@@ -401,7 +405,14 @@ export class ChatWebSession extends EventEmitter {
|
|
|
401
405
|
return undefined;
|
|
402
406
|
}
|
|
403
407
|
}
|
|
404
|
-
async runTurn(promptText, { onProgress = null } = {}) {
|
|
408
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null } = {}) {
|
|
409
|
+
if (normalizeContextFiles(contextFiles).length > 0) {
|
|
410
|
+
const error = new Error("chat-web does not support context file input");
|
|
411
|
+
error.reason = "unsupported_context_files";
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
|
|
415
|
+
assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT]);
|
|
405
416
|
const prompt = String(promptText || "").trim();
|
|
406
417
|
if (!prompt) {
|
|
407
418
|
return {
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
2
2
|
static capabilities: Readonly<{
|
|
3
3
|
goal: true;
|
|
4
|
+
media: Readonly<{
|
|
5
|
+
image: "native";
|
|
6
|
+
}>;
|
|
4
7
|
}>;
|
|
5
8
|
constructor(backend: any, options?: {});
|
|
6
9
|
getCapabilities(): {
|
|
7
10
|
goal: true;
|
|
11
|
+
media: Readonly<{
|
|
12
|
+
image: "native";
|
|
13
|
+
}>;
|
|
8
14
|
};
|
|
9
15
|
backend: string;
|
|
10
16
|
options: {};
|
|
@@ -68,6 +74,9 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
68
74
|
currentTurnStatus: any;
|
|
69
75
|
capabilities: {
|
|
70
76
|
goal: true;
|
|
77
|
+
media: Readonly<{
|
|
78
|
+
image: "native";
|
|
79
|
+
}>;
|
|
71
80
|
};
|
|
72
81
|
};
|
|
73
82
|
getSessionInfo(): {
|
|
@@ -131,7 +140,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
131
140
|
onProgress: any;
|
|
132
141
|
}): Promise<void>;
|
|
133
142
|
interruptCurrentTurn(): Promise<boolean>;
|
|
134
|
-
runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
|
|
143
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
|
|
135
144
|
useInitialImages?: boolean | undefined;
|
|
136
145
|
onProgress?: null | undefined;
|
|
137
146
|
jsonSchema?: null | undefined;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in-backends.js";
|
|
3
|
+
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
4
|
+
import { PROVIDER_MEDIA_CAPABILITIES, buildClaudeContent } from "../media-adapters.js";
|
|
5
|
+
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
3
6
|
import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
4
7
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
5
8
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
@@ -8,6 +11,16 @@ const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
|
8
11
|
function waitForever() {
|
|
9
12
|
return new Promise(() => { });
|
|
10
13
|
}
|
|
14
|
+
async function* buildClaudeInput(promptText, media) {
|
|
15
|
+
yield {
|
|
16
|
+
type: "user",
|
|
17
|
+
message: {
|
|
18
|
+
role: "user",
|
|
19
|
+
content: buildClaudeContent(promptText, media),
|
|
20
|
+
},
|
|
21
|
+
parent_tool_use_id: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
11
24
|
function createTurnError(message, extras = {}) {
|
|
12
25
|
const error = new Error(message);
|
|
13
26
|
for (const [key, value] of Object.entries(extras)) {
|
|
@@ -129,7 +142,10 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
129
142
|
// Capability advertised via getSnapshot().capabilities so worker proxies
|
|
130
143
|
// can short-circuit runGoal without an IPC round trip. Claude exposes
|
|
131
144
|
// native `/goal` slash command, so this is true.
|
|
132
|
-
static capabilities = Object.freeze({
|
|
145
|
+
static capabilities = Object.freeze({
|
|
146
|
+
goal: true,
|
|
147
|
+
media: PROVIDER_MEDIA_CAPABILITIES[CLAUDE_PROVIDER_VARIANT],
|
|
148
|
+
});
|
|
133
149
|
getCapabilities() {
|
|
134
150
|
return { ...ClaudeAgentSdkSession.capabilities };
|
|
135
151
|
}
|
|
@@ -722,11 +738,15 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
722
738
|
}
|
|
723
739
|
return true;
|
|
724
740
|
}
|
|
725
|
-
async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
|
|
741
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
|
|
726
742
|
if (this.closeRequested) {
|
|
727
743
|
throw this.createSessionClosedError();
|
|
728
744
|
}
|
|
729
|
-
const
|
|
745
|
+
const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
|
|
746
|
+
assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CLAUDE_PROVIDER_VARIANT]);
|
|
747
|
+
let effectivePrompt = this.buildPrompt(promptText, { useInitialImages: false }) ||
|
|
748
|
+
(media.length ? defaultPromptForMedia(media) : "");
|
|
749
|
+
effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
|
|
730
750
|
if (!effectivePrompt) {
|
|
731
751
|
return {
|
|
732
752
|
text: "",
|
|
@@ -776,7 +796,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
776
796
|
status_line: "claude is working",
|
|
777
797
|
}, onProgress);
|
|
778
798
|
const query = sdkModule.query({
|
|
779
|
-
prompt: effectivePrompt,
|
|
799
|
+
prompt: media.length ? buildClaudeInput(effectivePrompt, media) : effectivePrompt,
|
|
780
800
|
options: this.buildSdkOptions(abortController),
|
|
781
801
|
});
|
|
782
802
|
currentTurn.query = query;
|
|
@@ -139,6 +139,17 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
139
139
|
thread_id: any;
|
|
140
140
|
};
|
|
141
141
|
emitWorkingStatus(payload: any): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Idempotent terminal working-status clear (see BUG-R6-01 lesson). The
|
|
144
|
+
* "reply_in_progress:false" signal that unsticks the composer must be emitted
|
|
145
|
+
* from a reliable, always-arriving path, and multiple paths emitting it must
|
|
146
|
+
* be a safe no-op. A one-time guard on the turn object ensures we clear
|
|
147
|
+
* exactly once regardless of how many code paths reach here.
|
|
148
|
+
*/
|
|
149
|
+
emitTurnCompletedStatus(currentTurn: any, { phase, status_done_line }?: {
|
|
150
|
+
phase?: string | undefined;
|
|
151
|
+
status_done_line?: string | undefined;
|
|
152
|
+
}): Promise<void>;
|
|
142
153
|
emitAssistantMessage(text: any): Promise<void>;
|
|
143
154
|
finalizeActiveAssistantMessage(currentTurn?: {
|
|
144
155
|
turnId: string;
|
|
@@ -212,7 +223,7 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
212
223
|
handleTransportExit(payload: any): void;
|
|
213
224
|
maybeEmitAuthRequired(error: any): void;
|
|
214
225
|
interruptCurrentTurn(): Promise<boolean>;
|
|
215
|
-
runTurn(promptText: any, { useInitialImages, jsonSchema }?: {
|
|
226
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, jsonSchema }?: {
|
|
216
227
|
useInitialImages?: boolean | undefined;
|
|
217
228
|
jsonSchema?: null | undefined;
|
|
218
229
|
}): Promise<{
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { CODEX_APP_SERVER_VARIANT } from "../built-in-backends.js";
|
|
3
|
+
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
4
|
+
import { PROVIDER_MEDIA_CAPABILITIES, buildCodexAppServerInput } from "../media-adapters.js";
|
|
5
|
+
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
3
6
|
import { CodexAppServerTransport } from "../transports/codex-app-server-transport.js";
|
|
4
7
|
import { TERMINAL_GOAL_STATUSES, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
5
8
|
const TERMINAL_GOAL_STATUSES_LOCAL = new Set(TERMINAL_GOAL_STATUSES);
|
|
@@ -35,14 +38,6 @@ function extractSessionConfiguredValue(params, ...keys) {
|
|
|
35
38
|
}
|
|
36
39
|
return undefined;
|
|
37
40
|
}
|
|
38
|
-
function buildTurnInput(promptText) {
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
41
|
-
type: "text",
|
|
42
|
-
text: promptText,
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
41
|
function injectJsonSchemaPrompt(promptText, jsonSchema) {
|
|
47
42
|
const schemaText = typeof jsonSchema === "string" ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
|
|
48
43
|
return `You must respond with valid JSON that strictly conforms to the following JSON Schema. Do not include any markdown formatting or explanation outside the JSON object.
|
|
@@ -179,7 +174,10 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
179
174
|
* Treat as read-only.
|
|
180
175
|
* @type {Readonly<import("../shared.js").SessionCapabilities>}
|
|
181
176
|
*/
|
|
182
|
-
static capabilities = Object.freeze({
|
|
177
|
+
static capabilities = Object.freeze({
|
|
178
|
+
goal: true,
|
|
179
|
+
media: PROVIDER_MEDIA_CAPABILITIES[CODEX_APP_SERVER_VARIANT],
|
|
180
|
+
});
|
|
183
181
|
/**
|
|
184
182
|
* @returns {import("../shared.js").SessionCapabilities}
|
|
185
183
|
*/
|
|
@@ -489,6 +487,26 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
489
487
|
}
|
|
490
488
|
this.emit("working_status", snapshot);
|
|
491
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* Idempotent terminal working-status clear (see BUG-R6-01 lesson). The
|
|
492
|
+
* "reply_in_progress:false" signal that unsticks the composer must be emitted
|
|
493
|
+
* from a reliable, always-arriving path, and multiple paths emitting it must
|
|
494
|
+
* be a safe no-op. A one-time guard on the turn object ensures we clear
|
|
495
|
+
* exactly once regardless of how many code paths reach here.
|
|
496
|
+
*/
|
|
497
|
+
async emitTurnCompletedStatus(currentTurn, { phase = "turn_completed", status_done_line = "codex finished" } = {}) {
|
|
498
|
+
if (currentTurn && currentTurn.terminalStatusEmitted) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (currentTurn) {
|
|
502
|
+
currentTurn.terminalStatusEmitted = true;
|
|
503
|
+
}
|
|
504
|
+
await this.emitWorkingStatus({
|
|
505
|
+
phase,
|
|
506
|
+
reply_in_progress: false,
|
|
507
|
+
status_done_line,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
492
510
|
async emitAssistantMessage(text) {
|
|
493
511
|
this.touchTurnActivity();
|
|
494
512
|
const payload = {
|
|
@@ -869,7 +887,15 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
869
887
|
return;
|
|
870
888
|
}
|
|
871
889
|
case "turn/completed": {
|
|
872
|
-
|
|
890
|
+
let currentTurn = this.ensureCurrentTurn(params);
|
|
891
|
+
if (!currentTurn && this.currentTurn && !this.currentTurn.goalMode) {
|
|
892
|
+
// Defense-in-depth (BUG-R6-01 class): a non-goal session has exactly
|
|
893
|
+
// one active turn. If turn matching fails (missing `turn/started`, a
|
|
894
|
+
// turnId mismatch, or an id carried on a different field), we must NOT
|
|
895
|
+
// strand the terminal clear here — otherwise the composer stays on
|
|
896
|
+
// "codex composing reply" until the 12-min idle deadline or a reload.
|
|
897
|
+
currentTurn = this.currentTurn;
|
|
898
|
+
}
|
|
873
899
|
if (!currentTurn) {
|
|
874
900
|
return;
|
|
875
901
|
}
|
|
@@ -887,9 +913,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
887
913
|
turnStatus: status,
|
|
888
914
|
turnError,
|
|
889
915
|
});
|
|
890
|
-
await this.
|
|
916
|
+
await this.emitTurnCompletedStatus(currentTurn, {
|
|
891
917
|
phase: "turn_failed",
|
|
892
|
-
reply_in_progress: false,
|
|
893
918
|
status_done_line: `codex failed (${status})`,
|
|
894
919
|
});
|
|
895
920
|
if (currentTurn.goalMode) {
|
|
@@ -927,11 +952,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
927
952
|
currentTurn.activeAssistantMessageText = "";
|
|
928
953
|
return;
|
|
929
954
|
}
|
|
930
|
-
await this.
|
|
931
|
-
phase: "turn_completed",
|
|
932
|
-
reply_in_progress: false,
|
|
933
|
-
status_done_line: "codex finished",
|
|
934
|
-
});
|
|
955
|
+
await this.emitTurnCompletedStatus(currentTurn);
|
|
935
956
|
currentTurn.resolve({
|
|
936
957
|
turn,
|
|
937
958
|
usage: this.tokenUsage ? { ...this.tokenUsage } : null,
|
|
@@ -1006,11 +1027,15 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1006
1027
|
}
|
|
1007
1028
|
return false;
|
|
1008
1029
|
}
|
|
1009
|
-
async runTurn(promptText, { useInitialImages = false, jsonSchema = null } = {}) {
|
|
1030
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, jsonSchema = null } = {}) {
|
|
1010
1031
|
if (this.closeRequested) {
|
|
1011
1032
|
throw this.createSessionClosedError();
|
|
1012
1033
|
}
|
|
1013
|
-
|
|
1034
|
+
const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
|
|
1035
|
+
assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CODEX_APP_SERVER_VARIANT]);
|
|
1036
|
+
let effectivePrompt = this.buildPrompt(promptText, { useInitialImages: false }) ||
|
|
1037
|
+
(media.length ? defaultPromptForMedia(media) : "");
|
|
1038
|
+
effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
|
|
1014
1039
|
if (jsonSchema && typeof jsonSchema === "object" && effectivePrompt) {
|
|
1015
1040
|
effectivePrompt = injectJsonSchemaPrompt(effectivePrompt, jsonSchema);
|
|
1016
1041
|
}
|
|
@@ -1058,7 +1083,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1058
1083
|
threadId: this.sessionId,
|
|
1059
1084
|
cwd: this.cwd,
|
|
1060
1085
|
approvalPolicy: "never",
|
|
1061
|
-
input:
|
|
1086
|
+
input: buildCodexAppServerInput(effectivePrompt, media),
|
|
1062
1087
|
});
|
|
1063
1088
|
const turnResult = await Promise.race([
|
|
1064
1089
|
(async () => {
|
|
@@ -1075,6 +1100,10 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1075
1100
|
await this.finalizeActiveAssistantMessage(currentTurn, {
|
|
1076
1101
|
messageId: currentTurn.activeAssistantMessageId,
|
|
1077
1102
|
});
|
|
1103
|
+
// Belt-and-suspenders terminal clear. Idempotent (guarded on the turn),
|
|
1104
|
+
// so when the wire `turn/completed` already cleared this is a no-op; it
|
|
1105
|
+
// only fires if the turn resolved without the wire clear reaching here.
|
|
1106
|
+
await this.emitTurnCompletedStatus(currentTurn);
|
|
1078
1107
|
if (currentTurn.fullText) {
|
|
1079
1108
|
this.history.push({ role: "assistant", content: currentTurn.fullText });
|
|
1080
1109
|
}
|
|
@@ -52,6 +52,11 @@ export class CodexExecSession extends EventEmitter<[never]> {
|
|
|
52
52
|
resumeReady: boolean;
|
|
53
53
|
manualResume: null;
|
|
54
54
|
currentTurnStatus: any;
|
|
55
|
+
capabilities: {
|
|
56
|
+
media: Readonly<{
|
|
57
|
+
image: "native";
|
|
58
|
+
}>;
|
|
59
|
+
};
|
|
55
60
|
pid: any;
|
|
56
61
|
};
|
|
57
62
|
getSessionInfo(): {
|
|
@@ -85,13 +90,13 @@ export class CodexExecSession extends EventEmitter<[never]> {
|
|
|
85
90
|
emitWorkingStatus(payload: any, onProgress?: null): Promise<any>;
|
|
86
91
|
emitAssistantMessage(text: any): Promise<void>;
|
|
87
92
|
buildPrompt(promptText: any): string;
|
|
88
|
-
buildExecArgs({
|
|
89
|
-
|
|
93
|
+
buildExecArgs({ media, schemaFilePath, lastMessageFilePath }?: {
|
|
94
|
+
media?: never[] | undefined;
|
|
90
95
|
schemaFilePath?: string | undefined;
|
|
91
96
|
lastMessageFilePath?: string | undefined;
|
|
92
97
|
}): string[];
|
|
93
98
|
maybeEmitAuthRequired(stderrTail: any): void;
|
|
94
|
-
runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
|
|
99
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
|
|
95
100
|
useInitialImages?: boolean | undefined;
|
|
96
101
|
onProgress?: null | undefined;
|
|
97
102
|
jsonSchema?: null | undefined;
|
|
@@ -6,6 +6,9 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
7
|
import readline from "node:readline";
|
|
8
8
|
import { CODEX_EXEC_VARIANT as CODEX_EXEC_PROVIDER_VARIANT } from "../built-in-backends.js";
|
|
9
|
+
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
10
|
+
import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
|
|
11
|
+
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
9
12
|
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
10
13
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
11
14
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
@@ -239,6 +242,7 @@ export class CodexExecSession extends EventEmitter {
|
|
|
239
242
|
resumeReady: false,
|
|
240
243
|
manualResume: null,
|
|
241
244
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
245
|
+
capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[CODEX_EXEC_PROVIDER_VARIANT] },
|
|
242
246
|
pid: this.currentTurn?.child?.pid || undefined,
|
|
243
247
|
};
|
|
244
248
|
}
|
|
@@ -338,7 +342,7 @@ export class CodexExecSession extends EventEmitter {
|
|
|
338
342
|
buildPrompt(promptText) {
|
|
339
343
|
return buildHistoryPrompt(this.history, promptText);
|
|
340
344
|
}
|
|
341
|
-
buildExecArgs({
|
|
345
|
+
buildExecArgs({ media = [], schemaFilePath = "", lastMessageFilePath = "" } = {}) {
|
|
342
346
|
const args = [...this.baseArgs];
|
|
343
347
|
args.push("exec");
|
|
344
348
|
args.push("--json");
|
|
@@ -354,11 +358,8 @@ export class CodexExecSession extends EventEmitter {
|
|
|
354
358
|
if (typeof this.options.model === "string" && this.options.model.trim()) {
|
|
355
359
|
args.push("--model", this.options.model.trim());
|
|
356
360
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
: [];
|
|
360
|
-
for (const imagePath of images) {
|
|
361
|
-
args.push("--image", imagePath);
|
|
361
|
+
for (const image of media.filter((item) => item.kind === "image")) {
|
|
362
|
+
args.push("--image", image.path);
|
|
362
363
|
}
|
|
363
364
|
return args;
|
|
364
365
|
}
|
|
@@ -376,7 +377,7 @@ export class CodexExecSession extends EventEmitter {
|
|
|
376
377
|
message: lastMessage || "Codex authentication required",
|
|
377
378
|
});
|
|
378
379
|
}
|
|
379
|
-
async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
|
|
380
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
|
|
380
381
|
if (this.closeRequested || this.closed) {
|
|
381
382
|
throw this.createSessionClosedError();
|
|
382
383
|
}
|
|
@@ -385,12 +386,11 @@ export class CodexExecSession extends EventEmitter {
|
|
|
385
386
|
reason: "turn_already_running",
|
|
386
387
|
});
|
|
387
388
|
}
|
|
389
|
+
const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
|
|
390
|
+
assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CODEX_EXEC_PROVIDER_VARIANT]);
|
|
388
391
|
const effectivePrompt = this.buildPrompt(promptText);
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
: [];
|
|
392
|
-
const stdinPrompt = effectivePrompt || (imagePaths.length > 0 ? "Analyze the attached image." : "");
|
|
393
|
-
if (!stdinPrompt && imagePaths.length === 0) {
|
|
392
|
+
const stdinPrompt = appendContextFilesToPrompt(effectivePrompt || (media.length > 0 ? defaultPromptForMedia(media) : ""), contextFiles).prompt;
|
|
393
|
+
if (!stdinPrompt && media.length === 0) {
|
|
394
394
|
return buildEmptyTurnResult();
|
|
395
395
|
}
|
|
396
396
|
const normalizedSchema = normalizeJsonSchema(jsonSchema);
|
|
@@ -416,7 +416,7 @@ export class CodexExecSession extends EventEmitter {
|
|
|
416
416
|
status_line: statusLineForPhase("turn_started"),
|
|
417
417
|
}, onProgress);
|
|
418
418
|
const args = this.buildExecArgs({
|
|
419
|
-
|
|
419
|
+
media,
|
|
420
420
|
schemaFilePath,
|
|
421
421
|
lastMessageFilePath,
|
|
422
422
|
});
|
|
@@ -83,6 +83,11 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
|
|
|
83
83
|
command: string;
|
|
84
84
|
} | null;
|
|
85
85
|
currentTurnStatus: any;
|
|
86
|
+
capabilities: {
|
|
87
|
+
media: Readonly<{
|
|
88
|
+
image: "native";
|
|
89
|
+
}>;
|
|
90
|
+
};
|
|
86
91
|
};
|
|
87
92
|
getSessionInfo(): {
|
|
88
93
|
backend: string;
|
|
@@ -165,7 +170,7 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
|
|
|
165
170
|
handleCopilotEventFailure(error: any): void;
|
|
166
171
|
handleCopilotEvent(event: any): Promise<void>;
|
|
167
172
|
interruptCurrentTurn(): Promise<void>;
|
|
168
|
-
runTurn(promptText: any, { useInitialImages, onProgress }?: {
|
|
173
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress }?: {
|
|
169
174
|
useInitialImages?: boolean | undefined;
|
|
170
175
|
onProgress?: null | undefined;
|
|
171
176
|
}): Promise<{
|