@giselles-ai/agent 0.1.22 → 0.1.24

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.
@@ -0,0 +1,75 @@
1
+ // src/hash.ts
2
+ import { createHash } from "crypto";
3
+ function computeConfigHash(config) {
4
+ const payload = JSON.stringify({
5
+ agentType: config.agentType ?? "gemini",
6
+ agentMd: config.agentMd ?? null,
7
+ files: (config.files ?? []).map((f) => ({
8
+ path: f.path,
9
+ content: f.content
10
+ }))
11
+ });
12
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
13
+ }
14
+
15
+ // src/request-build.ts
16
+ function trimTrailingSlash(url) {
17
+ return url.replace(/\/+$/, "");
18
+ }
19
+ function resolveFiles(agent) {
20
+ const files = [
21
+ ...agent.files ?? []
22
+ ];
23
+ if (agent.agentMd !== void 0) {
24
+ files.push(
25
+ {
26
+ path: "/home/vercel-sandbox/.codex/AGENTS.md",
27
+ content: agent.agentMd
28
+ },
29
+ {
30
+ path: "/home/vercel-sandbox/.gemini/GEMINI.md",
31
+ content: agent.agentMd
32
+ }
33
+ );
34
+ }
35
+ return files;
36
+ }
37
+ async function requestBuild(agent, options) {
38
+ const baseUrl = trimTrailingSlash(
39
+ options?.baseUrl ?? process.env.GISELLE_AGENT_BASE_URL ?? "https://studio.giselles.ai/agent-api"
40
+ );
41
+ const apiUrl = `${baseUrl}/build`;
42
+ const token = options?.token ?? process.env.GISELLE_AGENT_API_KEY;
43
+ if (!token) {
44
+ throw new Error(
45
+ "Missing API token. Set GISELLE_AGENT_API_KEY or pass options.token."
46
+ );
47
+ }
48
+ const configHash = computeConfigHash(agent);
49
+ const files = resolveFiles(agent);
50
+ const requestBody = {
51
+ config_hash: configHash,
52
+ agent_type: agent.agentType ?? "gemini",
53
+ files
54
+ };
55
+ const requestHeaders = {
56
+ "content-type": "application/json",
57
+ authorization: `Bearer ${token}`,
58
+ ...options?.headers
59
+ };
60
+ const response = await fetch(apiUrl, {
61
+ method: "POST",
62
+ headers: requestHeaders,
63
+ body: JSON.stringify(requestBody)
64
+ });
65
+ if (!response.ok) {
66
+ const body = await response.text().catch(() => "");
67
+ throw new Error(`Build failed (${response.status}): ${body}`);
68
+ }
69
+ return await response.json();
70
+ }
71
+
72
+ export {
73
+ computeConfigHash,
74
+ requestBuild
75
+ };
package/dist/index.d.ts CHANGED
@@ -5,4 +5,18 @@ declare function defineAgent(config: AgentConfig): DefinedAgent;
5
5
 
6
6
  declare function computeConfigHash(config: AgentConfig): string;
7
7
 
8
- export { AgentConfig, DefinedAgent, computeConfigHash, defineAgent };
8
+ type RequestBuildOptions = {
9
+ /** Base URL for the agent API. Default: process.env.GISELLE_AGENT_BASE_URL ?? "https://studio.giselles.ai/agent-api" */
10
+ baseUrl?: string;
11
+ /** Bearer token. Default: process.env.GISELLE_AGENT_API_KEY */
12
+ token?: string;
13
+ /** Additional headers to include in the build API request */
14
+ headers?: Record<string, string | undefined>;
15
+ };
16
+ type BuildResult = {
17
+ snapshot_id: string;
18
+ cached: boolean;
19
+ };
20
+ declare function requestBuild(agent: AgentConfig, options?: RequestBuildOptions): Promise<BuildResult>;
21
+
22
+ export { AgentConfig, type BuildResult, DefinedAgent, type RequestBuildOptions, computeConfigHash, defineAgent, requestBuild };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
- computeConfigHash
3
- } from "./chunk-K2DPJIHU.js";
2
+ computeConfigHash,
3
+ requestBuild
4
+ } from "./chunk-TAQV6ZOB.js";
4
5
 
5
6
  // src/define-agent.ts
