@love-moon/ai-sdk 0.8.0 → 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 +6 -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 +1 -1
- package/dist/providers/codex-app-server-session.js +14 -12
- 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 +6 -1
- package/dist/providers/kimi-cli-session.js +13 -3
- 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;
|
|
@@ -223,7 +223,7 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
223
223
|
handleTransportExit(payload: any): void;
|
|
224
224
|
maybeEmitAuthRequired(error: any): void;
|
|
225
225
|
interruptCurrentTurn(): Promise<boolean>;
|
|
226
|
-
runTurn(promptText: any, { useInitialImages, jsonSchema }?: {
|
|
226
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, jsonSchema }?: {
|
|
227
227
|
useInitialImages?: boolean | undefined;
|
|
228
228
|
jsonSchema?: null | undefined;
|
|
229
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
|
*/
|
|
@@ -1029,11 +1027,15 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1029
1027
|
}
|
|
1030
1028
|
return false;
|
|
1031
1029
|
}
|
|
1032
|
-
async runTurn(promptText, { useInitialImages = false, jsonSchema = null } = {}) {
|
|
1030
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, jsonSchema = null } = {}) {
|
|
1033
1031
|
if (this.closeRequested) {
|
|
1034
1032
|
throw this.createSessionClosedError();
|
|
1035
1033
|
}
|
|
1036
|
-
|
|
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;
|
|
1037
1039
|
if (jsonSchema && typeof jsonSchema === "object" && effectivePrompt) {
|
|
1038
1040
|
effectivePrompt = injectJsonSchemaPrompt(effectivePrompt, jsonSchema);
|
|
1039
1041
|
}
|
|
@@ -1081,7 +1083,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1081
1083
|
threadId: this.sessionId,
|
|
1082
1084
|
cwd: this.cwd,
|
|
1083
1085
|
approvalPolicy: "never",
|
|
1084
|
-
input:
|
|
1086
|
+
input: buildCodexAppServerInput(effectivePrompt, media),
|
|
1085
1087
|
});
|
|
1086
1088
|
const turnResult = await Promise.race([
|
|
1087
1089
|
(async () => {
|
|
@@ -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<{
|
|
@@ -3,6 +3,9 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { COPILOT_SDK_VARIANT as COPILOT_PROVIDER_VARIANT } from "../built-in-backends.js";
|
|
6
|
+
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
7
|
+
import { PROVIDER_MEDIA_CAPABILITIES, buildCopilotAttachments } from "../media-adapters.js";
|
|
8
|
+
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
6
9
|
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, } from "../shared.js";
|
|
7
10
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
8
11
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
@@ -591,6 +594,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
591
594
|
}
|
|
592
595
|
: null,
|
|
593
596
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
597
|
+
capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT] },
|
|
594
598
|
};
|
|
595
599
|
}
|
|
596
600
|
getSessionInfo() {
|
|
@@ -1368,12 +1372,14 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1368
1372
|
}
|
|
1369
1373
|
}
|
|
1370
1374
|
}
|
|
1371
|
-
async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
|
|
1375
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null } = {}) {
|
|
1372
1376
|
if (this.currentTurn) {
|
|
1373
1377
|
throw this.createTurnAlreadyRunningError();
|
|
1374
1378
|
}
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1379
|
+
const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
|
|
1380
|
+
assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT]);
|
|
1381
|
+
const prompt = appendContextFilesToPrompt(this.buildPrompt(promptText, { useInitialImages: false }) || (media.length ? defaultPromptForMedia(media) : ""), contextFiles).prompt;
|
|
1382
|
+
if (!prompt && media.length === 0) {
|
|
1377
1383
|
return {
|
|
1378
1384
|
text: "",
|
|
1379
1385
|
usage: this.lastUsage ? { ...this.lastUsage } : null,
|
|
@@ -1419,6 +1425,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1419
1425
|
const completion = await Promise.race([
|
|
1420
1426
|
this.requestOrThrow(this.session.sendAndWait({
|
|
1421
1427
|
prompt,
|
|
1428
|
+
attachments: buildCopilotAttachments(media),
|
|
1422
1429
|
mode: "immediate",
|
|
1423
1430
|
}, this.turnDeadlineMs + SDK_SEND_AND_WAIT_TIMEOUT_GRACE_MS)),
|
|
1424
1431
|
closeGuard.promise,
|