@soba-so/sdk 0.0.8 → 0.0.9

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
@@ -73,18 +73,40 @@ export interface RunParams {
73
73
  approve?: Approver;
74
74
  metadata?: Record<string, string>;
75
75
  signal?: AbortSignal;
76
+ /**
77
+ * Keep the run going if this request goes away, and keep its frames so it
78
+ * can be read again: `soba.runs.attach(run.runId, { user, tools, after:
79
+ * run.cursor })`. For a run that may outlast the function serving it — a
80
+ * serverless time limit, a deploy — without a webhook, so the same code works
81
+ * on localhost. A run nobody attaches to again for fifteen minutes is
82
+ * stopped. https://soba.so/docs/runs#runs-that-outlive-the-request
83
+ */
84
+ resumable?: boolean;
85
+ }
86
+ /** Reading a resumable run again. See `RunParams.resumable`. */
87
+ export interface AttachParams {
88
+ /** The same `user` the run was started for: your tools are handed it, as on `soba.run`. */
89
+ user: string;
90
+ /** The same tools the run was started with. Tool calls the run is still waiting on are executed here. */
91
+ tools?: readonly ToolDefinition[];
92
+ approve?: Approver;
93
+ /** The last frame already read (`run.cursor`). Omitted reads from the start. */
94
+ after?: number;
95
+ signal?: AbortSignal;
76
96
  }
77
97
  /**
78
- * A DETACHED run: answered with its id at once, its events and tool calls
79
- * delivered to `webhook` instead of streamed back. For a run that outlives the
98
+ * A DETACHED run: answered with its id at once, for a run that outlives the
80
99
  * request that starts it, like a session somebody works in for an hour. Served
81
100
  * only by one of the user's own machines.
82
101
  *
83
- * The app's webhook route verifies each delivery with `verifyWebhook` and
84
- * answers it with `handleRunWebhook`, passing the same tools.
102
+ * With `webhook`, its events and tool calls are delivered there: the app's
103
+ * webhook route verifies each with `verifyWebhook` and answers it with
104
+ * `handleRunWebhook`, passing the same tools. Without one, nothing is pushed
105
+ * anywhere and the app reads it with `soba.runs.attach(id, …)` — no public URL,
106
+ * no tunnel on a laptop.
85
107
  */
86
- export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal"> {
87
- webhook: {
108
+ export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal" | "resumable"> {
109
+ webhook?: {
88
110
  url: string;
89
111
  secret: string;
90
112
  };
@@ -159,11 +181,13 @@ export declare class Soba {
159
181
  user?: string;
160
182
  }): Promise<PlanListResponse>;
161
183
  };
