@giselles-ai/sandbox-agent 0.1.8 → 0.1.10
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/dist/index.d.ts +24 -1
- package/dist/index.js +161 -13
- package/package.json +1 -10
- package/dist/react/index.d.ts +0 -48
- package/dist/react/index.js +0 -520
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ type ChatCommand = {
|
|
|
11
11
|
args: string[];
|
|
12
12
|
env?: Record<string, string>;
|
|
13
13
|
};
|
|
14
|
+
type StdoutMapper = {
|
|
15
|
+
push(chunk: string): string[];
|
|
16
|
+
flush(): string[];
|
|
17
|
+
};
|
|
14
18
|
type ChatAgent<TRequest extends BaseChatRequest> = {
|
|
15
19
|
requestSchema: z.ZodType<TRequest>;
|
|
16
20
|
snapshotId?: string;
|
|
@@ -21,6 +25,7 @@ type ChatAgent<TRequest extends BaseChatRequest> = {
|
|
|
21
25
|
createCommand(input: {
|
|
22
26
|
input: TRequest;
|
|
23
27
|
}): ChatCommand;
|
|
28
|
+
createStdoutMapper?: () => StdoutMapper;
|
|
24
29
|
};
|
|
25
30
|
type RunChatInput<TRequest extends BaseChatRequest> = {
|
|
26
31
|
agent: ChatAgent<TRequest>;
|
|
@@ -29,6 +34,24 @@ type RunChatInput<TRequest extends BaseChatRequest> = {
|
|
|
29
34
|
};
|
|
30
35
|
declare function runChat<TRequest extends BaseChatRequest>(input: RunChatInput<TRequest>): Promise<Response>;
|
|
31
36
|
|
|
37
|
+
declare const codexRequestSchema: z.ZodObject<{
|
|
38
|
+
message: z.ZodString;
|
|
39
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
40
|
+
sandbox_id: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
type CodexAgentRequest = z.infer<typeof codexRequestSchema>;
|
|
43
|
+
type CodexAgentOptions = {
|
|
44
|
+
snapshotId?: string;
|
|
45
|
+
env?: Record<string, string>;
|
|
46
|
+
};
|
|
47
|
+
declare function createCodexAgent(options?: CodexAgentOptions): ChatAgent<CodexAgentRequest>;
|
|
48
|
+
|
|
49
|
+
type CodexStdoutMapper = {
|
|
50
|
+
push(chunk: string): string[];
|
|
51
|
+
flush(): string[];
|
|
52
|
+
};
|
|
53
|
+
declare function createCodexStdoutMapper(): CodexStdoutMapper;
|
|
54
|
+
|
|
32
55
|
declare const geminiRequestSchema: z.ZodObject<{
|
|
33
56
|
message: z.ZodString;
|
|
34
57
|
session_id: z.ZodOptional<z.ZodString>;
|
|
@@ -48,4 +71,4 @@ type GeminiAgentOptions = {
|
|
|
48
71
|
};
|
|
49
72
|
declare function createGeminiAgent(options?: GeminiAgentOptions): ChatAgent<GeminiAgentRequest>;
|
|
50
73
|
|
|
51
|
-
export { type BaseChatRequest, type ChatAgent, type ChatCommand, type RunChatInput, createGeminiAgent, runChat };
|
|
74
|
+
export { type BaseChatRequest, type ChatAgent, type ChatCommand, type RunChatInput, type StdoutMapper, createCodexAgent, createCodexStdoutMapper, createGeminiAgent, runChat };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,93 @@
|
|
|
1
|
-
// src/agents/
|
|
1
|
+
// src/agents/codex-agent.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
// src/agents/codex-mapper.ts
|
|
5
|
+
function mapEvent(event) {
|
|
6
|
+
const type = event.type;
|
|
7
|
+
if (type === "session.created") {
|
|
8
|
+
return {
|
|
9
|
+
type: "init",
|
|
10
|
+
session_id: event.id ?? void 0,
|
|
11
|
+
modelId: event.model ?? void 0
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (type === "message.output_text.delta") {
|
|
15
|
+
return {
|
|
16
|
+
type: "message",
|
|
17
|
+
role: "assistant",
|
|
18
|
+
content: event.delta ?? "",
|
|
19
|
+
delta: true
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (type === "message.output_text.done") {
|
|
23
|
+
return {
|
|
24
|
+
type: "message",
|
|
25
|
+
role: "assistant",
|
|
26
|
+
content: event.text ?? "",
|
|
27
|
+
delta: false
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (type === "error") {
|
|
31
|
+
return {
|
|
32
|
+
type: "stderr",
|
|
33
|
+
content: typeof event.message === "string" ? event.message : JSON.stringify(event)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (type === "response.completed") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function processLines(text) {
|
|
42
|
+
const lines = text.split("\n");
|
|
43
|
+
const completeLines = lines.slice(0, -1);
|
|
44
|
+
const output = [];
|
|
45
|
+
for (const line of completeLines) {
|
|
46
|
+
const trimmedLine = line.trim();
|
|
47
|
+
if (trimmedLine.length === 0) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const event = JSON.parse(trimmedLine);
|
|
52
|
+
const mapped = mapEvent(event);
|
|
53
|
+
if (!mapped) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
output.push(`${JSON.stringify(mapped)}
|
|
57
|
+
`);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
function createCodexStdoutMapper() {
|
|
64
|
+
let buffer = "";
|
|
65
|
+
return {
|
|
66
|
+
push(chunk) {
|
|
67
|
+
buffer += chunk;
|
|
68
|
+
const combined = buffer;
|
|
69
|
+
const lines = combined.split("\n");
|
|
70
|
+
buffer = lines.pop() ?? "";
|
|
71
|
+
return processLines(`${lines.join("\n")}
|
|
72
|
+
`);
|
|
73
|
+
},
|
|
74
|
+
flush() {
|
|
75
|
+
if (buffer.trim().length === 0) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const remaining = buffer;
|
|
79
|
+
buffer = "";
|
|
80
|
+
return processLines(`${remaining}
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/agents/codex-agent.ts
|
|
87
|
+
var codexRequestSchema = z.object({
|
|
5
88
|
message: z.string().min(1),
|
|
6
89
|
session_id: z.string().min(1).optional(),
|
|
7
|
-
sandbox_id: z.string().min(1).optional()
|
|
8
|
-
relay_session_id: z.string().min(1).optional(),
|
|
9
|
-
relay_token: z.string().min(1).optional()
|
|
90
|
+
sandbox_id: z.string().min(1).optional()
|
|
10
91
|
});
|
|
11
92
|
function requiredEnv(env, name) {
|
|
12
93
|
const value = env[name]?.trim();
|
|
@@ -15,6 +96,59 @@ function requiredEnv(env, name) {
|
|
|
15
96
|
}
|
|
16
97
|
return value;
|
|
17
98
|
}
|
|
99
|
+
function createCodexAgent(options = {}) {
|
|
100
|
+
const env = options.env ?? {};
|
|
101
|
+
const snapshotId = options.snapshotId?.trim() || requiredEnv(env, "SANDBOX_SNAPSHOT_ID");
|
|
102
|
+
const apiKey = env.CODEX_API_KEY?.trim() || env.OPENAI_API_KEY?.trim();
|
|
103
|
+
if (!apiKey) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Missing required environment variable: CODEX_API_KEY or OPENAI_API_KEY"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
requestSchema: codexRequestSchema,
|
|
110
|
+
snapshotId,
|
|
111
|
+
async prepareSandbox(_input) {
|
|
112
|
+
},
|
|
113
|
+
createCommand({ input }) {
|
|
114
|
+
const args = [
|
|
115
|
+
"exec",
|
|
116
|
+
"--json",
|
|
117
|
+
"--yolo",
|
|
118
|
+
"--skip-git-repo-check",
|
|
119
|
+
input.message
|
|
120
|
+
];
|
|
121
|
+
return {
|
|
122
|
+
cmd: "codex",
|
|
123
|
+
args,
|
|
124
|
+
env: {
|
|
125
|
+
OPENAI_API_KEY: apiKey
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
createStdoutMapper() {
|
|
130
|
+
return createCodexStdoutMapper();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/agents/gemini-agent.ts
|
|
136
|
+
import { z as z2 } from "zod";
|
|
137
|
+
var GEMINI_SETTINGS_PATH = "/home/vercel-sandbox/.gemini/settings.json";
|
|
138
|
+
var geminiRequestSchema = z2.object({
|
|
139
|
+
message: z2.string().min(1),
|
|
140
|
+
session_id: z2.string().min(1).optional(),
|
|
141
|
+
sandbox_id: z2.string().min(1).optional(),
|
|
142
|
+
relay_session_id: z2.string().min(1).optional(),
|
|
143
|
+
relay_token: z2.string().min(1).optional()
|
|
144
|
+
});
|
|
145
|
+
function requiredEnv2(env, name) {
|
|
146
|
+
const value = env[name]?.trim();
|
|
147
|
+
if (!value) {
|
|
148
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
18
152
|
async function patchGeminiSettingsTransportEnv(sandbox, bridgeTransportEnv) {
|
|
19
153
|
const buffer = await sandbox.readFileToBuffer({
|
|
20
154
|
path: GEMINI_SETTINGS_PATH
|
|
@@ -71,13 +205,13 @@ function assertBrowserToolRelayCredentials(parsed) {
|
|
|
71
205
|
}
|
|
72
206
|
function createGeminiAgent(options = {}) {
|
|
73
207
|
const env = options.env ?? {};
|
|
74
|
-
const snapshotId = options.snapshotId?.trim() ||
|
|
208
|
+
const snapshotId = options.snapshotId?.trim() || requiredEnv2(env, "SANDBOX_SNAPSHOT_ID");
|
|
75
209
|
const browserToolEnabled = options.tools?.browser !== void 0;
|
|
76
210
|
const browserToolRelayUrl = options.tools?.browser?.relayUrl?.trim();
|
|
77
211
|
if (browserToolEnabled) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
212
|
+
requiredEnv2(env, "BROWSER_TOOL_RELAY_URL");
|
|
213
|
+
requiredEnv2(env, "BROWSER_TOOL_RELAY_SESSION_ID");
|
|
214
|
+
requiredEnv2(env, "BROWSER_TOOL_RELAY_TOKEN");
|
|
81
215
|
}
|
|
82
216
|
if (browserToolEnabled && !browserToolRelayUrl) {
|
|
83
217
|
throw new Error("tools.browser.relayUrl is empty.");
|
|
@@ -89,7 +223,7 @@ function createGeminiAgent(options = {}) {
|
|
|
89
223
|
if (!browserToolEnabled) {
|
|
90
224
|
return;
|
|
91
225
|
}
|
|
92
|
-
|
|
226
|
+
requiredEnv2(env, "VERCEL_OIDC_TOKEN");
|
|
93
227
|
assertBrowserToolRelayCredentials(input);
|
|
94
228
|
await patchGeminiSettingsTransportEnv(sandbox, env);
|
|
95
229
|
},
|
|
@@ -109,7 +243,7 @@ function createGeminiAgent(options = {}) {
|
|
|
109
243
|
cmd: "gemini",
|
|
110
244
|
args,
|
|
111
245
|
env: {
|
|
112
|
-
GEMINI_API_KEY:
|
|
246
|
+
GEMINI_API_KEY: requiredEnv2(env, "GEMINI_API_KEY")
|
|
113
247
|
}
|
|
114
248
|
};
|
|
115
249
|
}
|
|
@@ -169,6 +303,7 @@ function runChat(input) {
|
|
|
169
303
|
}
|
|
170
304
|
emitText(controller, text, encoder);
|
|
171
305
|
};
|
|
306
|
+
const mapper = input.agent.createStdoutMapper?.();
|
|
172
307
|
void (async () => {
|
|
173
308
|
try {
|
|
174
309
|
const sandbox = parsed.sandbox_id ? await Sandbox.get({ sandboxId: parsed.sandbox_id }) : await (async () => {
|
|
@@ -200,7 +335,13 @@ function runChat(input) {
|
|
|
200
335
|
stdout: new Writable({
|
|
201
336
|
write(chunk, _encoding, callback) {
|
|
202
337
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
203
|
-
|
|
338
|
+
if (mapper) {
|
|
339
|
+
for (const line of mapper.push(text)) {
|
|
340
|
+
enqueueStdout(line);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
enqueueStdout(text);
|
|
344
|
+
}
|
|
204
345
|
callback();
|
|
205
346
|
}
|
|
206
347
|
}),
|
|
@@ -220,6 +361,11 @@ function runChat(input) {
|
|
|
220
361
|
const message = error instanceof Error ? error.message : String(error);
|
|
221
362
|
enqueueEvent({ type: "stderr", content: `[error] ${message}` });
|
|
222
363
|
} finally {
|
|
364
|
+
if (mapper) {
|
|
365
|
+
for (const line of mapper.flush()) {
|
|
366
|
+
enqueueStdout(line);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
223
369
|
input.signal.removeEventListener("abort", onAbort);
|
|
224
370
|
close();
|
|
225
371
|
}
|
|
@@ -236,6 +382,8 @@ function runChat(input) {
|
|
|
236
382
|
);
|
|
237
383
|
}
|
|
238
384
|
export {
|
|
385
|
+
createCodexAgent,
|
|
386
|
+
createCodexStdoutMapper,
|
|
239
387
|
createGeminiAgent,
|
|
240
388
|
runChat
|
|
241
389
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giselles-ai/sandbox-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,11 +17,6 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/index.js",
|
|
19
19
|
"default": "./dist/index.js"
|
|
20
|
-
},
|
|
21
|
-
"./react": {
|
|
22
|
-
"types": "./dist/react/index.d.ts",
|
|
23
|
-
"import": "./dist/react/index.js",
|
|
24
|
-
"default": "./dist/react/index.js"
|
|
25
20
|
}
|
|
26
21
|
},
|
|
27
22
|
"scripts": {
|
|
@@ -35,10 +30,6 @@
|
|
|
35
30
|
"@vercel/sandbox": "1.6.0",
|
|
36
31
|
"zod": "4.3.6"
|
|
37
32
|
},
|
|
38
|
-
"peerDependencies": {
|
|
39
|
-
"react": ">=19.0.0",
|
|
40
|
-
"react-dom": ">=19.0.0"
|
|
41
|
-
},
|
|
42
33
|
"devDependencies": {
|
|
43
34
|
"@google/gemini-cli-core": "0.29.5",
|
|
44
35
|
"@types/node": "25.3.0",
|
package/dist/react/index.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
type AgentMessage = {
|
|
2
|
-
id: string;
|
|
3
|
-
role: "user" | "assistant";
|
|
4
|
-
content: string;
|
|
5
|
-
};
|
|
6
|
-
type StreamEvent = {
|
|
7
|
-
type?: string;
|
|
8
|
-
[key: string]: unknown;
|
|
9
|
-
};
|
|
10
|
-
type ToolEvent = {
|
|
11
|
-
id: string;
|
|
12
|
-
toolId: string;
|
|
13
|
-
toolName: string;
|
|
14
|
-
status?: "success" | "error";
|
|
15
|
-
parameters?: unknown;
|
|
16
|
-
output?: unknown;
|
|
17
|
-
};
|
|
18
|
-
type RelayStatus = "idle" | "connecting" | "connected" | "error";
|
|
19
|
-
type AgentStatus = RelayStatus | "running";
|
|
20
|
-
type AgentToolContext = {
|
|
21
|
-
sendRelayResponse: (payload: Record<string, unknown>) => Promise<void>;
|
|
22
|
-
setError: (message: string) => void;
|
|
23
|
-
addWarnings: (warnings: string[]) => void;
|
|
24
|
-
};
|
|
25
|
-
type AgentToolHandler = {
|
|
26
|
-
handleRelayRequest: (event: StreamEvent, context: AgentToolContext) => Promise<boolean>;
|
|
27
|
-
};
|
|
28
|
-
type UseAgentOptions = {
|
|
29
|
-
endpoint: string;
|
|
30
|
-
tools?: Record<string, AgentToolHandler>;
|
|
31
|
-
};
|
|
32
|
-
type AgentHookState = {
|
|
33
|
-
status: AgentStatus;
|
|
34
|
-
messages: AgentMessage[];
|
|
35
|
-
tools: ToolEvent[];
|
|
36
|
-
warnings: string[];
|
|
37
|
-
stderrLogs: string[];
|
|
38
|
-
sandboxId: string | null;
|
|
39
|
-
geminiSessionId: string | null;
|
|
40
|
-
error: string | null;
|
|
41
|
-
sendMessage: (input: {
|
|
42
|
-
message: string;
|
|
43
|
-
document?: string;
|
|
44
|
-
}) => Promise<void>;
|
|
45
|
-
};
|
|
46
|
-
declare function useAgent({ endpoint, tools }: UseAgentOptions): AgentHookState;
|
|
47
|
-
|
|
48
|
-
export { type AgentHookState, type AgentMessage, type AgentStatus, type AgentToolContext, type AgentToolHandler, type ToolEvent, type UseAgentOptions, useAgent };
|
package/dist/react/index.js
DELETED
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
// src/react/use-agent.ts
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
var LOG_PREFIX = "[agent-relay-client]";
|
|
4
|
-
function isRecord(value) {
|
|
5
|
-
return Boolean(value) && typeof value === "object";
|
|
6
|
-
}
|
|
7
|
-
function asString(value) {
|
|
8
|
-
return typeof value === "string" ? value : null;
|
|
9
|
-
}
|
|
10
|
-
function asNumber(value) {
|
|
11
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
12
|
-
}
|
|
13
|
-
function extractWarnings(value) {
|
|
14
|
-
if (!isRecord(value) || !Array.isArray(value.warnings)) {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
return value.warnings.filter(
|
|
18
|
-
(warning) => typeof warning === "string"
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
function extractJsonObjects(buffer) {
|
|
22
|
-
const objects = [];
|
|
23
|
-
let depth = 0;
|
|
24
|
-
let inString = false;
|
|
25
|
-
let escaped = false;
|
|
26
|
-
let startIndex = -1;
|
|
27
|
-
for (let index = 0; index < buffer.length; index += 1) {
|
|
28
|
-
const char = buffer[index];
|
|
29
|
-
if (escaped) {
|
|
30
|
-
escaped = false;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (char === "\\") {
|
|
34
|
-
escaped = true;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (char === '"') {
|
|
38
|
-
inString = !inString;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (inString) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (char === "{") {
|
|
45
|
-
if (depth === 0) {
|
|
46
|
-
startIndex = index;
|
|
47
|
-
}
|
|
48
|
-
depth += 1;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (char === "}") {
|
|
52
|
-
depth -= 1;
|
|
53
|
-
if (depth === 0 && startIndex >= 0) {
|
|
54
|
-
objects.push(buffer.slice(startIndex, index + 1));
|
|
55
|
-
startIndex = -1;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (depth > 0 && startIndex >= 0) {
|
|
60
|
-
return {
|
|
61
|
-
objects,
|
|
62
|
-
rest: buffer.slice(startIndex)
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
objects,
|
|
67
|
-
rest: ""
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function dedupeStringArray(next) {
|
|
71
|
-
const nextSet = new Set(next);
|
|
72
|
-
return Array.from(nextSet);
|
|
73
|
-
}
|
|
74
|
-
function normalizeTools(tools) {
|
|
75
|
-
if (!tools) {
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
return Object.values(tools);
|
|
79
|
-
}
|
|
80
|
-
function useAgent({ endpoint, tools }) {
|
|
81
|
-
const [relayStatus, setRelayStatus] = useState("idle");
|
|
82
|
-
const [running, setRunning] = useState(false);
|
|
83
|
-
const [messages, setMessages] = useState([]);
|
|
84
|
-
const [toolEvents, setToolEvents] = useState([]);
|
|
85
|
-
const [warnings, setWarnings] = useState([]);
|
|
86
|
-
const [stderrLogs, setStderrLogs] = useState([]);
|
|
87
|
-
const [sandboxId, setSandboxId] = useState(null);
|
|
88
|
-
const [geminiSessionId, setGeminiSessionId] = useState(null);
|
|
89
|
-
const [error, setError] = useState(null);
|
|
90
|
-
const [toolEventHandlers] = useMemo(() => [normalizeTools(tools)], [tools]);
|
|
91
|
-
const normalizedEndpoint = useMemo(
|
|
92
|
-
() => endpoint.replace(/\/+$/, ""),
|
|
93
|
-
[endpoint]
|
|
94
|
-
);
|
|
95
|
-
const eventSourceRef = useRef(null);
|
|
96
|
-
const reconnectTimerRef = useRef(null);
|
|
97
|
-
const mountedRef = useRef(true);
|
|
98
|
-
const sessionRef = useRef(null);
|
|
99
|
-
const messagesAssistantId = useRef(null);
|
|
100
|
-
const assistantBufferRef = useRef("");
|
|
101
|
-
const cleanupRelay = useCallback(() => {
|
|
102
|
-
if (eventSourceRef.current) {
|
|
103
|
-
eventSourceRef.current.close();
|
|
104
|
-
eventSourceRef.current = null;
|
|
105
|
-
}
|
|
106
|
-
if (reconnectTimerRef.current) {
|
|
107
|
-
clearTimeout(reconnectTimerRef.current);
|
|
108
|
-
reconnectTimerRef.current = null;
|
|
109
|
-
}
|
|
110
|
-
}, []);
|
|
111
|
-
const handleRelayResponse = useCallback(
|
|
112
|
-
async (payload) => {
|
|
113
|
-
const currentSession = sessionRef.current;
|
|
114
|
-
if (!currentSession) {
|
|
115
|
-
console.warn(`${LOG_PREFIX} respond.skip.no-session`, {
|
|
116
|
-
payloadType: payload.type
|
|
117
|
-
});
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const requestId = typeof payload.requestId === "string" ? payload.requestId : void 0;
|
|
121
|
-
const responseType = typeof payload.type === "string" ? payload.type : void 0;
|
|
122
|
-
console.info(`${LOG_PREFIX} respond.out`, {
|
|
123
|
-
sessionId: currentSession.sessionId,
|
|
124
|
-
requestId,
|
|
125
|
-
responseType
|
|
126
|
-
});
|
|
127
|
-
const relayBase = currentSession.relayUrl ?? normalizedEndpoint;
|
|
128
|
-
const response = await fetch(relayBase, {
|
|
129
|
-
method: "POST",
|
|
130
|
-
headers: {
|
|
131
|
-
"content-type": "application/json"
|
|
132
|
-
},
|
|
133
|
-
body: JSON.stringify({
|
|
134
|
-
type: "relay.respond",
|
|
135
|
-
sessionId: currentSession.sessionId,
|
|
136
|
-
token: currentSession.token,
|
|
137
|
-
response: payload
|
|
138
|
-
})
|
|
139
|
-
});
|
|
140
|
-
console.info(`${LOG_PREFIX} respond.result`, {
|
|
141
|
-
sessionId: currentSession.sessionId,
|
|
142
|
-
requestId,
|
|
143
|
-
status: response.status,
|
|
144
|
-
ok: response.ok
|
|
145
|
-
});
|
|
146
|
-
},
|
|
147
|
-
[normalizedEndpoint]
|
|
148
|
-
);
|
|
149
|
-
const addWarnings = useCallback((next) => {
|
|
150
|
-
if (next.length === 0) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
setWarnings((current) => dedupeStringArray([...current, ...next]));
|
|
154
|
-
}, []);
|
|
155
|
-
const handleRelayEvent = useCallback(
|
|
156
|
-
async (event) => {
|
|
157
|
-
if (!isRecord(event) || typeof event.type !== "string") {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (event.type === "ready") {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const requestId = asString(event.requestId);
|
|
164
|
-
if (!requestId) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (event.type === "error_response") {
|
|
168
|
-
const message = asString(event.message);
|
|
169
|
-
if (message) {
|
|
170
|
-
setError(message);
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (event.type === "snapshot_request" || event.type === "execute_request") {
|
|
175
|
-
const handlerContext = {
|
|
176
|
-
sendRelayResponse: handleRelayResponse,
|
|
177
|
-
setError,
|
|
178
|
-
addWarnings
|
|
179
|
-
};
|
|
180
|
-
for (const handler of toolEventHandlers) {
|
|
181
|
-
try {
|
|
182
|
-
const handled = await handler.handleRelayRequest(
|
|
183
|
-
event,
|
|
184
|
-
handlerContext
|
|
185
|
-
);
|
|
186
|
-
if (handled) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
} catch (error2) {
|
|
190
|
-
const message = error2 instanceof Error ? error2.message : "Relay execution failed.";
|
|
191
|
-
setError(message);
|
|
192
|
-
setRelayStatus("error");
|
|
193
|
-
await handleRelayResponse({
|
|
194
|
-
type: "error_response",
|
|
195
|
-
requestId,
|
|
196
|
-
message
|
|
197
|
-
});
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
await handleRelayResponse({
|
|
202
|
-
type: "error_response",
|
|
203
|
-
requestId,
|
|
204
|
-
message: `Unsupported relay request type: ${event.type}`
|
|
205
|
-
});
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
await handleRelayResponse({
|
|
209
|
-
type: "error_response",
|
|
210
|
-
requestId,
|
|
211
|
-
message: `Unsupported relay request type: ${event.type}`
|
|
212
|
-
});
|
|
213
|
-
},
|
|
214
|
-
[addWarnings, handleRelayResponse, toolEventHandlers]
|
|
215
|
-
);
|
|
216
|
-
const connect = useCallback(() => {
|
|
217
|
-
const currentSession = sessionRef.current;
|
|
218
|
-
if (!currentSession) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
cleanupRelay();
|
|
222
|
-
setRelayStatus("connecting");
|
|
223
|
-
const relayBase = currentSession.relayUrl ?? normalizedEndpoint;
|
|
224
|
-
const source = new EventSource(
|
|
225
|
-
`${relayBase}?type=relay.events&sessionId=${encodeURIComponent(currentSession.sessionId)}&token=${encodeURIComponent(currentSession.token)}`
|
|
226
|
-
);
|
|
227
|
-
console.info(`${LOG_PREFIX} sse.connect`, {
|
|
228
|
-
sessionId: currentSession.sessionId
|
|
229
|
-
});
|
|
230
|
-
eventSourceRef.current = source;
|
|
231
|
-
source.onopen = () => {
|
|
232
|
-
if (!mountedRef.current) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
setRelayStatus("connected");
|
|
236
|
-
console.info(`${LOG_PREFIX} sse.open`, {
|
|
237
|
-
sessionId: currentSession.sessionId
|
|
238
|
-
});
|
|
239
|
-
};
|
|
240
|
-
source.onmessage = (message) => {
|
|
241
|
-
console.info(`${LOG_PREFIX} sse.message.raw`, {
|
|
242
|
-
sessionId: currentSession.sessionId,
|
|
243
|
-
data: message.data
|
|
244
|
-
});
|
|
245
|
-
try {
|
|
246
|
-
const payload = JSON.parse(message.data);
|
|
247
|
-
console.info(`${LOG_PREFIX} sse.message.parsed`, {
|
|
248
|
-
sessionId: currentSession.sessionId,
|
|
249
|
-
type: typeof payload === "object" && payload && "type" in payload && typeof payload.type === "string" ? payload.type : "unknown"
|
|
250
|
-
});
|
|
251
|
-
void handleRelayEvent(payload);
|
|
252
|
-
} catch (error2) {
|
|
253
|
-
console.error(`${LOG_PREFIX} sse.message.parse_error`, {
|
|
254
|
-
sessionId: currentSession.sessionId,
|
|
255
|
-
error: error2 instanceof Error ? error2.message : String(error2),
|
|
256
|
-
data: message.data
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
source.onerror = () => {
|
|
261
|
-
if (!mountedRef.current) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
setRelayStatus("connecting");
|
|
265
|
-
console.warn(`${LOG_PREFIX} sse.error`, {
|
|
266
|
-
sessionId: currentSession.sessionId
|
|
267
|
-
});
|
|
268
|
-
if (!reconnectTimerRef.current) {
|
|
269
|
-
reconnectTimerRef.current = setTimeout(() => {
|
|
270
|
-
reconnectTimerRef.current = null;
|
|
271
|
-
connect();
|
|
272
|
-
}, 1500);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
}, [cleanupRelay, handleRelayEvent, normalizedEndpoint]);
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
mountedRef.current = true;
|
|
278
|
-
return () => {
|
|
279
|
-
mountedRef.current = false;
|
|
280
|
-
cleanupRelay();
|
|
281
|
-
};
|
|
282
|
-
}, [cleanupRelay]);
|
|
283
|
-
const handleStreamEvent = useCallback(
|
|
284
|
-
(event) => {
|
|
285
|
-
if (typeof event.type !== "string") {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (event.type === "relay.session") {
|
|
289
|
-
const sessionId = asString(event.sessionId);
|
|
290
|
-
const token = asString(event.token);
|
|
291
|
-
const relayUrl = asString(event.relayUrl);
|
|
292
|
-
const expiresAt = asNumber(event.expiresAt) ?? Date.now() + 10 * 60 * 1e3;
|
|
293
|
-
if (!sessionId || !token) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
sessionRef.current = {
|
|
297
|
-
sessionId,
|
|
298
|
-
token,
|
|
299
|
-
expiresAt,
|
|
300
|
-
relayUrl
|
|
301
|
-
};
|
|
302
|
-
console.info(`${LOG_PREFIX} stream.relay_session`, {
|
|
303
|
-
sessionId
|
|
304
|
-
});
|
|
305
|
-
connect();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (event.type === "sandbox") {
|
|
309
|
-
const nextSandboxId = asString(event.sandbox_id);
|
|
310
|
-
if (nextSandboxId) {
|
|
311
|
-
setSandboxId(nextSandboxId);
|
|
312
|
-
}
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (event.type === "init") {
|
|
316
|
-
const nextSessionId = asString(event.session_id);
|
|
317
|
-
if (nextSessionId) {
|
|
318
|
-
setGeminiSessionId(nextSessionId);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (event.type === "stderr") {
|
|
323
|
-
const text = asString(event.content);
|
|
324
|
-
if (text) {
|
|
325
|
-
setStderrLogs((current) => [...current, text]);
|
|
326
|
-
}
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
if (event.type === "message") {
|
|
330
|
-
const role = asString(event.role);
|
|
331
|
-
const content = asString(event.content) ?? "";
|
|
332
|
-
const isDelta = Boolean(event.delta);
|
|
333
|
-
if (role === "assistant") {
|
|
334
|
-
if (isDelta) {
|
|
335
|
-
const currentId = messagesAssistantId.current;
|
|
336
|
-
if (!currentId) {
|
|
337
|
-
const nextId = crypto.randomUUID();
|
|
338
|
-
messagesAssistantId.current = nextId;
|
|
339
|
-
assistantBufferRef.current = content;
|
|
340
|
-
setMessages((current) => [
|
|
341
|
-
...current,
|
|
342
|
-
{ id: nextId, role: "assistant", content }
|
|
343
|
-
]);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const merged = `${assistantBufferRef.current}${content}`;
|
|
347
|
-
assistantBufferRef.current = merged;
|
|
348
|
-
setMessages(
|
|
349
|
-
(current) => current.map(
|
|
350
|
-
(message) => message.id === currentId ? { ...message, content: merged } : message
|
|
351
|
-
)
|
|
352
|
-
);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (content.trim().length > 0) {
|
|
356
|
-
const nextId = crypto.randomUUID();
|
|
357
|
-
messagesAssistantId.current = nextId;
|
|
358
|
-
assistantBufferRef.current = content;
|
|
359
|
-
setMessages((current) => [
|
|
360
|
-
...current,
|
|
361
|
-
{ id: nextId, role: "assistant", content }
|
|
362
|
-
]);
|
|
363
|
-
}
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (role === "user" && content.trim().length > 0) {
|
|
367
|
-
setMessages((current) => {
|
|
368
|
-
const last = current[current.length - 1];
|
|
369
|
-
if (last && last.role === "user" && last.content === content) {
|
|
370
|
-
return current;
|
|
371
|
-
}
|
|
372
|
-
return [
|
|
373
|
-
...current,
|
|
374
|
-
{ id: crypto.randomUUID(), role: "user", content }
|
|
375
|
-
];
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
if (event.type === "tool_use") {
|
|
381
|
-
const toolId = asString(event.tool_id) ?? crypto.randomUUID();
|
|
382
|
-
const toolName = asString(event.tool_name) ?? "tool";
|
|
383
|
-
setToolEvents((current) => [
|
|
384
|
-
...current,
|
|
385
|
-
{
|
|
386
|
-
id: crypto.randomUUID(),
|
|
387
|
-
toolId,
|
|
388
|
-
toolName,
|
|
389
|
-
parameters: event.parameters
|
|
390
|
-
}
|
|
391
|
-
]);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (event.type === "tool_result") {
|
|
395
|
-
const toolId = asString(event.tool_id);
|
|
396
|
-
if (!toolId) {
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
const status2 = asString(event.status);
|
|
400
|
-
const nextStatus = status2 === "success" || status2 === "error" ? status2 : void 0;
|
|
401
|
-
setToolEvents(
|
|
402
|
-
(current) => current.map(
|
|
403
|
-
(tool) => tool.toolId === toolId ? {
|
|
404
|
-
...tool,
|
|
405
|
-
status: nextStatus,
|
|
406
|
-
output: event.output
|
|
407
|
-
} : tool
|
|
408
|
-
)
|
|
409
|
-
);
|
|
410
|
-
const parsedWarnings = extractWarnings(event.output);
|
|
411
|
-
if (parsedWarnings.length > 0) {
|
|
412
|
-
setWarnings(
|
|
413
|
-
(current) => dedupeStringArray([...current, ...parsedWarnings])
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
[connect]
|
|
419
|
-
);
|
|
420
|
-
const sendMessage = useCallback(
|
|
421
|
-
async ({ message, document }) => {
|
|
422
|
-
const trimmedMessage = message.trim();
|
|
423
|
-
if (!trimmedMessage || running) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
cleanupRelay();
|
|
427
|
-
sessionRef.current = null;
|
|
428
|
-
setRelayStatus("idle");
|
|
429
|
-
setError(null);
|
|
430
|
-
setRunning(true);
|
|
431
|
-
assistantBufferRef.current = "";
|
|
432
|
-
messagesAssistantId.current = null;
|
|
433
|
-
setMessages((current) => [
|
|
434
|
-
...current,
|
|
435
|
-
{ id: crypto.randomUUID(), role: "user", content: trimmedMessage }
|
|
436
|
-
]);
|
|
437
|
-
try {
|
|
438
|
-
console.info(`${LOG_PREFIX} run.start`, {
|
|
439
|
-
messageLength: trimmedMessage.length,
|
|
440
|
-
hasDocument: Boolean(document?.trim())
|
|
441
|
-
});
|
|
442
|
-
const response = await fetch(normalizedEndpoint, {
|
|
443
|
-
method: "POST",
|
|
444
|
-
headers: {
|
|
445
|
-
"content-type": "application/json"
|
|
446
|
-
},
|
|
447
|
-
body: JSON.stringify({
|
|
448
|
-
type: "agent.run",
|
|
449
|
-
message: trimmedMessage,
|
|
450
|
-
document: document?.trim() ? document.trim() : void 0,
|
|
451
|
-
session_id: geminiSessionId ?? void 0,
|
|
452
|
-
sandbox_id: sandboxId ?? void 0
|
|
453
|
-
})
|
|
454
|
-
});
|
|
455
|
-
if (!response.ok || !response.body) {
|
|
456
|
-
throw new Error(`Failed to start stream (${response.status}).`);
|
|
457
|
-
}
|
|
458
|
-
console.info(`${LOG_PREFIX} run.stream.open`, {
|
|
459
|
-
status: response.status
|
|
460
|
-
});
|
|
461
|
-
const reader = response.body.getReader();
|
|
462
|
-
const decoder = new TextDecoder();
|
|
463
|
-
let buffer = "";
|
|
464
|
-
while (true) {
|
|
465
|
-
const { done, value } = await reader.read();
|
|
466
|
-
if (done) {
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
buffer += decoder.decode(value, { stream: true });
|
|
470
|
-
const parsed = extractJsonObjects(buffer);
|
|
471
|
-
buffer = parsed.rest;
|
|
472
|
-
for (const objectText of parsed.objects) {
|
|
473
|
-
try {
|
|
474
|
-
const parsedEvent = JSON.parse(objectText);
|
|
475
|
-
handleStreamEvent(parsedEvent);
|
|
476
|
-
} catch {
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (buffer.trim().length > 0) {
|
|
481
|
-
try {
|
|
482
|
-
const parsedEvent = JSON.parse(buffer);
|
|
483
|
-
handleStreamEvent(parsedEvent);
|
|
484
|
-
} catch {
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
} catch (sendError) {
|
|
488
|
-
const messageText = sendError instanceof Error ? sendError.message : "Failed to send message.";
|
|
489
|
-
setError(messageText);
|
|
490
|
-
setRelayStatus("error");
|
|
491
|
-
throw sendError;
|
|
492
|
-
} finally {
|
|
493
|
-
setRunning(false);
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
[
|
|
497
|
-
cleanupRelay,
|
|
498
|
-
geminiSessionId,
|
|
499
|
-
handleStreamEvent,
|
|
500
|
-
normalizedEndpoint,
|
|
501
|
-
running,
|
|
502
|
-
sandboxId
|
|
503
|
-
]
|
|
504
|
-
);
|
|
505
|
-
const status = running ? "running" : relayStatus;
|
|
506
|
-
return {
|
|
507
|
-
status,
|
|
508
|
-
messages,
|
|
509
|
-
tools: toolEvents,
|
|
510
|
-
warnings,
|
|
511
|
-
stderrLogs,
|
|
512
|
-
sandboxId,
|
|
513
|
-
geminiSessionId,
|
|
514
|
-
error,
|
|
515
|
-
sendMessage
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
export {
|
|
519
|
-
useAgent
|
|
520
|
-
};
|