@soba-so/sdk 0.0.9 → 0.0.10

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 CHANGED
@@ -56,7 +56,12 @@ export interface RunParams {
56
56
  prompt?: string;
57
57
  messages?: Message[];
58
58
  system?: string;
59
- /** Defaults to "auto": let the route decide. */
59
+ /**
60
+ * Defaults to "auto": the machine's own default model. `"small"` asks for its
61
+ * quick, cheap model and `"large"` for its most capable one, so a summary or a
62
+ * quick lookup does not spend the person's plan on the biggest model. A model
63
+ * name pins one.
64
+ */
60
65
  model?: string;
61
66
  /** Name a specific runtime only when you have a reason to. `route` is the
62
67
  * declaration that survives a user connecting something different. */
@@ -73,6 +78,13 @@ export interface RunParams {
73
78
  approve?: Approver;
74
79
  metadata?: Record<string, string>;
75
80
  signal?: AbortSignal;
81
+ /**
82
+ * Stop the run after this many milliseconds. It can only shorten a run: the
83
+ * app's and the organisation's limits still apply, and the lowest wins. A run
84
+ * stopped at its limit ends with an `error` whose `reason` is `run_too_long`.
85
+ * https://soba.so/docs/runs#how-long-a-run-may-last
86
+ */
87
+ maxDurationMs?: number;
76
88
  /**
77
89
  * Keep the run going if this request goes away, and keep its frames so it
78
90
  * can be read again: `soba.runs.attach(run.runId, { user, tools, after:
@@ -83,6 +95,22 @@ export interface RunParams {
83
95
  */
84
96
  resumable?: boolean;
85
97
  }
98
+ /** A detached run, started. */
99
+ export interface StartedRun {
100
+ id: string;
101
+ /**
102
+ * WHERE A PAGE CAN WATCH IT, read-only: hand both to the browser and read the
103
+ * run there (`watchRun` in `@soba-so/react`) rather than streaming it through
104
+ * your server. Absent when the gateway has no run-token key.
105
+ */
106
+ stream?: {
107
+ url: string;
108
+ token: string;
109
+ expiresAt: number;
110
+ };
111
+ /** With `answerTools: "here"`: settles when the run ends, after its last tool call is answered. */
112
+ answered?: Promise<void>;
113
+ }
86
114
  /** Reading a resumable run again. See `RunParams.resumable`. */
87
115
  export interface AttachParams {
88
116
  /** The same `user` the run was started for: your tools are handed it, as on `soba.run`. */
@@ -106,10 +134,25 @@ export interface AttachParams {
106
134
  * no tunnel on a laptop.
107
135
  */
108
136
  export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal" | "resumable"> {
137
+ /**
138
+ * Where Soba sends the run's tool calls (and, unless `events: "tools"`, its
139
+ * every event). `events: "tools"` is for an app whose page watches the run
140
+ * itself with `stream`: one request per tool call and one for the ending,
141
+ * instead of one per event.
142
+ */
109
143
  webhook?: {
110
144
  url: string;
111
145
  secret: string;
146
+ events?: "all" | "tools";
112
147
  };
148
+ /**
149
+ * `"here"`: answer the run's tool calls from this process, with `tools`,
150
+ * instead of a webhook. For localhost and long-lived servers, where nothing
151
+ * public can be called back; a serverless function would be stopped mid-run.
152
+ * The work continues in the background; `answered` on the result settles
153
+ * when the run ends.
154
+ */
155
+ answerTools?: "here";
113
156
  /** `terminal`: open it as the machine owner's own interactive session, in a
114
157
  * window they can watch and type into. The machine must allow it. */
115
158
  surface?: "headless" | "terminal";
@@ -183,9 +226,7 @@ export declare class Soba {
183
226
  };
184
227
  /** Detached and resumable runs. See `DetachedRunParams` and `RunParams.resumable`. */
185
228
  readonly runs: {
186
- start(params: DetachedRunParams): Promise<{
187
- id: string;
188
- }>;
229
+ start(params: DetachedRunParams): Promise<StartedRun>;
189
230
  /** Read a resumable run from where you left off, then live. Tool calls it is waiting on run here. */
190
231
  attach(id: string, params: AttachParams): Promise<Run>;
191
232
  /** Stop a run. For a terminal run, the window closes. */
package/dist/errors.d.ts CHANGED
@@ -13,6 +13,13 @@ export type SobaErrorCode =
13
13
  | "not_found"
14
14
  /** 409 nothing this run is allowed to use is connected and awake */
15
15
  | "no_compute"
16
+ /** 404 on `runs.attach`, a tool result or a cancel: Soba no longer has this
17
+ * run, because it ended long enough ago or Soba restarted under it */
18
+ | "unknown_run"
19
+ /** 409 on `runs.attach`: the run was not started resumable */
20
+ | "not_resumable"
21
+ /** 409 on a tool result or approval: the machine serving the run has gone */
22
+ | "machine_gone"
16
23
  /** 429 back off; `retryAfterMs` says how long */
17
24
  | "rate_limited"
18
25
  /** 5xx ours */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type AttachParams, DEFAULT_BASE_URL, type DetachedRunParams, type RunParams, Soba, type SobaOptions, } from "./client.js";
1
+ export { type AttachParams, type StartedRun, DEFAULT_BASE_URL, type DetachedRunParams, type RunParams, Soba, type SobaOptions, } from "./client.js";
2
2
  export { SobaError, type SobaErrorCode } from "./errors.js";
3
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
4
  export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, MachineListItem, MachineListState, MachineRuntime, PlanListItem, PlanListResponse, PlanSummary, RuntimeListItem, SobaUser, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var i=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??j.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},j=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),B={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function G(t){let e=B[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function M(t){let e=t.status,r="";try{r=await t.text()}catch{}let n=K(r);return new i({code:n?.code??G(e),type:n?.type,message:n?.message??J(e,t.statusText,r),status:e,retryAfterMs:z(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function K(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let n=r;return{message:typeof n.message=="string"&&n.message?n.message:void 0,type:typeof n.type=="string"&&n.type?n.type:void 0,code:H.has(n.code)?n.code:void 0}}var H=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function J(t,e,r){let n=r.trim().slice(0,200),o=n?`: ${n}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function z(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(t);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function b(t){if(t instanceof i)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new i({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new i({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var E="0.0.9";var Y=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function V(t){if(t.code==="rate_limited")return!0;if(t.code!=="connection")return!1;let e=t.cause;for(let r=0;e&&r<4;r++){let n=e.code;if(typeof n=="string"&&Y.has(n))return!0;e=e.cause}return!1}var Q=8e3,X=3e4;async function h(t,e){let r=te(t.baseURL,e.path,e.query),n=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o,s=a=>a.retryable&&(e.retry!=="unsent"||V(a));for(let a=0;a<n;a++){a>0&&await re(ne(a,o),e.signal);let u=new AbortController,g=x(e.signal,u),y=setTimeout(()=>{u.abort(new i({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),c;try{c=await t.fetch(r,{method:e.method,headers:Z(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(g(),o=b(u.signal.aborted?u.signal.reason??p:p),!s(o)||a===n-1)throw o;continue}finally{clearTimeout(y)}if(c.ok)return c;if(g(),o=await M(c),!s(o)||a===n-1)throw o}throw o??new i({code:"unknown",message:"The request failed with no error."})}async function k(t,e){let r=await h(t,{accept:"application/json",...e}),n=await r.text();if(n)try{return JSON.parse(n)}catch(o){throw new i({code:"server_error",message:`Soba returned a body that is not JSON: ${n.slice(0,200)}`,status:r.status,cause:o})}}function Z(t,e){let r={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":ee(),...t.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function ee(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${E}${t}`}function te(t,e,r){let n=t.replace(/\/+$/,""),o=new URL(`${n}${e.startsWith("/")?e:`/${e}`}`);for(let[s,a]of Object.entries(r??{}))a!==void 0&&o.searchParams.set(s,String(a));return o.toString()}function ne(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,X);let r=Math.min(Q,500*2**(t-1));return Math.round(r*(.5+Math.random()/2))}function re(t,e){return new Promise((r,n)=>{if(e?.aborted)return n(b(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},t);function s(){clearTimeout(o),n(b(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function x(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let r=()=>e.abort(t.reason);return t.addEventListener("abort",r,{once:!0}),()=>t.removeEventListener("abort",r)}var U=["open-weights","frontier"];function oe(t){return U.includes(t)}function se(t){return t==="user-hardware"||t==="user-subscription"||t==="user-api-key"||t==="open-weights"||t==="frontier"}function A(t){return t.type==="done"||t.type==="error"}var ie=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var D=new Set(["delta","thinking","status","done","error"]);function _(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,n=l(r.runId);if(typeof r.type=="string"&&D.has(r.type)){let o=O(r);return o?{kind:"event",runId:n,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:n,event:o}:null}case"usage":{let o=$(r.usage);return o?{kind:"usage",runId:n,usage:o}:null}case"tool_call":{let o=l(r.callId),s=l(r.name);return!o||!s?null:{kind:"tool_call",runId:n,callId:o,name:s,input:r.input,...r.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=l(r.approvalId),s=l(r.tool);return!o||!s?null:{kind:"approval",runId:n,approvalId:o,tool:s,input:r.input,cwd:l(r.cwd),...r.answered===!0?{answered:!0}:{}}}default:return null}}function O(t){let e=typeof t.type=="string"&&D.has(t.type)?t.type:null;if(!e)return null;let r={type:e},n=l(t.text);n!==void 0&&(r.text=n);let o=l(t.message);o!==void 0&&(r.message=o);let s=l(t.tool);s!==void 0&&(r.tool=s);let a=l(t.tier);a!==void 0&&(r.tier=a);let u=$(t.usage);return u&&(r.usage=u),r}function $(t){if(!t||typeof t!="object")return null;let e=t,r={model:l(e.model)??"unknown",inputTokens:v(e.inputTokens),outputTokens:v(e.outputTokens),cacheReadTokens:v(e.cacheReadTokens),cacheWriteTokens:v(e.cacheWriteTokens),webSearches:v(e.webSearches)},n=l(e.costClass);n!==void 0&&(r.costClass=n);let o=l(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function l(t){return typeof t=="string"&&t.length>0?t:void 0}function v(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*F(t){let e=t.body;if(!e)throw new i({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let r=t.headers.get("content-type")??"",n=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,a="",u=[],g,y=c=>g===void 0?c:{...c,seq:g};try{for(;;){let p=await o.read();if(p.done)break;for(a+=s.decode(p.value,{stream:!0});;){let m=a.indexOf(`
2
- `);if(m===-1)break;let f=L(a.slice(0,m));if(a=a.slice(m+1),n){let d=f.trim()?_(f):null;d&&(yield d);continue}if(f===""){let d=u.join(`
3
- `);if(u=[],d==="[DONE]")return;let q=d?_(d):null;q&&(yield y(q)),g=void 0;continue}if(f.startsWith("id:")){let d=Number(f.slice(3).trim());g=Number.isInteger(d)&&d>0?d:void 0;continue}N(f,u)}}!n&&a&&N(L(a),u);let c=n?a.trim():u.join(`
4
- `);if(c&&c!=="[DONE]"){let p=_(c);p&&(yield n?p:y(p))}}finally{o.cancel().catch(()=>{})}}function N(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 L(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 i({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new i({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 i({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 R(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 T(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=x(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 i({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 i({code:"canceled",message:"The run was cancelled."})),this.#i.abort(new i({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await h(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 i({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 i({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#i.abort(new i({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(a){n=T(a),o=!0}}await this.#d(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:R(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=T(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 h(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,P=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 i({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 i({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 i({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 i({code:"user_required",status:422,message:"`user` is required."});let o=w(n.tools),s={user:n.user,model:n.model??"auto",detached:!0};return 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.title&&(s.title=n.title),n.worktree&&(s.worktree=n.worktree),{id:(await k(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"})).id}},attach:async(n,o)=>{if(!o.user)throw new i({code:"user_required",status:422,message:"`user` is required."});let s=await h(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 i({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 i({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);let o=await h(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 k(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(f=>{let d=f.indexOf("=");return[f.slice(0,d).trim(),f.slice(d+1).trim()]})),s=Number(o.t),a=o.v1??"",u=f=>new i({code:"unauthorized",status:401,message:f});if(!Number.isFinite(s)||!a)throw u("Missing or malformed Soba-Signature.");let g=Math.floor((n.now??Date.now())/1e3);if(Math.abs(g-s)>(n.toleranceSeconds??le))throw u("This delivery is too old to accept.");let y=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),c=pe(await globalThis.crypto.subtle.sign("HMAC",y,new TextEncoder().encode(`${s}.${t}`)));if(!fe(c,a))throw u("The Soba-Signature does not match.");let p;try{p=JSON.parse(t)}catch{throw new i({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 i({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 a=await s.execute(o??{},{runId:t.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:R(a)}}catch(a){return{result:T(a),is_error:!0}}}export{W as DEFAULT_BASE_URL,U as METERED_CLASSES,S as Run,P as Soba,i as SobaError,ie as TOOL_SLUGS,E 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??j.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},j=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),B={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function G(t){let e=B[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function q(t){let e=t.status,r="";try{r=await t.text()}catch{}let n=K(r);return new a({code:n?.code??G(e),type:n?.type,message:n?.message??J(e,t.statusText,r),status:e,retryAfterMs:z(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function K(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let n=r;return{message:typeof n.message=="string"&&n.message?n.message:void 0,type:typeof n.type=="string"&&n.type?n.type:void 0,code:H.has(n.code)?n.code:void 0}}var H=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 J(t,e,r){let n=r.trim().slice(0,200),o=n?`: ${n}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function z(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(t);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function b(t){if(t instanceof a)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new a({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new a({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var x="0.0.10";var Y=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function V(t){if(t.code==="rate_limited")return!0;if(t.code!=="connection")return!1;let e=t.cause;for(let r=0;e&&r<4;r++){let n=e.code;if(typeof n=="string"&&Y.has(n))return!0;e=e.cause}return!1}var Q=8e3,X=3e4;async function y(t,e){let r=te(t.baseURL,e.path,e.query),n=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o,s=i=>i.retryable&&(e.retry!=="unsent"||V(i));for(let i=0;i<n;i++){i>0&&await re(ne(i,o),e.signal);let u=new AbortController,c=k(e.signal,u),h=setTimeout(()=>{u.abort(new a({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),l;try{l=await t.fetch(r,{method:e.method,headers:Z(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(f){if(c(),o=b(u.signal.aborted?u.signal.reason??f:f),!s(o)||i===n-1)throw o;continue}finally{clearTimeout(h)}if(l.ok)return l;if(c(),o=await q(l),!s(o)||i===n-1)throw o}throw o??new a({code:"unknown",message:"The request failed with no error."})}async function E(t,e){let r=await y(t,{accept:"application/json",...e}),n=await r.text();if(n)try{return JSON.parse(n)}catch(o){throw new a({code:"server_error",message:`Soba returned a body that is not JSON: ${n.slice(0,200)}`,status:r.status,cause:o})}}function Z(t,e){let r={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":ee(),...t.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function ee(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${x}${t}`}function te(t,e,r){let n=t.replace(/\/+$/,""),o=new URL(`${n}${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 ne(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,X);let r=Math.min(Q,500*2**(t-1));return Math.round(r*(.5+Math.random()/2))}function re(t,e){return new Promise((r,n)=>{if(e?.aborted)return n(b(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},t);function s(){clearTimeout(o),n(b(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function k(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let r=()=>e.abort(t.reason);return t.addEventListener("abort",r,{once:!0}),()=>t.removeEventListener("abort",r)}var U=["open-weights","frontier"];function oe(t){return U.includes(t)}function se(t){return t==="user-hardware"||t==="user-subscription"||t==="user-api-key"||t==="open-weights"||t==="frontier"}function A(t){return t.type==="done"||t.type==="error"}var ie=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var L=new Set(["delta","thinking","status","done","error"]);function _(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,n=d(r.runId);if(typeof r.type=="string"&&L.has(r.type)){let o=O(r);return o?{kind:"event",runId:n,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:n,event:o}:null}case"usage":{let o=$(r.usage);return o?{kind:"usage",runId:n,usage:o}:null}case"tool_call":{let o=d(r.callId),s=d(r.name);return!o||!s?null:{kind:"tool_call",runId:n,callId:o,name:s,input:r.input,...r.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=d(r.approvalId),s=d(r.tool);return!o||!s?null:{kind:"approval",runId:n,approvalId:o,tool:s,input:r.input,cwd:d(r.cwd),...r.answered===!0?{answered:!0}:{}}}default:return null}}function O(t){let e=typeof t.type=="string"&&L.has(t.type)?t.type:null;if(!e)return null;let r={type:e},n=d(t.text);n!==void 0&&(r.text=n);let o=d(t.message);o!==void 0&&(r.message=o);let s=d(t.tool);s!==void 0&&(r.tool=s);let i=d(t.tier);i!==void 0&&(r.tier=i);let u=$(t.usage);u&&(r.usage=u);let c=d(t.reason);return c!==void 0&&(r.reason=c),typeof t.retryable=="boolean"&&(r.retryable=t.retryable),(t.state==="interrupted"||t.state==="resumed")&&(r.state=t.state),r}function $(t){if(!t||typeof t!="object")return null;let e=t,r={model:d(e.model)??"unknown",inputTokens:v(e.inputTokens),outputTokens:v(e.outputTokens),cacheReadTokens:v(e.cacheReadTokens),cacheWriteTokens:v(e.cacheWriteTokens),webSearches:v(e.webSearches)},n=d(e.costClass);n!==void 0&&(r.costClass=n);let o=d(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function d(t){return typeof t=="string"&&t.length>0?t:void 0}function v(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*F(t){let e=t.body;if(!e)throw new a({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let r=t.headers.get("content-type")??"",n=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,i="",u=[],c,h=l=>c===void 0?l:{...l,seq:c};try{for(;;){let f=await o.read();if(f.done)break;for(i+=s.decode(f.value,{stream:!0});;){let m=i.indexOf(`
2
+ `);if(m===-1)break;let g=N(i.slice(0,m));if(i=i.slice(m+1),n){let p=g.trim()?_(g):null;p&&(yield p);continue}if(g===""){let p=u.join(`
3
+ `);if(u=[],p==="[DONE]")return;let P=p?_(p):null;P&&(yield h(P)),c=void 0;continue}if(g.startsWith("id:")){let p=Number(g.slice(3).trim());c=Number.isInteger(p)&&p>0?p:void 0;continue}D(g,u)}}!n&&i&&D(N(i),u);let l=n?i.trim():u.join(`
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};
@@ -43,7 +43,17 @@ export interface AgentEvent {
43
43
  usage?: RunUsage;
44
44
  /** Which economics is serving this run, announced on the opening `status`,
45
45
  * BEFORE any output. A visible downgrade beats a silent one. */
46
- tier?: CostClass;
46
+ tier?: CostClass; /**
47
+ * On an `error`: a stable code to branch on, beside the prose in `message`.
48
+ * `run_too_long` is a run stopped at its time limit; `subscription_exhausted`
49
+ * a user's own plan that has run out.
50
+ */
51
+ reason?: string;
52
+ /** On an `error`: false when starting the same run again will end the same way. */
53
+ retryable?: boolean;
54
+ /** On a `status` from Soba: the computer serving the run went offline
55
+ * (`interrupted`) or came back (`resumed`). The run goes on either way. */
56
+ state?: "interrupted" | "resumed";
47
57
  }
48
58
  export declare function isTerminal(e: AgentEvent): boolean;
49
59
  /** Stable tool slugs. Adapters map their runtime's native names onto these. */
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.9";
1
+ export declare const VERSION = "0.0.10";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soba-so/sdk",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
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",