162
- /** Detached runs. See `DetachedRunParams`. */
184
+ /** Detached and resumable runs. See `DetachedRunParams` and `RunParams.resumable`. */
163
185
  readonly runs: {
164
186
  start(params: DetachedRunParams): Promise<{
165
187
  id: string;
166
188
  }>;
189
+ /** Read a resumable run from where you left off, then live. Tool calls it is waiting on run here. */
190
+ attach(id: string, params: AttachParams): Promise<Run>;
167
191
  /** Stop a run. For a terminal run, the window closes. */
168
192
  cancel(id: string): Promise<void>;
169
193
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { DEFAULT_BASE_URL, type DetachedRunParams, type RunParams, Soba, type SobaOptions, } from "./client.js";
1
+ export { type AttachParams, 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??F.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},F=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),W={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function B(t){let e=W[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function P(t){let e=t.status,r="";try{r=await t.text()}catch{}let n=G(r);return new i({code:n?.code??B(e),type:n?.type,message:n?.message??H(e,t.statusText,r),status:e,retryAfterMs:J(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function G(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:K.has(n.code)?n.code:void 0}}var K=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 H(t,e,r){let n=r.trim().slice(0,200),o=n?`: ${n}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function J(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 h(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 R="0.0.8";var z=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function Y(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"&&z.has(n))return!0;e=e.cause}return!1}var V=8e3,Q=3e4;async function y(t,e){let r=ee(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"||Y(a));for(let a=0;a<n;a++){a>0&&await ne(te(a,o),e.signal);let u=new AbortController,g=x(e.signal,u),f=setTimeout(()=>{u.abort(new i({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),l;try{l=await t.fetch(r,{method:e.method,headers:X(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(g(),o=h(u.signal.aborted?u.signal.reason??p:p),!s(o)||a===n-1)throw o;continue}finally{clearTimeout(f)}if(l.ok)return l;if(g(),o=await P(l),!s(o)||a===n-1)throw o}throw o??new i({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 i({code:"server_error",message:`Soba returned a body that is not JSON: ${n.slice(0,200)}`,status:r.status,cause:o})}}function X(t,e){let r={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":Z(),...t.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function Z(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${R}${t}`}function ee(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 te(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Q);let r=Math.min(V,500*2**(t-1));return Math.round(r*(.5+Math.random()/2))}function ne(t,e){return new Promise((r,n)=>{if(e?.aborted)return n(h(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},t);function s(){clearTimeout(o),n(h(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 q=["open-weights","frontier"];function re(t){return q.includes(t)}function oe(t){return t==="user-hardware"||t==="user-subscription"||t==="user-api-key"||t==="open-weights"||t==="frontier"}function k(t){return t.type==="done"||t.type==="error"}var se=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var L=new Set(["delta","thinking","status","done","error"]);function A(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=c(r.runId);if(typeof r.type=="string"&&L.has(r.type)){let o=U(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=U(r.event);return o?{kind:"event",runId:n,event:o}:null}case"usage":{let o=D(r.usage);return o?{kind:"usage",runId:n,usage:o}:null}case"tool_call":{let o=c(r.callId),s=c(r.name);return!o||!s?null:{kind:"tool_call",runId:n,callId:o,name:s,input:r.input}}case"approval_request":{let o=c(r.approvalId),s=c(r.tool);return!o||!s?null:{kind:"approval",runId:n,approvalId:o,tool:s,input:r.input,cwd:c(r.cwd)}}default:return null}}function U(t){let e=typeof t.type=="string"&&L.has(t.type)?t.type:null;if(!e)return null;let r={type:e},n=c(t.text);n!==void 0&&(r.text=n);let o=c(t.message);o!==void 0&&(r.message=o);let s=c(t.tool);s!==void 0&&(r.tool=s);let a=c(t.tier);a!==void 0&&(r.tier=a);let u=D(t.usage);return u&&(r.usage=u),r}function D(t){if(!t||typeof t!="object")return null;let e=t,r={model:c(e.model)??"unknown",inputTokens:b(e.inputTokens),outputTokens:b(e.outputTokens),cacheReadTokens:b(e.cacheReadTokens),cacheWriteTokens:b(e.cacheWriteTokens),webSearches:b(e.webSearches)},n=c(e.costClass);n!==void 0&&(r.costClass=n);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(t){return typeof t=="string"&&t.length>0?t:void 0}function b(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*$(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=[];try{for(;;){let f=await o.read();if(f.done)break;for(a+=s.decode(f.value,{stream:!0});;){let l=a.indexOf(`
2
- `);if(l===-1)break;let p=N(a.slice(0,l));if(a=a.slice(l+1),n){let d=p.trim()?A(p):null;d&&(yield d);continue}if(p===""){let d=u.join(`
3
- `);if(u=[],d==="[DONE]")return;let m=d?A(d):null;m&&(yield m);continue}O(p,u)}}!n&&a&&O(N(a),u);let g=n?a.trim():u.join(`
4
- `);if(g&&g!=="[DONE]"){let f=A(g);f&&(yield f)}}finally{o.cancel().catch(()=>{})}}function O(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 ie(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 _(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function v(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 S(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var T=class{#e;#n;#r;#a=!1;#t;#o=new AbortController;#s=new AbortController;#i=new Set;#u;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#u=x(e.signal,this.#o)}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#d()}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.#o.abort(new i({code:"canceled",message:"The run was cancelled."})),this.#s.abort(new i({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*#d(){if(this.#a)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.#a=!0;let e=!1;try{for await(let r of $(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.kind==="event"){let n=r.event;if(n.tier&&(this.#n=n.tier),n.usage&&(this.#r=n.usage),e=k(n),yield n,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#l(this.#p(r))}throw new i({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#s.abort(new i({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#i]),this.#o.abort(),this.#u()}}#l(e){this.#i.add(e),e.finally(()=>this.#i.delete(e)).catch(()=>{})}async#p(e){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#g(e)}async#f(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.#s.signal};try{n=await r.execute(e.input,s)}catch(a){n=S(a),o=!0}}await this.#c(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:v(n),isError:o})}async#g(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=S(o)}await this.#c(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...n?{reason:n}:{}})}async#c(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}.`,h(n))}}};var j="https://api.soba.so/v1",ae=3e4,ue=2,I=class{#e;#n;users;usage;connect;plans;runs;constructor(e={}){let r=e.apiKey??C("SOBA_KEY")??C("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(ce()&&!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??C("SOBA_BASE_URL")??j,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??ae,maxRetries:e.maxRetries??ue},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,webhook:n.webhook};return 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=_(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 E(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"})).id}},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=_(r)),e.route&&(n.route=e.route),e.policy&&(n.policy=e.policy),e.metadata&&(n.metadata=e.metadata);let o=await y(this.#e,{method:"POST",path:"/runs",body:n,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new T({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 C(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function ce(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}var de=300;function le(t){return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,"0")).join("")}function pe(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 fe(t,e,r,n={}){let o=Object.fromEntries((e??"").split(",").map(m=>{let M=m.indexOf("=");return[m.slice(0,M).trim(),m.slice(M+1).trim()]})),s=Number(o.t),a=o.v1??"",u=m=>new i({code:"unauthorized",status:401,message:m});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??de))throw u("This delivery is too old to accept.");let f=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=le(await globalThis.crypto.subtle.sign("HMAC",f,new TextEncoder().encode(`${s}.${t}`)));if(!pe(l,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 d=p;if(d.type!=="run.event"&&d.type!=="run.tool_call"||typeof d.run_id!="string")throw new i({code:"invalid_request",message:"Not a Soba run delivery."});return d}async function ge(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:v(a)}}catch(a){return{result:S(a),is_error:!0}}}export{j as DEFAULT_BASE_URL,q as METERED_CLASSES,T as Run,I as Soba,i as SobaError,se as TOOL_SLUGS,R as VERSION,ge as handleRunWebhook,oe as isCostClass,re as isMetered,k as isTerminal,ie as tool,fe as verifyWebhook};
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};
@@ -75,7 +75,7 @@ export interface UsageQuery {
75
75
  /** ISO 8601. */
76
76
  from?: string;
77
77
  to?: string;
78
- environment?: "development" | "preview" | "production" | string;
78
+ environment?: "development" | "production" | string;
79
79
  limit?: number;
80
80
  cursor?: string;
81
81
  }
@@ -200,4 +200,6 @@ export interface ConnectContextResponse {
200
200
  publishable_key: string | null;
201
201
  connect_url: string | null;
202
202
  command: string | null;
203
+ /** Where a browser polls this user's machines (the gateway). Hand the whole context to `<SobaProvider initialConnect>` and it is used. */
204
+ machines_url?: string | null;
203
205
  }
package/dist/run.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface RunInit {
20
20
  approve: Approver | undefined;
21
21
  onWarning: (message: string, detail?: unknown) => void;
22
22
  signal: AbortSignal | undefined;
23
+ /** Attaching to a resumable run: the last frame already read. */
24
+ after?: number;
23
25
  }
24
26
  /**
25
27
  * A run in flight.
@@ -35,6 +37,15 @@ export interface RunInit {
35
37
  export declare class Run implements AsyncIterable<AgentEvent> {
36
38
  #private;
37
39
  constructor(init: RunInit);
40
+ /**
41
+ * On a run started with `resumable: true`: the number of the last frame
42
+ * read. Keep it with the run's id, and `soba.runs.attach(id, { after:
43
+ * cursor })` reads on from there when this request has to stop — a function
44
+ * about to hit its time limit, a deploy, a closed tab.
45
+ */
46
+ get cursor(): number;
47
+ /** Whether the gateway is keeping this run's frames, so it can be attached to again. */
48
+ get resumable(): boolean;
38
49
  /** Empty only until the first frame, if the server omitted the header. */
39
50
  get runId(): string;
40
51
  /** Which economics served this run, from the opening `status`. */
package/dist/stream.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { AgentEvent, RunUsage } from "./protocol.js";
2
- export type StreamFrame = {
2
+ /**
3
+ * `seq` is the frame's SSE `id:` on a resumable run — its place in the run's
4
+ * log, which is what `runs.attach(id, { after })` reads on from. `answered` is a
5
+ * tool call or approval replayed on attach that was already answered: it must
6
+ * not run again.
7
+ */
8
+ export type StreamFrame = ({
3
9
  kind: "event";
4
10
  runId?: string;
5
11
  event: AgentEvent;
@@ -13,6 +19,7 @@ export type StreamFrame = {
13
19
  callId: string;
14
20
  name: string;
15
21
  input: unknown;
22
+ answered?: boolean;
16
23
  } | {
17
24
  kind: "approval";
18
25
  runId?: string;
@@ -20,6 +27,9 @@ export type StreamFrame = {
20
27
  tool: string;
21
28
  input: unknown;
22
29
  cwd?: string;
30
+ answered?: boolean;
31
+ }) & {
32
+ seq?: number;
23
33
  };
24
34
  export declare function parseStreamFrame(raw: string): StreamFrame | null;
25
35
  /**
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.8";
1
+ export declare const VERSION = "0.0.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soba-so/sdk",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
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",