@sandagent/sdk 0.8.3 → 0.8.5

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/README.md CHANGED
@@ -170,7 +170,7 @@ import { SandockSandbox } from "@sandagent/sandbox-sandock";
170
170
 
171
171
  const sandbox = new SandockSandbox({
172
172
  apiKey: process.env.SANDOCK_API_KEY, // Get yours at https://sandock.ai
173
- image: "vikadata/sandagent:0.1.0", // Pre-built image (fast startup)
173
+ image: "ghcr.io/vikadata/sandagent:latest", // Pre-built image (fast startup)
174
174
  skipBootstrap: true,
175
175
  workdir: "/workspace",
176
176
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
@@ -186,7 +186,7 @@ const sandagent = createSandAgent({ sandbox, cwd: sandbox.getWorkdir() });
186
186
  | Sandock Option | Description |
187
187
  |----------------|-------------|
188
188
  | `apiKey` | Your Sandock API key ([sandock.ai](https://sandock.ai)) |
189
- | `image` | Docker image — use `vikadata/sandagent:0.1.0` for pre-built, or `sandockai/sandock-code:latest` |
189
+ | `image` | Docker image — use `ghcr.io/vikadata/sandagent:latest` for pre-built, or `sandockai/sandock-code:latest` |
190
190
  | `skipBootstrap` | `true` when using pre-built image (skips npm install inside sandbox) |
191
191
  | `volumes` | Named volumes for persistent workspace & session storage |
192
192
  | `keep` | `true` (default) keeps sandbox alive ~30 min after execution |
@@ -253,6 +253,8 @@ import {
253
253
 
254
254
  ### Provider
255
255
 
256
+ #### `createSandAgent` — sandbox transport (cloud / local filesystem)
257
+
256
258
  ```typescript
257
259
  import { createSandAgent, LocalSandbox } from "@sandagent/sdk";
258
260
 
@@ -265,18 +267,32 @@ const sandagent = createSandAgent({
265
267
  verbose?: boolean, // Debug logging
266
268
  });
267
269
 
268
- // Use as AI SDK model
269
- const model = sandagent("sonnet"); // Claude Sonnet (latest)
270
- const model = sandagent("opus"); // Claude Opus
271
- const model = sandagent("haiku"); // Claude Haiku
272
- const model = sandagent("claude-sonnet-4-20250514"); // Specific version
270
+ const model = sandagent("claude-sonnet-4-20250514");
271
+ ```
272
+
273
+ #### `createSandAgentDaemon` daemon HTTP transport (local dev / buda embed)
274
+
275
+ Use this when `sandagent-daemon` is running — either as a standalone process in a container, or embedded in a Next.js app via `createNextHandler`.
276
+
277
+ ```typescript
278
+ import { createSandAgentDaemon } from "@sandagent/sdk";
279
+
280
+ const sandagent = createSandAgentDaemon({
281
+ daemonUrl: "http://localhost:3080", // or /api/daemon when embedded in Next.js
282
+ runner: "claude", // claude · pi · gemini · codex · opencode
283
+ cwd: "/workspace",
284
+ });
285
+
286
+ const model = sandagent("claude-sonnet-4-20250514");
273
287
  ```
274
288
 
289
+ Both return the same `LanguageModelV3` interface — swap transports without changing any other code.
290
+
275
291
  ### Exports
276
292
 
277
293
  | Entry Point | Exports |
278
294
  |-------------|---------|
279
- | `@sandagent/sdk` | `createSandAgent`, `LocalSandbox`, `SandAgentLanguageModel`, `submitAnswer` |
295
+ | `@sandagent/sdk` | `createSandAgent`, `createSandAgentDaemon`, `LocalSandbox`, `SandAgentLanguageModel`, `submitAnswer` |
280
296
  | `@sandagent/sdk/react` | `useSandAgentChat`, `useArtifacts`, `useWriteTool`, `useAskUserQuestion` |
281
297
 
282
298
  ---
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { LanguageModelV3StreamPart, ProviderV3, LanguageModelV3, EmbeddingModelV3, ImageModelV3, LanguageModelV3CallOptions, LanguageModelV3GenerateResult, LanguageModelV3StreamResult } from '@ai-sdk/provider';
2
- export { LanguageModelV3StreamPart } from '@ai-sdk/provider';
3
- import { SandAgentOptions, RunnerSpec, SandboxAdapter } from '@sandagent/manager';
1
+ import { SandAgentOptions, SandboxAdapter, RunnerSpec } from '@sandagent/manager';
4
2
  export { LocalSandbox, LocalSandboxOptions, Message, RunnerEnvParams, RunnerType, SandboxAdapter, SandboxHandle, TranscriptEntry, buildRunnerEnv } from '@sandagent/manager';
3
+ import { LanguageModelV3StreamPart, LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3GenerateResult, LanguageModelV3StreamResult, ProviderV3, EmbeddingModelV3, ImageModelV3 } from '@ai-sdk/provider';
4
+ export { LanguageModelV3StreamPart } from '@ai-sdk/provider';
5
5
 
6
6
  /**
7
7
  * Artifact Processor result
@@ -101,39 +101,31 @@ interface SandAgentProviderSettings extends Omit<SandAgentOptions, "runner" | "s
101
101
  type SandAgentModelId = string;
102
102
 
103
103
  /**
104
- * SandAgent provider interface that extends the AI SDK's ProviderV3.
104
+ * Options for submitAnswer
105
105
  */
106
- interface SandAgentProvider extends ProviderV3 {
107
- (modelId: SandAgentModelId, options?: Partial<SandAgentProviderSettings>): LanguageModelV3;
108
- languageModel(modelId: SandAgentModelId, options?: Partial<SandAgentProviderSettings>): LanguageModelV3;
109
- chat(modelId: SandAgentModelId, options?: Partial<SandAgentProviderSettings>): LanguageModelV3;
110
- embeddingModel(modelId: string): EmbeddingModelV3;
111
- textEmbeddingModel(modelId: string): EmbeddingModelV3;
112
- imageModel(modelId: string): ImageModelV3;
106
+ interface SubmitAnswerOptions {
107
+ /**
108
+ * Optional base path prefix for approval files
109
+ * @default ".sandagent/approvals"
110
+ */
111
+ basePath?: string;
113
112
  }
114
113
  /**
115
- * Creates a SandAgent provider instance with the specified configuration.
114
+ * Submit user's answer for an AskUserQuestion tool call.
115
+ * Writes the answer file to `.sandagent/approvals/{toolCallId}.json` in the sandbox workdir.
116
116
  *
117
117
  * @example
118
118
  * ```typescript
119
- * import { createSandAgent } from '@sandagent/sdk';
120
- * import { E2BSandbox } from '@sandagent/sandbox-e2b';
121
- * import { generateText } from 'ai';
122
- *
123
- * const sandagent = createSandAgent({
124
- * sandbox: new E2BSandbox({ apiKey: process.env.E2B_API_KEY! }),
125
- * env: {
126
- * ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
127
- * },
128
- * });
119
+ * import { submitAnswer } from "@sandagent/sdk";
129
120
  *
130
- * const { text } = await generateText({
131
- * model: sandagent('sonnet'),
132
- * prompt: 'Create a hello world program',
121
+ * await submitAnswer(sandbox, {
122
+ * toolCallId: "tool-456",
123
+ * questions: [...],
124
+ * answers: { "Question 1": "Answer 1" },
133
125
  * });
134
126
  * ```
135
127
  */
136
- declare function createSandAgent(defaultOptions: SandAgentProviderSettings): SandAgentProvider;
128
+ declare function submitAnswer(sandbox: SandboxAdapter, params: SubmitAnswerParams, options?: SubmitAnswerOptions): Promise<void>;
137
129
 
138
130
  /**
139
131
  * Options for creating a SandAgent language model instance.
@@ -167,31 +159,86 @@ declare class SandAgentLanguageModel implements LanguageModelV3 {
167
159
  }
168
160
 
169
161
  /**
170
- * Options for submitAnswer
162
+ * SandAgent provider interface that extends the AI SDK's ProviderV3.
171
163
  */
172
- interface SubmitAnswerOptions {
173
- /**
174
- * Optional base path prefix for approval files
175
- * @default ".sandagent/approvals"
176
- */
177
- basePath?: string;
164
+ interface SandAgentProvider extends ProviderV3 {
165
+ (modelId: SandAgentModelId, options?: Partial<SandAgentProviderSettings>): LanguageModelV3;
166
+ languageModel(modelId: SandAgentModelId, options?: Partial<SandAgentProviderSettings>): LanguageModelV3;
167
+ chat(modelId: SandAgentModelId, options?: Partial<SandAgentProviderSettings>): LanguageModelV3;
168
+ embeddingModel(modelId: string): EmbeddingModelV3;
169
+ textEmbeddingModel(modelId: string): EmbeddingModelV3;
170
+ imageModel(modelId: string): ImageModelV3;
178
171
  }
179
172
  /**
180
- * Submit user's answer for an AskUserQuestion tool call.
181
- * Writes the answer file to `.sandagent/approvals/{toolCallId}.json` in the sandbox workdir.
173
+ * Creates a SandAgent provider instance with the specified configuration.
182
174
  *
183
175
  * @example
184
176
  * ```typescript
185
- * import { submitAnswer } from "@sandagent/sdk";
177
+ * import { createSandAgent } from '@sandagent/sdk';
178
+ * import { E2BSandbox } from '@sandagent/sandbox-e2b';
179
+ * import { generateText } from 'ai';
186
180
  *
187
- * await submitAnswer(sandbox, {
188
- * toolCallId: "tool-456",
189
- * questions: [...],
190
- * answers: { "Question 1": "Answer 1" },
181
+ * const sandagent = createSandAgent({
182
+ * sandbox: new E2BSandbox({ apiKey: process.env.E2B_API_KEY! }),
183
+ * env: {
184
+ * ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
185
+ * },
186
+ * });
187
+ *
188
+ * const { text } = await generateText({
189
+ * model: sandagent('sonnet'),
190
+ * prompt: 'Create a hello world program',
191
191
  * });
192
192
  * ```
193
193
  */
194
- declare function submitAnswer(sandbox: SandboxAdapter, params: SubmitAnswerParams, options?: SubmitAnswerOptions): Promise<void>;
194
+ declare function createSandAgent(defaultOptions: SandAgentProviderSettings): SandAgentProvider;
195
+
196
+ interface DaemonProviderSettings {
197
+ /** Base URL of the sandagent-daemon, e.g. "http://localhost:3080" */
198
+ daemonUrl: string;
199
+ /** Runner to use: claude, pi, gemini, codex, opencode (default: claude) */
200
+ runner?: string;
201
+ /** Working directory inside the sandbox (default: SANDAGENT_ROOT) */
202
+ cwd?: string;
203
+ /** Resume session ID */
204
+ resume?: string;
205
+ /** Override system prompt */
206
+ systemPrompt?: string;
207
+ /** Max agent turns */
208
+ maxTurns?: number;
209
+ /** Allowed tools */
210
+ allowedTools?: string[];
211
+ /** Extra skill paths (pi runner) */
212
+ skillPaths?: string[];
213
+ }
214
+ /**
215
+ * Create a SandAgent provider that uses sandagent-daemon as transport.
216
+ * Use this for local dev (Next.js embed) or when daemon is running in a container.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * import { createSandAgentDaemon } from "@sandagent/sdk";
221
+ *
222
+ * const sandagent = createSandAgentDaemon({
223
+ * daemonUrl: "http://localhost:3080",
224
+ * runner: "claude",
225
+ * });
226
+ *
227
+ * const { text } = await generateText({
228
+ * model: sandagent("claude-sonnet-4-20250514"),
229
+ * prompt: "Build a REST API",
230
+ * });
231
+ * ```
232
+ */
233
+ declare function createSandAgentDaemon(settings: DaemonProviderSettings): {
234
+ (modelId: SandAgentModelId, overrides?: Partial<DaemonProviderSettings>): LanguageModelV3;
235
+ languageModel: (modelId: SandAgentModelId, overrides?: Partial<DaemonProviderSettings>) => LanguageModelV3;
236
+ chat: (modelId: SandAgentModelId, overrides?: Partial<DaemonProviderSettings>) => LanguageModelV3;
237
+ specificationVersion: "v3";
238
+ embeddingModel(): never;
239
+ textEmbeddingModel(): never;
240
+ imageModel(): never;
241
+ };
195
242
 
196
243
  /**
197
244
  * @sandagent/sdk
@@ -216,4 +263,4 @@ declare function submitAnswer(sandbox: SandboxAdapter, params: SubmitAnswerParam
216
263
 
217
264
  declare const VERSION = "0.1.0";
218
265
 
219
- export { type ArtifactProcessor, type ArtifactResult, type Logger, type Question, SandAgentLanguageModel, type SandAgentLanguageModelOptions, type SandAgentModelId, type SandAgentProvider, type SandAgentProviderSettings, type StreamWriter, type SubmitAnswerOptions, type SubmitAnswerParams, VERSION, createSandAgent, submitAnswer };
266
+ export { type ArtifactProcessor, type ArtifactResult, type DaemonProviderSettings, type Logger, type Question, SandAgentLanguageModel, type SandAgentLanguageModelOptions, type SandAgentModelId, type SandAgentProvider, type SandAgentProviderSettings, type StreamWriter, type SubmitAnswerOptions, type SubmitAnswerParams, VERSION, createSandAgent, createSandAgentDaemon, submitAnswer };
package/dist/index.js CHANGED
@@ -1,5 +1,31 @@
1
- // src/provider/sandagent-provider.ts
2
- import { NoSuchModelError } from "@ai-sdk/provider";
1
+ // src/index.ts
2
+ import { buildRunnerEnv, LocalSandbox } from "@sandagent/manager";
3
+
4
+ // src/provider/question-processor.ts
5
+ async function submitAnswer(sandbox, params, options) {
6
+ const { toolCallId, questions, answers } = params;
7
+ const basePath = options?.basePath ?? ".sandagent/approvals";
8
+ const allAnswered = questions.every(
9
+ (q) => answers[q.question] !== void 0 && answers[q.question] !== ""
10
+ );
11
+ const answerData = {
12
+ questions,
13
+ answers,
14
+ status: allAnswered ? "completed" : "pending",
15
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16
+ };
17
+ const filename = `${toolCallId}.json`;
18
+ const handle = sandbox.getHandle() ?? await sandbox.attach();
19
+ const workdir = handle.getWorkdir();
20
+ const targetDir = workdir ? `${workdir.replace(/\/$/, "")}/${basePath}` : basePath;
21
+ await handle.upload(
22
+ [{ path: filename, content: JSON.stringify(answerData, null, 2) }],
23
+ targetDir
24
+ );
25
+ console.log(
26
+ `[submitAnswer] Answer submitted: ${targetDir}/${filename} (status: ${answerData.status}, workdir: ${workdir ?? "(none)"})`
27
+ );
28
+ }
3
29
 
4
30
  // src/provider/sandagent-language-model.ts
5
31
  import { SandAgent } from "@sandagent/manager";
@@ -547,6 +573,9 @@ var SandAgentLanguageModel = class {
547
573
  }
548
574
  };
549
575
 
576
+ // src/provider/sandagent-provider.ts
577
+ import { NoSuchModelError } from "@ai-sdk/provider";
578
+
550
579
  // src/provider/types.ts
551
580
  function getRunnerKindForModel(_modelId) {
552
581
  return "claude-agent-sdk";
@@ -648,35 +677,157 @@ function createSandAgent(defaultOptions) {
648
677
  return provider;
649
678
  }
650
679
 
651
- // src/provider/question-processor.ts
652
- async function submitAnswer(sandbox, params, options) {
653
- const { toolCallId, questions, answers } = params;
654
- const basePath = options?.basePath ?? ".sandagent/approvals";
655
- const allAnswered = questions.every(
656
- (q) => answers[q.question] !== void 0 && answers[q.question] !== ""
657
- );
658
- const answerData = {
659
- questions,
660
- answers,
661
- status: allAnswered ? "completed" : "pending",
662
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
680
+ // src/provider/sandagent-daemon-provider.ts
681
+ var emptyUsage = {
682
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
683
+ outputTokens: { total: 0, text: void 0, reasoning: void 0 }
684
+ };
685
+ var DaemonLanguageModel = class {
686
+ constructor(modelId, settings) {
687
+ this.settings = settings;
688
+ this.modelId = modelId;
689
+ }
690
+ specificationVersion = "v3";
691
+ provider = "sandagent-daemon";
692
+ modelId;
693
+ supportedUrls = {};
694
+ async doGenerate(options) {
695
+ const { stream, request } = await this.doStream(options);
696
+ const reader = stream.getReader();
697
+ const parts = [];
698
+ try {
699
+ while (true) {
700
+ const { done, value } = await reader.read();
701
+ if (done) break;
702
+ parts.push(value);
703
+ }
704
+ } finally {
705
+ reader.releaseLock();
706
+ }
707
+ const textMap = /* @__PURE__ */ new Map();
708
+ for (const part of parts) {
709
+ if (part.type === "text-start") textMap.set(part.id, "");
710
+ if (part.type === "text-delta") {
711
+ textMap.set(part.id, (textMap.get(part.id) ?? "") + part.delta);
712
+ }
713
+ }
714
+ const content = [...textMap.entries()].map(([, text]) => ({
715
+ type: "text",
716
+ text
717
+ }));
718
+ const finish = parts.find((p) => p.type === "finish");
719
+ return {
720
+ content,
721
+ finishReason: finish?.type === "finish" ? finish.finishReason : { unified: "other", raw: void 0 },
722
+ usage: finish?.type === "finish" ? finish.usage : emptyUsage,
723
+ warnings: [],
724
+ request
725
+ };
726
+ }
727
+ async doStream(options) {
728
+ const { prompt, abortSignal } = options;
729
+ const lastUser = [...prompt].reverse().find((m) => m.role === "user");
730
+ const userInput = lastUser?.content.filter((p) => p.type === "text").map((p) => p.text).join("\n") ?? "";
731
+ const body = {
732
+ runner: this.settings.runner ?? "claude",
733
+ model: this.modelId,
734
+ userInput,
735
+ cwd: this.settings.cwd,
736
+ resume: this.settings.resume,
737
+ systemPrompt: this.settings.systemPrompt,
738
+ maxTurns: this.settings.maxTurns,
739
+ allowedTools: this.settings.allowedTools,
740
+ skillPaths: this.settings.skillPaths
741
+ };
742
+ const response = await fetch(`${this.settings.daemonUrl}/api/coding/run`, {
743
+ method: "POST",
744
+ headers: { "Content-Type": "application/json" },
745
+ body: JSON.stringify(body),
746
+ signal: abortSignal
747
+ });
748
+ if (!response.ok || !response.body) {
749
+ throw new Error(
750
+ `daemon error: ${response.status} ${response.statusText}`
751
+ );
752
+ }
753
+ const reader = response.body.getReader();
754
+ const decoder = new TextDecoder();
755
+ const stream = new ReadableStream({
756
+ async start(controller) {
757
+ let buffer = "";
758
+ try {
759
+ while (true) {
760
+ const { done, value } = await reader.read();
761
+ if (done) {
762
+ controller.close();
763
+ break;
764
+ }
765
+ buffer += decoder.decode(value, { stream: true });
766
+ const lines = buffer.split("\n");
767
+ buffer = lines.pop() ?? "";
768
+ for (const line of lines) {
769
+ if (!line.trim()) continue;
770
+ try {
771
+ const msg = JSON.parse(line);
772
+ if (msg.type === "text-delta") {
773
+ controller.enqueue({
774
+ type: "text-delta",
775
+ id: msg.id,
776
+ delta: msg.delta
777
+ });
778
+ } else if (msg.type === "text-start") {
779
+ controller.enqueue({
780
+ type: "text-start",
781
+ id: msg.id
782
+ });
783
+ } else if (msg.type === "text-end") {
784
+ controller.enqueue({
785
+ type: "text-end",
786
+ id: msg.id
787
+ });
788
+ } else if (msg.type === "finish") {
789
+ controller.enqueue({
790
+ type: "finish",
791
+ finishReason: { unified: "stop", raw: "stop" },
792
+ usage: emptyUsage
793
+ });
794
+ }
795
+ } catch {
796
+ }
797
+ }
798
+ }
799
+ } catch (err) {
800
+ controller.error(err);
801
+ }
802
+ },
803
+ cancel() {
804
+ reader.cancel();
805
+ }
806
+ });
807
+ return { stream, request: { body: JSON.stringify(body) } };
808
+ }
809
+ };
810
+ function createSandAgentDaemon(settings) {
811
+ const createModel = (modelId, overrides = {}) => {
812
+ return new DaemonLanguageModel(modelId, { ...settings, ...overrides });
663
813
  };
664
- const filename = `${toolCallId}.json`;
665
- const handle = sandbox.getHandle() ?? await sandbox.attach();
666
- const workdir = handle.getWorkdir();
667
- const targetDir = workdir ? `${workdir.replace(/\/$/, "")}/${basePath}` : basePath;
668
- await handle.upload(
669
- [{ path: filename, content: JSON.stringify(answerData, null, 2) }],
670
- targetDir
671
- );
672
- console.log(
673
- `[submitAnswer] Answer submitted: ${targetDir}/${filename} (status: ${answerData.status}, workdir: ${workdir ?? "(none)"})`
674
- );
814
+ const provider = (modelId, overrides) => createModel(modelId, overrides);
815
+ provider.languageModel = createModel;
816
+ provider.chat = createModel;
817
+ provider.specificationVersion = "v3";
818
+ provider.embeddingModel = () => {
819
+ throw new Error("not supported");
820
+ };
821
+ provider.textEmbeddingModel = () => {
822
+ throw new Error("not supported");
823
+ };
824
+ provider.imageModel = () => {
825
+ throw new Error("not supported");
826
+ };
827
+ return provider;
675
828
  }
676
829
 
677
830
  // src/index.ts
678
- import { LocalSandbox } from "@sandagent/manager";
679
- import { buildRunnerEnv } from "@sandagent/manager";
680
831
  var VERSION = "0.1.0";
681
832
  export {
682
833
  LocalSandbox,
@@ -684,6 +835,7 @@ export {
684
835
  VERSION,
685
836
  buildRunnerEnv,
686
837
  createSandAgent,
838
+ createSandAgentDaemon,
687
839
  submitAnswer
688
840
  };
689
841
  //# sourceMappingURL=index.js.map