@sandagent/sdk 0.8.2 → 0.8.4

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
@@ -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";
@@ -428,13 +454,34 @@ var SandAgentLanguageModel = class {
428
454
  break;
429
455
  }
430
456
  case "user": {
431
- const textParts = message.content.filter(
432
- (part) => part.type === "text"
433
- ).map((part) => part.text);
434
- if (textParts.length > 0) {
457
+ const parts = message.content.map((part) => {
458
+ if (part.type === "text") {
459
+ return { type: "text", text: part.text };
460
+ }
461
+ if (part.type === "file") {
462
+ let dataStr = "";
463
+ if (part.data instanceof Uint8Array) {
464
+ dataStr = `data:${part.mediaType};base64,${Buffer.from(part.data).toString("base64")}`;
465
+ } else if (part.data instanceof URL) {
466
+ dataStr = part.data.toString();
467
+ } else if (typeof part.data === "string") {
468
+ dataStr = part.data;
469
+ }
470
+ return {
471
+ type: "image",
472
+ mimeType: part.mediaType || "image/png",
473
+ data: dataStr
474
+ };
475
+ }
476
+ return null;
477
+ }).filter(
478
+ (p) => p !== null
479
+ );
480
+ if (parts.length > 0) {
481
+ const isAllText = parts.every((p) => p.type === "text");
435
482
  messages.push({
436
483
  role: "user",
437
- content: textParts.join("\n")
484
+ content: isAllText ? parts.map((p) => p.text).join("\n") : parts
438
485
  });
439
486
  }
440
487
  break;
@@ -526,6 +573,9 @@ var SandAgentLanguageModel = class {
526
573
  }
527
574
  };
528
575
 
576
+ // src/provider/sandagent-provider.ts
577
+ import { NoSuchModelError } from "@ai-sdk/provider";
578
+
529
579
  // src/provider/types.ts
530
580
  function getRunnerKindForModel(_modelId) {
531
581
  return "claude-agent-sdk";
@@ -627,35 +677,157 @@ function createSandAgent(defaultOptions) {
627
677
  return provider;
628
678
  }
629
679
 
630
- // src/provider/question-processor.ts
631
- async function submitAnswer(sandbox, params, options) {
632
- const { toolCallId, questions, answers } = params;
633
- const basePath = options?.basePath ?? ".sandagent/approvals";
634
- const allAnswered = questions.every(
635
- (q) => answers[q.question] !== void 0 && answers[q.question] !== ""
636
- );
637
- const answerData = {
638
- questions,
639
- answers,
640
- status: allAnswered ? "completed" : "pending",
641
- 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 });
642
813
  };
643
- const filename = `${toolCallId}.json`;
644
- const handle = sandbox.getHandle() ?? await sandbox.attach();
645
- const workdir = handle.getWorkdir();
646
- const targetDir = workdir ? `${workdir.replace(/\/$/, "")}/${basePath}` : basePath;
647
- await handle.upload(
648
- [{ path: filename, content: JSON.stringify(answerData, null, 2) }],
649
- targetDir
650
- );
651
- console.log(
652
- `[submitAnswer] Answer submitted: ${targetDir}/${filename} (status: ${answerData.status}, workdir: ${workdir ?? "(none)"})`
653
- );
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;
654
828
  }
655
829
 
656
830
  // src/index.ts
657
- import { LocalSandbox } from "@sandagent/manager";
658
- import { buildRunnerEnv } from "@sandagent/manager";
659
831
  var VERSION = "0.1.0";
660
832
  export {
661
833
  LocalSandbox,
@@ -663,6 +835,7 @@ export {
663
835
  VERSION,
664
836
  buildRunnerEnv,
665
837
  createSandAgent,
838
+ createSandAgentDaemon,
666
839
  submitAnswer
667
840
  };
668
841
  //# sourceMappingURL=index.js.map