@spredr/sdk 0.0.0-stage → 0.1.0
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 +33 -0
- package/README.md +423 -2
- package/TRADEMARKS.md +18 -0
- package/dist/agent.d.ts +96 -0
- package/dist/caller.d.ts +30 -0
- package/dist/context.d.ts +108 -0
- package/dist/errors.d.ts +45 -0
- package/dist/http.d.ts +48 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +969 -0
- package/dist/owner.d.ts +162 -0
- package/dist/schema.d.ts +28 -0
- package/dist/sign.d.ts +26 -0
- package/dist/tag.d.ts +7 -0
- package/dist/tasks.d.ts +98 -0
- package/dist/types.d.ts +293 -0
- package/dist/usage.d.ts +3 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
spredr SDK License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Candhrly, LLC. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This license covers @spredr/sdk («the SDK»): the code in this package and in its repository. Candhrly, LLC («we»)
|
|
6
|
+
owns the SDK. It is not open source.
|
|
7
|
+
|
|
8
|
+
1. What you may do. We grant you a worldwide, non-exclusive, royalty-free license to install, run and copy the SDK,
|
|
9
|
+
and to adapt it as far as your own software needs, only to use spredr's services (spredr.cloud). You may
|
|
10
|
+
distribute the SDK only as part of your own software that uses spredr, with this license and the copyright notice
|
|
11
|
+
kept.
|
|
12
|
+
|
|
13
|
+
2. What you may not do. You may not:
|
|
14
|
+
a. distribute the SDK on its own, or publish a modified version or a fork of it;
|
|
15
|
+
b. use the SDK, or any part of it, to reach a service other than spredr, or to build a service that competes with
|
|
16
|
+
spredr;
|
|
17
|
+
c. remove or change this license, the copyright notice or the trademark notice.
|
|
18
|
+
|
|
19
|
+
3. Ownership and trademarks. We keep every right we do not grant here. The spredr name and logo are trademarks of
|
|
20
|
+
Candhrly, LLC and are not licensed by this file (see TRADEMARKS.md).
|
|
21
|
+
|
|
22
|
+
4. Ending. This license ends at once if you break its terms. When it ends, you stop using and distributing the SDK.
|
|
23
|
+
|
|
24
|
+
5. Future versions. We may publish later versions of the SDK under an open-source license. A version keeps the
|
|
25
|
+
license it was published with.
|
|
26
|
+
|
|
27
|
+
6. No warranty. THE SDK IS PROVIDED «AS IS», WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
28
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
29
|
+
|
|
30
|
+
7. Limit of liability. IN NO EVENT SHALL CANDHRLY, LLC BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
31
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SDK OR ITS USE.
|
|
32
|
+
|
|
33
|
+
Your use of spredr's services is also governed by spredr's terms of service.
|
package/README.md
CHANGED
|
@@ -1,3 +1,424 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://spredr.cloud"><img src="https://spredr.cloud/branding/banner/github-social-dark-1280x640.png" alt="spredr: put your backends online" width="720"></a>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@spredr/sdk</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>AI agents that run as your own services.</b><br>
|
|
9
|
+
They take tasks, ask before they act, remember your customers, learn from your corrections, and sleep when idle.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@spredr/sdk"><img src="https://img.shields.io/npm/v/@spredr/sdk?color=C2410C&label=npm" alt="npm version"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/dependencies-0-14161C" alt="no dependencies">
|
|
15
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A520-14161C" alt="Node 20 or newer">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="#why-spredr-for-your-agents">Why</a> ·
|
|
20
|
+
<a href="#quick-start">Quick start</a> ·
|
|
21
|
+
<a href="#the-agent">The agent</a> ·
|
|
22
|
+
<a href="#your-backend">Your backend</a> ·
|
|
23
|
+
<a href="#one-agent-many-users-at-once">Many users</a> ·
|
|
24
|
+
<a href="#you-with-your-token">Your token</a> ·
|
|
25
|
+
<a href="#traces">Traces</a> ·
|
|
26
|
+
<a href="https://spredr.cloud/pricing">Pricing</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why spredr for your agents
|
|
32
|
+
|
|
33
|
+
- **Nothing happens twice.** When a task comes back (after a question, or after a crash), every answer and every
|
|
34
|
+
step it already took gives back what it gave: the model is not paid twice, a customer is not charged twice. No
|
|
35
|
+
«write it to run twice» rule.
|
|
36
|
+
- **It asks before it acts.** A risky action waits for a person; approved often enough, it is offered to run alone,
|
|
37
|
+
and an undo takes that back. `autoPerDay` and `spendPerDayUsd` keep a mistake small.
|
|
38
|
+
- **It remembers, and it learns.** Memory by customer (and forgetting one on request); your corrections become
|
|
39
|
+
lessons, tried on past tasks before they go live.
|
|
40
|
+
- **One agent, thousands of users at once.** Each task says whom it works for (a person, an account, a team, a
|
|
41
|
+
business), and spredr keeps each one apart: its turn in the line, its memory, what it lets the agent do alone, its
|
|
42
|
+
own rules and its caps. One busy user never holds the others back; measured with 301 of them and 1,900 tasks at once.
|
|
43
|
+
- **Any model, your key.** Claude, OpenAI, Gemini or a model of your own, called by your code with your key: spredr
|
|
44
|
+
never sits between your agent and its model. `ctx.model()` reads the tokens of Claude, OpenAI and the AI SDK.
|
|
45
|
+
- **Light.** An agent takes about 25 MB idle and 36 MB working, measured with a real Claude agent; an agent asleep
|
|
46
|
+
uses no CPU, and spredr adds about 10 ms of its own CPU per task.
|
|
47
|
+
- **Yours to watch and to take.** Each task is a trace with its steps, tokens and cost, sent to your
|
|
48
|
+
OpenTelemetry tools (Langfuse, Datadog, Honeycomb); every task exports as NDJSON.
|
|
49
|
+
- **Built to be trusted.** Every request signed, no shared secrets, no dependencies, typed end to end, and tested:
|
|
50
|
+
61 tests over 99% of the code, plus the SDK against spredr's real agents service on every change there.
|
|
51
|
+
|
|
52
|
+
Agents run as your plan's services, with their tasks included: see [pricing](https://spredr.cloud/pricing).
|
|
53
|
+
|
|
54
|
+
## Three pieces, one agent
|
|
55
|
+
|
|
56
|
+
An agent on spredr is a normal service: your code, your repo, deployed like any backend. This package is how the
|
|
57
|
+
three parties around it talk to each other.
|
|
58
|
+
|
|
59
|
+
| You are | You use | You can |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| **The agent** | `Agent` | take tasks, ask a person before a risky action, remember, learn, report each step |
|
|
62
|
+
| **Your backend** | `Caller` | give the agent tasks, follow them, answer its questions, receive callbacks |
|
|
63
|
+
| **You** | `Spredr` | register agents, link backends, schedule work, read memory, approve lessons, set limits |
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="https://spredr.cloud/branding/sdk/flow.webp" width="760" alt="Your backend (Caller) sends a task to spredr (ai), which hands it to your agent (Agent); the result comes back the same way. You (Spredr, with your token) answer questions and manage memory, lessons and limits. Every request is signed with ed25519, with no shared secrets.">
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
**Requirements.** Node 22.12 or newer, and nothing else: the SDK has no dependencies. It is written in TypeScript and
|
|
70
|
+
ships its types.
|
|
71
|
+
|
|
72
|
+
**No shared secrets.** Every request is signed with ed25519, and each side keeps only the other's public key. When you
|
|
73
|
+
register an agent or link a backend, spredr writes the keys into that service's variables for you: there is nothing to
|
|
74
|
+
copy or paste.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
**1. Install the CLI and sign in**
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
curl -fsSL https://spredr.cloud/install | sh
|
|
82
|
+
spredr login
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**2. Write the agent**
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npm install @spredr/sdk @anthropic-ai/sdk
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// index.ts
|
|
93
|
+
import { Agent } from '@spredr/sdk';
|
|
94
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
95
|
+
|
|
96
|
+
const claude = new Anthropic(); // ANTHROPIC_API_KEY, set with: spredr vars set shop-agent ANTHROPIC_API_KEY=…
|
|
97
|
+
|
|
98
|
+
export const shop = new Agent({ description: 'Answers the shop\'s customers', sleepAfterMinutes: 15 })
|
|
99
|
+
.on('reply', async ({ text }: { text: string }, ctx) => {
|
|
100
|
+
const message = await ctx.model(() => claude.messages.create({ // timed, model and tokens reported for you
|
|
101
|
+
model: 'claude-haiku-5-5',
|
|
102
|
+
max_tokens: 400,
|
|
103
|
+
system: ['You answer for a small shop. Be brief.', ...ctx.lessons].join('\n'),
|
|
104
|
+
messages: [{ role: 'user', content: text }],
|
|
105
|
+
}));
|
|
106
|
+
const answer = message.content.find((block) => block.type === 'text');
|
|
107
|
+
return { reply: answer?.type === 'text' ? answer.text : '' };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
shop.listen(); // PORT, 8080 by default
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**3. Deploy it and make it an agent**
|
|
114
|
+
|
|
115
|
+
Push it to a repo, then:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
spredr new you/shop-agent --port 8080 # a service, built from your repo
|
|
119
|
+
spredr agents add shop shop-agent # the service becomes the agent «shop»
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**4. Give it a task**
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
spredr ask shop reply '{"text":"Are you open on Sunday?"}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**5. Call it from your backend**
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { Spredr } from '@spredr/sdk';
|
|
132
|
+
|
|
133
|
+
// once, with your token: link your backend's service to the agent
|
|
134
|
+
await new Spredr().agent('shop').addCaller('shop-api', { service: 'shop-api' }); // reads SPREDR_TOKEN
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// in shop-api: the variables are already there
|
|
139
|
+
import { Caller } from '@spredr/sdk';
|
|
140
|
+
|
|
141
|
+
const { reply } = await new Caller().result('reply', { text: 'Are you open on Sunday?' });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
That is the whole loop. Everything below is reference.
|
|
145
|
+
|
|
146
|
+
## The agent
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { Agent } from '@spredr/sdk';
|
|
150
|
+
|
|
151
|
+
new Agent({
|
|
152
|
+
description: 'Answers the shop\'s customers',
|
|
153
|
+
tasks: [{ kind: 'reply', description: 'Answer one message' }],
|
|
154
|
+
actions: [{ name: 'refund', neverAuto: true }],
|
|
155
|
+
sleepAfterMinutes: 15,
|
|
156
|
+
concurrency: 4,
|
|
157
|
+
})
|
|
158
|
+
.on('reply', async ({ customer, text, wantsRefund }: { customer: string; text: string; wantsRefund?: boolean }, ctx) => {
|
|
159
|
+
const known = await ctx.memory.recall({ scope: `customer:${customer}` });
|
|
160
|
+
if (wantsRefund) {
|
|
161
|
+
const answer = await ctx.ask('refund', `Refund ${customer}?`);
|
|
162
|
+
if (!answer.yes) return { reply: 'We cannot refund this one.' };
|
|
163
|
+
}
|
|
164
|
+
await ctx.memory.remember(`customer:${customer}`, `Asked: ${text}`, { key: 'last' });
|
|
165
|
+
return { reply: await think(text, known, ctx.lessons) }; // think(): your model call
|
|
166
|
+
})
|
|
167
|
+
.listen();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each task kind gets a handler. **What it returns is the task's result, and what it throws is its error.** To check
|
|
171
|
+
the input before the handler runs, give `on()` a schema: zod, valibot, arktype or any
|
|
172
|
+
[Standard Schema](https://standardschema.dev). A task whose input does not match fails, saying where.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { z } from 'zod';
|
|
176
|
+
|
|
177
|
+
agent.on('reply', z.object({ text: z.string().min(1) }), async ({ text }, ctx) => ({ reply: text }));
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The options, besides the manifest (`description`, `tasks`, `actions`, `tools`, `sleepAfterMinutes`, `concurrency`,
|
|
181
|
+
`timeoutSeconds`, `judge`) and the shared ones under [Errors and retries](#errors-and-retries):
|
|
182
|
+
|
|
183
|
+
| Option | Default | |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `shutdownGraceMs` | 25 000 | on a stop, how long running tasks may finish |
|
|
186
|
+
| `log` | the console | where its lines go; `false` says nothing |
|
|
187
|
+
| `tagLogs` | `true` | what a task prints with `console` starts with its id; `false` leaves console alone |
|
|
188
|
+
| `id`, `key`, `ai`, `aiKey` | its variables | written by spredr when it registers the agent |
|
|
189
|
+
|
|
190
|
+
`ctx` gives the handler the rest:
|
|
191
|
+
|
|
192
|
+
| | |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `ctx.ask(action, question)` | Asks a person first, and stops the task until someone answers. An action marked `neverAuto` asks every time. |
|
|
195
|
+
| `ctx.files`, `ctx.file(name)` | The files its session holds (what the backend left with `attach`), and one of them as its bytes and type. spredr only delivers them: how your code reads a CSV, a JSON or a PDF is yours. |
|
|
196
|
+
| `ctx.memory` | `remember(scope, text)`, `recall({ scope, q })`, `forget(id)`. A scope is whatever groups facts, such as `customer:ana`. `remember` gives back `null` when the fact was not kept (the agent's memory is full, or its tenant's share): the task goes on, and why is in its logs. |
|
|
197
|
+
| `ctx.lessons` | The lessons you approved. Put them in your prompt. |
|
|
198
|
+
| `ctx.trial` | `true` when an old task is replayed to test new lessons: **do nothing real**, just answer. |
|
|
199
|
+
| `ctx.model(() => call)` | A model call, timed and reported with its model and tokens, read from the answer of Claude, OpenAI or the AI SDK. |
|
|
200
|
+
| `ctx.track(kind, name, work, usage?)` | Times any other step (`tool`, `memory`, `other`) and reports it: a charge, an email, a call to your API. Steps are the task's trace, its cost and its timeline. |
|
|
201
|
+
| `ctx.later(kind, input, at)` | A task for later: a reminder, a follow-up, for the same tenant and thread. |
|
|
202
|
+
| `ctx.tenant`, `ctx.thread` | Whom this task works for (a user, an account, a team, a business; `""` when no one in particular) and its conversation. |
|
|
203
|
+
| `ctx.progress(data)` | Tells the caller how it is going, and keeps a long task alive. |
|
|
204
|
+
| `ctx.stream(text)` | The next piece of its answer, for a caller watching live (`live()` or `stream(id)`). Sent in order, a few at a time, and the last pieces before its result. A view: the result is what stays. |
|
|
205
|
+
|
|
206
|
+
**Nothing happens twice.** When a task comes back (after a question, or given again after a crash), its handler runs
|
|
207
|
+
from the top, but everything it already did through `ctx` gives back what it gave the first time instead of happening
|
|
208
|
+
again: each answered question, each `ctx.model`, `ctx.track`, `ctx.memory.remember` and `ctx.later`. The model is not
|
|
209
|
+
paid twice and a customer is not charged twice. So put side effects in `ctx.track`; only code outside `ctx` runs again.
|
|
210
|
+
If the handler takes another way than before (another question, another step), it runs for real from there on. What a
|
|
211
|
+
step gives back comes back as JSON (a `Date` comes back as its text), up to 256 KB; a bigger one simply runs again.
|
|
212
|
+
|
|
213
|
+
**Learning.** The agent reads the verdicts on its tasks with `agent.feedback()` and turns what it sees into a lesson
|
|
214
|
+
with `agent.propose(text, why)`. A lesson goes live only when you approve it, and before that it can be tried on old
|
|
215
|
+
tasks (`ctx.trial`).
|
|
216
|
+
|
|
217
|
+
**Sleeping.** An agent that is quiet for `sleepAfterMinutes` is put to sleep and uses no CPU. The next task wakes it, and waits until
|
|
218
|
+
it answers.
|
|
219
|
+
|
|
220
|
+
**Stopping.** On a deploy or a stop, the agent takes no new task and lets the running ones finish, up to
|
|
221
|
+
`shutdownGraceMs` (25 s); `/health` answers 503 meanwhile. A task still running then is given again by spredr. To stop
|
|
222
|
+
it yourself, `await agent.close()`.
|
|
223
|
+
|
|
224
|
+
**Inside your own server.** An agent can live in a server you already run (Fastify, Express, Hono) instead of
|
|
225
|
+
`listen()`: route its path to `handle()` with the raw body, since the signature is over those bytes, and send back what
|
|
226
|
+
it returns. `start()` says hello; your server's shutdown calls `close()`.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
app.post(shop.path, async (request, reply) => { // with the body kept raw for this route
|
|
230
|
+
const answer = await shop.handle({ method: 'POST', path: request.url, headers: request.headers, body: request.rawBody });
|
|
231
|
+
return reply.code(answer.status).headers(answer.headers).send(answer.body);
|
|
232
|
+
});
|
|
233
|
+
await shop.start();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Its logs, task by task
|
|
237
|
+
|
|
238
|
+
What a task prints with `console` starts with its id, however deep in your code (`[tsk_7f3a…] looking up the
|
|
239
|
+
catalog`), and the agent writes one line when each task ends (`tsk_7f3a… reply done in 4.6 s`, or why it failed). So
|
|
240
|
+
searching the deployment's logs for a task's id, as the panel's «Ver sus registros» does, finds what it did. A logger
|
|
241
|
+
that writes on its own adds the id with `currentTask()`:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import pino from 'pino';
|
|
245
|
+
import { currentTask } from '@spredr/sdk';
|
|
246
|
+
|
|
247
|
+
const log = pino({ mixin: () => ({ task: currentTask() ?? undefined }) }); // {"task":"tsk_7f3a…","msg":"…"}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Your backend
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { Caller } from '@spredr/sdk';
|
|
254
|
+
|
|
255
|
+
const agent = new Caller(); // reads the variables spredr wrote into this service
|
|
256
|
+
|
|
257
|
+
const task = await agent.run('reply', { customer: 'ana', text: 'hola' }, { key: messageId, thread: 'customer:ana' });
|
|
258
|
+
|
|
259
|
+
if (task.status === 'paused') {
|
|
260
|
+
const [question] = await agent.pauses(); // show it in your app
|
|
261
|
+
await agent.answer(question.id, { yes: true });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await agent.feedback(task.id, 'bad', { correction: 'Offer two times.' }); // kept for the agent to learn from
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
| | |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `result(kind, input)` | Asks, waits, and gives back the result. A task that fails, is canceled or waits for an answer throws a `TaskError` carrying it. |
|
|
270
|
+
| `run(kind, input)` | Asks and waits for the outcome, whatever it is. |
|
|
271
|
+
| `ask(kind, input)` | Asks and returns at once. `settle(id)` waits for it later. |
|
|
272
|
+
| `key` | Your own id for the task: asking twice with the same key is the same task, never two. |
|
|
273
|
+
| `thread` | Tasks with the same thread read as one conversation. |
|
|
274
|
+
| `at` | When to start. |
|
|
275
|
+
| `list()`, `events(id)`, `steps(id)`, `cancel(id)` | Follow and stop tasks. |
|
|
276
|
+
| `pauses()`, `answer(id, answer)` | The questions the agent is waiting on. |
|
|
277
|
+
| `feedback(id, 'good' \| 'bad', { correction })` | Your verdict, with what it should have done. |
|
|
278
|
+
| `live(kind, input)`, `stream(id)` | The task as it happens: each piece of its live answer (`{ type: 'delta', text }`), its other events, and last `{ type: 'end', task }`. A stream that closes early is opened again after what was seen. |
|
|
279
|
+
| `attach(thread, name, data, { type })`, `files(thread)`, `detach(thread, name)` | Leaves a file in a session for the agent to read in its tasks (a price list, a catalogue, a document): sent once, every task of that thread lists it. The same name again replaces it. Up to 10 MB each; they go with the agent's record's days. |
|
|
280
|
+
|
|
281
|
+
**Types, end to end.** Give the backend your agent's type and TypeScript checks each kind's name, input and result:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import type { shop } from '../shop-agent/index.ts';
|
|
285
|
+
import { Caller, type KindsOf } from '@spredr/sdk';
|
|
286
|
+
|
|
287
|
+
const agent = new Caller<KindsOf<typeof shop>>();
|
|
288
|
+
const { reply } = await agent.result('reply', { text: 'hola' }); // reply: string
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**What the agent needs to know.** Give it in the task (`input`, up to 256 KB), as an address its code opens, or as a
|
|
292
|
+
session's file when it is bigger or the same for many tasks:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const shop = agent.for('shop_a');
|
|
296
|
+
await shop.attach('chat:ana', 'precios.csv', csv, { type: 'text/csv' });
|
|
297
|
+
await shop.result('reply', { text: 'hola' }, { thread: 'chat:ana' }); // the agent reads it with ctx.file('precios.csv')
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Callbacks.** Link the backend with a `callbackUrl` and spredr POSTs `task.done`, `task.failed`, `task.paused` and
|
|
301
|
+
`task.canceled` to it. Believe one only when `agent.callback({ method, path, headers, body })` returns it, and give it
|
|
302
|
+
the raw body. With `everything: true` the backend receives every task's events, not only its own.
|
|
303
|
+
|
|
304
|
+
## One agent, many users at once
|
|
305
|
+
|
|
306
|
+
Most agents serve many people at once: an assistant with thousands of users, a support agent for many accounts, a
|
|
307
|
+
booking agent for many businesses. Each task can say whom it works for, its **tenant**: whatever your app calls one of
|
|
308
|
+
them (a user id, an account, a team, a business). Open a session for one with `for(tenant)`:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const user = agent.for('user_42'); // agent is your Caller
|
|
312
|
+
|
|
313
|
+
const { reply } = await user.result('reply', { text: 'remind me to call mom on Friday' }, { thread: 'chat_9' });
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
spredr keeps each tenant apart, with nothing for your agent to remember:
|
|
317
|
+
|
|
318
|
+
| | |
|
|
319
|
+
|---|---|
|
|
320
|
+
| **Its turn** | The line goes in turns between tenants: one with a thousand tasks waiting never holds the others back. |
|
|
321
|
+
| **Its memory** | `ctx.memory` in a task reaches that tenant's facts alone, whatever the model is talked into. All tenants share the agent's room; `setLimits({ facts })` caps one's share. |
|
|
322
|
+
| **Its freedom** | `user.setFreedom('send_email', 'auto')` lets the agent act alone for this tenant, never for another. |
|
|
323
|
+
| **Its rules** | `user.teach('Call me Ana, never by my surname')` reaches this tenant's tasks from that moment on, and no other's. |
|
|
324
|
+
| **Its caps** | `user.setLimits({ spendPerDayUsd: 0.5, autoPerDay: 20 })`: past them it waits for tomorrow, and the rest go on. |
|
|
325
|
+
| **Its questions** | `user.pauses()`, and every `task.paused` callback names the tenant, so it reaches the right person. |
|
|
326
|
+
| **What it used** | `user.usage()`: tasks, cost and tokens, today and this month, to bill or limit each one for what it used. |
|
|
327
|
+
| **Forgetting it** | `user.forget()` erases its tasks, memory, questions, freedom, rules and caps. The others stay. |
|
|
328
|
+
|
|
329
|
+
In the agent, `ctx.tenant` says whom the task works for. With your token, `spredr.agent('assistant').tenants()` lists
|
|
330
|
+
them with what each used, and `setTenantLimits()` caps every tenant without caps of its own. A task with no tenant is
|
|
331
|
+
the agent's own work, as before.
|
|
332
|
+
|
|
333
|
+
## You, with your token
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { Spredr } from '@spredr/sdk';
|
|
337
|
+
|
|
338
|
+
const spredr = new Spredr(); // SPREDR_TOKEN, or new Spredr({ token })
|
|
339
|
+
const shop = spredr.agent('shop');
|
|
340
|
+
|
|
341
|
+
await spredr.register('shop', 'shop-agent'); // a service becomes an agent
|
|
342
|
+
await shop.addCaller('shop-api', { service: 'shop-api' }); // a backend may call it
|
|
343
|
+
await shop.schedule('morning', 'digest', '0 8 * * *', { tz: 'America/Bogota' });
|
|
344
|
+
await shop.hook('orders', 'new-order'); // a webhook URL that makes tasks
|
|
345
|
+
await shop.setFreedom('refund', 'ask'); // ask before refunds
|
|
346
|
+
await shop.setLimits({ autoPerDay: 200, spendPerDayUsd: 15 });
|
|
347
|
+
|
|
348
|
+
await shop.summary(); // its last day: tasks, failures, times, tokens, cost
|
|
349
|
+
await shop.memory({ scope: 'customer:ana' });
|
|
350
|
+
await shop.forget('customer:ana'); // a customer who asks to be forgotten
|
|
351
|
+
await shop.lessons(); // proposed and live lessons; decide(id, 'accept') to approve one
|
|
352
|
+
await shop.export(); // every task with its steps
|
|
353
|
+
await spredr.inbox(); // what waits for you, across every agent
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Limits keep a mistake small.** `autoPerDay` caps the actions it takes alone in a day; past it, it asks again.
|
|
357
|
+
`spendPerDayUsd` caps what its model calls cost in a day, read from the steps it reports; past it, new tasks are
|
|
358
|
+
refused until tomorrow.
|
|
359
|
+
|
|
360
|
+
## Traces
|
|
361
|
+
|
|
362
|
+
Every finished task can go to your OpenTelemetry endpoint as a trace: the task as the root span, each step as a child
|
|
363
|
+
with the GenAI attributes (`gen_ai.*`) that Langfuse, Datadog, Honeycomb and Grafana read.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { Spredr } from '@spredr/sdk';
|
|
367
|
+
|
|
368
|
+
await new Spredr().agent('shop').setTraces({
|
|
369
|
+
endpoint: 'https://cloud.langfuse.com/api/public/otel/v1/traces',
|
|
370
|
+
headers: { authorization: `Basic ${btoa(`${publicKey}:${secretKey}`)}` },
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The headers are sealed by spredr and never shown back. The task's input and result travel only with
|
|
375
|
+
`withContent: true`.
|
|
376
|
+
|
|
377
|
+
## Errors and retries
|
|
378
|
+
|
|
379
|
+
Every error is a `SpredrError`, with ai's own words, its `status` and its `trace`. Catch the base, or the case you
|
|
380
|
+
handle: `BadRequestError` (400), `AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError` (404),
|
|
381
|
+
`ConflictError` (409), `RateLimitError` (429, with `retryAfter`), `InternalServerError` (5xx), `ConnectionError` and
|
|
382
|
+
`TimeoutError` (no answer). A task that ends badly is a `TaskError`.
|
|
383
|
+
|
|
384
|
+
A request is tried again only when that is safe: no answer came or spredr failed, on a call that does the same thing
|
|
385
|
+
twice (reads, updates, deletes, and a task asked with its own `key`). A 429 is tried again only when spredr says how
|
|
386
|
+
long to wait, so a cap reached comes straight back to you. Each side takes the same options:
|
|
387
|
+
|
|
388
|
+
| Option | Default | |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| `timeoutMs` | 75 000 | how long one request may take |
|
|
391
|
+
| `maxRetries` | 2 | how many more tries, when safe; 0 never |
|
|
392
|
+
| `retryDelayMs` | 500 | the first wait, doubling up to 8 s |
|
|
393
|
+
| `fetch` | the global one | a fetch of your own |
|
|
394
|
+
|
|
395
|
+
## Variables
|
|
396
|
+
|
|
397
|
+
spredr writes these into your services when you register an agent or link a backend. You only set `SPREDR_TOKEN`.
|
|
398
|
+
|
|
399
|
+
| Variable | Read by | What it is |
|
|
400
|
+
|---|---|---|
|
|
401
|
+
| `SPREDR_AGENT_ID`, `SPREDR_AGENT_KEY` | `Agent` | the agent's id and private key |
|
|
402
|
+
| `SPREDR_CALLER_ID`, `SPREDR_CALLER_KEY`, `SPREDR_AGENT` | `new Caller()` | the backend's id, its private key, the agent it calls |
|
|
403
|
+
| `SPREDR_AI_URL`, `SPREDR_AI_KEY` | `Agent`, `Caller` | where spredr's agents live, and its public key |
|
|
404
|
+
| `SPREDR_TOKEN` | `Spredr` | an API token of yours, made in the panel (Account › API tokens) |
|
|
405
|
+
| `PORT` | `Agent.listen()` | the port it listens on, 8080 by default |
|
|
406
|
+
|
|
407
|
+
## Plans
|
|
408
|
+
|
|
409
|
+
An agent runs as one of your plan's services, so Starter runs up to 3 and Pro up to 10. The plan also sets the tasks a
|
|
410
|
+
month, how much each agent remembers and how long finished tasks are kept. Exporting and traces are always included.
|
|
411
|
+
See [spredr.cloud/pricing](https://spredr.cloud/pricing).
|
|
412
|
+
|
|
413
|
+
## Without the SDK
|
|
414
|
+
|
|
415
|
+
It is plain HTTP and works from any language: signed requests one way, signed callbacks the other. The SDK has no
|
|
416
|
+
dependencies; write to us if you need the protocol for another language.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
<p align="center">
|
|
421
|
+
<a href="https://spredr.cloud"><img src="https://spredr.cloud/branding/icon/icon-192.png" alt="spredr" width="56"></a><br>
|
|
422
|
+
<sub><a href="https://spredr.cloud">spredr.cloud</a> · © 2026 Candhrly, LLC · <a href="LICENSE">license</a></sub><br>
|
|
423
|
+
<sub>Free to use with spredr. spredr and its logo are trademarks of Candhrly, LLC.</sub>
|
|
424
|
+
</p>
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Trademarks
|
|
2
|
+
|
|
3
|
+
**spredr**, the spredr logo (the three leaning bars, «Sesgo») and the rest of the spredr brand are trademarks of
|
|
4
|
+
**Candhrly, LLC**. The [SDK's license](LICENSE) covers its code. It does not grant any right to the name, the logo
|
|
5
|
+
or the brand.
|
|
6
|
+
|
|
7
|
+
**You may**
|
|
8
|
+
|
|
9
|
+
- say that your project works with spredr, is built on spredr, or uses `@spredr/sdk`;
|
|
10
|
+
- use the word «spredr» in plain text to refer to spredr and this SDK, truthfully.
|
|
11
|
+
|
|
12
|
+
**Please ask first before you**
|
|
13
|
+
|
|
14
|
+
- use the logo or any brand asset;
|
|
15
|
+
- name a product, package, fork, domain or account «spredr», or anything close to it;
|
|
16
|
+
- publish a package under the `@spredr` scope, or describe a fork as official.
|
|
17
|
+
|
|
18
|
+
Questions: open an issue in this repository.
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type Server } from 'node:http';
|
|
2
|
+
import { type Context } from './context.js';
|
|
3
|
+
import { type ClientOptions } from './http.js';
|
|
4
|
+
import { type InferInput, type InferOutput, type StandardSchemaV1 } from './schema.js';
|
|
5
|
+
import { type Incoming } from './sign.js';
|
|
6
|
+
import type { Feedback, Json, Kinds, Task } from './types.js';
|
|
7
|
+
export type { Context, Delivery } from './context.js';
|
|
8
|
+
export interface Manifest {
|
|
9
|
+
description?: string;
|
|
10
|
+
tasks?: {
|
|
11
|
+
kind: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}[];
|
|
14
|
+
/** The actions it asks about first; neverAuto: it asks every time, however often it was approved. */
|
|
15
|
+
actions?: {
|
|
16
|
+
name: string;
|
|
17
|
+
neverAuto?: boolean;
|
|
18
|
+
}[];
|
|
19
|
+
sleepAfterMinutes?: number | null;
|
|
20
|
+
concurrency?: number;
|
|
21
|
+
timeoutSeconds?: number;
|
|
22
|
+
judge?: string | null;
|
|
23
|
+
/** The tools it can use, for its owner to see; asks: it asks before using it. */
|
|
24
|
+
tools?: {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
asks?: boolean;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** What to do with one kind of task: its input (checked by a schema when given one) in, its result out. */
|
|
31
|
+
export type Handler<Input = Json, Output = unknown> = (input: Input, ctx: Context) => Output | Promise<Output>;
|
|
32
|
+
export interface AgentOptions extends Manifest, ClientOptions {
|
|
33
|
+
/** ag_…: SPREDR_AGENT_ID. */
|
|
34
|
+
id?: string;
|
|
35
|
+
/** Its private key: SPREDR_AGENT_KEY. */
|
|
36
|
+
key?: string;
|
|
37
|
+
/** Where ai is: SPREDR_AI_URL. */
|
|
38
|
+
ai?: string;
|
|
39
|
+
/** ai's public key, to check that a task comes from ai: SPREDR_AI_KEY. */
|
|
40
|
+
aiKey?: string;
|
|
41
|
+
/** Where its lines go; false says nothing. */
|
|
42
|
+
log?: ((line: string) => void) | false;
|
|
43
|
+
/** console's lines printed while a task works start with its id (`[tsk_…] …`), so its logs find them; true unless
|
|
44
|
+
* false. A logger of its own adds it with currentTask(). */
|
|
45
|
+
tagLogs?: boolean;
|
|
46
|
+
/** On SIGTERM, how long running tasks may finish before it exits, in ms: 25 s by default. */
|
|
47
|
+
shutdownGraceMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/** What handle() answers ai with: send it back as it is. */
|
|
50
|
+
export interface AgentAnswer {
|
|
51
|
+
status: number;
|
|
52
|
+
headers: Record<string, string>;
|
|
53
|
+
body: string;
|
|
54
|
+
}
|
|
55
|
+
/** The kinds an agent handles, to type a Caller with: `new Caller<KindsOf<typeof agent>>()`. */
|
|
56
|
+
export type KindsOf<A> = A extends Agent<infer K> ? K : never;
|
|
57
|
+
/** An agent. K starts empty and each on() adds its kind, which is what KindsOf reads. */
|
|
58
|
+
export declare class Agent<K extends Kinds = {}> {
|
|
59
|
+
#private;
|
|
60
|
+
constructor(options?: AgentOptions);
|
|
61
|
+
/** What to do with one kind of task. The kinds you name are the ones ai sends you. With a schema (zod, valibot,
|
|
62
|
+
* arktype: any Standard Schema) the input is checked first, and a task whose input does not match fails saying where. */
|
|
63
|
+
on<Name extends string, Input = Json, Output = unknown>(kind: Name, handler: Handler<Input, Output>): Agent<K & {
|
|
64
|
+
[P in Name]: {
|
|
65
|
+
input: Input;
|
|
66
|
+
output: Awaited<Output>;
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
on<Name extends string, Schema extends StandardSchemaV1, Output = unknown>(kind: Name, schema: Schema, handler: Handler<InferOutput<Schema>, Output>): Agent<K & {
|
|
70
|
+
[P in Name]: {
|
|
71
|
+
input: InferInput<Schema>;
|
|
72
|
+
output: Awaited<Output>;
|
|
73
|
+
};
|
|
74
|
+
}>;
|
|
75
|
+
/** Says hello with the manifest (retrying while ai is not reachable yet). */
|
|
76
|
+
hello(): Promise<void>;
|
|
77
|
+
/** Listens for tasks (and answers /health), then says hello. On SIGTERM it closes and exits. */
|
|
78
|
+
listen(port?: number): Promise<Server>;
|
|
79
|
+
/** Takes no new task, and waits for the running ones to finish (up to shutdownGraceMs). */
|
|
80
|
+
close(): Promise<void>;
|
|
81
|
+
/** Where ai sends its tasks: route this path to `handle()` when the agent lives inside your own server. */
|
|
82
|
+
get path(): string;
|
|
83
|
+
/** One request from ai, answered: for an agent inside a server you already run (Fastify, Express, Hono). Give it the
|
|
84
|
+
* raw body exactly as it arrived, since the signature is over those bytes, and send back what it returns. The task's
|
|
85
|
+
* handler runs after the answer, as with listen(). Call start() once to say hello, and close() when your server stops. */
|
|
86
|
+
handle(request: Incoming): Promise<AgentAnswer>;
|
|
87
|
+
/** Says hello to ai, for an agent inside your own server (listen() does this itself). Your server owns the shutdown:
|
|
88
|
+
* call close() in it, and the running tasks get their grace. */
|
|
89
|
+
start(): Promise<void>;
|
|
90
|
+
/** What apps said about this agent's tasks after `after` (a feedback seq): what it learns from. */
|
|
91
|
+
feedback(after?: number): Promise<(Feedback & {
|
|
92
|
+
task: Task | null;
|
|
93
|
+
})[]>;
|
|
94
|
+
/** Proposes a lesson to its owner, drawn from feedback. */
|
|
95
|
+
propose(text: string, why?: string, sources?: string[]): Promise<void>;
|
|
96
|
+
}
|
package/dist/caller.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ClientOptions } from './http.js';
|
|
2
|
+
import { type Incoming } from './sign.js';
|
|
3
|
+
import { Tasks } from './tasks.js';
|
|
4
|
+
import type { AnyKinds, Kinds, Task } from './types.js';
|
|
5
|
+
export interface CallerOptions extends ClientOptions {
|
|
6
|
+
/** cl_…: SPREDR_CALLER_ID. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Its private key (DER pkcs8, base64): SPREDR_CALLER_KEY. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** The agent it talks to: SPREDR_AGENT. */
|
|
11
|
+
agent: string;
|
|
12
|
+
/** Where ai is: SPREDR_AI_URL. */
|
|
13
|
+
ai: string;
|
|
14
|
+
/** ai's public key, to check callbacks: SPREDR_AI_KEY. */
|
|
15
|
+
aiKey?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface Callback {
|
|
18
|
+
event: 'task.done' | 'task.failed' | 'task.paused' | 'task.canceled';
|
|
19
|
+
task: Task;
|
|
20
|
+
}
|
|
21
|
+
/** A backend linked to one agent. With no options it reads the variables spredr writes into a linked service; give it
|
|
22
|
+
* the agent's kinds (`new Caller<KindsOf<typeof assistant>>()`) and TypeScript checks every name, input and result. */
|
|
23
|
+
export declare class Caller<K extends Kinds = AnyKinds> extends Tasks<K> {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(options?: Partial<CallerOptions>);
|
|
26
|
+
/** The same as `new Caller(overrides)`: from the variables spredr writes into a linked service. */
|
|
27
|
+
static fromEnv<K extends Kinds = AnyKinds>(overrides?: Partial<CallerOptions>): Caller<K>;
|
|
28
|
+
/** The callback, when it really comes from ai (signed, recent, once); null otherwise. Give it the raw body. */
|
|
29
|
+
callback(request: Incoming): Callback | null;
|
|
30
|
+
}
|