@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 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/gemini-agent.ts
1
+ // src/agents/codex-agent.ts
2
2
  import { z } from "zod";
3
- var GEMINI_SETTINGS_PATH = "/home/vercel-sandbox/.gemini/settings.json";
4
- var geminiRequestSchema = z.object({
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() || requiredEnv(env, "SANDBOX_SNAPSHOT_ID");
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
- requiredEnv(env, "BROWSER_TOOL_RELAY_URL");
79
- requiredEnv(env, "BROWSER_TOOL_RELAY_SESSION_ID");
80
- requiredEnv(env, "BROWSER_TOOL_RELAY_TOKEN");
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
- requiredEnv(env, "VERCEL_OIDC_TOKEN");
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: requiredEnv(env, "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
- enqueueStdout(text);
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.8",
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",
@@ -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 };
@@ -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
- };