6
7
  function defineAgent(config) {
@@ -21,5 +22,6 @@ function defineAgent(config) {
21
22
  }
22
23
  export {
23
24
  computeConfigHash,
24
- defineAgent
25
+ defineAgent,
26
+ requestBuild
25
27
  };
@@ -10,6 +10,6 @@ type GiselleAgentPluginOptions = {
10
10
  headers?: Record<string, string | undefined>;
11
11
  };
12
12
 
13
- declare function withGiselleAgent(nextConfig: NextConfig, agent: AgentConfig, options?: GiselleAgentPluginOptions): () => Promise<NextConfig>;
13
+ declare function withGiselleAgent(nextConfig: NextConfig, agent: AgentConfig, options?: GiselleAgentPluginOptions): (phase: string) => Promise<NextConfig>;
14
14
 
15
15
  export { type GiselleAgentPluginOptions, withGiselleAgent };
@@ -1,73 +1,112 @@
1
1
  import {
2
- computeConfigHash
3
- } from "../chunk-K2DPJIHU.js";
2
+ requestBuild
3
+ } from "../chunk-TAQV6ZOB.js";
4
4
 
5
5
  // src/next/with-giselle-agent.ts
6
- function trimTrailingSlash(url) {
7
- return url.replace(/\/+$/, "");
6
+ import crypto from "crypto";
7
+ import fs from "fs";
8
+ import { createRequire } from "module";
9
+ import path from "path";
10
+ var require2 = createRequire(import.meta.url);
11
+ var { version: PKG_VERSION } = require2("../../package.json");
12
+ var bold = (s) => `\x1B[1m${s}\x1B[22m`;
13
+ var green = (s) => `\x1B[32m${s}\x1B[39m`;
14
+ var red = (s) => `\x1B[31m${s}\x1B[39m`;
15
+ var magenta = (s) => `\x1B[35m${s}\x1B[39m`;
16
+ var dim = (s) => `\x1B[2m${s}\x1B[22m`;
17
+ function resolveBaseUrl(options) {
18
+ return (options?.baseUrl ?? process.env.GISELLE_AGENT_BASE_URL ?? "https://studio.giselles.ai/agent-api").replace(/\/+$/, "");
19
+ }
20
+ function getSnapshotFile(agent) {
21
+ const key = {
22
+ agentType: agent.agentType,
23
+ agentMd: agent.agentMd,
24
+ files: agent.files
25
+ };
26
+ const hash = crypto.createHash("sha256").update(JSON.stringify(key)).digest("hex").slice(0, 16);
27
+ return path.join(process.cwd(), ".next", "giselle", hash);
28
+ }
29
+ function hasRunRecently() {
30
+ const lockFile = path.join(process.cwd(), ".next", "giselle", ".lock");
31
+ try {
32
+ const ts = Number(fs.readFileSync(lockFile, "utf-8").trim());
33
+ if (Date.now() - ts < 2e3) {
34
+ return true;
35
+ }
36
+ } catch {
37
+ }
38
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
39
+ fs.writeFileSync(lockFile, String(Date.now()));
40
+ return false;
8
41
  }
9
42
  function withGiselleAgent(nextConfig, agent, options) {
10
43
  return async () => {
11
- const baseUrl = trimTrailingSlash(
12
- options?.baseUrl ?? process.env.GISELLE_AGENT_BASE_URL ?? "https://studio.giselles.ai/agent-api"
13
- );
14
- const apiUrl = `${baseUrl}/build`;
15
- const token = options?.token ?? process.env.GISELLE_AGENT_API_KEY;
16
- if (!token) {
17
- console.warn("[withGiselleAgent] Skipped snapshot build: missing token.");
44
+ if (hasRunRecently()) {
18
45
  return nextConfig;
19
46
  }
20
- const configHash = computeConfigHash(agent);
21
- const files = [
22
- ...agent.files ?? []
23
- ];
24
- if (agent.agentMd !== void 0) {
25
- files.push(
26
- {
27
- path: "/home/vercel-sandbox/.codex/AGENTS.md",
28
- content: agent.agentMd
29
- },
30
- {
31
- path: "/home/vercel-sandbox/.gemini/GEMINI.md",
32
- content: agent.agentMd
47
+ const baseUrl = resolveBaseUrl(options);
48
+ const snapshotFile = getSnapshotFile(agent);
49
+ const cached = fs.existsSync(snapshotFile) ? fs.readFileSync(snapshotFile, "utf-8").trim() : void 0;
50
+ if (cached) {
51
+ return {
52
+ ...nextConfig,
53
+ env: {
54
+ ...nextConfig.env,
55
+ GISELLE_AGENT_SNAPSHOT_ID: cached
33
56
  }
57
+ };
58
+ }
59
+ console.log("");
60
+ console.log(`${magenta(bold(`\u2726 Giselle Agent ${PKG_VERSION}`))}`);
61
+ console.log(`${dim("- Base URL:")} ${baseUrl}`);
62
+ console.log("");
63
+ const token = options?.token ?? process.env.GISELLE_AGENT_API_KEY;
64
+ if (!token) {
65
+ console.log(`${red("\u2717")} Missing API key`);
66
+ console.log("");
67
+ console.log(
68
+ " Create an account at https://studio.giselles.ai and generate"
69
+ );
70
+ console.log(
71
+ " an API key, then set it as GISELLE_AGENT_API_KEY in your environment."
34
72
  );
73
+ console.log("");
74
+ throw new Error("Missing GISELLE_AGENT_API_KEY");
35
75
  }
36
- const requestBody = {
37
- config_hash: configHash,
38
- agent_type: agent.agentType ?? "gemini",
39
- files
40
- };
41
- const requestHeaders = {
42
- "content-type": "application/json",
43
- authorization: `Bearer ${token}`,
44
- ...options?.headers
45
- };
46
- console.debug("[withGiselleAgent] POST %s", apiUrl);
47
- console.debug(
48
- "[withGiselleAgent] headers:",
49
- JSON.stringify(requestHeaders, null, 2)
50
- );
51
- console.debug(
52
- "[withGiselleAgent] body:",
53
- JSON.stringify(requestBody, null, 2)
54
- );
55
- const response = await fetch(apiUrl, {
76
+ const authResponse = await fetch(`${baseUrl}/auth`, {
56
77
  method: "POST",
57
- headers: requestHeaders,
58
- body: JSON.stringify(requestBody)
78
+ headers: {
79
+ authorization: `Bearer ${token}`,
80
+ ...options?.headers
81
+ }
59
82
  });
60
- if (!response.ok) {
61
- const body = await response.text().catch(() => "");
62
- throw new Error(
63
- `[withGiselleAgent] Build failed (${response.status}): ${body}`
83
+ if (!authResponse.ok) {
84
+ console.log(`${red("\u2717")} Authentication failed`);
85
+ console.log("");
86
+ console.log(
87
+ " Create an account at https://studio.giselles.ai and generate"
88
+ );
89
+ console.log(
90
+ " an API key, then set it as GISELLE_AGENT_API_KEY in your environment."
64
91
  );
92
+ console.log("");
93
+ throw new Error("Authentication failed");
65
94
  }
66
- const result = await response.json();
67
- console.debug(
68
- "[withGiselleAgent] result:",
69
- JSON.stringify(result, null, 2)
70
- );
95
+ console.log(`${green("\u2713")} Authenticated`);
96
+ const start = performance.now();
97
+ const result = await requestBuild(agent, {
98
+ baseUrl: options?.baseUrl,
99
+ token,
100
+ headers: options?.headers
101
+ });
102
+ fs.mkdirSync(path.dirname(snapshotFile), { recursive: true });
103
+ fs.writeFileSync(snapshotFile, result.snapshot_id);
104
+ const elapsed = Math.round(performance.now() - start);
105
+ const elapsedStr = elapsed < 1e3 ? `${elapsed}ms` : `${(elapsed / 1e3).toFixed(1)}s`;
106
+ console.log(`${green("\u2713")} Building...`);
107
+ console.log(`${green("\u2713")} Ready in ${elapsedStr}`);
108
+ console.log("");
109
+ console.log("");
71
110
  return {
72
111
  ...nextConfig,
73
112
  env: {
@@ -3,30 +3,6 @@ import { z } from 'zod';
3
3
  import { RelayRequest, RelayResponse } from '@giselles-ai/browser-tool';
4
4
  import { RelayRequestSubscription } from '@giselles-ai/browser-tool/relay';
5
5
 
6
- type AgentType$1 = "codex" | "gemini";
7
- declare class Agent {
8
- private _type;
9
- private _snapshotId;
10
- private _pendingOps;
11
- private constructor();
12
- static create(type: AgentType$1, options: {
13
- snapshotId: string;
14
- }): Agent;
15
- get type(): AgentType$1;
16
- get snapshotId(): string;
17
- get dirty(): boolean;
18
- addFiles(files: Array<{
19
- path: string;
20
- content: Buffer;
21
- }>): this;
22
- setAgentMd(content: string | Buffer): this;
23
- runCommands(commands: Array<{
24
- cmd: string;
25
- args?: string[];
26
- }>): this;
27
- prepare(): Promise<void>;
28
- }
29
-
30
6
  type BaseChatRequest = {
31
7
  message: string;
32
8
  session_id?: string;
@@ -143,6 +119,7 @@ type CloudChatSessionState = {
143
119
  chatId: string;
144
120
  agentSessionId?: string;
145
121
  sandboxId?: string;
122
+ snapshotId?: string;
146
123
  relay?: CloudRelaySession;
147
124
  pendingTool?: PendingToolState | null;
148
125
  updatedAt: number;
@@ -150,6 +127,7 @@ type CloudChatSessionState = {
150
127
  type CloudChatSessionPatch = {
151
128
  agentSessionId?: string;
152
129
  sandboxId?: string;
130
+ snapshotId?: string;
153
131
  relay?: CloudRelaySession;
154
132
  pendingTool?: PendingToolState | null;
155
133
  };
@@ -251,4 +229,4 @@ declare function runCloudChat<TRequest extends CloudChatRequest & {
251
229
  }) => Promise<void>;
252
230
  }): Promise<Response>;
253
231
 
254
- export { Agent, type AgentApiOptions, type AgentApiStoreConfig, type AgentParam, type AgentRequest, type AgentType, type BaseChatRequest, type ChatAgent, type ChatCommand, type CloudChatRequest, type CloudChatRunRequest, type CloudChatSessionPatch, type CloudChatSessionState, type CloudChatStateStore, type CloudRelaySession, type CloudToolName, type CloudToolResult, type CreateAgentOptions, type PendingToolState, type RelaySessionFactoryResult, type RunChatImpl, type RunChatInput, type StdoutMapper, applyCloudChatPatch, cloudChatRunRequestSchema, createAgent, createAgentApi, createCodexAgent, createCodexStdoutMapper, createGeminiAgent, reduceCloudChatEvent, runChat, runCloudChat, toolNameFromRelayRequest };
232
+ export { type AgentApiOptions, type AgentApiStoreConfig, type AgentParam, type AgentRequest, type AgentType, type BaseChatRequest, type ChatAgent, type ChatCommand, type CloudChatRequest, type CloudChatRunRequest, type CloudChatSessionPatch, type CloudChatSessionState, type CloudChatStateStore, type CloudRelaySession, type CloudToolName, type CloudToolResult, type CreateAgentOptions, type PendingToolState, type RelaySessionFactoryResult, type RunChatImpl, type RunChatInput, type StdoutMapper, applyCloudChatPatch, cloudChatRunRequestSchema, createAgent, createAgentApi, createCodexAgent, createCodexStdoutMapper, createGeminiAgent, reduceCloudChatEvent, runChat, runCloudChat, toolNameFromRelayRequest };
@@ -1,96 +1,41 @@
1
- // src/agent.ts
1
+ // src/agent-api.ts
2
+ import {
3
+ createRelayHandler,
4
+ createRelaySession
5
+ } from "@giselles-ai/browser-tool/relay";
6
+
7
+ // src/build.ts
2
8
  import { Sandbox } from "@vercel/sandbox";
3
- var Agent = class _Agent {
4
- _type;
5
- _snapshotId;
6
- _pendingOps = [];
7
- constructor(type, snapshotId) {
8
- this._type = type;
9
- this._snapshotId = snapshotId;
10
- }
11
- static create(type, options) {
12
- const trimmed = options.snapshotId.trim();
13
- if (!trimmed) {
14
- throw new Error("snapshotId is required.");
15
- }
16
- return new _Agent(type, trimmed);
9
+ var CACHE_KEY_PREFIX = "agent-build:snapshot:";
10
+ var CACHE_TTL_SEC = 60 * 60 * 24;
11
+ var memoryCache = /* @__PURE__ */ new Map();
12
+ var MemorySnapshotCache = class {
13
+ async get(key2) {
14
+ return memoryCache.get(key2) ?? null;
17
15
  }
18
- get type() {
19
- return this._type;
16
+ async set(key2, value) {
17
+ memoryCache.set(key2, value);
20
18
  }
21
- get snapshotId() {
22
- return this._snapshotId;
19
+ };
20
+ var RedisSnapshotCache = class {
21
+ constructor(redis) {
22
+ this.redis = redis;
23
23
  }
24
- get dirty() {
25
- return this._pendingOps.length > 0;
24
+ async get(key2) {
25
+ return this.redis.get(`${CACHE_KEY_PREFIX}${key2}`);
26
26
  }
27
- addFiles(files) {
28
- if (files.length === 0) {
29
- return this;
30
- }
31
- this._pendingOps.push({
32
- kind: "writeFiles",
33
- files
34
- });
35
- return this;
36
- }
37
- setAgentMd(content) {
38
- const buffer = typeof content === "string" ? Buffer.from(content) : content;
39
- console.log(`[agent] setAgentMd called, content length=${buffer.length}`);
40
- return this.addFiles([
41
- { path: "/home/vercel-sandbox/.codex/AGENTS.md", content: buffer }
42
- ]);
43
- }
44
- runCommands(commands) {
45
- for (const command of commands) {
46
- this._pendingOps.push({
47
- kind: "runCommand",
48
- cmd: command.cmd,
49
- args: command.args ?? []
50
- });
51
- }
52
- return this;
53
- }
54
- async prepare() {
55
- console.log(
56
- `[agent] prepare called, dirty=${this.dirty}, pendingOps=${this._pendingOps.length}`
57
- );
58
- if (!this.dirty) {
59
- return;
60
- }
61
- const ops = this._pendingOps;
62
- const sandbox = await Sandbox.create({
63
- source: { type: "snapshot", snapshotId: this._snapshotId }
64
- });
65
- console.log(
66
- `[sandbox] created sandbox=${sandbox.sandboxId} from snapshot=${this._snapshotId}`
27
+ async set(key2, value) {
28
+ await this.redis.set(
29
+ `${CACHE_KEY_PREFIX}${key2}`,
30
+ value,
31
+ "EX",
32
+ CACHE_TTL_SEC
67
33
  );
68
- for (const op of ops) {
69
- switch (op.kind) {
70
- case "writeFiles":
71
- await sandbox.writeFiles(op.files);
72
- break;
73
- case "runCommand":
74
- await sandbox.runCommand(op.cmd, op.args);
75
- break;
76
- }
77
- }
78
- const snapshot = await sandbox.snapshot();
79
- console.log(`[agent] prepare done, new snapshotId=${snapshot.snapshotId}`);
80
- this._snapshotId = snapshot.snapshotId;
81
- this._pendingOps = [];
82
34
  }
83
35
  };
84
-
85
- // src/agent-api.ts
86
- import {
87
- createRelayHandler,
88
- createRelaySession
89
- } from "@giselles-ai/browser-tool/relay";
90
-
91
- // src/build.ts
92
- import { Sandbox as Sandbox2 } from "@vercel/sandbox";
93
- var snapshotCache = /* @__PURE__ */ new Map();
36
+ function createSnapshotCache(redis) {
37
+ return redis ? new RedisSnapshotCache(redis) : new MemorySnapshotCache();
38
+ }
94
39
  function resolveBaseSnapshotId(configured) {
95
40
  const envValue = process.env.GISELLE_AGENT_SANDBOX_BASE_SNAPSHOT_ID?.trim();
96
41
  if (envValue) {
@@ -136,6 +81,7 @@ function parseBuildRequest(body) {
136
81
  };
137
82
  }
138
83
  async function buildAgent(input) {
84
+ const cache = input.cache ?? new MemorySnapshotCache();
139
85
  const body = await input.request.json().catch(() => null);
140
86
  const parsed = parseBuildRequest(body);
141
87
  if (!parsed) {
@@ -155,7 +101,7 @@ async function buildAgent(input) {
155
101
  );
156
102
  }
157
103
  const cacheKey = `${baseSnapshotId}:${parsed.config_hash}`;
158
- const cached = snapshotCache.get(cacheKey);
104
+ const cached = await cache.get(cacheKey);
159
105
  if (cached) {
160
106
  console.log(
161
107
  `[agent-build] cache hit: hash=${cacheKey} -> snapshot=${cached}`
@@ -163,7 +109,7 @@ async function buildAgent(input) {
163
109
  const response2 = { snapshot_id: cached, cached: true };
164
110
  return Response.json(response2);
165
111
  }
166
- const sandbox = await Sandbox2.create({
112
+ const sandbox = await Sandbox.create({
167
113
  source: { type: "snapshot", snapshotId: baseSnapshotId }
168
114
  });
169
115
  console.log(
@@ -179,7 +125,7 @@ async function buildAgent(input) {
179
125
  }
180
126
  const snapshot = await sandbox.snapshot();
181
127
  console.log(`[agent-build] snapshot created: ${snapshot.snapshotId}`);
182
- snapshotCache.set(cacheKey, snapshot.snapshotId);
128
+ await cache.set(cacheKey, snapshot.snapshotId);
183
129
  const response = {
184
130
  snapshot_id: snapshot.snapshotId,
185
131
  cached: false
@@ -603,7 +549,7 @@ function createAgent(options) {
603
549
 
604
550
  // src/chat-run.ts
605
551
  import { Writable } from "stream";
606
- import { Sandbox as Sandbox3 } from "@vercel/sandbox";
552
+ import { Sandbox as Sandbox2 } from "@vercel/sandbox";
607
553
  function emitText(controller, text, encoder) {
608
554
  if (text.length === 0) {
609
555
  return;
@@ -657,23 +603,39 @@ function runChat(input) {
657
603
  const mapper = input.agent.createStdoutMapper?.();
658
604
  void (async () => {
659
605
  try {
660
- const sandbox = parsed.sandbox_id ? await Sandbox3.get({ sandboxId: parsed.sandbox_id }) : await (async () => {
661
- const snapshotId = parsed.snapshot_id?.trim() || input.agent.snapshotId?.trim();
662
- if (!snapshotId) {
663
- throw new Error(
664
- "Agent must provide snapshotId when sandbox_id is not provided."
665
- );
666
- }
667
- const created = await Sandbox3.create({
606
+ const createFromSnapshot = async (snapshotId2) => {
607
+ const created = await Sandbox2.create({
668
608
  source: {
669
609
  type: "snapshot",
670
- snapshotId
610
+ snapshotId: snapshotId2
671
611
  }
672
612
  });
673
613
  console.log(
674
- `[sandbox] created sandbox=${created.sandboxId} from snapshot=${snapshotId}`
614
+ `[sandbox] created sandbox=${created.sandboxId} from snapshot=${snapshotId2}`
675
615
  );
676
616
  return created;
617
+ };
618
+ const snapshotId = parsed.snapshot_id?.trim() || input.agent.snapshotId?.trim();
619
+ const sandbox = await (async () => {
620
+ if (parsed.sandbox_id) {
621
+ try {
622
+ return await Sandbox2.get({ sandboxId: parsed.sandbox_id });
623
+ } catch (error) {
624
+ if (!snapshotId) {
625
+ throw error;
626
+ }
627
+ console.log(
628
+ `[sandbox] sandbox=${parsed.sandbox_id} expired, recreating from snapshot=${snapshotId}`
629
+ );
630
+ return createFromSnapshot(snapshotId);
631
+ }
632
+ }
633
+ if (!snapshotId) {
634
+ throw new Error(
635
+ "Agent must provide snapshotId when sandbox_id is not provided."
636
+ );
637
+ }
638
+ return createFromSnapshot(snapshotId);
677
639
  })();
678
640
  enqueueEvent({ type: "sandbox", sandbox_id: sandbox.sandboxId });
679
641
  await input.agent.prepareSandbox({
@@ -710,6 +672,11 @@ function runChat(input) {
710
672
  }),
711
673
  signal: abortController.signal
712
674
  });
675
+ const snapshot = await sandbox.snapshot();
676
+ enqueueEvent({
677
+ type: "snapshot",
678
+ snapshot_id: snapshot.snapshotId
679
+ });
713
680
  } catch (error) {
714
681
  if (abortController.signal.aborted) {
715
682
  return;
@@ -825,6 +792,9 @@ function reduceCloudChatEvent(event) {
825
792
  if (event.type === "sandbox" && typeof event.sandbox_id === "string") {
826
793
  return { sandboxId: event.sandbox_id };
827
794
  }
795
+ if (event.type === "snapshot" && typeof event.snapshot_id === "string") {
796
+ return { snapshotId: event.snapshot_id };
797
+ }
828
798
  if (event.type === "relay.session" && typeof event.sessionId === "string" && typeof event.token === "string" && typeof event.relayUrl === "string" && typeof event.expiresAt === "number") {
829
799
  return {
830
800
  relay: {
@@ -865,6 +835,7 @@ function applyCloudChatPatch(input) {
865
835
  chatId: input.chatId,
866
836
  agentSessionId: input.patch?.agentSessionId ?? input.base?.agentSessionId,
867
837
  sandboxId: input.patch?.sandboxId ?? input.base?.sandboxId,
838
+ snapshotId: input.patch?.snapshotId ?? input.base?.snapshotId,
868
839
  relay: input.patch?.relay ?? input.base?.relay,
869
840
  pendingTool: input.patch?.pendingTool !== void 0 ? input.patch.pendingTool : input.base?.pendingTool,
870
841
  updatedAt: input.now
@@ -986,6 +957,7 @@ async function runCloudChat(input) {
986
957
  ...input.request,
987
958
  ...existing?.agentSessionId ? { session_id: existing.agentSessionId } : {},
988
959
  ...existing?.sandboxId ? { sandbox_id: existing.sandboxId } : {},
960
+ ...existing?.snapshotId ? { snapshot_id: existing.snapshotId } : {},
989
961
  relay_session_id: relaySession.sessionId,
990
962
  relay_token: relaySession.token
991
963
  };
@@ -1086,6 +1058,7 @@ async function resumeCloudChat(input) {
1086
1058
  ...input.request,
1087
1059
  ...input.existing.agentSessionId ? { session_id: input.existing.agentSessionId } : {},
1088
1060
  ...input.existing.sandboxId ? { sandbox_id: input.existing.sandboxId } : {},
1061
+ ...input.existing.snapshotId ? { snapshot_id: input.existing.snapshotId } : {},
1089
1062
  relay_session_id: relaySession.sessionId,
1090
1063
  relay_token: relaySession.token
1091
1064
  };
@@ -1328,14 +1301,10 @@ function continueManagedCloudResponseFromLiveConnection(input) {
1328
1301
  return managed.response;
1329
1302
  }
1330
1303
 
1331
- // src/cloud-chat-store.ts
1332
- var CHAT_STATE_TTL_SEC = 60 * 60;
1304
+ // src/redis.ts
1333
1305
  var REDIS_URL_ENV_NAME = "REDIS_URL";
1334
- function key(chatId) {
1335
- return `cloud-chat:${chatId}`;
1336
- }
1337
1306
  function resolveRedisUrl(url) {
1338
- const resolved = url?.trim() || process.env.REDIS_URL?.trim();
1307
+ const resolved = url?.trim() || process.env[REDIS_URL_ENV_NAME]?.trim();
1339
1308
  if (resolved) {
1340
1309
  return resolved;
1341
1310
  }
@@ -1343,6 +1312,26 @@ function resolveRedisUrl(url) {
1343
1312
  `Missing Redis URL. Set ${REDIS_URL_ENV_NAME} or pass store.url.`
1344
1313
  );
1345
1314
  }
1315
+ async function createRedisClient(url) {
1316
+ let RedisCtor;
1317
+ try {
1318
+ const module = await import("ioredis");
1319
+ RedisCtor = module.default;
1320
+ } catch {
1321
+ throw new Error(
1322
+ "Redis store adapter requires `ioredis` to be installed as a peer dependency."
1323
+ );
1324
+ }
1325
+ return new RedisCtor(resolveRedisUrl(url), {
1326
+ maxRetriesPerRequest: 2
1327
+ });
1328
+ }
1329
+
1330
+ // src/cloud-chat-store.ts
1331
+ var CHAT_STATE_TTL_SEC = 60 * 60;
1332
+ function key(chatId) {
1333
+ return `cloud-chat:${chatId}`;
1334
+ }
1346
1335
  var RedisCloudChatStateStore = class {
1347
1336
  constructor(redis) {
1348
1337
  this.redis = redis;
@@ -1363,25 +1352,12 @@ var RedisCloudChatStateStore = class {
1363
1352
  await this.redis.del(key(chatId));
1364
1353
  }
1365
1354
  };
1366
- async function createRedisStore(url) {
1367
- let RedisCtor;
1368
- try {
1369
- const module = await import("ioredis");
1370
- RedisCtor = module.default;
1371
- } catch {
1372
- throw new Error(
1373
- "Redis store adapter requires `ioredis` to be installed as a peer dependency."
1374
- );
1375
- }
1376
- const redis = new RedisCtor(resolveRedisUrl(url), {
1377
- maxRetriesPerRequest: 2
1378
- });
1379
- return new RedisCloudChatStateStore(redis);
1380
- }
1381
1355
  async function resolveCloudChatStateStore(store) {
1382
1356
  switch (store.adapter) {
1383
- case "redis":
1384
- return createRedisStore(store.url);
1357
+ case "redis": {
1358
+ const redis = await createRedisClient(store.url);
1359
+ return new RedisCloudChatStateStore(redis);
1360
+ }
1385
1361
  }
1386
1362
  }
1387
1363
 
@@ -1413,6 +1389,8 @@ function createAgentApi(options) {
1413
1389
  const relay = createRelayHandler();
1414
1390
  const { basePath, agent: agentOptions } = options;
1415
1391
  let storePromise = null;
1392
+ let snapshotCachePromise = null;
1393
+ const authPath = `${basePath}/auth`;
1416
1394
  const runPath = `${basePath}/run`;
1417
1395
  const buildPath = `${basePath}/build`;
1418
1396
  const relayPrefix = `${basePath}/relay`;
@@ -1420,6 +1398,13 @@ function createAgentApi(options) {
1420
1398
  storePromise ??= resolveCloudChatStateStore(options.store);
1421
1399
  return storePromise;
1422
1400
  }
1401
+ function getSnapshotCache() {
1402
+ snapshotCachePromise ??= (async () => {
1403
+ const redis = await createRedisClient(options.store.url);
1404
+ return createSnapshotCache(redis);
1405
+ })();
1406
+ return snapshotCachePromise;
1407
+ }
1423
1408
  async function handleBuild(request) {
1424
1409
  const baseSnapshotId = process.env.GISELLE_AGENT_SANDBOX_BASE_SNAPSHOT_ID?.trim() || options.build?.baseSnapshotId;
1425
1410
  if (!options.build && !baseSnapshotId) {
@@ -1432,7 +1417,8 @@ function createAgentApi(options) {
1432
1417
  }
1433
1418
  return await buildAgent({
1434
1419
  request,
1435
- baseSnapshotId
1420
+ baseSnapshotId,
1421
+ cache: await getSnapshotCache()
1436
1422
  });
1437
1423
  } catch (error) {
1438
1424
  const message = error instanceof Error ? error.message : "Failed to process build request.";
@@ -1478,9 +1464,24 @@ function createAgentApi(options) {
1478
1464
  return errorResponse(500, "INTERNAL_ERROR", message);
1479
1465
  }
1480
1466
  }
1467
+ async function handleAuth(request) {
1468
+ try {
1469
+ const hookResult = await options.hooks?.build?.before?.(request);
1470
+ if (hookResult instanceof Response) {
1471
+ return hookResult;
1472
+ }
1473
+ return Response.json({ ok: true });
1474
+ } catch (error) {
1475
+ const message = error instanceof Error ? error.message : "Authentication failed.";
1476
+ return errorResponse(401, "UNAUTHORIZED", message);
1477
+ }
1478
+ }
1481
1479
  function matchSubPath(request) {
1482
1480
  const url = new URL(request.url);
1483
1481
  const pathname = url.pathname;
1482
+ if (pathname === authPath || pathname === `${authPath}/`) {
1483
+ return "auth";
1484
+ }
1484
1485
  if (pathname === runPath || pathname === `${runPath}/`) {
1485
1486
  return "run";
1486
1487
  }
@@ -1502,6 +1503,9 @@ function createAgentApi(options) {
1502
1503
  },
1503
1504
  POST: async (request) => {
1504
1505
  const sub = matchSubPath(request);
1506
+ if (sub === "auth") {
1507
+ return handleAuth(request);
1508
+ }
1505
1509
  if (sub === "run") {
1506
1510
  return handleRun(request);
1507
1511
  }
@@ -1523,7 +1527,6 @@ function createAgentApi(options) {
1523
1527
  };
1524
1528
  }
1525
1529
  export {
1526
- Agent,
1527
1530
  applyCloudChatPatch,
1528
1531
  cloudChatRunRequestSchema,
1529
1532
  createAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giselles-ai/agent",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "Apache-2.0",
@@ -37,7 +37,7 @@
37
37
  "format": "pnpm exec biome check --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@giselles-ai/browser-tool": "0.1.22",
40
+ "@giselles-ai/browser-tool": "0.1.24",
41
41
  "@vercel/sandbox": "1.6.0",
42
42
  "@iarna/toml": "3.0.0",
43
43
  "zod": "4.3.6"
@@ -1,17 +0,0 @@
1
- // src/hash.ts
2
- import { createHash } from "crypto";
3
- function computeConfigHash(config) {
4
- const payload = JSON.stringify({
5
- agentType: config.agentType ?? "gemini",
6
- agentMd: config.agentMd ?? null,
7
- files: (config.files ?? []).map((f) => ({
8
- path: f.path,
9
- content: f.content
10
- }))
11
- });
12
- return createHash("sha256").update(payload).digest("hex").slice(0, 16);
13
- }
14
-
15
- export {
16
- computeConfigHash
17
- };