@giselles-ai/sandbox-agent 0.1.6 → 0.1.7
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 +48 -7
- package/dist/index.js +220 -180
- package/dist/react/index.d.ts +15 -40
- package/dist/react/index.js +76 -377
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Sandbox } from '@vercel/sandbox';
|
|
3
|
+
|
|
4
|
+
type BaseChatRequest = {
|
|
5
|
+
message: string;
|
|
6
|
+
session_id?: string;
|
|
7
|
+
sandbox_id?: string;
|
|
8
|
+
};
|
|
9
|
+
type ChatCommand = {
|
|
10
|
+
cmd: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
type ChatAgent<TRequest extends BaseChatRequest> = {
|
|
15
|
+
requestSchema: z.ZodType<TRequest>;
|
|
16
|
+
snapshotId?: string;
|
|
17
|
+
prepareSandbox(input: {
|
|
18
|
+
input: TRequest;
|
|
19
|
+
sandbox: Sandbox;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
createCommand(input: {
|
|
22
|
+
input: TRequest;
|
|
23
|
+
}): ChatCommand;
|
|
4
24
|
};
|
|
5
|
-
type
|
|
6
|
-
|
|
25
|
+
type RunChatInput<TRequest extends BaseChatRequest> = {
|
|
26
|
+
agent: ChatAgent<TRequest>;
|
|
27
|
+
signal: AbortSignal;
|
|
28
|
+
input: TRequest;
|
|
29
|
+
};
|
|
30
|
+
declare function runChat<TRequest extends BaseChatRequest>(input: RunChatInput<TRequest>): Promise<Response>;
|
|
31
|
+
|
|
32
|
+
declare const geminiRequestSchema: z.ZodObject<{
|
|
33
|
+
message: z.ZodString;
|
|
34
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
35
|
+
sandbox_id: z.ZodOptional<z.ZodString>;
|
|
36
|
+
relay_session_id: z.ZodOptional<z.ZodString>;
|
|
37
|
+
relay_token: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
type GeminiAgentRequest = z.infer<typeof geminiRequestSchema>;
|
|
40
|
+
type GeminiAgentOptions = {
|
|
41
|
+
snapshotId?: string;
|
|
42
|
+
env?: Record<string, string>;
|
|
43
|
+
tools?: {
|
|
44
|
+
browser?: {
|
|
45
|
+
relayUrl?: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
7
48
|
};
|
|
8
|
-
declare function
|
|
49
|
+
declare function createGeminiAgent(options?: GeminiAgentOptions): ChatAgent<GeminiAgentRequest>;
|
|
9
50
|
|
|
10
|
-
export { type
|
|
51
|
+
export { type BaseChatRequest, type ChatAgent, type ChatCommand, type RunChatInput, createGeminiAgent, runChat };
|
package/dist/index.js
CHANGED
|
@@ -1,201 +1,241 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/agents/gemini-agent.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
function debugLog(context, message) {
|
|
6
|
-
if (!DEBUG_ENABLED) {
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
console.info("[agent-runner]", JSON.stringify({ ...context, message }));
|
|
10
|
-
}
|
|
11
|
-
var agentRunSchema = z.object({
|
|
12
|
-
type: z.literal("agent.run"),
|
|
3
|
+
var GEMINI_SETTINGS_PATH = "/home/vercel-sandbox/.gemini/settings.json";
|
|
4
|
+
var geminiRequestSchema = z.object({
|
|
13
5
|
message: z.string().min(1),
|
|
14
|
-
document: z.string().optional(),
|
|
15
6
|
session_id: z.string().min(1).optional(),
|
|
16
|
-
sandbox_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()
|
|
17
10
|
});
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
error: detail
|
|
25
|
-
},
|
|
26
|
-
{ status: 400 }
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
function invalidConfigResponse(message) {
|
|
30
|
-
return Response.json(
|
|
31
|
-
{
|
|
32
|
-
ok: false,
|
|
33
|
-
errorCode: "INVALID_CONFIG",
|
|
34
|
-
message
|
|
35
|
-
},
|
|
36
|
-
{ status: 500 }
|
|
37
|
-
);
|
|
11
|
+
function requiredEnv(env, name) {
|
|
12
|
+
const value = env[name]?.trim();
|
|
13
|
+
if (!value) {
|
|
14
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
38
17
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
18
|
+
async function patchGeminiSettingsTransportEnv(sandbox, bridgeTransportEnv) {
|
|
19
|
+
const buffer = await sandbox.readFileToBuffer({
|
|
20
|
+
path: GEMINI_SETTINGS_PATH
|
|
21
|
+
});
|
|
22
|
+
if (!buffer) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Gemini settings not found in sandbox at ${GEMINI_SETTINGS_PATH}. Ensure the snapshot contains a pre-configured settings.json.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const settings = JSON.parse(new TextDecoder().decode(buffer));
|
|
28
|
+
if (settings.mcpServers) {
|
|
29
|
+
settings.mcpServers = Object.fromEntries(
|
|
30
|
+
Object.entries(settings.mcpServers).map(([key, server]) => [
|
|
31
|
+
key,
|
|
32
|
+
{
|
|
33
|
+
...server,
|
|
34
|
+
env: { ...server.env, ...bridgeTransportEnv }
|
|
35
|
+
}
|
|
36
|
+
])
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
await sandbox.writeFiles([
|
|
41
40
|
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{ status: 502 }
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
function resolveApiKey(input) {
|
|
50
|
-
return input?.apiKey?.trim() || process.env.GISELLE_SANDBOX_AGENT_API_KEY?.trim() || process.env.GISELLE_API_KEY?.trim() || "";
|
|
41
|
+
path: GEMINI_SETTINGS_PATH,
|
|
42
|
+
content: Buffer.from(JSON.stringify(settings, null, 2))
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
51
45
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
function createGeminiRequestSchema(browserEnabled) {
|
|
47
|
+
if (!browserEnabled) {
|
|
48
|
+
return geminiRequestSchema;
|
|
49
|
+
}
|
|
50
|
+
return geminiRequestSchema.superRefine((value, ctx) => {
|
|
51
|
+
if (!value.relay_session_id) {
|
|
52
|
+
ctx.addIssue({
|
|
53
|
+
code: "custom",
|
|
54
|
+
path: ["relay_session_id"],
|
|
55
|
+
message: "relay_session_id is required when tools.browser is enabled."
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!value.relay_token) {
|
|
59
|
+
ctx.addIssue({
|
|
60
|
+
code: "custom",
|
|
61
|
+
path: ["relay_token"],
|
|
62
|
+
message: "relay_token is required when tools.browser is enabled."
|
|
63
|
+
});
|
|
57
64
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function assertBrowserToolRelayCredentials(parsed) {
|
|
68
|
+
if (!parsed.relay_session_id || !parsed.relay_token) {
|
|
69
|
+
throw new Error("relay_session_id and relay_token are required.");
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
function createGeminiAgent(options = {}) {
|
|
73
|
+
const env = options.env ?? {};
|
|
74
|
+
const snapshotId = options.snapshotId?.trim() || requiredEnv(env, "SANDBOX_SNAPSHOT_ID");
|
|
75
|
+
const browserToolEnabled = options.tools?.browser !== void 0;
|
|
76
|
+
const browserToolRelayUrl = options.tools?.browser?.relayUrl?.trim();
|
|
77
|
+
if (browserToolEnabled) {
|
|
78
|
+
requiredEnv(env, "BROWSER_TOOL_RELAY_URL");
|
|
79
|
+
requiredEnv(env, "BROWSER_TOOL_RELAY_SESSION_ID");
|
|
80
|
+
requiredEnv(env, "BROWSER_TOOL_RELAY_TOKEN");
|
|
81
|
+
}
|
|
82
|
+
if (browserToolEnabled && !browserToolRelayUrl) {
|
|
83
|
+
throw new Error("tools.browser.relayUrl is empty.");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
requestSchema: createGeminiRequestSchema(browserToolEnabled),
|
|
87
|
+
snapshotId,
|
|
88
|
+
async prepareSandbox({ input, sandbox }) {
|
|
89
|
+
if (!browserToolEnabled) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
requiredEnv(env, "VERCEL_OIDC_TOKEN");
|
|
93
|
+
assertBrowserToolRelayCredentials(input);
|
|
94
|
+
await patchGeminiSettingsTransportEnv(sandbox, env);
|
|
95
|
+
},
|
|
96
|
+
createCommand({ input }) {
|
|
97
|
+
const args = [
|
|
98
|
+
"--prompt",
|
|
99
|
+
input.message,
|
|
100
|
+
"--output-format",
|
|
101
|
+
"stream-json",
|
|
102
|
+
"--approval-mode",
|
|
103
|
+
"yolo"
|
|
104
|
+
];
|
|
105
|
+
if (input.session_id) {
|
|
106
|
+
args.push("--resume", input.session_id);
|
|
107
|
+
}
|
|
68
108
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
cmd: "gemini",
|
|
110
|
+
args,
|
|
111
|
+
env: {
|
|
112
|
+
GEMINI_API_KEY: requiredEnv(env, "GEMINI_API_KEY")
|
|
113
|
+
}
|
|
71
114
|
};
|
|
72
115
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/chat-run.ts
|
|
120
|
+
import { Writable } from "stream";
|
|
121
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
122
|
+
function emitText(controller, text, encoder) {
|
|
123
|
+
if (text.length === 0) {
|
|
124
|
+
return;
|
|
80
125
|
}
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
baseUrl: input.baseUrl
|
|
84
|
-
},
|
|
85
|
-
`resolveEndpoint: using default -> ${DEFAULT_ENDPOINT}`
|
|
86
|
-
);
|
|
87
|
-
return { ok: true, endpoint: DEFAULT_ENDPOINT };
|
|
126
|
+
controller.enqueue(encoder.encode(text));
|
|
88
127
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const payload = await request.json().catch(() => null);
|
|
110
|
-
const parsed = agentRunSchema.safeParse(payload);
|
|
111
|
-
if (!parsed.success) {
|
|
112
|
-
debugLog(
|
|
113
|
-
{
|
|
114
|
-
baseUrl: optionSnapshot.baseUrl
|
|
115
|
-
},
|
|
116
|
-
`payload invalid: ${parsed.error.issues.length} issues`
|
|
117
|
-
);
|
|
118
|
-
return invalidRequestResponse(parsed.error.flatten());
|
|
119
|
-
}
|
|
120
|
-
const endpointResult = resolveEndpoint({
|
|
121
|
-
baseUrl: options?.baseUrl
|
|
122
|
-
});
|
|
123
|
-
if (!endpointResult.ok) {
|
|
124
|
-
debugLog(
|
|
125
|
-
{
|
|
126
|
-
baseUrl: optionSnapshot.baseUrl
|
|
127
|
-
},
|
|
128
|
-
`invalid endpoint config: ${endpointResult.message}`
|
|
129
|
-
);
|
|
130
|
-
return invalidConfigResponse(endpointResult.message);
|
|
131
|
-
}
|
|
132
|
-
debugLog(
|
|
133
|
-
{
|
|
134
|
-
baseUrl: optionSnapshot.baseUrl
|
|
135
|
-
},
|
|
136
|
-
`POST forwarding to endpoint: ${endpointResult.endpoint}`
|
|
137
|
-
);
|
|
138
|
-
const headers = {
|
|
139
|
-
"content-type": "application/json"
|
|
128
|
+
function emitEvent(controller, payload, encoder) {
|
|
129
|
+
emitText(controller, `${JSON.stringify(payload)}
|
|
130
|
+
`, encoder);
|
|
131
|
+
}
|
|
132
|
+
function runChat(input) {
|
|
133
|
+
const parsed = input.input;
|
|
134
|
+
const stream = new ReadableStream({
|
|
135
|
+
start(controller) {
|
|
136
|
+
const encoder = new TextEncoder();
|
|
137
|
+
const abortController = new AbortController();
|
|
138
|
+
let closed = false;
|
|
139
|
+
const close = () => {
|
|
140
|
+
if (closed) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
closed = true;
|
|
144
|
+
try {
|
|
145
|
+
controller.close();
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
140
148
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
debugLog(
|
|
151
|
-
{
|
|
152
|
-
baseUrl: optionSnapshot.baseUrl
|
|
153
|
-
},
|
|
154
|
-
"Authorization header skipped (apiKey missing)"
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
let upstreamResponse;
|
|
158
|
-
try {
|
|
159
|
-
upstreamResponse = await fetch(endpointResult.endpoint, {
|
|
160
|
-
method: "POST",
|
|
161
|
-
headers,
|
|
162
|
-
body: JSON.stringify(parsed.data),
|
|
163
|
-
signal: request.signal
|
|
164
|
-
});
|
|
165
|
-
} catch (error) {
|
|
166
|
-
debugLog(
|
|
167
|
-
{
|
|
168
|
-
baseUrl: optionSnapshot.baseUrl
|
|
169
|
-
},
|
|
170
|
-
`upstream fetch failed: ${error instanceof Error ? error.message : String(error)}`
|
|
171
|
-
);
|
|
172
|
-
const message = error instanceof Error ? error.message : "Failed to connect to upstream API.";
|
|
173
|
-
return badGatewayResponse(message);
|
|
149
|
+
const onAbort = () => {
|
|
150
|
+
if (!abortController.signal.aborted) {
|
|
151
|
+
abortController.abort();
|
|
152
|
+
}
|
|
153
|
+
close();
|
|
154
|
+
};
|
|
155
|
+
if (input.signal.aborted) {
|
|
156
|
+
onAbort();
|
|
157
|
+
return;
|
|
174
158
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
159
|
+
input.signal.addEventListener("abort", onAbort);
|
|
160
|
+
const enqueueEvent = (payload) => {
|
|
161
|
+
if (closed) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
emitEvent(controller, payload, encoder);
|
|
165
|
+
};
|
|
166
|
+
const enqueueStdout = (text) => {
|
|
167
|
+
if (closed) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
emitText(controller, text, encoder);
|
|
171
|
+
};
|
|
172
|
+
void (async () => {
|
|
173
|
+
try {
|
|
174
|
+
const sandbox = parsed.sandbox_id ? await Sandbox.get({ sandboxId: parsed.sandbox_id }) : await (async () => {
|
|
175
|
+
const snapshotId = input.agent.snapshotId?.trim();
|
|
176
|
+
if (!snapshotId) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"Agent must provide snapshotId when sandbox_id is not provided."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return Sandbox.create({
|
|
182
|
+
source: {
|
|
183
|
+
type: "snapshot",
|
|
184
|
+
snapshotId
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
})();
|
|
188
|
+
enqueueEvent({ type: "sandbox", sandbox_id: sandbox.sandboxId });
|
|
189
|
+
await input.agent.prepareSandbox({
|
|
190
|
+
input: parsed,
|
|
191
|
+
sandbox
|
|
192
|
+
});
|
|
193
|
+
const command = input.agent.createCommand({
|
|
194
|
+
input: parsed
|
|
195
|
+
});
|
|
196
|
+
await sandbox.runCommand({
|
|
197
|
+
cmd: command.cmd,
|
|
198
|
+
args: command.args,
|
|
199
|
+
env: command.env ?? {},
|
|
200
|
+
stdout: new Writable({
|
|
201
|
+
write(chunk, _encoding, callback) {
|
|
202
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
203
|
+
enqueueStdout(text);
|
|
204
|
+
callback();
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
stderr: new Writable({
|
|
208
|
+
write(chunk, _encoding, callback) {
|
|
209
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
210
|
+
enqueueEvent({ type: "stderr", content: text });
|
|
211
|
+
callback();
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
signal: abortController.signal
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (abortController.signal.aborted) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
221
|
+
enqueueEvent({ type: "stderr", content: `[error] ${message}` });
|
|
222
|
+
} finally {
|
|
223
|
+
input.signal.removeEventListener("abort", onAbort);
|
|
224
|
+
close();
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
196
227
|
}
|
|
197
|
-
};
|
|
228
|
+
});
|
|
229
|
+
return Promise.resolve(
|
|
230
|
+
new Response(stream, {
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
233
|
+
"Cache-Control": "no-cache, no-transform"
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
);
|
|
198
237
|
}
|
|
199
238
|
export {
|
|
200
|
-
|
|
239
|
+
createGeminiAgent,
|
|
240
|
+
runChat
|
|
201
241
|
};
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,44 +1,12 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ReactNode, Dispatch, SetStateAction } from 'react';
|
|
3
|
-
import { BrowserToolStatus, SnapshotField, BrowserToolAction, ExecutionReport } from '@giselles-ai/browser-tool';
|
|
4
|
-
|
|
5
|
-
type PromptPanelProps = {
|
|
6
|
-
defaultInstruction?: string;
|
|
7
|
-
defaultDocument?: string;
|
|
8
|
-
mount?: "bottom-right" | "inline";
|
|
9
|
-
};
|
|
10
|
-
declare function PromptPanel({ defaultInstruction, defaultDocument, mount, }: PromptPanelProps): react_jsx_runtime.JSX.Element;
|
|
11
|
-
|
|
12
|
-
type RunInput = {
|
|
13
|
-
instruction: string;
|
|
14
|
-
document?: string;
|
|
15
|
-
};
|
|
16
|
-
type BrowserToolPlan = {
|
|
17
|
-
fields: SnapshotField[];
|
|
18
|
-
actions: BrowserToolAction[];
|
|
19
|
-
warnings: string[];
|
|
20
|
-
};
|
|
21
|
-
type BrowserToolProviderProps = {
|
|
22
|
-
endpoint: string;
|
|
23
|
-
children: ReactNode;
|
|
24
|
-
};
|
|
25
|
-
type BrowserToolContextValue = {
|
|
26
|
-
endpoint: string;
|
|
27
|
-
status: BrowserToolStatus;
|
|
28
|
-
lastPlan: BrowserToolPlan | null;
|
|
29
|
-
lastExecution: ExecutionReport | null;
|
|
30
|
-
error: string | null;
|
|
31
|
-
setError: Dispatch<SetStateAction<string | null>>;
|
|
32
|
-
run: (input: RunInput) => Promise<BrowserToolPlan>;
|
|
33
|
-
apply: (actions: BrowserToolAction[], fields: SnapshotField[]) => ExecutionReport;
|
|
34
|
-
};
|
|
35
|
-
declare function BrowserToolProvider({ endpoint, children, }: BrowserToolProviderProps): react_jsx_runtime.JSX.Element;
|
|
36
|
-
|
|
37
1
|
type AgentMessage = {
|
|
38
2
|
id: string;
|
|
39
3
|
role: "user" | "assistant";
|
|
40
4
|
content: string;
|
|
41
5
|
};
|
|
6
|
+
type StreamEvent = {
|
|
7
|
+
type?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
42
10
|
type ToolEvent = {
|
|
43
11
|
id: string;
|
|
44
12
|
toolId: string;
|
|
@@ -49,8 +17,17 @@ type ToolEvent = {
|
|
|
49
17
|
};
|
|
50
18
|
type RelayStatus = "idle" | "connecting" | "connected" | "error";
|
|
51
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
|
+
};
|
|
52
28
|
type UseAgentOptions = {
|
|
53
29
|
endpoint: string;
|
|
30
|
+
tools?: Record<string, AgentToolHandler>;
|
|
54
31
|
};
|
|
55
32
|
type AgentHookState = {
|
|
56
33
|
status: AgentStatus;
|
|
@@ -66,8 +43,6 @@ type AgentHookState = {
|
|
|
66
43
|
document?: string;
|
|
67
44
|
}) => Promise<void>;
|
|
68
45
|
};
|
|
69
|
-
declare function useAgent({ endpoint }: UseAgentOptions): AgentHookState;
|
|
70
|
-
|
|
71
|
-
declare function useBrowserTool(): BrowserToolContextValue;
|
|
46
|
+
declare function useAgent({ endpoint, tools }: UseAgentOptions): AgentHookState;
|
|
72
47
|
|
|
73
|
-
export { type AgentHookState, type AgentMessage, type AgentStatus,
|
|
48
|
+
export { type AgentHookState, type AgentMessage, type AgentStatus, type AgentToolContext, type AgentToolHandler, type ToolEvent, type UseAgentOptions, useAgent };
|
package/dist/react/index.js
CHANGED
|
@@ -1,302 +1,7 @@
|
|
|
1
|
-
// src/react/prompt-panel.tsx
|
|
2
|
-
import { useMemo as useMemo2, useState as useState2 } from "react";
|
|
3
|
-
|
|
4
|
-
// src/react/use-browser-tool.ts
|
|
5
|
-
import { useContext } from "react";
|
|
6
|
-
|
|
7
|
-
// src/react/provider.tsx
|
|
8
|
-
import { execute, snapshot } from "@giselles-ai/browser-tool/dom";
|
|
9
|
-
import {
|
|
10
|
-
createContext,
|
|
11
|
-
useCallback,
|
|
12
|
-
useMemo,
|
|
13
|
-
useState
|
|
14
|
-
} from "react";
|
|
15
|
-
import { jsx } from "react/jsx-runtime";
|
|
16
|
-
var BrowserToolContext = createContext(
|
|
17
|
-
null
|
|
18
|
-
);
|
|
19
|
-
function isRecord(value) {
|
|
20
|
-
return Boolean(value) && typeof value === "object";
|
|
21
|
-
}
|
|
22
|
-
function isAction(value) {
|
|
23
|
-
if (!isRecord(value) || typeof value.action !== "string" || typeof value.fieldId !== "string") {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
if (value.action === "click") {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
if ((value.action === "fill" || value.action === "select") && typeof value.value === "string") {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
function parseWarnings(value) {
|
|
35
|
-
if (!Array.isArray(value)) {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
return value.filter((item) => typeof item === "string");
|
|
39
|
-
}
|
|
40
|
-
function BrowserToolProvider({
|
|
41
|
-
endpoint,
|
|
42
|
-
children
|
|
43
|
-
}) {
|
|
44
|
-
const [status, setStatus] = useState("idle");
|
|
45
|
-
const [lastPlan, setLastPlan] = useState(null);
|
|
46
|
-
const [lastExecution, setLastExecution] = useState(
|
|
47
|
-
null
|
|
48
|
-
);
|
|
49
|
-
const [error, setError] = useState(null);
|
|
50
|
-
const run = useCallback(
|
|
51
|
-
async ({ instruction, document }) => {
|
|
52
|
-
const trimmedInstruction = instruction.trim();
|
|
53
|
-
if (!trimmedInstruction) {
|
|
54
|
-
throw new Error("Instruction is required.");
|
|
55
|
-
}
|
|
56
|
-
setError(null);
|
|
57
|
-
setStatus("snapshotting");
|
|
58
|
-
try {
|
|
59
|
-
const fields = snapshot();
|
|
60
|
-
setStatus("planning");
|
|
61
|
-
const response = await fetch(endpoint, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: {
|
|
64
|
-
"content-type": "application/json"
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify({
|
|
67
|
-
instruction: trimmedInstruction,
|
|
68
|
-
document: document?.trim() ? document.trim() : void 0,
|
|
69
|
-
fields
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
const payload = await response.json().catch(() => null);
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
const message = isRecord(payload) && typeof payload.error === "string" ? payload.error : `Planning failed with status ${response.status}`;
|
|
75
|
-
throw new Error(message);
|
|
76
|
-
}
|
|
77
|
-
const actions = isRecord(payload) && Array.isArray(payload.actions) ? payload.actions.filter(isAction) : [];
|
|
78
|
-
const warnings = isRecord(payload) ? parseWarnings(payload.warnings) : [];
|
|
79
|
-
const plan = {
|
|
80
|
-
fields,
|
|
81
|
-
actions,
|
|
82
|
-
warnings
|
|
83
|
-
};
|
|
84
|
-
setLastPlan(plan);
|
|
85
|
-
setLastExecution(null);
|
|
86
|
-
setStatus("ready");
|
|
87
|
-
return plan;
|
|
88
|
-
} catch (runError) {
|
|
89
|
-
const message = runError instanceof Error ? runError.message : "Failed to run planner.";
|
|
90
|
-
setError(message);
|
|
91
|
-
setStatus("error");
|
|
92
|
-
throw runError;
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
[endpoint]
|
|
96
|
-
);
|
|
97
|
-
const apply = useCallback(
|
|
98
|
-
(actions, fields) => {
|
|
99
|
-
setError(null);
|
|
100
|
-
setStatus("applying");
|
|
101
|
-
try {
|
|
102
|
-
const report = execute(actions, fields);
|
|
103
|
-
setLastExecution(report);
|
|
104
|
-
setStatus("idle");
|
|
105
|
-
return report;
|
|
106
|
-
} catch (applyError) {
|
|
107
|
-
const message = applyError instanceof Error ? applyError.message : "Failed to apply actions.";
|
|
108
|
-
setError(message);
|
|
109
|
-
setStatus("error");
|
|
110
|
-
throw applyError;
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
[]
|
|
114
|
-
);
|
|
115
|
-
const contextValue = useMemo(
|
|
116
|
-
() => ({
|
|
117
|
-
endpoint,
|
|
118
|
-
status,
|
|
119
|
-
lastPlan,
|
|
120
|
-
lastExecution,
|
|
121
|
-
error,
|
|
122
|
-
setError,
|
|
123
|
-
run,
|
|
124
|
-
apply
|
|
125
|
-
}),
|
|
126
|
-
[apply, endpoint, error, lastExecution, lastPlan, run, status]
|
|
127
|
-
);
|
|
128
|
-
return /* @__PURE__ */ jsx(BrowserToolContext.Provider, { value: contextValue, children });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/react/use-browser-tool.ts
|
|
132
|
-
function useBrowserTool() {
|
|
133
|
-
const context = useContext(BrowserToolContext);
|
|
134
|
-
if (!context) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
"useBrowserTool must be used inside <BrowserToolProvider />."
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
return context;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/react/prompt-panel.tsx
|
|
143
|
-
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
144
|
-
function describeAction(action) {
|
|
145
|
-
if (action.action === "click") {
|
|
146
|
-
return `click ${action.fieldId}`;
|
|
147
|
-
}
|
|
148
|
-
if (action.action === "fill") {
|
|
149
|
-
return `fill ${action.fieldId} = ${JSON.stringify(action.value)}`;
|
|
150
|
-
}
|
|
151
|
-
return `select ${action.fieldId} = ${JSON.stringify(action.value)}`;
|
|
152
|
-
}
|
|
153
|
-
function PromptPanel({
|
|
154
|
-
defaultInstruction = "",
|
|
155
|
-
defaultDocument = "",
|
|
156
|
-
mount = "bottom-right"
|
|
157
|
-
}) {
|
|
158
|
-
const { status, run, apply, lastPlan, lastExecution, error, setError } = useBrowserTool();
|
|
159
|
-
const [instruction, setInstruction] = useState2(defaultInstruction);
|
|
160
|
-
const [documentText, setDocumentText] = useState2(defaultDocument);
|
|
161
|
-
const [notice, setNotice] = useState2(null);
|
|
162
|
-
const isPlanning = status === "snapshotting" || status === "planning";
|
|
163
|
-
const isApplying = status === "applying";
|
|
164
|
-
const combinedWarnings = useMemo2(() => {
|
|
165
|
-
const planWarnings = lastPlan?.warnings ?? [];
|
|
166
|
-
const executionWarnings = lastExecution?.warnings ?? [];
|
|
167
|
-
return [...planWarnings, ...executionWarnings];
|
|
168
|
-
}, [lastExecution, lastPlan]);
|
|
169
|
-
async function handlePlan() {
|
|
170
|
-
if (!instruction.trim()) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
setNotice(null);
|
|
174
|
-
setError(null);
|
|
175
|
-
try {
|
|
176
|
-
const plan = await run({
|
|
177
|
-
instruction: instruction.trim(),
|
|
178
|
-
document: documentText.trim() || void 0
|
|
179
|
-
});
|
|
180
|
-
if (plan.actions.length === 0) {
|
|
181
|
-
setNotice(
|
|
182
|
-
"No actions were generated. Try giving more explicit instructions."
|
|
183
|
-
);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
setNotice(
|
|
187
|
-
`Plan created: ${plan.actions.length} action(s). Review and click Apply.`
|
|
188
|
-
);
|
|
189
|
-
} catch (planError) {
|
|
190
|
-
setNotice(
|
|
191
|
-
planError instanceof Error ? planError.message : "Planning failed."
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
function handleApply() {
|
|
196
|
-
if (!lastPlan || lastPlan.actions.length === 0) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
setNotice(null);
|
|
200
|
-
setError(null);
|
|
201
|
-
const report = apply(lastPlan.actions, lastPlan.fields);
|
|
202
|
-
setNotice(
|
|
203
|
-
`Applied ${report.applied} action(s), skipped ${report.skipped}.`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
const wrapperClass = mount === "bottom-right" ? "fixed bottom-4 right-4 z-50 w-[min(28rem,calc(100vw-2rem))]" : "w-full max-w-xl";
|
|
207
|
-
return /* @__PURE__ */ jsx2("section", { className: wrapperClass, children: /* @__PURE__ */ jsxs("div", { className: "rounded-2xl border border-slate-700 bg-slate-950/95 p-4 shadow-2xl backdrop-blur", children: [
|
|
208
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
|
|
209
|
-
/* @__PURE__ */ jsx2("p", { className: "text-xs uppercase tracking-[0.15em] text-cyan-300", children: "Browser Tool Prompt Panel" }),
|
|
210
|
-
/* @__PURE__ */ jsxs("p", { className: "text-[11px] text-slate-400", children: [
|
|
211
|
-
"status: ",
|
|
212
|
-
status
|
|
213
|
-
] })
|
|
214
|
-
] }),
|
|
215
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
216
|
-
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
217
|
-
/* @__PURE__ */ jsx2("span", { className: "mb-1 block text-xs text-slate-300", children: "Instruction" }),
|
|
218
|
-
/* @__PURE__ */ jsx2(
|
|
219
|
-
"textarea",
|
|
220
|
-
{
|
|
221
|
-
rows: 2,
|
|
222
|
-
value: instruction,
|
|
223
|
-
onChange: (event) => setInstruction(event.target.value),
|
|
224
|
-
placeholder: "Fill title and body with a concise summary...",
|
|
225
|
-
className: "w-full rounded-lg border border-slate-700 bg-slate-900/90 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
|
|
226
|
-
}
|
|
227
|
-
)
|
|
228
|
-
] }),
|
|
229
|
-
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
230
|
-
/* @__PURE__ */ jsx2("span", { className: "mb-1 block text-xs text-slate-300", children: "Document (optional)" }),
|
|
231
|
-
/* @__PURE__ */ jsx2(
|
|
232
|
-
"textarea",
|
|
233
|
-
{
|
|
234
|
-
rows: 4,
|
|
235
|
-
value: documentText,
|
|
236
|
-
onChange: (event) => setDocumentText(event.target.value),
|
|
237
|
-
placeholder: "Paste source document here",
|
|
238
|
-
className: "w-full rounded-lg border border-slate-700 bg-slate-900/90 px-3 py-2 text-sm text-slate-100 outline-none focus:border-cyan-400"
|
|
239
|
-
}
|
|
240
|
-
)
|
|
241
|
-
] }),
|
|
242
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
243
|
-
/* @__PURE__ */ jsx2(
|
|
244
|
-
"button",
|
|
245
|
-
{
|
|
246
|
-
type: "button",
|
|
247
|
-
onClick: handlePlan,
|
|
248
|
-
disabled: !instruction.trim() || isPlanning || isApplying,
|
|
249
|
-
className: "rounded-lg bg-cyan-400 px-3 py-2 text-sm font-semibold text-slate-950 transition hover:bg-cyan-300 disabled:cursor-not-allowed disabled:opacity-50",
|
|
250
|
-
children: isPlanning ? "Planning..." : "Plan"
|
|
251
|
-
}
|
|
252
|
-
),
|
|
253
|
-
/* @__PURE__ */ jsx2(
|
|
254
|
-
"button",
|
|
255
|
-
{
|
|
256
|
-
type: "button",
|
|
257
|
-
onClick: handleApply,
|
|
258
|
-
disabled: !lastPlan || lastPlan.actions.length === 0 || isPlanning || isApplying,
|
|
259
|
-
className: "rounded-lg border border-slate-600 px-3 py-2 text-sm font-semibold text-slate-100 transition hover:border-slate-400 disabled:cursor-not-allowed disabled:opacity-50",
|
|
260
|
-
children: isApplying ? "Applying..." : "Apply"
|
|
261
|
-
}
|
|
262
|
-
)
|
|
263
|
-
] }),
|
|
264
|
-
notice ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-slate-300", children: notice }) : null,
|
|
265
|
-
error ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-rose-400", children: error }) : null
|
|
266
|
-
] }),
|
|
267
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-4 border-t border-slate-800 pt-3", children: [
|
|
268
|
-
/* @__PURE__ */ jsx2("p", { className: "text-xs font-medium uppercase tracking-[0.12em] text-slate-400", children: "Action Plan" }),
|
|
269
|
-
!lastPlan ? /* @__PURE__ */ jsx2("p", { className: "mt-2 text-xs text-slate-500", children: "No plan yet." }) : lastPlan.actions.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "mt-2 text-xs text-slate-500", children: "Planner returned no actions." }) : /* @__PURE__ */ jsx2("ul", { className: "mt-2 space-y-1", children: lastPlan.actions.map((action, index) => /* @__PURE__ */ jsx2(
|
|
270
|
-
"li",
|
|
271
|
-
{
|
|
272
|
-
className: "rounded-md border border-slate-800 bg-slate-900/80 px-2 py-1 text-xs text-slate-200",
|
|
273
|
-
children: describeAction(action)
|
|
274
|
-
},
|
|
275
|
-
`${action.fieldId}-${action.action}-${index}`
|
|
276
|
-
)) }),
|
|
277
|
-
combinedWarnings.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-md border border-amber-400/30 bg-amber-400/10 p-2", children: [
|
|
278
|
-
/* @__PURE__ */ jsx2("p", { className: "text-[11px] font-semibold uppercase tracking-[0.12em] text-amber-200", children: "Warnings" }),
|
|
279
|
-
/* @__PURE__ */ jsx2("ul", { className: "mt-1 space-y-1 text-xs text-amber-100", children: combinedWarnings.map((warning, index) => /* @__PURE__ */ jsxs(
|
|
280
|
-
"li",
|
|
281
|
-
{
|
|
282
|
-
children: [
|
|
283
|
-
"- ",
|
|
284
|
-
warning
|
|
285
|
-
]
|
|
286
|
-
},
|
|
287
|
-
`${warning}-${// biome-ignore lint/suspicious/noArrayIndexKey: wip
|
|
288
|
-
index}`
|
|
289
|
-
)) })
|
|
290
|
-
] }) : null
|
|
291
|
-
] })
|
|
292
|
-
] }) });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
1
|
// src/react/use-agent.ts
|
|
296
|
-
import {
|
|
297
|
-
import { useCallback as useCallback2, useEffect, useMemo as useMemo3, useRef, useState as useState3 } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
298
3
|
var LOG_PREFIX = "[agent-relay-client]";
|
|
299
|
-
function
|
|
4
|
+
function isRecord(value) {
|
|
300
5
|
return Boolean(value) && typeof value === "object";
|
|
301
6
|
}
|
|
302
7
|
function asString(value) {
|
|
@@ -306,7 +11,7 @@ function asNumber(value) {
|
|
|
306
11
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
307
12
|
}
|
|
308
13
|
function extractWarnings(value) {
|
|
309
|
-
if (!
|
|
14
|
+
if (!isRecord(value) || !Array.isArray(value.warnings)) {
|
|
310
15
|
return [];
|
|
311
16
|
}
|
|
312
17
|
return value.warnings.filter(
|
|
@@ -366,17 +71,24 @@ function dedupeStringArray(next) {
|
|
|
366
71
|
const nextSet = new Set(next);
|
|
367
72
|
return Array.from(nextSet);
|
|
368
73
|
}
|
|
369
|
-
function
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const [
|
|
377
|
-
const [
|
|
378
|
-
const [
|
|
379
|
-
const
|
|
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(
|
|
380
92
|
() => endpoint.replace(/\/+$/, ""),
|
|
381
93
|
[endpoint]
|
|
382
94
|
);
|
|
@@ -386,7 +98,7 @@ function useAgent({ endpoint }) {
|
|
|
386
98
|
const sessionRef = useRef(null);
|
|
387
99
|
const messagesAssistantId = useRef(null);
|
|
388
100
|
const assistantBufferRef = useRef("");
|
|
389
|
-
const cleanupRelay =
|
|
101
|
+
const cleanupRelay = useCallback(() => {
|
|
390
102
|
if (eventSourceRef.current) {
|
|
391
103
|
eventSourceRef.current.close();
|
|
392
104
|
eventSourceRef.current = null;
|
|
@@ -396,7 +108,7 @@ function useAgent({ endpoint }) {
|
|
|
396
108
|
reconnectTimerRef.current = null;
|
|
397
109
|
}
|
|
398
110
|
}, []);
|
|
399
|
-
const handleRelayResponse =
|
|
111
|
+
const handleRelayResponse = useCallback(
|
|
400
112
|
async (payload) => {
|
|
401
113
|
const currentSession = sessionRef.current;
|
|
402
114
|
if (!currentSession) {
|
|
@@ -434,9 +146,15 @@ function useAgent({ endpoint }) {
|
|
|
434
146
|
},
|
|
435
147
|
[normalizedEndpoint]
|
|
436
148
|
);
|
|
437
|
-
const
|
|
149
|
+
const addWarnings = useCallback((next) => {
|
|
150
|
+
if (next.length === 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
setWarnings((current) => dedupeStringArray([...current, ...next]));
|
|
154
|
+
}, []);
|
|
155
|
+
const handleRelayEvent = useCallback(
|
|
438
156
|
async (event) => {
|
|
439
|
-
if (!
|
|
157
|
+
if (!isRecord(event) || typeof event.type !== "string") {
|
|
440
158
|
return;
|
|
441
159
|
}
|
|
442
160
|
if (event.type === "ready") {
|
|
@@ -446,72 +164,56 @@ function useAgent({ endpoint }) {
|
|
|
446
164
|
if (!requestId) {
|
|
447
165
|
return;
|
|
448
166
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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.";
|
|
453
191
|
setError(message);
|
|
192
|
+
setRelayStatus("error");
|
|
193
|
+
await handleRelayResponse({
|
|
194
|
+
type: "error_response",
|
|
195
|
+
requestId,
|
|
196
|
+
message
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
454
199
|
}
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
if (event.type === "snapshot_request") {
|
|
458
|
-
console.info(`${LOG_PREFIX} sse.snapshot_request`, {
|
|
459
|
-
requestId
|
|
460
|
-
});
|
|
461
|
-
const fields = snapshot2();
|
|
462
|
-
console.info(`${LOG_PREFIX} sse.snapshot_result`, {
|
|
463
|
-
requestId,
|
|
464
|
-
fieldCount: fields.length
|
|
465
|
-
});
|
|
466
|
-
await handleRelayResponse({
|
|
467
|
-
type: "snapshot_response",
|
|
468
|
-
requestId,
|
|
469
|
-
fields
|
|
470
|
-
});
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
if (event.type === "execute_request") {
|
|
474
|
-
console.info(`${LOG_PREFIX} sse.execute_request`, {
|
|
475
|
-
requestId,
|
|
476
|
-
actionCount: Array.isArray(event.actions) ? event.actions.length : 0
|
|
477
|
-
});
|
|
478
|
-
const actions = Array.isArray(event.actions) ? event.actions : [];
|
|
479
|
-
const fields = Array.isArray(event.fields) ? event.fields : [];
|
|
480
|
-
const report = execute2(actions, fields);
|
|
481
|
-
console.info(`${LOG_PREFIX} sse.execute_result`, {
|
|
482
|
-
requestId,
|
|
483
|
-
applied: report.applied,
|
|
484
|
-
skipped: report.skipped
|
|
485
|
-
});
|
|
486
|
-
setWarnings(
|
|
487
|
-
(current) => dedupeStringArray([...current, ...report.warnings])
|
|
488
|
-
);
|
|
489
|
-
await handleRelayResponse({
|
|
490
|
-
type: "execute_response",
|
|
491
|
-
requestId,
|
|
492
|
-
report
|
|
493
|
-
});
|
|
494
|
-
return;
|
|
495
200
|
}
|
|
496
201
|
await handleRelayResponse({
|
|
497
202
|
type: "error_response",
|
|
498
203
|
requestId,
|
|
499
204
|
message: `Unsupported relay request type: ${event.type}`
|
|
500
205
|
});
|
|
501
|
-
|
|
502
|
-
const message = relayError instanceof Error ? relayError.message : "Relay execution failed.";
|
|
503
|
-
setError(message);
|
|
504
|
-
setRelayStatus("error");
|
|
505
|
-
await handleRelayResponse({
|
|
506
|
-
type: "error_response",
|
|
507
|
-
requestId,
|
|
508
|
-
message
|
|
509
|
-
});
|
|
206
|
+
return;
|
|
510
207
|
}
|
|
208
|
+
await handleRelayResponse({
|
|
209
|
+
type: "error_response",
|
|
210
|
+
requestId,
|
|
211
|
+
message: `Unsupported relay request type: ${event.type}`
|
|
212
|
+
});
|
|
511
213
|
},
|
|
512
|
-
[handleRelayResponse]
|
|
214
|
+
[addWarnings, handleRelayResponse, toolEventHandlers]
|
|
513
215
|
);
|
|
514
|
-
const connect =
|
|
216
|
+
const connect = useCallback(() => {
|
|
515
217
|
const currentSession = sessionRef.current;
|
|
516
218
|
if (!currentSession) {
|
|
517
219
|
return;
|
|
@@ -578,7 +280,7 @@ function useAgent({ endpoint }) {
|
|
|
578
280
|
cleanupRelay();
|
|
579
281
|
};
|
|
580
282
|
}, [cleanupRelay]);
|
|
581
|
-
const handleStreamEvent =
|
|
283
|
+
const handleStreamEvent = useCallback(
|
|
582
284
|
(event) => {
|
|
583
285
|
if (typeof event.type !== "string") {
|
|
584
286
|
return;
|
|
@@ -678,7 +380,7 @@ function useAgent({ endpoint }) {
|
|
|
678
380
|
if (event.type === "tool_use") {
|
|
679
381
|
const toolId = asString(event.tool_id) ?? crypto.randomUUID();
|
|
680
382
|
const toolName = asString(event.tool_name) ?? "tool";
|
|
681
|
-
|
|
383
|
+
setToolEvents((current) => [
|
|
682
384
|
...current,
|
|
683
385
|
{
|
|
684
386
|
id: crypto.randomUUID(),
|
|
@@ -696,7 +398,7 @@ function useAgent({ endpoint }) {
|
|
|
696
398
|
}
|
|
697
399
|
const status2 = asString(event.status);
|
|
698
400
|
const nextStatus = status2 === "success" || status2 === "error" ? status2 : void 0;
|
|
699
|
-
|
|
401
|
+
setToolEvents(
|
|
700
402
|
(current) => current.map(
|
|
701
403
|
(tool) => tool.toolId === toolId ? {
|
|
702
404
|
...tool,
|
|
@@ -715,7 +417,7 @@ function useAgent({ endpoint }) {
|
|
|
715
417
|
},
|
|
716
418
|
[connect]
|
|
717
419
|
);
|
|
718
|
-
const sendMessage =
|
|
420
|
+
const sendMessage = useCallback(
|
|
719
421
|
async ({ message, document }) => {
|
|
720
422
|
const trimmedMessage = message.trim();
|
|
721
423
|
if (!trimmedMessage || running) {
|
|
@@ -804,7 +506,7 @@ function useAgent({ endpoint }) {
|
|
|
804
506
|
return {
|
|
805
507
|
status,
|
|
806
508
|
messages,
|
|
807
|
-
tools,
|
|
509
|
+
tools: toolEvents,
|
|
808
510
|
warnings,
|
|
809
511
|
stderrLogs,
|
|
810
512
|
sandboxId,
|
|
@@ -814,8 +516,5 @@ function useAgent({ endpoint }) {
|
|
|
814
516
|
};
|
|
815
517
|
}
|
|
816
518
|
export {
|
|
817
|
-
|
|
818
|
-
PromptPanel,
|
|
819
|
-
useAgent,
|
|
820
|
-
useBrowserTool
|
|
519
|
+
useAgent
|
|
821
520
|
};
|
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.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
"build": "pnpm clean && tsup --config tsup.ts",
|
|
29
29
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
30
30
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "pnpm exec vitest run",
|
|
31
32
|
"format": "pnpm exec biome check --write ."
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@
|
|
35
|
+
"@vercel/sandbox": "^1.0.0",
|
|
35
36
|
"zod": "4.3.6"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"react-dom": ">=19.0.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
43
|
+
"@google/gemini-cli-core": "^0.28.2",
|
|
42
44
|
"@types/node": "25.0.10",
|
|
43
45
|
"@types/react": "19.2.10",
|
|
44
46
|
"@types/react-dom": "19.2.3",
|