@soba-so/sdk 0.0.10 → 0.0.12
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/client.d.ts +119 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/protocol.d.ts +40 -0
- package/dist/resources.d.ts +108 -2
- package/dist/version.d.ts +1 -1
- package/package.json +7 -6
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message, RouteRequest, RunPolicy } from "./protocol.js";
|
|
2
|
-
import type { ConnectContextResponse, MachineListItem, PlanListResponse, SobaUser, UsagePage, UsageQuery } from "./resources.js";
|
|
2
|
+
import type { ConnectContextResponse, FeatureCheckResult, MachineListItem, PlanListResponse, QuantityResult, SobaUser, TrackResult, UsagePage, UsageQuery } from "./resources.js";
|
|
3
3
|
import { type Approver, Run } from "./run.js";
|
|
4
4
|
import { type ToolDefinition } from "./tools.js";
|
|
5
5
|
/**
|
|
@@ -15,11 +15,18 @@ import { type ToolDefinition } from "./tools.js";
|
|
|
15
15
|
* two hosts both exist and why only this one is the run endpoint.
|
|
16
16
|
*/
|
|
17
17
|
export declare const DEFAULT_BASE_URL = "https://api.soba.so/v1";
|
|
18
|
+
export declare const DEFAULT_WEB_BASE_URL = "https://soba.so/v1";
|
|
18
19
|
export interface SobaOptions {
|
|
19
20
|
/** Defaults to `SOBA_KEY`, then `SOBA_API_KEY`. Shaped `sk_soba_<id>.<secret>`. */
|
|
20
21
|
apiKey?: string;
|
|
21
22
|
/** Defaults to `SOBA_BASE_URL`, then https://api.soba.so/v1. */
|
|
22
23
|
baseURL?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Where the calls that touch your Stripe go: `users.setQuantity`. Defaults
|
|
26
|
+
* to `SOBA_WEB_URL`, then https://soba.so/v1 — the dashboard holds the
|
|
27
|
+
* Stripe App connection, and the run gateway at `baseURL` does not.
|
|
28
|
+
*/
|
|
29
|
+
webBaseURL?: string;
|
|
23
30
|
fetch?: typeof globalThis.fetch;
|
|
24
31
|
/** Retries before the first byte only. Never after a stream has started. */
|
|
25
32
|
maxRetries?: number;
|
|
@@ -77,6 +84,13 @@ export interface RunParams {
|
|
|
77
84
|
* means every escalation is denied. */
|
|
78
85
|
approve?: Approver;
|
|
79
86
|
metadata?: Record<string, string>;
|
|
87
|
+
/**
|
|
88
|
+
* The task in your app this run is part of — any id you choose, like
|
|
89
|
+
* `report_8812`. On a plan limited by tasks, every run with the same id
|
|
90
|
+
* counts once, so a task that takes several agent runs is one task. A run
|
|
91
|
+
* without one is a task on its own.
|
|
92
|
+
*/
|
|
93
|
+
task?: string;
|
|
80
94
|
signal?: AbortSignal;
|
|
81
95
|
/**
|
|
82
96
|
* Stop the run after this many milliseconds. It can only shorten a run: the
|
|
@@ -133,6 +147,57 @@ export interface AttachParams {
|
|
|
133
147
|
* anywhere and the app reads it with `soba.runs.attach(id, …)` — no public URL,
|
|
134
148
|
* no tunnel on a laptop.
|
|
135
149
|
*/
|
|
150
|
+
export type OpKind = "git.snapshot" | "git.diff" | "git.push" | "git.update" | "git.worktree.add" | "git.worktree.remove" | "fs.list" | "fs.read" | "fs.write" | "fs.copy";
|
|
151
|
+
export interface OpParams {
|
|
152
|
+
/** Whose machine. Resolve it on your server, never from the browser. */
|
|
153
|
+
user: string;
|
|
154
|
+
kind: OpKind;
|
|
155
|
+
/** The repository checkout on that machine. Must be inside a granted root. */
|
|
156
|
+
repo: string;
|
|
157
|
+
/** The workspace's branch; its worktree lives under `repo`. */
|
|
158
|
+
branch?: string;
|
|
159
|
+
/** The branch it compares to or updates from. */
|
|
160
|
+
base?: string;
|
|
161
|
+
/** `git.diff`, `fs.read`, `fs.write`: one file. `fs.list`: a folder. Relative to the worktree. */
|
|
162
|
+
path?: string;
|
|
163
|
+
/** `fs.write`: the file's new text. Only an existing file is written. */
|
|
164
|
+
text?: string;
|
|
165
|
+
/** `fs.write`: the hash `fs.read` gave; a file changed since is refused with `conflict`. */
|
|
166
|
+
expect?: string;
|
|
167
|
+
/** `fs.copy`: files relative to `repo` to copy into the worktree. */
|
|
168
|
+
files?: string[];
|
|
169
|
+
mode?: "rebase" | "merge";
|
|
170
|
+
/** `git.worktree.add`: fetch the branch from this remote first. */
|
|
171
|
+
remote?: string;
|
|
172
|
+
}
|
|
173
|
+
/** The machine's answer. A refusal is an answer: `ok: false` with its reason. */
|
|
174
|
+
export interface OpResult<T = unknown> {
|
|
175
|
+
ok: boolean;
|
|
176
|
+
data?: T;
|
|
177
|
+
error?: string;
|
|
178
|
+
/** `not_allowed` · `outside_roots` · `no_worktree` · `conflict` · `dirty` · `failed` */
|
|
179
|
+
code?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface TerminalParams {
|
|
182
|
+
/** Whose machine. Resolve it on your server, never from the browser. */
|
|
183
|
+
user: string;
|
|
184
|
+
/** The repository checkout on that machine. Must be inside a granted root. */
|
|
185
|
+
repo: string;
|
|
186
|
+
/** A workspace's branch: the shell opens in its worktree under `repo`. */
|
|
187
|
+
branch?: string;
|
|
188
|
+
cols?: number;
|
|
189
|
+
rows?: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A booked web terminal. Hand `url` to your page; it opens a WebSocket there
|
|
193
|
+
* within a minute, once. Frames: send `{t:"i",d}` keystrokes and
|
|
194
|
+
* `{t:"r",cols,rows}` sizes; receive `{t:"o",d}` output and `{t:"x",code,error?}`.
|
|
195
|
+
*/
|
|
196
|
+
export interface TerminalBooking {
|
|
197
|
+
termId: string;
|
|
198
|
+
url: string;
|
|
199
|
+
expiresAt: string;
|
|
200
|
+
}
|
|
136
201
|
export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal" | "resumable"> {
|
|
137
202
|
/**
|
|
138
203
|
* Where Soba sends the run's tool calls (and, unless `events: "tools"`, its
|
|
@@ -158,10 +223,14 @@ export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal"
|
|
|
158
223
|
surface?: "headless" | "terminal";
|
|
159
224
|
/** What the terminal window is called. */
|
|
160
225
|
title?: string;
|
|
161
|
-
/** Work in a git worktree on this branch, inside `policy.cwd`.
|
|
226
|
+
/** Work in a git worktree on this branch, inside `policy.cwd`. A new branch
|
|
227
|
+
* starts from `base`, which is also what the run's git report compares to. */
|
|
162
228
|
worktree?: {
|
|
163
229
|
branch: string;
|
|
230
|
+
base?: string;
|
|
164
231
|
};
|
|
232
|
+
/** Continue the conversation a previous run reported as its `session`. */
|
|
233
|
+
resume?: string;
|
|
165
234
|
}
|
|
166
235
|
/**
|
|
167
236
|
* The Soba client.
|
|
@@ -198,6 +267,39 @@ export declare class Soba {
|
|
|
198
267
|
* client built for it: `new Soba({ maxRetries: 0, connectTimeoutMs: 1500 })`.
|
|
199
268
|
*/
|
|
200
269
|
compute(id: string): Promise<MachineListItem[]>;
|
|
270
|
+
/**
|
|
271
|
+
* HOW MANY OF A PRICED FEATURE THIS USER HAS IN USE NOW — seats.
|
|
272
|
+
*
|
|
273
|
+
* SETS THE TOTAL, never adds: call it with the true count wherever it
|
|
274
|
+
* changes (a member added, a member removed, a bulk import), and a missed
|
|
275
|
+
* or doubled call is corrected by the next. A count that already matches
|
|
276
|
+
* writes nothing to Stripe, so calling it on a schedule as a resync is
|
|
277
|
+
* free. Stripe bills what is past the plan's included units on the next
|
|
278
|
+
* invoice; Soba never counts seats itself.
|
|
279
|
+
*/
|
|
280
|
+
setQuantity(id: string, feature: string, quantity: number): Promise<QuantityResult>;
|
|
281
|
+
/**
|
|
282
|
+
* MAY THIS PERSON DO THIS — ask before a gated action: create a workspace,
|
|
283
|
+
* run an export. Answers from their plan, whether it has ended, and how
|
|
284
|
+
* many are in use, with a `reason` when the answer is no.
|
|
285
|
+
*
|
|
286
|
+
* Pass `use` to count it in the same call: a yes then records that many,
|
|
287
|
+
* atomically against the limit, so two requests for the last one cannot
|
|
288
|
+
* both get it. Without `use` it only asks.
|
|
289
|
+
*
|
|
290
|
+
* ```ts
|
|
291
|
+
* const { allowed, reason } = await soba.users.check(userId, "workspaces", { use: 1 })
|
|
292
|
+
* if (!allowed) return showUpgrade(reason)
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
check(id: string, feature: string, options?: {
|
|
296
|
+
use?: number;
|
|
297
|
+
}): Promise<FeatureCheckResult>;
|
|
298
|
+
/**
|
|
299
|
+
* THEY JUST DID IT — record use without asking. `amount` defaults to 1 and
|
|
300
|
+
* may be negative: a workspace deleted is `-1`. For seats, use `setQuantity`.
|
|
301
|
+
*/
|
|
302
|
+
track(id: string, feature: string, amount?: number): Promise<TrackResult>;
|
|
201
303
|
};
|
|
202
304
|
readonly usage: {
|
|
203
305
|
list(query?: UsageQuery): Promise<UsagePage>;
|
|
@@ -232,6 +334,21 @@ export declare class Soba {
|
|
|
232
334
|
/** Stop a run. For a terminal run, the window closes. */
|
|
233
335
|
cancel(id: string): Promise<void>;
|
|
234
336
|
};
|
|
337
|
+
/**
|
|
338
|
+
* Agent workspaces: ask one of a user's machines about, or act on, a git
|
|
339
|
+
* worktree it made for a run — a diff, a push, an update from the base. The
|
|
340
|
+
* machine only answers with `agentOps` on in its owner's policy.
|
|
341
|
+
*/
|
|
342
|
+
readonly ops: {
|
|
343
|
+
run<T = unknown>(params: OpParams): Promise<OpResult<T>>;
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Web terminals: a shell on one of a user's machines, typed into from a
|
|
347
|
+
* browser. Only on a machine whose owner set `webTerminal` in its policy.
|
|
348
|
+
*/
|
|
349
|
+
readonly terminals: {
|
|
350
|
+
open(params: TerminalParams): Promise<TerminalBooking>;
|
|
351
|
+
};
|
|
235
352
|
constructor(options?: SobaOptions);
|
|
236
353
|
get baseURL(): string;
|
|
237
354
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { type AttachParams, type
|
|
1
|
+
export { type AttachParams, DEFAULT_BASE_URL, DEFAULT_WEB_BASE_URL, type DetachedRunParams, type OpKind, type OpParams, type OpResult, type RunParams, Soba, type SobaOptions, type StartedRun, type TerminalBooking, type TerminalParams, } from "./client.js";
|
|
2
2
|
export { SobaError, type SobaErrorCode } from "./errors.js";
|
|
3
|
-
export { type AgentEvent, type AgentEventType, type CostClass, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
|
|
4
|
-
export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, MachineListItem, MachineListState, MachineRuntime, PlanListItem, PlanListResponse, PlanSummary, RuntimeListItem, SobaUser, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
|
|
3
|
+
export { type AgentEvent, type AgentEventType, type CostClass, type GitSnapshot, type ImageInput, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
|
|
4
|
+
export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, FeatureCheckResult, FeatureDenial, MachineListItem, MachineListState, MachineRuntime, PlanFeatureWire, PlanListItem, PlanListResponse, PlanSummary, QuantityResult, RuntimeListItem, SobaUser, TrackResult, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
|
|
5
5
|
export { type ApprovalDecision, type ApprovalRequest, type Approver, Run, } from "./run.js";
|
|
6
6
|
export { type ToolContext, type ToolDefinition, tool } from "./tools.js";
|
|
7
7
|
export { VERSION } from "./version.js";
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var a=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??
|
|
2
|
-
`);if(m===-1)break;let
|
|
3
|
-
`);if(u=[],
|
|
4
|
-
`);if(l&&l!=="[DONE]"){let f=_(l);f&&(yield n?f:h(f))}}finally{o.cancel().catch(()=>{})}}function D(t,e){if(t.startsWith(":"))return;let r=t.indexOf(":");if(r===-1||t.slice(0,r)!=="data")return;let n=t.slice(r+1);e.push(n.startsWith(" ")?n.slice(1):n)}function N(t){return t.endsWith("\r")?t.slice(0,-1):t}function ae(t){return t}function w(t){let e=new Map;for(let r of t??[]){if(!r?.name)throw new a({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new a({code:"invalid_request",message:`Tool "${r.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(r.name))throw new a({code:"invalid_request",message:`Two tools are named "${r.name}". Tool names must be unique within a run.`});e.set(r.name,r)}return e}function C(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function T(t){if(t===void 0)return null;if(typeof t=="string")return t;try{return JSON.parse(JSON.stringify(t))}catch{return String(t)}}function R(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var S=class{#e;#n;#r;#u=!1;#o;#t;#s=new AbortController;#i=new AbortController;#a=new Set;#c;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#o=e.after??0,this.#c=k(e.signal,this.#s)}get cursor(){return this.#o}get resumable(){return this.#t.response.headers.get("soba-resumable")==="1"}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#l()}async text(){let e="";for await(let r of this)if(r.type==="delta"&&r.text&&(e+=r.text),r.type==="error")throw new a({code:"run_failed",message:r.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#s.abort(new a({code:"canceled",message:"The run was cancelled."})),this.#i.abort(new a({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await y(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#l(){if(this.#u)throw new a({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#u=!0;let e=!1;try{for await(let r of F(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.seq!==void 0&&(this.#o=r.seq),r.kind==="event"){let n=r.event;if(n.tier&&(this.#n=n.tier),n.usage&&(this.#r=n.usage),e=A(n),yield n,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#p(this.#f(r))}throw new a({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#i.abort(new a({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#a]),this.#s.abort(),this.#c()}}#p(e){this.#a.add(e),e.finally(()=>this.#a.delete(e)).catch(()=>{})}async#f(e){if(!((e.kind==="tool_call"||e.kind==="approval")&&e.answered)){if(e.kind==="tool_call")return this.#g(e);if(e.kind==="approval")return this.#m(e)}}async#g(e){let r=this.#t.tools.get(e.name),n,o=!1;if(!r)n=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#i.signal};try{n=await r.execute(e.input,s)}catch(i){n=R(i),o=!0}}await this.#d(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:T(n),isError:o})}async#m(e){let r="deny",n;if(!this.#t.approve)n="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),r=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){n=R(o)}await this.#d(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...n?{reason:n}:{}})}async#d(e,r){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",r);return}try{await(await y(this.#t.config,{method:"POST",path:e,body:r})).text()}catch(n){this.#t.onWarning(`Could not deliver an answer to ${e}.`,b(n))}}};var W="https://api.soba.so/v1",ue=3e4,ce=2,M=class{#e;#n;users;usage;connect;plans;runs;constructor(e={}){let r=e.apiKey??I("SOBA_KEY")??I("SOBA_API_KEY");if(!r)throw new a({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(de()&&!e.dangerouslyAllowBrowser)throw new a({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#n=e.onWarning??(()=>{}),this.#e={apiKey:r,baseURL:e.baseURL??I("SOBA_BASE_URL")??W,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??ue,maxRetries:e.maxRetries??ce},typeof this.#e.fetch!="function")throw new a({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:n=>this.request("GET",`/users/${encodeURIComponent(n)}`),compute:async n=>(await this.request("GET",`/users/${encodeURIComponent(n)}/compute`)).machines},this.usage={list:n=>this.request("GET","/usage",{query:{...n}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async n=>{if(!n.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let o=w(n.tools),s={user:n.user,model:n.model??"auto",detached:!0};n.webhook&&(s.webhook=n.webhook),n.prompt&&(s.prompt=n.prompt),n.messages?.length&&(s.messages=n.messages),n.system&&(s.system=n.system),n.runtime&&(s.runtime=n.runtime),o.size&&(s.tools=C(o)),n.route&&(s.route=n.route),n.policy&&(s.policy=n.policy),n.metadata&&(s.metadata=n.metadata),n.surface&&(s.surface=n.surface),n.maxDurationMs&&(s.maxDurationMs=n.maxDurationMs),n.title&&(s.title=n.title),n.worktree&&(s.worktree=n.worktree);let i=await E(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"}),u={id:i.id};return i.stream&&(u.stream={url:i.stream.url,token:i.stream.token,expiresAt:i.stream.expires_at}),n.answerTools==="here"&&(u.answered=(async()=>{let c=await this.runs.attach(i.id,{user:n.user,tools:n.tools??[]});for await(let h of c);})().catch(c=>this.#n("Answering this run's tool calls stopped.",c))),u},attach:async(n,o)=>{if(!o.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let s=await y(this.#e,{method:"GET",path:`/runs/${encodeURIComponent(n)}/stream`,...o.after?{query:{after:o.after}}:{},accept:"text/event-stream",signal:o.signal});return new S({config:this.#e,response:s,user:o.user,tools:w(o.tools),approve:o.approve,onWarning:this.#n,signal:o.signal,after:o.after??0})},cancel:async n=>{await this.request("POST",`/runs/${encodeURIComponent(n)}/cancel`,{body:{}})}},this.plans={list:n=>this.request("GET","/plans",{...n?.user?{query:{user:n.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new a({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new a({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let r=w(e.tools),n={user:e.user,model:e.model??"auto"};e.prompt&&(n.prompt=e.prompt),e.messages?.length&&(n.messages=e.messages),e.system&&(n.system=e.system),e.runtime&&(n.runtime=e.runtime),r.size&&(n.tools=C(r)),e.route&&(n.route=e.route),e.policy&&(n.policy=e.policy),e.metadata&&(n.metadata=e.metadata),e.resumable&&(n.resumable=!0),e.maxDurationMs&&(n.maxDurationMs=e.maxDurationMs);let o=await y(this.#e,{method:"POST",path:"/runs",body:n,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new S({config:this.#e,response:o,user:e.user,tools:r,approve:e.approve,onWarning:this.#n,signal:e.signal})}async request(e,r,n={}){return E(this.#e,{method:e,path:r,...n})}};function I(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function de(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}var le=300;function pe(t){return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,"0")).join("")}function fe(t,e){if(t.length!==e.length)return!1;let r=0;for(let n=0;n<t.length;n++)r|=t.charCodeAt(n)^e.charCodeAt(n);return r===0}async function ge(t,e,r,n={}){let o=Object.fromEntries((e??"").split(",").map(g=>{let p=g.indexOf("=");return[g.slice(0,p).trim(),g.slice(p+1).trim()]})),s=Number(o.t),i=o.v1??"",u=g=>new a({code:"unauthorized",status:401,message:g});if(!Number.isFinite(s)||!i)throw u("Missing or malformed Soba-Signature.");let c=Math.floor((n.now??Date.now())/1e3);if(Math.abs(c-s)>(n.toleranceSeconds??le))throw u("This delivery is too old to accept.");let h=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=pe(await globalThis.crypto.subtle.sign("HMAC",h,new TextEncoder().encode(`${s}.${t}`)));if(!fe(l,i))throw u("The Soba-Signature does not match.");let f;try{f=JSON.parse(t)}catch{throw new a({code:"invalid_request",message:"The delivery is not JSON."})}let m=f;if(m.type!=="run.event"&&m.type!=="run.tool_call"||typeof m.run_id!="string")throw new a({code:"invalid_request",message:"Not a Soba run delivery."});return m}async function me(t,e){if(t.type!=="run.tool_call")return null;let{call_id:r,name:n,input:o}=t.data,s=w(e.tools).get(n);if(!s)return{result:`Unknown tool: ${n}`,is_error:!0};try{let i=await s.execute(o??{},{runId:t.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:T(i)}}catch(i){return{result:R(i),is_error:!0}}}export{W as DEFAULT_BASE_URL,U as METERED_CLASSES,S as Run,M as Soba,a as SobaError,ie as TOOL_SLUGS,x as VERSION,me as handleRunWebhook,se as isCostClass,oe as isMetered,A as isTerminal,ae as tool,ge as verifyWebhook};
|
|
1
|
+
var a=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??B.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},B=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),G={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function K(n){let e=G[n];return e||(n>=500?"server_error":n>=400?"invalid_request":"unknown")}async function q(n){let e=n.status,r="";try{r=await n.text()}catch{}let t=H(r);return new a({code:t?.code??K(e),type:t?.type,message:t?.message??z(e,n.statusText,r),status:e,retryAfterMs:Q(n.headers.get("retry-after")),runId:n.headers.get("soba-run-id")??void 0})}function H(n){if(!n)return null;let e;try{e=JSON.parse(n)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let t=r;return{message:typeof t.message=="string"&&t.message?t.message:void 0,type:typeof t.type=="string"&&t.type?t.type:void 0,code:J.has(t.code)?t.code:void 0}}var J=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","unknown_run","not_resumable","machine_gone","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function z(n,e,r){let t=r.trim().slice(0,200),o=t?`: ${t}`:"";return`Soba returned ${n}${e?` ${e}`:""}${o}`}function Q(n){if(!n)return;let e=Number(n);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(n);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function w(n){if(n instanceof a)return n;let e=n;return e?.name==="AbortError"||e?.name==="TimeoutError"?new a({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:n}):new a({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:n})}var E="0.0.12";var Y=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function V(n){if(n.code==="rate_limited")return!0;if(n.code!=="connection")return!1;let e=n.cause;for(let r=0;e&&r<4;r++){let t=e.code;if(typeof t=="string"&&Y.has(t))return!0;e=e.cause}return!1}var X=8e3,Z=3e4;async function b(n,e){let r=ne(n.baseURL,e.path,e.query),t=e.retry===!1?1:Math.max(0,n.maxRetries)+1,o,s=i=>i.retryable&&(e.retry!=="unsent"||V(i));for(let i=0;i<t;i++){i>0&&await oe(re(i,o),e.signal);let u=new AbortController,d=A(e.signal,u),h=setTimeout(()=>{u.abort(new a({code:"timeout",message:`Soba did not answer within ${n.connectTimeoutMs}ms.`}))},n.connectTimeoutMs),l;try{l=await n.fetch(r,{method:e.method,headers:ee(n,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(d(),o=w(u.signal.aborted?u.signal.reason??p:p),!s(o)||i===t-1)throw o;continue}finally{clearTimeout(h)}if(l.ok)return l;if(d(),o=await q(l),!s(o)||i===t-1)throw o}throw o??new a({code:"unknown",message:"The request failed with no error."})}async function R(n,e){let r=await b(n,{accept:"application/json",...e}),t=await r.text();if(t)try{return JSON.parse(t)}catch(o){throw new a({code:"server_error",message:`Soba returned a body that is not JSON: ${t.slice(0,200)}`,status:r.status,cause:o})}}function ee(n,e){let r={authorization:`Bearer ${n.apiKey}`,accept:e.accept??"application/json","user-agent":te(),...n.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function te(){let n=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${E}${n}`}function ne(n,e,r){let t=n.replace(/\/+$/,""),o=new URL(`${t}${e.startsWith("/")?e:`/${e}`}`);for(let[s,i]of Object.entries(r??{}))i!==void 0&&o.searchParams.set(s,String(i));return o.toString()}function re(n,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Z);let r=Math.min(X,500*2**(n-1));return Math.round(r*(.5+Math.random()/2))}function oe(n,e){return new Promise((r,t)=>{if(e?.aborted)return t(w(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},n);function s(){clearTimeout(o),t(w(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function A(n,e){if(!n)return()=>{};if(n.aborted)return e.abort(n.reason),()=>{};let r=()=>e.abort(n.reason);return n.addEventListener("abort",r,{once:!0}),()=>n.removeEventListener("abort",r)}var M=["open-weights","frontier"];function se(n){return M.includes(n)}function ie(n){return n==="user-hardware"||n==="user-subscription"||n==="user-api-key"||n==="open-weights"||n==="frontier"}function _(n){return n.type==="done"||n.type==="error"}var ae=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var N=new Set(["delta","thinking","status","done","error"]);function C(n){let e;try{e=JSON.parse(n)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,t=c(r.runId);if(typeof r.type=="string"&&N.has(r.type)){let o=O(r);return o?{kind:"event",runId:t,event:o}:null}switch(r.type){case"event":{if(!r.event||typeof r.event!="object")return null;let o=O(r.event);return o?{kind:"event",runId:t,event:o}:null}case"usage":{let o=$(r.usage);return o?{kind:"usage",runId:t,usage:o}:null}case"tool_call":{let o=c(r.callId),s=c(r.name);return!o||!s?null:{kind:"tool_call",runId:t,callId:o,name:s,input:r.input,...r.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=c(r.approvalId),s=c(r.tool);return!o||!s?null:{kind:"approval",runId:t,approvalId:o,tool:s,input:r.input,cwd:c(r.cwd),...r.answered===!0?{answered:!0}:{}}}default:return null}}function O(n){let e=typeof n.type=="string"&&N.has(n.type)?n.type:null;if(!e)return null;let r={type:e},t=c(n.text);t!==void 0&&(r.text=t);let o=c(n.message);o!==void 0&&(r.message=o);let s=c(n.tool);s!==void 0&&(r.tool=s);let i=c(n.tier);i!==void 0&&(r.tier=i);let u=$(n.usage);u&&(r.usage=u);let d=c(n.reason);d!==void 0&&(r.reason=d),typeof n.retryable=="boolean"&&(r.retryable=n.retryable);let h=n.turn&&typeof n.turn=="object"?c(n.turn.text):void 0;h!==void 0&&(r.turn={text:h});let l=c(n.session);l!==void 0&&(r.session=l);let p=c(n.toolInput);p!==void 0&&(r.toolInput=p);let m=ue(n.git);return m&&(r.git=m),(n.state==="interrupted"||n.state==="resumed")&&(r.state=n.state),r}function ue(n){if(!n||typeof n!="object")return null;let e=n,r=c(e.branch);return!r||!Array.isArray(e.files)||!Array.isArray(e.commits)?null:{branch:r,base:c(e.base)??"",head:c(e.head)??"",ahead:y(e.ahead),behind:y(e.behind),files:e.files.filter(t=>!!t&&typeof t=="object"&&typeof t.path=="string").map(t=>({path:t.path,status:t.status==="A"||t.status==="D"||t.status==="R"?t.status:"M",added:y(t.added),removed:y(t.removed)})),commits:e.commits.filter(t=>!!t&&typeof t=="object").map(t=>({sha:c(t.sha)??"",subject:c(t.subject)??""})),dirty:e.dirty===!0}}function $(n){if(!n||typeof n!="object")return null;let e=n,r={model:c(e.model)??"unknown",inputTokens:y(e.inputTokens),outputTokens:y(e.outputTokens),cacheReadTokens:y(e.cacheReadTokens),cacheWriteTokens:y(e.cacheWriteTokens),webSearches:y(e.webSearches)},t=c(e.costClass);t!==void 0&&(r.costClass=t);let o=c(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function c(n){return typeof n=="string"&&n.length>0?n:void 0}function y(n){return typeof n=="number"&&Number.isFinite(n)?n:0}async function*F(n){let e=n.body;if(!e)throw new a({code:"connection",message:"The run response arrived with no body to stream.",status:n.status});let r=n.headers.get("content-type")??"",t=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,i="",u=[],d,h=l=>d===void 0?l:{...l,seq:d};try{for(;;){let p=await o.read();if(p.done)break;for(i+=s.decode(p.value,{stream:!0});;){let m=i.indexOf(`
|
|
2
|
+
`);if(m===-1)break;let f=L(i.slice(0,m));if(i=i.slice(m+1),t){let g=f.trim()?C(f):null;g&&(yield g);continue}if(f===""){let g=u.join(`
|
|
3
|
+
`);if(u=[],g==="[DONE]")return;let U=g?C(g):null;U&&(yield h(U)),d=void 0;continue}if(f.startsWith("id:")){let g=Number(f.slice(3).trim());d=Number.isInteger(g)&&g>0?g:void 0;continue}D(f,u)}}!t&&i&&D(L(i),u);let l=t?i.trim():u.join(`
|
|
4
|
+
`);if(l&&l!=="[DONE]"){let p=C(l);p&&(yield t?p:h(p))}}finally{o.cancel().catch(()=>{})}}function D(n,e){if(n.startsWith(":"))return;let r=n.indexOf(":");if(r===-1||n.slice(0,r)!=="data")return;let t=n.slice(r+1);e.push(t.startsWith(" ")?t.slice(1):t)}function L(n){return n.endsWith("\r")?n.slice(0,-1):n}function ce(n){return n}function S(n){let e=new Map;for(let r of n??[]){if(!r?.name)throw new a({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new a({code:"invalid_request",message:`Tool "${r.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(r.name))throw new a({code:"invalid_request",message:`Two tools are named "${r.name}". Tool names must be unique within a run.`});e.set(r.name,r)}return e}function I(n){return[...n.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function x(n){if(n===void 0)return null;if(typeof n=="string")return n;try{return JSON.parse(JSON.stringify(n))}catch{return String(n)}}function T(n){return n instanceof Error?n.message||n.name:typeof n=="string"?n:String(n)}var k=class{#e;#n;#r;#u=!1;#o;#t;#s=new AbortController;#i=new AbortController;#a=new Set;#c;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#o=e.after??0,this.#c=A(e.signal,this.#s)}get cursor(){return this.#o}get resumable(){return this.#t.response.headers.get("soba-resumable")==="1"}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#l()}async text(){let e="";for await(let r of this)if(r.type==="delta"&&r.text&&(e+=r.text),r.type==="error")throw new a({code:"run_failed",message:r.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#s.abort(new a({code:"canceled",message:"The run was cancelled."})),this.#i.abort(new a({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await b(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#l(){if(this.#u)throw new a({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#u=!0;let e=!1;try{for await(let r of F(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.seq!==void 0&&(this.#o=r.seq),r.kind==="event"){let t=r.event;if(t.tier&&(this.#n=t.tier),t.usage&&(this.#r=t.usage),e=_(t),yield t,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#p(this.#g(r))}throw new a({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#i.abort(new a({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#a]),this.#s.abort(),this.#c()}}#p(e){this.#a.add(e),e.finally(()=>this.#a.delete(e)).catch(()=>{})}async#g(e){if(!((e.kind==="tool_call"||e.kind==="approval")&&e.answered)){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#m(e)}}async#f(e){let r=this.#t.tools.get(e.name),t,o=!1;if(!r)t=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#i.signal};try{t=await r.execute(e.input,s)}catch(i){t=T(i),o=!0}}await this.#d(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:x(t),isError:o})}async#m(e){let r="deny",t;if(!this.#t.approve)t="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),r=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){t=T(o)}await this.#d(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...t?{reason:t}:{}})}async#d(e,r){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",r);return}try{await(await b(this.#t.config,{method:"POST",path:e,body:r})).text()}catch(t){this.#t.onWarning(`Could not deliver an answer to ${e}.`,w(t))}}};var j="https://api.soba.so/v1",W="https://soba.so/v1",de=3e4,le=2,P=class{#e;#n;#r;users;usage;connect;plans;runs;ops;terminals;constructor(e={}){let r=e.apiKey??v("SOBA_KEY")??v("SOBA_API_KEY");if(!r)throw new a({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(pe()&&!e.dangerouslyAllowBrowser)throw new a({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#r=e.onWarning??(()=>{}),this.#e={apiKey:r,baseURL:e.baseURL??v("SOBA_BASE_URL")??j,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??de,maxRetries:e.maxRetries??le},this.#n={...this.#e,baseURL:e.webBaseURL??v("SOBA_WEB_URL")??W},typeof this.#e.fetch!="function")throw new a({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:t=>this.request("GET",`/users/${encodeURIComponent(t)}`),compute:async t=>(await this.request("GET",`/users/${encodeURIComponent(t)}/compute`)).machines,setQuantity:(t,o,s)=>R(this.#n,{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/quantity`,body:{quantity:s}}),check:(t,o,s)=>R(s?.use?{...this.#n,maxRetries:0}:this.#n,{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/check`,body:s?.use?{use:s.use}:{}}),track:(t,o,s=1)=>R({...this.#n,maxRetries:0},{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/track`,body:{amount:s}})},this.usage={list:t=>this.request("GET","/usage",{query:{...t}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let o=S(t.tools),s={user:t.user,model:t.model??"auto",detached:!0};t.webhook&&(s.webhook=t.webhook),t.prompt&&(s.prompt=t.prompt),t.messages?.length&&(s.messages=t.messages),t.system&&(s.system=t.system),t.runtime&&(s.runtime=t.runtime),o.size&&(s.tools=I(o)),t.route&&(s.route=t.route),t.policy&&(s.policy=t.policy),t.metadata&&(s.metadata=t.metadata),t.task&&(s.task=t.task),t.surface&&(s.surface=t.surface),t.maxDurationMs&&(s.maxDurationMs=t.maxDurationMs),t.title&&(s.title=t.title),t.worktree&&(s.worktree=t.worktree),t.resume&&(s.resume=t.resume);let i=await R(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"}),u={id:i.id};return i.stream&&(u.stream={url:i.stream.url,token:i.stream.token,expiresAt:i.stream.expires_at}),t.answerTools==="here"&&(u.answered=(async()=>{let d=await this.runs.attach(i.id,{user:t.user,tools:t.tools??[]});for await(let h of d);})().catch(d=>this.#r("Answering this run's tool calls stopped.",d))),u},attach:async(t,o)=>{if(!o.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let s=await b(this.#e,{method:"GET",path:`/runs/${encodeURIComponent(t)}/stream`,...o.after?{query:{after:o.after}}:{},accept:"text/event-stream",signal:o.signal});return new k({config:this.#e,response:s,user:o.user,tools:S(o.tools),approve:o.approve,onWarning:this.#r,signal:o.signal,after:o.after??0})},cancel:async t=>{await this.request("POST",`/runs/${encodeURIComponent(t)}/cancel`,{body:{}})}},this.ops={run:t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to ask."});return this.request("POST","/ops",{body:{...t}})}},this.terminals={open:async t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to open it on."});let o=await this.request("POST","/term",{body:{...t}}),s=new URL(this.#e.baseURL);s.protocol=s.protocol==="http:"?"ws:":"wss:";let i=new URL(o.path,`${s.protocol}//${s.host}`);return i.searchParams.set("ticket",o.ticket),{termId:o.termId,url:i.toString(),expiresAt:o.expiresAt}}},this.plans={list:t=>this.request("GET","/plans",{...t?.user?{query:{user:t.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new a({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new a({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let r=S(e.tools),t={user:e.user,model:e.model??"auto"};e.prompt&&(t.prompt=e.prompt),e.messages?.length&&(t.messages=e.messages),e.system&&(t.system=e.system),e.runtime&&(t.runtime=e.runtime),r.size&&(t.tools=I(r)),e.route&&(t.route=e.route),e.policy&&(t.policy=e.policy),e.metadata&&(t.metadata=e.metadata),e.task&&(t.task=e.task),e.resumable&&(t.resumable=!0),e.maxDurationMs&&(t.maxDurationMs=e.maxDurationMs);let o=await b(this.#e,{method:"POST",path:"/runs",body:t,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new k({config:this.#e,response:o,user:e.user,tools:r,approve:e.approve,onWarning:this.#r,signal:e.signal})}async request(e,r,t={}){return R(this.#e,{method:e,path:r,...t})}};function v(n){if(typeof process>"u")return;let e=process.env?.[n];return e||void 0}function pe(){let n=globalThis;return typeof n.window=="object"&&typeof n.window?.document=="object"}var ge=300;function fe(n){return[...new Uint8Array(n)].map(e=>e.toString(16).padStart(2,"0")).join("")}function me(n,e){if(n.length!==e.length)return!1;let r=0;for(let t=0;t<n.length;t++)r|=n.charCodeAt(t)^e.charCodeAt(t);return r===0}async function he(n,e,r,t={}){let o=Object.fromEntries((e??"").split(",").map(f=>{let g=f.indexOf("=");return[f.slice(0,g).trim(),f.slice(g+1).trim()]})),s=Number(o.t),i=o.v1??"",u=f=>new a({code:"unauthorized",status:401,message:f});if(!Number.isFinite(s)||!i)throw u("Missing or malformed Soba-Signature.");let d=Math.floor((t.now??Date.now())/1e3);if(Math.abs(d-s)>(t.toleranceSeconds??ge))throw u("This delivery is too old to accept.");let h=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=fe(await globalThis.crypto.subtle.sign("HMAC",h,new TextEncoder().encode(`${s}.${n}`)));if(!me(l,i))throw u("The Soba-Signature does not match.");let p;try{p=JSON.parse(n)}catch{throw new a({code:"invalid_request",message:"The delivery is not JSON."})}let m=p;if(m.type!=="run.event"&&m.type!=="run.tool_call"||typeof m.run_id!="string")throw new a({code:"invalid_request",message:"Not a Soba run delivery."});return m}async function ye(n,e){if(n.type!=="run.tool_call")return null;let{call_id:r,name:t,input:o}=n.data,s=S(e.tools).get(t);if(!s)return{result:`Unknown tool: ${t}`,is_error:!0};try{let i=await s.execute(o??{},{runId:n.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:x(i)}}catch(i){return{result:T(i),is_error:!0}}}export{j as DEFAULT_BASE_URL,W as DEFAULT_WEB_BASE_URL,M as METERED_CLASSES,k as Run,P as Soba,a as SobaError,ae as TOOL_SLUGS,E as VERSION,ye as handleRunWebhook,ie as isCostClass,se as isMetered,_ as isTerminal,ce as tool,he as verifyWebhook};
|
package/dist/protocol.d.ts
CHANGED
|
@@ -54,6 +54,36 @@ export interface AgentEvent {
|
|
|
54
54
|
/** On a `status` from Soba: the computer serving the run went offline
|
|
55
55
|
* (`interrupted`) or came back (`resumed`). The run goes on either way. */
|
|
56
56
|
state?: "interrupted" | "resumed";
|
|
57
|
+
/** On a `status` from a terminal run: what the agent said at the end of a turn. */
|
|
58
|
+
turn?: {
|
|
59
|
+
text: string;
|
|
60
|
+
};
|
|
61
|
+
/** On `done` / `error`: the runtime's conversation id. Send it back as `resume`
|
|
62
|
+
* to make the next run a follow-up of this one. */
|
|
63
|
+
session?: string;
|
|
64
|
+
/** On a tool `status`: what the tool was pointed at — a path, a command. */
|
|
65
|
+
toolInput?: string;
|
|
66
|
+
/** On a `status` from a run in a git checkout: where its branch stands. */
|
|
67
|
+
git?: GitSnapshot;
|
|
68
|
+
}
|
|
69
|
+
/** A branch against the branch it forked from, counted from the merge base. */
|
|
70
|
+
export interface GitSnapshot {
|
|
71
|
+
branch: string;
|
|
72
|
+
base: string;
|
|
73
|
+
head: string;
|
|
74
|
+
ahead: number;
|
|
75
|
+
behind: number;
|
|
76
|
+
files: {
|
|
77
|
+
path: string;
|
|
78
|
+
status: "A" | "M" | "D" | "R";
|
|
79
|
+
added: number;
|
|
80
|
+
removed: number;
|
|
81
|
+
}[];
|
|
82
|
+
commits: {
|
|
83
|
+
sha: string;
|
|
84
|
+
subject: string;
|
|
85
|
+
}[];
|
|
86
|
+
dirty: boolean;
|
|
57
87
|
}
|
|
58
88
|
export declare function isTerminal(e: AgentEvent): boolean;
|
|
59
89
|
/** Stable tool slugs. Adapters map their runtime's native names onto these. */
|
|
@@ -63,6 +93,14 @@ export type ToolSlug = (typeof TOOL_SLUGS)[number];
|
|
|
63
93
|
export interface Message {
|
|
64
94
|
role: "user" | "assistant";
|
|
65
95
|
content: string;
|
|
96
|
+
/** Pictures attached to this turn, in order. A runtime that cannot see
|
|
97
|
+
* images gets `content` alone and says so in a status line. */
|
|
98
|
+
images?: ImageInput[];
|
|
99
|
+
}
|
|
100
|
+
/** One attached picture, inline: base64 without the `data:` prefix. */
|
|
101
|
+
export interface ImageInput {
|
|
102
|
+
mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
103
|
+
data: string;
|
|
66
104
|
}
|
|
67
105
|
/** An app-defined tool as it travels on the wire: schema only, no implementation. */
|
|
68
106
|
export interface ToolSchema {
|
|
@@ -90,6 +128,8 @@ export interface RunPolicy {
|
|
|
90
128
|
timeoutMs?: number;
|
|
91
129
|
permissionMode?: PermissionMode;
|
|
92
130
|
allowNetwork?: string[];
|
|
131
|
+
/** The owner's own browser, every action approved live. Only if their machine grants it. */
|
|
132
|
+
browser?: boolean;
|
|
93
133
|
}
|
|
94
134
|
/**
|
|
95
135
|
* WHICH ECONOMICS a run may use. You declare intent, not a runtime, which is
|
package/dist/resources.d.ts
CHANGED
|
@@ -105,7 +105,15 @@ export interface PlanListItem {
|
|
|
105
105
|
price: number;
|
|
106
106
|
interval: string;
|
|
107
107
|
unit: string;
|
|
108
|
+
/** Runs a month on AI the app pays for. Null is unlimited. */
|
|
108
109
|
included_units: number | null;
|
|
110
|
+
/** Runs a month on the user's own AI, counted apart from `included_units`.
|
|
111
|
+
* Null is unlimited. Optional so an older server that omits it parses. */
|
|
112
|
+
own_included_units?: number | null;
|
|
113
|
+
/** What `own_included_units` counts: `unit` unless the plan splits —
|
|
114
|
+
* "200 runs on your own AI, 2,000 credits on ours". Optional for older
|
|
115
|
+
* servers, which mean `unit`. */
|
|
116
|
+
own_unit?: string;
|
|
109
117
|
/** What happens past the allowance: `stop` or `meter`. The difference
|
|
110
118
|
* between a wall and a bill, and not guessable from the price. */
|
|
111
119
|
overage: string;
|
|
@@ -116,9 +124,20 @@ export interface PlanListItem {
|
|
|
116
124
|
route_allow: string[] | null;
|
|
117
125
|
/** Whether it can be taken without a payment rail. */
|
|
118
126
|
selectable: boolean;
|
|
127
|
+
/** What the app calls a run, plural: "tasks". Optional for older servers;
|
|
128
|
+
* absent reads "runs". */
|
|
129
|
+
task_noun?: string;
|
|
130
|
+
/** About how many runs a credit plan's allowance buys at this app's typical
|
|
131
|
+
* run cost: "About 265 tasks a month". Null when it cannot be said. */
|
|
132
|
+
estimated_tasks?: number | null;
|
|
133
|
+
/** A plan sold by conversation: "Talk to us" links here. */
|
|
134
|
+
contact_url?: string | null;
|
|
135
|
+
/** "Everything in" this plan id; its card lists that, then `features_own`. */
|
|
136
|
+
features_from?: string | null;
|
|
137
|
+
features_own?: PlanFeatureWire[] | null;
|
|
119
138
|
/**
|
|
120
|
-
* What connecting their own compute earns: `none` | `
|
|
121
|
-
*
|
|
139
|
+
* What connecting their own compute earns: `none` | `included` | `credit`.
|
|
140
|
+
* Here for the same reason every other field is: these objects
|
|
122
141
|
* are handed straight to `<SobaProvider initialPlans>` and rendered, so the
|
|
123
142
|
* shape has to be the component's, not a report's.
|
|
124
143
|
*/
|
|
@@ -127,12 +146,46 @@ export interface PlanListItem {
|
|
|
127
146
|
connected_included_units: number | null;
|
|
128
147
|
/** Dollars back per interval on a `credit` reward. Zero otherwise. */
|
|
129
148
|
connected_credit: number;
|
|
149
|
+
/** What the vendor's app enforces, as the card lists it (INT-91). Optional
|
|
150
|
+
* so an older server that omits it parses. */
|
|
151
|
+
features?: PlanFeatureWire[];
|
|
130
152
|
}
|
|
131
153
|
export interface CurrentPlanItem {
|
|
132
154
|
plan_id: string;
|
|
155
|
+
/** Spent against `included_units`. */
|
|
133
156
|
units_used: number;
|
|
157
|
+
/** Spent against `own_included_units`. Optional for older servers. */
|
|
158
|
+
own_units_used?: number;
|
|
159
|
+
/**
|
|
160
|
+
* WHAT SOBA ENFORCES: both allowances, what is left, when they reset. The
|
|
161
|
+
* gateway stops a run here; your app never has to. Optional for older
|
|
162
|
+
* servers, null when the plan is no longer in the catalogue.
|
|
163
|
+
*/
|
|
164
|
+
limits?: {
|
|
165
|
+
unit: string;
|
|
166
|
+
included_units: number | null;
|
|
167
|
+
units_used: number;
|
|
168
|
+
remaining: number | null;
|
|
169
|
+
own_included_units: number | null;
|
|
170
|
+
/** What the own_* figures count. Optional for older servers: `unit`. */
|
|
171
|
+
own_unit?: string;
|
|
172
|
+
own_units_used: number;
|
|
173
|
+
own_remaining: number | null;
|
|
174
|
+
resets_at: string | null;
|
|
175
|
+
} | null;
|
|
176
|
+
/**
|
|
177
|
+
* WHAT YOUR APP ENFORCES (INT-91): feature id to value, as this person
|
|
178
|
+
* bought it. Soba stops nothing on these; read them and decide.
|
|
179
|
+
*/
|
|
180
|
+
features?: Record<string, boolean | number | string | null>;
|
|
181
|
+
/** How many of each priced feature are in use, as your app last reported
|
|
182
|
+
* (`{ seats: 12 }`). Separate from `features`, where `seats` is the cap. */
|
|
183
|
+
quantities?: Record<string, number>;
|
|
134
184
|
period_end: string | null;
|
|
135
185
|
trial_ends_at: string | null;
|
|
186
|
+
/** A free plan past its time limit: runs are refused, and every feature
|
|
187
|
+
* reads as off, so stop what the plan carried too. Absent on older servers. */
|
|
188
|
+
ended?: boolean;
|
|
136
189
|
}
|
|
137
190
|
export interface PlanListResponse {
|
|
138
191
|
plans: PlanListItem[];
|
|
@@ -203,3 +256,56 @@ export interface ConnectContextResponse {
|
|
|
203
256
|
/** Where a browser polls this user's machines (the gateway). Hand the whole context to `<SobaProvider initialConnect>` and it is used. */
|
|
204
257
|
machines_url?: string | null;
|
|
205
258
|
}
|
|
259
|
+
/** A plan feature: what the vendor's app enforces, never Soba (INT-91). */
|
|
260
|
+
export interface PlanFeatureWire {
|
|
261
|
+
id: string;
|
|
262
|
+
name: string;
|
|
263
|
+
type: "boolean" | "number" | "text";
|
|
264
|
+
/** `true`/`false`, a whole number, or null for unlimited. */
|
|
265
|
+
value: boolean | number | string | null;
|
|
266
|
+
unit?: string;
|
|
267
|
+
/** "month": the count starts over each billing period. Absent: never. */
|
|
268
|
+
resets?: "month";
|
|
269
|
+
/** Seats: a price per unit, in micros, past `included` units. */
|
|
270
|
+
price?: {
|
|
271
|
+
amountMicros: number;
|
|
272
|
+
included: number;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** What `users.setQuantity` answers. */
|
|
276
|
+
/**
|
|
277
|
+
* WHY A FEATURE IS NOT AVAILABLE: no plan, a free plan whose time is up, a
|
|
278
|
+
* feature the plan does not carry, or a counted one with nothing left.
|
|
279
|
+
*/
|
|
280
|
+
export type FeatureDenial = "no_plan" | "plan_ended" | "not_included" | "limit_reached";
|
|
281
|
+
/** The answer to `users.check`. */
|
|
282
|
+
export interface FeatureCheckResult {
|
|
283
|
+
feature: string;
|
|
284
|
+
allowed: boolean;
|
|
285
|
+
/** Null when allowed. */
|
|
286
|
+
reason: FeatureDenial | null;
|
|
287
|
+
/** The plan's cap, or null when unlimited or a switch. */
|
|
288
|
+
limit: number | null;
|
|
289
|
+
/** In use now, after this call's `use` if it was counted. */
|
|
290
|
+
used: number;
|
|
291
|
+
/** Left under the cap. Null when unlimited or a switch. */
|
|
292
|
+
remaining: number | null;
|
|
293
|
+
unlimited: boolean;
|
|
294
|
+
}
|
|
295
|
+
/** The answer to `users.track`. */
|
|
296
|
+
export interface TrackResult {
|
|
297
|
+
feature: string;
|
|
298
|
+
/** The running total now. */
|
|
299
|
+
used: number;
|
|
300
|
+
}
|
|
301
|
+
export interface QuantityResult {
|
|
302
|
+
feature: string;
|
|
303
|
+
quantity: number;
|
|
304
|
+
/** The count before this report. Null on the first one. */
|
|
305
|
+
previous: number | null;
|
|
306
|
+
/** False when the count already matched, and nothing was written. */
|
|
307
|
+
changed: boolean;
|
|
308
|
+
/** `updated` when it reached the subscription; `no_subscription` when the
|
|
309
|
+
* user's plan was not bought through Stripe, so there is nothing to bill. */
|
|
310
|
+
billing?: "updated" | "no_subscription";
|
|
311
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.12";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soba-so/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "One request that comes back finished, executed on compute the end user owns.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://soba.so/docs/runs",
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"!dist/**/*.map",
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node ../../scripts/build-sdk.mjs",
|
|
27
|
+
"prepublishOnly": "node ../../scripts/build-sdk.mjs"
|
|
28
|
+
},
|
|
25
29
|
"engines": {
|
|
26
30
|
"node": ">=22"
|
|
27
31
|
},
|
|
@@ -34,12 +38,9 @@
|
|
|
34
38
|
"byoc"
|
|
35
39
|
],
|
|
36
40
|
"devDependencies": {
|
|
37
|
-
"@soba-so/protocol": "
|
|
41
|
+
"@soba-so/protocol": "workspace:*"
|
|
38
42
|
},
|
|
39
43
|
"publishConfig": {
|
|
40
44
|
"access": "public"
|
|
41
|
-
},
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "node ../../scripts/build-sdk.mjs"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|