@soba-so/sdk 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +144 -0
- package/dist/client.d.ts +111 -0
- package/dist/errors.d.ts +67 -0
- package/dist/http.d.ts +33 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +4 -0
- package/dist/protocol.d.ts +94 -0
- package/dist/resources.d.ts +85 -0
- package/dist/run.d.ts +54 -0
- package/dist/stream.d.ts +31 -0
- package/dist/tools.d.ts +42 -0
- package/dist/version.d.ts +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright © 2026 Soba. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is licensed, not sold, and it is not open source.
|
|
4
|
+
|
|
5
|
+
Subject to your agreement with Soba, you are granted a non-exclusive,
|
|
6
|
+
non-transferable, non-sublicensable, revocable licence to install this package
|
|
7
|
+
and use it in your own applications for the sole purpose of interfacing with
|
|
8
|
+
the Soba service.
|
|
9
|
+
|
|
10
|
+
You may not redistribute it, publish it, sell it, sublicense it, incorporate it
|
|
11
|
+
into another distributed library, or create and distribute derivative works of
|
|
12
|
+
it. No rights are granted in the Soba name or logo.
|
|
13
|
+
|
|
14
|
+
This software is provided "as is", without warranty of any kind, express or
|
|
15
|
+
implied. To the maximum extent permitted by law, Soba shall not be liable for
|
|
16
|
+
any claim, damages or other liability arising from the software or its use.
|
|
17
|
+
|
|
18
|
+
Nothing here limits any right you have that cannot be limited by the law that
|
|
19
|
+
applies to you.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @soba-so/sdk
|
|
2
|
+
|
|
3
|
+
**One request that comes back finished, executed on compute the end user owns.**
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { Soba } from "@soba-so/sdk"
|
|
7
|
+
|
|
8
|
+
const soba = new Soba({ apiKey: process.env.SOBA_KEY })
|
|
9
|
+
|
|
10
|
+
const run = await soba.run({
|
|
11
|
+
user: session.user.id,
|
|
12
|
+
model: "auto",
|
|
13
|
+
prompt: "Summarise everything I saved this week",
|
|
14
|
+
tools: [listNotes, readNote], // your code, executed in your app
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
for await (const event of run) {
|
|
18
|
+
if (event.type === "delta") process.stdout.write(event.text)
|
|
19
|
+
if (event.type === "done") console.log(event.usage)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Why this exists alongside `chat/completions`
|
|
24
|
+
|
|
25
|
+
`POST /v1/chat/completions` is stateless and **the caller owns the loop**: you
|
|
26
|
+
send messages, you get a tool call back, you execute it, you send everything
|
|
27
|
+
again. That works for a model. It cannot work for an agent.
|
|
28
|
+
|
|
29
|
+
Claude Code and Codex run their own loop, and they never hand a tool call back to
|
|
30
|
+
wait on an HTTP round trip. So reaching them means **Soba owns the loop**, and
|
|
31
|
+
your tools have to be registered up front rather than returned mid-conversation.
|
|
32
|
+
That is the whole difference between the two entry points, and this package is
|
|
33
|
+
the second one.
|
|
34
|
+
|
|
35
|
+
If you only need models, you do not need this package at all: point an OpenAI
|
|
36
|
+
client at `https://api.soba.so/v1` and pass `user`.
|
|
37
|
+
|
|
38
|
+
## What it is
|
|
39
|
+
|
|
40
|
+
A convenience over `POST /v1/runs`, not a different product. It adds four things
|
|
41
|
+
that are tedious to write once and easy to write wrongly:
|
|
42
|
+
|
|
43
|
+
| | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| The stream | Server-sent events parsed into the five [event types](https://soba.so/docs/reference/events) |
|
|
46
|
+
| The tool loop | A `tool_call` arrives, **your** function runs here, the result goes back |
|
|
47
|
+
| Approvals | An escalation is answered, and **fails closed** if it cannot be |
|
|
48
|
+
| Failure | The three kinds of failure told apart, and retries only where they are safe |
|
|
49
|
+
|
|
50
|
+
## The shapes
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
new Soba({ apiKey, baseURL, maxRetries, connectTimeoutMs, onWarning })
|
|
54
|
+
|
|
55
|
+
soba.run({ user, prompt | messages, system, model, runtime, tools, route, policy, approve, signal })
|
|
56
|
+
|
|
57
|
+
soba.users.get(id) // plan, allowance, usage this period
|
|
58
|
+
soba.users.compute(id) // each connected machine and its live state
|
|
59
|
+
soba.usage.list(query) // the ledger, in integer micros
|
|
60
|
+
soba.plans.list()
|
|
61
|
+
soba.request(method, path, options) // the escape hatch
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`await soba.run(…)` resolves when the response **headers** arrive, not when the
|
|
65
|
+
run finishes. That is the same boundary the API draws: a failure before the first
|
|
66
|
+
byte throws here, and a failure after it arrives as a terminal `error` event.
|
|
67
|
+
|
|
68
|
+
### `user` is required, and cannot be defaulted
|
|
69
|
+
|
|
70
|
+
It is how a run is attributed to a person, therefore how it is metered, and
|
|
71
|
+
therefore how Soba knows whose machine it may reach. Sending a run without one is
|
|
72
|
+
refused locally rather than at the server, because it is the mistake every
|
|
73
|
+
integration makes exactly once.
|
|
74
|
+
|
|
75
|
+
### Tools are your code
|
|
76
|
+
|
|
77
|
+
The schema travels to the model. The implementation never leaves your process.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { tool } from "@soba-so/sdk"
|
|
81
|
+
|
|
82
|
+
const listNotes = tool({
|
|
83
|
+
name: "listNotes",
|
|
84
|
+
description: "List the notes this user saved.",
|
|
85
|
+
inputSchema: { type: "object", properties: { since: { type: "string" } } },
|
|
86
|
+
execute: async ({ since }, ctx) => db.notes.since(ctx.user, since),
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Throwing inside `execute` is **not** a run failure: the error is handed to the
|
|
91
|
+
model as a failed call, which it can see and work around. `ctx.signal` is aborted
|
|
92
|
+
when the run ends, and long tools should honour it.
|
|
93
|
+
|
|
94
|
+
### Approvals fail closed
|
|
95
|
+
|
|
96
|
+
A tool inside the machine's askable set needs a live answer. No approver, a
|
|
97
|
+
throw, or anything that is not the string `"allow"` is a denial, because an
|
|
98
|
+
approval channel that fails open turns "ask" into "allow".
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
await soba.run({
|
|
102
|
+
user, prompt,
|
|
103
|
+
approve: async ({ tool, input, cwd }) => (tool === "bash" ? "deny" : "allow"),
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Retries happen in one place, and only before the first byte
|
|
108
|
+
|
|
109
|
+
`POST /v1/runs` is not idempotent: retrying a run that already started is a
|
|
110
|
+
second run, billed as one. So the SDK retries `429`, `5xx` and transport
|
|
111
|
+
failures **before** a response arrives and never after, and it does not retry
|
|
112
|
+
`409 no_compute` at all: that clears when a laptop wakes up, which is minutes
|
|
113
|
+
away, not milliseconds. Your app owns that decision.
|
|
114
|
+
|
|
115
|
+
## Errors
|
|
116
|
+
|
|
117
|
+
One class, `SobaError`, carrying `code`, `type`, `status`, `retryable` and
|
|
118
|
+
`retryAfterMs`. A stream that ends without `done` or `error` is
|
|
119
|
+
`stream_truncated`: a transport failure, retryable, and deliberately distinct
|
|
120
|
+
from a delivered `error` event, which is final.
|
|
121
|
+
|
|
122
|
+
## The HTTP contract
|
|
123
|
+
|
|
124
|
+
Everything here is the documented API plus three answer-back routes, which exist
|
|
125
|
+
because a tool loop needs a return path and an SSE stream only goes one way:
|
|
126
|
+
|
|
127
|
+
| | |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `POST /v1/runs` | Start a run. Responds `text/event-stream` with the run id in `Soba-Run-Id` |
|
|
130
|
+
| `POST /v1/runs/:id/tool_result` | `{ callId, result, isError }` |
|
|
131
|
+
| `POST /v1/runs/:id/approval` | `{ approvalId, decision, reason? }` |
|
|
132
|
+
| `POST /v1/runs/:id/cancel` | Best effort. The local stream drops immediately |
|
|
133
|
+
|
|
134
|
+
The stream carries the run's [events](https://soba.so/docs/reference/events), plus
|
|
135
|
+
a tool call or an approval request when one is waiting on you. Anything the SDK does
|
|
136
|
+
not recognise is skipped rather than fatal, so a server that ships ahead of your
|
|
137
|
+
installed version does not break the stream.
|
|
138
|
+
|
|
139
|
+
## Types
|
|
140
|
+
|
|
141
|
+
Ships its own `.d.ts`. Every shape above — the run options, the event types, the
|
|
142
|
+
usage record, `SobaError` — is typed, with no separate `@types` package to install.
|
|
143
|
+
|
|
144
|
+
© 2026 Soba. All rights reserved. Licensed for use with the Soba service; see LICENSE.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Message, RouteRequest, RunPolicy } from "./protocol.js";
|
|
2
|
+
import type { ConnectedMachine, PlanSummary, SobaUser, UsagePage, UsageQuery } from "./resources.js";
|
|
3
|
+
import { type Approver, Run } from "./run.js";
|
|
4
|
+
import { type ToolDefinition } from "./tools.js";
|
|
5
|
+
/**
|
|
6
|
+
* Where a run goes when nobody says otherwise.
|
|
7
|
+
*
|
|
8
|
+
* `api.`, which now exists — for months it had no DNS record, and the default
|
|
9
|
+
* pointed at `www` because a host that answers beat a host that does not.
|
|
10
|
+
*
|
|
11
|
+
* `www` was never right, only reachable. A run needs a socket held open for its
|
|
12
|
+
* whole duration and a round trip to Postgres per turn; Vercel gives neither,
|
|
13
|
+
* so `/v1/runs` is not served there and never will be. It 404s. Everything else
|
|
14
|
+
* in this API — sessions, pairing, machines — IS served there, which is why the
|
|
15
|
+
* two hosts both exist and why only this one is the run endpoint.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_BASE_URL = "https://api.soba.so/v1";
|
|
18
|
+
export interface SobaOptions {
|
|
19
|
+
/** Defaults to `SOBA_KEY`, then `SOBA_API_KEY`. Shaped `sk_soba_<id>.<secret>`. */
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/** Defaults to `SOBA_BASE_URL`, then https://api.soba.so/v1. */
|
|
22
|
+
baseURL?: string;
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
/** Retries before the first byte only. Never after a stream has started. */
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
/** How long to wait for response HEADERS. The run itself is not capped here. */
|
|
27
|
+
connectTimeoutMs?: number;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
/** Told about failures that have nowhere else to go: an undeliverable tool
|
|
30
|
+
* result, a cancel the broker never heard. Silent by default. */
|
|
31
|
+
onWarning?: (message: string, detail?: unknown) => void;
|
|
32
|
+
/** An app key is a SERVER credential. Shipping it to a browser hands every
|
|
33
|
+
* visitor the ability to spend your account. Only set this for a trusted
|
|
34
|
+
* first-party context you control. */
|
|
35
|
+
dangerouslyAllowBrowser?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface RunParams {
|
|
38
|
+
/**
|
|
39
|
+
* The end user this run belongs to. Load-bearing, and the SDK refuses without
|
|
40
|
+
* it: it is how a run is attributed to a person, therefore how it is metered,
|
|
41
|
+
* and therefore how Soba knows whose machine it may reach.
|
|
42
|
+
*/
|
|
43
|
+
user: string;
|
|
44
|
+
/** A single user turn. Shorthand for `messages: [{role:"user", content}]`. */
|
|
45
|
+
prompt?: string;
|
|
46
|
+
messages?: Message[];
|
|
47
|
+
system?: string;
|
|
48
|
+
/** Defaults to "auto": let the route decide. */
|
|
49
|
+
model?: string;
|
|
50
|
+
/** Name a specific runtime only when you have a reason to. `route` is the
|
|
51
|
+
* declaration that survives a user connecting something different. */
|
|
52
|
+
runtime?: string;
|
|
53
|
+
/** Your code, executed in your app. Schemas go to the model; nothing else moves. */
|
|
54
|
+
tools?: readonly ToolDefinition[];
|
|
55
|
+
/** Which economics this run may use. A plan compiles to one on every run, so
|
|
56
|
+
* most apps never write this by hand. */
|
|
57
|
+
route?: RouteRequest;
|
|
58
|
+
/** What the run REQUESTS of the machine. The machine's own grant still wins. */
|
|
59
|
+
policy?: RunPolicy;
|
|
60
|
+
/** Answers escalations for tools inside the machine's askable set. Absent
|
|
61
|
+
* means every escalation is denied. */
|
|
62
|
+
approve?: Approver;
|
|
63
|
+
metadata?: Record<string, string>;
|
|
64
|
+
signal?: AbortSignal;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The Soba client.
|
|
68
|
+
*
|
|
69
|
+
* ```js
|
|
70
|
+
* const soba = new Soba({ apiKey: process.env.SOBA_KEY })
|
|
71
|
+
* const run = await soba.run({ user: session.user.id, prompt: "…" })
|
|
72
|
+
* for await (const event of run) …
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* `soba.run()` exists alongside `chat/completions` for one reason:
|
|
76
|
+
* `chat/completions` is stateless and the CALLER owns the agent loop, so it
|
|
77
|
+
* reaches models. Claude Code and Codex run their own loop and never hand a
|
|
78
|
+
* tool call back to wait on an HTTP round trip, so reaching them means SOBA
|
|
79
|
+
* owns the loop, and that is this call.
|
|
80
|
+
*/
|
|
81
|
+
export declare class Soba {
|
|
82
|
+
#private;
|
|
83
|
+
readonly users: {
|
|
84
|
+
/** What this user has: plan, allowance, usage this period, what they connected. */
|
|
85
|
+
get(id: string): Promise<SobaUser>;
|
|
86
|
+
/** Each connected machine, its runtimes, cost classes and live state. */
|
|
87
|
+
compute(id: string): Promise<ConnectedMachine[]>;
|
|
88
|
+
};
|
|
89
|
+
readonly usage: {
|
|
90
|
+
list(query?: UsageQuery): Promise<UsagePage>;
|
|
91
|
+
};
|
|
92
|
+
readonly plans: {
|
|
93
|
+
list(): Promise<PlanSummary[]>;
|
|
94
|
+
};
|
|
95
|
+
constructor(options?: SobaOptions);
|
|
96
|
+
get baseURL(): string;
|
|
97
|
+
/**
|
|
98
|
+
* Start a run and get back something you iterate.
|
|
99
|
+
*
|
|
100
|
+
* Resolves when the response HEADERS arrive, not when the run finishes. That
|
|
101
|
+
* is the same boundary the API draws: a failure before the first byte throws
|
|
102
|
+
* here, and a failure after it arrives as a terminal `error` event.
|
|
103
|
+
*/
|
|
104
|
+
run(params: RunParams): Promise<Run>;
|
|
105
|
+
/** The escape hatch. Every typed read above is this call with a shape on it. */
|
|
106
|
+
request<T>(method: "GET" | "POST" | "DELETE", path: string, options?: {
|
|
107
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
108
|
+
body?: unknown;
|
|
109
|
+
signal?: AbortSignal;
|
|
110
|
+
}): Promise<T>;
|
|
111
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type SobaErrorCode =
|
|
2
|
+
/** 400 malformed, or caught here before the round trip */
|
|
3
|
+
"invalid_request"
|
|
4
|
+
/** 422 no user. It cannot be defaulted: a run with no user cannot be metered */
|
|
5
|
+
| "user_required"
|
|
6
|
+
/** 401 missing, malformed, revoked, or another app's key */
|
|
7
|
+
| "unauthorized"
|
|
8
|
+
/** 402 the plan is exhausted and its overage is `stop` */
|
|
9
|
+
| "payment_required"
|
|
10
|
+
/** 403 asked for something this key may not have, e.g. a metered class on a dev key */
|
|
11
|
+
| "forbidden"
|
|
12
|
+
/** 404 no such model or runtime alias */
|
|
13
|
+
| "not_found"
|
|
14
|
+
/** 409 nothing this run is allowed to use is connected and awake */
|
|
15
|
+
| "no_compute"
|
|
16
|
+
/** 429 back off; `retryAfterMs` says how long */
|
|
17
|
+
| "rate_limited"
|
|
18
|
+
/** 5xx ours */
|
|
19
|
+
| "server_error"
|
|
20
|
+
/** the request never reached a server */
|
|
21
|
+
| "connection"
|
|
22
|
+
/** no response headers within `connectTimeoutMs` */
|
|
23
|
+
| "timeout"
|
|
24
|
+
/** the stream ended without `done` or `error`: transport, not outcome */
|
|
25
|
+
| "stream_truncated"
|
|
26
|
+
/** a delivered terminal `error` event, surfaced by `run.text()` */
|
|
27
|
+
| "run_failed"
|
|
28
|
+
/** the caller aborted */
|
|
29
|
+
| "canceled" | "unknown";
|
|
30
|
+
export interface SobaErrorInit {
|
|
31
|
+
message: string;
|
|
32
|
+
code: SobaErrorCode;
|
|
33
|
+
/** The server's own `error.type`, when it sent one. */
|
|
34
|
+
type?: string;
|
|
35
|
+
status?: number;
|
|
36
|
+
retryable?: boolean;
|
|
37
|
+
retryAfterMs?: number;
|
|
38
|
+
runId?: string;
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `retryable` is advice, not an instruction, and it is deliberately FALSE for
|
|
43
|
+
* `no_compute`. That failure clears when a laptop wakes up, so retrying it on a
|
|
44
|
+
* millisecond backoff inside the SDK would spin against a machine that is
|
|
45
|
+
* asleep. Minutes, not milliseconds, and the app owns that decision.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SobaError extends Error {
|
|
48
|
+
readonly code: SobaErrorCode;
|
|
49
|
+
readonly type: string;
|
|
50
|
+
readonly status: number | undefined;
|
|
51
|
+
readonly retryable: boolean;
|
|
52
|
+
readonly retryAfterMs: number | undefined;
|
|
53
|
+
readonly runId: string | undefined;
|
|
54
|
+
constructor(init: SobaErrorInit);
|
|
55
|
+
}
|
|
56
|
+
export declare function codeForStatus(status: number): SobaErrorCode;
|
|
57
|
+
/**
|
|
58
|
+
* Build an error from a failed response. The envelope is
|
|
59
|
+
* `{ error: { type, message, code } }` on every endpoint, but a proxy, a
|
|
60
|
+
* gateway timeout page or an HTML error can arrive here too, so nothing about
|
|
61
|
+
* the body is assumed.
|
|
62
|
+
*/
|
|
63
|
+
export declare function errorFromResponse(response: Response): Promise<SobaError>;
|
|
64
|
+
/** RFC 7231 allows seconds or an HTTP date. Both appear in the wild. */
|
|
65
|
+
export declare function retryAfterMs(header: string | null): number | undefined;
|
|
66
|
+
/** Anything thrown by `fetch` or an abort, normalised. */
|
|
67
|
+
export declare function asSobaError(err: unknown): SobaError;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SobaError } from "./errors.js";
|
|
2
|
+
export interface HttpConfig {
|
|
3
|
+
baseURL: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
fetch: typeof globalThis.fetch;
|
|
7
|
+
/** Covers HEADERS ONLY. A run streams for minutes; a deadline that kept
|
|
8
|
+
* running would kill the answer it was meant to protect. */
|
|
9
|
+
connectTimeoutMs: number;
|
|
10
|
+
maxRetries: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RequestOptions {
|
|
13
|
+
method: "GET" | "POST" | "DELETE";
|
|
14
|
+
path: string;
|
|
15
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
16
|
+
body?: unknown;
|
|
17
|
+
accept?: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Default true. Set false for anything that must happen at most once. */
|
|
20
|
+
retry?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function httpRequest(cfg: HttpConfig, opts: RequestOptions): Promise<Response>;
|
|
23
|
+
export declare function requestJson<T>(cfg: HttpConfig, opts: RequestOptions): Promise<T>;
|
|
24
|
+
export declare function buildUrl(baseURL: string, path: string, query?: Record<string, string | number | boolean | undefined>): string;
|
|
25
|
+
/**
|
|
26
|
+
* Exponential backoff with jitter, and `Retry-After` wins when the server sent
|
|
27
|
+
* one. The jitter is not decoration: a fleet of servers retrying a rate limit
|
|
28
|
+
* in lockstep re-creates the burst that caused it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function backoffMs(attempt: number, last: SobaError | undefined): number;
|
|
31
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
32
|
+
/** Forward one signal's abort onto a controller, and stop forwarding on demand. */
|
|
33
|
+
export declare function chain(parent: AbortSignal | undefined, child: AbortController): () => void;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_BASE_URL, type RunParams, Soba, type SobaOptions } from "./client.js";
|
|
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, ConnectedMachine, MachineRuntime, PlanSummary, SobaUser, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
|
|
5
|
+
export { type ApprovalDecision, type ApprovalRequest, type Approver, Run, } from "./run.js";
|
|
6
|
+
export { type ToolContext, type ToolDefinition, tool } from "./tools.js";
|
|
7
|
+
export { VERSION } from "./version.js";
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var s=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"]),j={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function W(t){let e=j[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function k(t){let e=t.status,n="";try{n=await t.text()}catch{}let r=B(n);return new s({code:r?.code??W(e),type:r?.type,message:r?.message??G(e,t.statusText,n),status:e,retryAfterMs:J(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function B(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let n=e.error;if(!n||typeof n!="object")return null;let r=n;return{message:typeof r.message=="string"&&r.message?r.message:void 0,type:typeof r.type=="string"&&r.type?r.type:void 0,code:K.has(r.code)?r.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 G(t,e,n){let r=n.trim().slice(0,200),o=r?`: ${r}`:"";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 n=Date.parse(t);if(!Number.isNaN(n))return Math.max(0,n-Date.now())}function f(t){if(t instanceof s)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new s({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new s({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var w="0.0.5";var H=8e3,Y=3e4;async function g(t,e){let n=Q(t.baseURL,e.path,e.query),r=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o;for(let i=0;i<r;i++){i>0&&await Z(X(i,o),e.signal);let a=new AbortController,c=S(e.signal,a),m=setTimeout(()=>{a.abort(new s({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),d;try{d=await t.fetch(n,{method:e.method,headers:z(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:a.signal})}catch(l){if(c(),o=f(a.signal.aborted?a.signal.reason??l:l),!o.retryable||i===r-1)throw o;continue}finally{clearTimeout(m)}if(d.ok)return d;if(c(),o=await k(d),!o.retryable||i===r-1)throw o}throw o??new s({code:"unknown",message:"The request failed with no error."})}async function C(t,e){let n=await g(t,{accept:"application/json",...e}),r=await n.text();if(r)try{return JSON.parse(r)}catch(o){throw new s({code:"server_error",message:`Soba returned a body that is not JSON: ${r.slice(0,200)}`,status:n.status,cause:o})}}function z(t,e){let n={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":V(),...t.headers};return e.body!==void 0&&(n["content-type"]="application/json"),n}function V(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${w}${t}`}function Q(t,e,n){let r=t.replace(/\/+$/,""),o=new URL(`${r}${e.startsWith("/")?e:`/${e}`}`);for(let[i,a]of Object.entries(n??{}))a!==void 0&&o.searchParams.set(i,String(a));return o.toString()}function X(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Y);let n=Math.min(H,500*2**(t-1));return Math.round(n*(.5+Math.random()/2))}function Z(t,e){return new Promise((n,r)=>{if(e?.aborted)return r(f(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",i),n()},t);function i(){clearTimeout(o),r(f(e?.reason))}e?.addEventListener("abort",i,{once:!0})})}function S(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let n=()=>e.abort(t.reason);return t.addEventListener("abort",n,{once:!0}),()=>t.removeEventListener("abort",n)}var _=["open-weights","frontier"];function ee(t){return _.includes(t)}function te(t){return t==="user-hardware"||t==="user-subscription"||t==="open-weights"||t==="frontier"}function T(t){return t.type==="done"||t.type==="error"}var ne=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var U=new Set(["delta","thinking","status","done","error"]);function v(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let n=e,r=u(n.runId);if(typeof n.type=="string"&&U.has(n.type)){let o=I(n);return o?{kind:"event",runId:r,event:o}:null}switch(n.type){case"event":{if(!n.event||typeof n.event!="object")return null;let o=I(n.event);return o?{kind:"event",runId:r,event:o}:null}case"usage":{let o=P(n.usage);return o?{kind:"usage",runId:r,usage:o}:null}case"tool_call":{let o=u(n.callId),i=u(n.name);return!o||!i?null:{kind:"tool_call",runId:r,callId:o,name:i,input:n.input}}case"approval_request":{let o=u(n.approvalId),i=u(n.tool);return!o||!i?null:{kind:"approval",runId:r,approvalId:o,tool:i,input:n.input,cwd:u(n.cwd)}}default:return null}}function I(t){let e=typeof t.type=="string"&&U.has(t.type)?t.type:null;if(!e)return null;let n={type:e},r=u(t.text);r!==void 0&&(n.text=r);let o=u(t.message);o!==void 0&&(n.message=o);let i=u(t.tool);i!==void 0&&(n.tool=i);let a=u(t.tier);a!==void 0&&(n.tier=a);let c=P(t.usage);return c&&(n.usage=c),n}function P(t){if(!t||typeof t!="object")return null;let e=t,n={model:u(e.model)??"unknown",inputTokens:y(e.inputTokens),outputTokens:y(e.outputTokens),cacheReadTokens:y(e.cacheReadTokens),cacheWriteTokens:y(e.cacheWriteTokens),webSearches:y(e.webSearches)},r=u(e.costClass);r!==void 0&&(n.costClass=r);let o=u(e.provider);return o!==void 0&&(n.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(n.costMicros=e.costMicros),n}function u(t){return typeof t=="string"&&t.length>0?t:void 0}function y(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*O(t){let e=t.body;if(!e)throw new s({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let n=t.headers.get("content-type")??"",r=n.includes("ndjson")||n.includes("jsonlines"),o=e.getReader(),i=new TextDecoder,a="",c=[];try{for(;;){let d=await o.read();if(d.done)break;for(a+=i.decode(d.value,{stream:!0});;){let l=a.indexOf(`
|
|
2
|
+
`);if(l===-1)break;let b=q(a.slice(0,l));if(a=a.slice(l+1),r){let p=b.trim()?v(b):null;p&&(yield p);continue}if(b===""){let p=c.join(`
|
|
3
|
+
`);if(c=[],p==="[DONE]")return;let A=p?v(p):null;A&&(yield A);continue}M(b,c)}}!r&&a&&M(q(a),c);let m=r?a.trim():c.join(`
|
|
4
|
+
`);if(m&&m!=="[DONE]"){let d=v(m);d&&(yield d)}}finally{o.cancel().catch(()=>{})}}function M(t,e){if(t.startsWith(":"))return;let n=t.indexOf(":");if(n===-1||t.slice(0,n)!=="data")return;let r=t.slice(n+1);e.push(r.startsWith(" ")?r.slice(1):r)}function q(t){return t.endsWith("\r")?t.slice(0,-1):t}function re(t){return t}function D(t){let e=new Map;for(let n of t??[]){if(!n?.name)throw new s({code:"invalid_request",message:"Every tool needs a name."});if(typeof n.execute!="function")throw new s({code:"invalid_request",message:`Tool "${n.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(n.name))throw new s({code:"invalid_request",message:`Two tools are named "${n.name}". Tool names must be unique within a run.`});e.set(n.name,n)}return e}function L(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function N(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 h=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=S(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 n of this)if(n.type==="delta"&&n.text&&(e+=n.text),n.type==="error")throw new s({code:"run_failed",message:n.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#o.abort(new s({code:"canceled",message:"The run was cancelled."})),this.#s.abort(new s({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await g(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 s({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 n of O(this.#t.response)){if(n.runId&&!this.#e&&(this.#e=n.runId),n.kind==="event"){let r=n.event;if(r.tier&&(this.#n=r.tier),r.usage&&(this.#r=r.usage),e=T(r),yield r,e)return;continue}if(n.kind==="usage"){this.#r=n.usage;continue}this.#l(this.#p(n))}throw new s({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#s.abort(new s({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 n=this.#t.tools.get(e.name),r,o=!1;if(!n)r=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let i={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#s.signal};try{r=await n.execute(e.input,i)}catch(a){r=R(a),o=!0}}await this.#c(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:N(r),isError:o})}async#g(e){let n="deny",r;if(!this.#t.approve)r="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),n=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){r=R(o)}await this.#c(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:n,...r?{reason:r}:{}})}async#c(e,n){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",n);return}try{await(await g(this.#t.config,{method:"POST",path:e,body:n})).text()}catch(r){this.#t.onWarning(`Could not deliver an answer to ${e}.`,f(r))}}};var $="https://api.soba.so/v1",oe=3e4,se=2,x=class{#e;#n;users;usage;plans;constructor(e={}){let n=e.apiKey??E("SOBA_KEY")??E("SOBA_API_KEY");if(!n)throw new s({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(ie()&&!e.dangerouslyAllowBrowser)throw new s({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:n,baseURL:e.baseURL??E("SOBA_BASE_URL")??$,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??oe,maxRetries:e.maxRetries??se},typeof this.#e.fetch!="function")throw new s({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:r=>this.request("GET",`/users/${encodeURIComponent(r)}`),compute:r=>this.request("GET",`/users/${encodeURIComponent(r)}/compute`)},this.usage={list:r=>this.request("GET","/usage",{query:{...r}})},this.plans={list:()=>this.request("GET","/plans")}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new s({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 s({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let n=D(e.tools),r={user:e.user,model:e.model??"auto"};e.prompt&&(r.prompt=e.prompt),e.messages?.length&&(r.messages=e.messages),e.system&&(r.system=e.system),e.runtime&&(r.runtime=e.runtime),n.size&&(r.tools=L(n)),e.route&&(r.route=e.route),e.policy&&(r.policy=e.policy),e.metadata&&(r.metadata=e.metadata);let o=await g(this.#e,{method:"POST",path:"/runs",body:r,accept:"text/event-stream",signal:e.signal});return new h({config:this.#e,response:o,user:e.user,tools:n,approve:e.approve,onWarning:this.#n,signal:e.signal})}async request(e,n,r={}){return C(this.#e,{method:e,path:n,...r})}};function E(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function ie(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}export{$ as DEFAULT_BASE_URL,_ as METERED_CLASSES,h as Run,x as Soba,s as SobaError,ne as TOOL_SLUGS,w as VERSION,te as isCostClass,ee as isMetered,T as isTerminal,re as tool};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHO PAYS for a run.
|
|
3
|
+
*
|
|
4
|
+
* user-hardware the user's own silicon. Free to everyone
|
|
5
|
+
* user-subscription a plan the user already pays for. Free to the broker
|
|
6
|
+
* open-weights an open model on GPU bought wholesale. METERED
|
|
7
|
+
* frontier a frontier provider API. METERED
|
|
8
|
+
*/
|
|
9
|
+
export type CostClass = "user-hardware" | "user-subscription" | "open-weights" | "frontier";
|
|
10
|
+
/** The classes somebody has to be billed for. The user-* classes are already paid. */
|
|
11
|
+
export declare const METERED_CLASSES: readonly ["open-weights", "frontier"];
|
|
12
|
+
export declare function isMetered(c: CostClass): boolean;
|
|
13
|
+
export declare function isCostClass(v: unknown): v is CostClass;
|
|
14
|
+
/** Token and tool usage from a completed run: what a meter bills against. */
|
|
15
|
+
export interface RunUsage {
|
|
16
|
+
/** Which economics served this run. Present so a usage frame is self-describing. */
|
|
17
|
+
costClass?: CostClass;
|
|
18
|
+
/** What it cost, in micros, for the metered classes. Absent when nothing is owed. */
|
|
19
|
+
costMicros?: number;
|
|
20
|
+
/** Provider-neutral model alias, e.g. "sonnet", "gpt-5-codex". */
|
|
21
|
+
model: string;
|
|
22
|
+
/** Which provider actually served it, since aliases collide. */
|
|
23
|
+
provider?: string;
|
|
24
|
+
inputTokens: number;
|
|
25
|
+
outputTokens: number;
|
|
26
|
+
cacheReadTokens: number;
|
|
27
|
+
cacheWriteTokens: number;
|
|
28
|
+
webSearches: number;
|
|
29
|
+
}
|
|
30
|
+
/** FROZEN. New information arrives as optional fields on these, never as a new type. */
|
|
31
|
+
export type AgentEventType = "delta" | "thinking" | "status" | "done" | "error";
|
|
32
|
+
export interface AgentEvent {
|
|
33
|
+
type: AgentEventType;
|
|
34
|
+
/** `delta`: the NEW text, append it. `thinking`: the RUNNING TOTAL, replace it. */
|
|
35
|
+
text?: string;
|
|
36
|
+
/** `status` / `error`: human-readable copy, safe to show a user. */
|
|
37
|
+
message?: string;
|
|
38
|
+
/** On a `status` raised by a tool: a stable slug the UI maps to an icon. */
|
|
39
|
+
tool?: string;
|
|
40
|
+
/** On `done`: what the run consumed. Absent when the runtime cannot report it. */
|
|
41
|
+
usage?: RunUsage;
|
|
42
|
+
/** Which economics is serving this run, announced on the opening `status`,
|
|
43
|
+
* BEFORE any output. A visible downgrade beats a silent one. */
|
|
44
|
+
tier?: CostClass;
|
|
45
|
+
}
|
|
46
|
+
export declare function isTerminal(e: AgentEvent): boolean;
|
|
47
|
+
/** Stable tool slugs. Adapters map their runtime's native names onto these. */
|
|
48
|
+
export declare const TOOL_SLUGS: readonly ["search", "fetch", "read", "write", "edit", "bash", "files", "agent", "todo", "think", "sources", "tool"];
|
|
49
|
+
export type ToolSlug = (typeof TOOL_SLUGS)[number];
|
|
50
|
+
/** One turn of a conversation. `prompt` is shorthand for a single user message. */
|
|
51
|
+
export interface Message {
|
|
52
|
+
role: "user" | "assistant";
|
|
53
|
+
content: string;
|
|
54
|
+
}
|
|
55
|
+
/** An app-defined tool as it travels on the wire: schema only, no implementation. */
|
|
56
|
+
export interface ToolSchema {
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
/** JSON Schema for the tool's input. */
|
|
60
|
+
inputSchema: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* - "ask" escalations round-trip to an approver (see `askTools`)
|
|
64
|
+
* - "deny" anything outside the allowlist fails the call, the run continues
|
|
65
|
+
* - "auto" the runtime proceeds within the resolved allowlist, no prompting
|
|
66
|
+
*/
|
|
67
|
+
export type PermissionMode = "ask" | "deny" | "auto";
|
|
68
|
+
/**
|
|
69
|
+
* What a run REQUESTS. Never a grant: the machine's owner has their own policy
|
|
70
|
+
* and the worker resolves the two by intersection, so this can only ever narrow
|
|
71
|
+
* what the machine already allows.
|
|
72
|
+
*/
|
|
73
|
+
export interface RunPolicy {
|
|
74
|
+
cwd?: string;
|
|
75
|
+
allowTools?: string[];
|
|
76
|
+
denyTools?: string[];
|
|
77
|
+
askTools?: string[];
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
permissionMode?: PermissionMode;
|
|
80
|
+
allowNetwork?: string[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* WHICH ECONOMICS a run may use. You declare intent, not a runtime, which is
|
|
84
|
+
* what lets one call site back a free-forever app and an enterprise app.
|
|
85
|
+
*/
|
|
86
|
+
export interface RouteRequest {
|
|
87
|
+
/** Ordered preference. The first reachable class wins. */
|
|
88
|
+
prefer: CostClass[];
|
|
89
|
+
/** Classes usable at all. Omitted = anything the broker can reach.
|
|
90
|
+
* A HARD CEILING, enforced on the worker as well as the broker. */
|
|
91
|
+
allow?: CostClass[];
|
|
92
|
+
/** Ceiling on what one run may cost, in micros. Metered classes only. */
|
|
93
|
+
maxCostMicros?: number;
|
|
94
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CostClass } from "./protocol.js";
|
|
2
|
+
export interface PlanSummary {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
priceMicros?: number;
|
|
6
|
+
interval?: "month" | "year" | string;
|
|
7
|
+
/** What a user earns for connecting their own compute, as a credit. */
|
|
8
|
+
connectedRewardMicros?: number;
|
|
9
|
+
/** What happens past the allowance. */
|
|
10
|
+
overage?: "stop" | "meter" | string;
|
|
11
|
+
}
|
|
12
|
+
export interface Allowance {
|
|
13
|
+
includedMicros?: number;
|
|
14
|
+
usedMicros?: number;
|
|
15
|
+
/** Credit earned by connecting compute, spent before money. */
|
|
16
|
+
earnedMicros?: number;
|
|
17
|
+
periodStart?: string;
|
|
18
|
+
periodEnd?: string;
|
|
19
|
+
}
|
|
20
|
+
/** `GET /v1/users/:id` — what this user has. */
|
|
21
|
+
export interface SobaUser {
|
|
22
|
+
id: string;
|
|
23
|
+
email?: string | null;
|
|
24
|
+
plan?: PlanSummary | null;
|
|
25
|
+
allowance?: Allowance | null;
|
|
26
|
+
/** Whether anything of theirs can serve a run right now. */
|
|
27
|
+
connected?: boolean;
|
|
28
|
+
machines?: number;
|
|
29
|
+
createdAt?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface MachineRuntime {
|
|
32
|
+
id: string;
|
|
33
|
+
displayName: string;
|
|
34
|
+
version?: string | null;
|
|
35
|
+
models?: string[];
|
|
36
|
+
costClass: CostClass;
|
|
37
|
+
supportsTools?: boolean;
|
|
38
|
+
/** TRI-STATE. `null` means the worker could not tell, which is not `false`. */
|
|
39
|
+
authenticated?: boolean | null;
|
|
40
|
+
authHint?: string | null;
|
|
41
|
+
}
|
|
42
|
+
/** `GET /v1/users/:id/compute` — one connected machine and its live state. */
|
|
43
|
+
export interface ConnectedMachine {
|
|
44
|
+
id: string;
|
|
45
|
+
label?: string | null;
|
|
46
|
+
platform?: string | null;
|
|
47
|
+
arch?: string | null;
|
|
48
|
+
state: "online" | "offline" | "busy" | string;
|
|
49
|
+
alwaysOn?: boolean;
|
|
50
|
+
active?: number;
|
|
51
|
+
capacity?: number;
|
|
52
|
+
lastSeenAt?: string | null;
|
|
53
|
+
runtimes: MachineRuntime[];
|
|
54
|
+
}
|
|
55
|
+
/** `GET /v1/usage` — one row, self-describing about who paid for it. */
|
|
56
|
+
export interface UsageRow {
|
|
57
|
+
runId: string;
|
|
58
|
+
user: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
model: string;
|
|
61
|
+
provider?: string | null;
|
|
62
|
+
costClass: CostClass;
|
|
63
|
+
inputTokens: number;
|
|
64
|
+
outputTokens: number;
|
|
65
|
+
cacheReadTokens: number;
|
|
66
|
+
cacheWriteTokens: number;
|
|
67
|
+
webSearches: number;
|
|
68
|
+
costMicros: number;
|
|
69
|
+
/** What this run saved: zero on a metered run, the counterfactual otherwise. */
|
|
70
|
+
avoidedCostMicros: number;
|
|
71
|
+
environment?: string | null;
|
|
72
|
+
}
|
|
73
|
+
export interface UsageQuery {
|
|
74
|
+
user?: string;
|
|
75
|
+
/** ISO 8601. */
|
|
76
|
+
from?: string;
|
|
77
|
+
to?: string;
|
|
78
|
+
environment?: "development" | "preview" | "production" | string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
cursor?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface UsagePage {
|
|
83
|
+
data: UsageRow[];
|
|
84
|
+
nextCursor?: string | null;
|
|
85
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type HttpConfig } from "./http.js";
|
|
2
|
+
import { type AgentEvent, type CostClass, type RunUsage } from "./protocol.js";
|
|
3
|
+
import { type ToolDefinition } from "./tools.js";
|
|
4
|
+
export type ApprovalDecision = "allow" | "deny";
|
|
5
|
+
export interface ApprovalRequest {
|
|
6
|
+
runId: string;
|
|
7
|
+
approvalId: string;
|
|
8
|
+
/** The tool the model wants to use. */
|
|
9
|
+
tool: string;
|
|
10
|
+
input: unknown;
|
|
11
|
+
/** The directory the run is working in, when the runtime reports one. */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}
|
|
14
|
+
export type Approver = (request: ApprovalRequest) => ApprovalDecision | Promise<ApprovalDecision>;
|
|
15
|
+
export interface RunInit {
|
|
16
|
+
config: HttpConfig;
|
|
17
|
+
response: Response;
|
|
18
|
+
user: string;
|
|
19
|
+
tools: Map<string, ToolDefinition>;
|
|
20
|
+
approve: Approver | undefined;
|
|
21
|
+
onWarning: (message: string, detail?: unknown) => void;
|
|
22
|
+
signal: AbortSignal | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A run in flight.
|
|
26
|
+
*
|
|
27
|
+
* `await soba.run()` resolves as soon as the response headers arrive, which is
|
|
28
|
+
* the boundary docs/errors.md draws: a failure before the first byte is an HTTP
|
|
29
|
+
* status you catch, and a failure after it is a terminal `error` event you read.
|
|
30
|
+
*
|
|
31
|
+
* Iterate it once. Tool calls and approvals are answered underneath the
|
|
32
|
+
* iteration, so a consumer that only cares about text never has to know they
|
|
33
|
+
* happened.
|
|
34
|
+
*/
|
|
35
|
+
export declare class Run implements AsyncIterable<AgentEvent> {
|
|
36
|
+
#private;
|
|
37
|
+
constructor(init: RunInit);
|
|
38
|
+
/** Empty only until the first frame, if the server omitted the header. */
|
|
39
|
+
get runId(): string;
|
|
40
|
+
/** Which economics served this run, from the opening `status`. */
|
|
41
|
+
get tier(): CostClass | undefined;
|
|
42
|
+
/** What the run consumed. Absent until `done`, and absent after it when the
|
|
43
|
+
* runtime could not report it, which is exactly when the run was free. */
|
|
44
|
+
get usage(): RunUsage | undefined;
|
|
45
|
+
[Symbol.asyncIterator](): AsyncIterator<AgentEvent>;
|
|
46
|
+
/** Drain the run and return the assembled text. Throws on a terminal error. */
|
|
47
|
+
text(): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Stop the run. Best effort by design: the local stream is dropped
|
|
50
|
+
* immediately, and the broker is told so the machine stops working, but a
|
|
51
|
+
* subprocess that has already produced tokens has already produced them.
|
|
52
|
+
*/
|
|
53
|
+
cancel(): Promise<void>;
|
|
54
|
+
}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AgentEvent, RunUsage } from "./protocol.js";
|
|
2
|
+
export type StreamFrame = {
|
|
3
|
+
kind: "event";
|
|
4
|
+
runId?: string;
|
|
5
|
+
event: AgentEvent;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "usage";
|
|
8
|
+
runId?: string;
|
|
9
|
+
usage: RunUsage;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "tool_call";
|
|
12
|
+
runId?: string;
|
|
13
|
+
callId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
input: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "approval";
|
|
18
|
+
runId?: string;
|
|
19
|
+
approvalId: string;
|
|
20
|
+
tool: string;
|
|
21
|
+
input: unknown;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function parseStreamFrame(raw: string): StreamFrame | null;
|
|
25
|
+
/**
|
|
26
|
+
* Yield frames off a live response body.
|
|
27
|
+
*
|
|
28
|
+
* SSE by default; newline-delimited JSON when the server says so, which is what
|
|
29
|
+
* a hand-rolled test server is likely to send.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readFrames(response: Response): AsyncGenerator<StreamFrame>;
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ToolSchema } from "./protocol.js";
|
|
2
|
+
export interface ToolContext {
|
|
3
|
+
/** The run that asked. */
|
|
4
|
+
runId: string;
|
|
5
|
+
/** The end user the run is attributed to. */
|
|
6
|
+
user: string;
|
|
7
|
+
/** This specific call, which the result is keyed to. */
|
|
8
|
+
callId: string;
|
|
9
|
+
/** Aborted when the run ends or the caller cancels. Honour it in long tools. */
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
export interface ToolDefinition<Input = Record<string, unknown>> {
|
|
13
|
+
name: string;
|
|
14
|
+
/** What the tool does, in the model's words. This is prompt, not documentation. */
|
|
15
|
+
description?: string;
|
|
16
|
+
/** JSON Schema for the input. Omitted means an object with no declared fields. */
|
|
17
|
+
inputSchema?: Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Your code. Return anything JSON-serialisable; a string is passed through
|
|
20
|
+
* verbatim. Throwing is not a run failure: the error is handed to the model
|
|
21
|
+
* as a failed call, which it can see and work around.
|
|
22
|
+
*/
|
|
23
|
+
execute(input: Input, ctx: ToolContext): unknown | Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** Identity, for the type inference. `tool({...})` reads better than a cast. */
|
|
26
|
+
export declare function tool<Input = Record<string, unknown>>(def: ToolDefinition<Input>): ToolDefinition<Input>;
|
|
27
|
+
/**
|
|
28
|
+
* Index the tools by name, refusing a duplicate rather than letting the second
|
|
29
|
+
* one win silently. Two tools with one name is a bug the model surfaces as
|
|
30
|
+
* baffling behaviour hours later.
|
|
31
|
+
*/
|
|
32
|
+
export declare function indexTools(tools: readonly ToolDefinition[] | undefined): Map<string, ToolDefinition>;
|
|
33
|
+
/** What goes on the wire: schema only. `execute` is stripped here, not by luck. */
|
|
34
|
+
export declare function toolSchemas(byName: Map<string, ToolDefinition>): ToolSchema[];
|
|
35
|
+
/**
|
|
36
|
+
* Make a tool's return value safe to put in a JSON body. A value that cannot
|
|
37
|
+
* be serialised (a BigInt, a cycle, a class instance with a throwing getter)
|
|
38
|
+
* becomes its string form rather than an exception that would strand the run
|
|
39
|
+
* waiting for a result that never arrives.
|
|
40
|
+
*/
|
|
41
|
+
export declare function jsonSafe(value: unknown): unknown;
|
|
42
|
+
export declare function errorText(err: unknown): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.0.5";
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soba-so/sdk",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "One request that comes back finished, executed on compute the end user owns.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://soba.so/docs/runs",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"!dist/**/*.map",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node ../../scripts/build-sdk.mjs",
|
|
23
|
+
"prepublishOnly": "node ../../scripts/build-sdk.mjs"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"soba",
|
|
30
|
+
"agent",
|
|
31
|
+
"claude",
|
|
32
|
+
"codex",
|
|
33
|
+
"runs",
|
|
34
|
+
"byoc"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@soba-so/protocol": "workspace:*"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|