@aithos/sdk 0.1.0-alpha.6 → 0.1.0-alpha.60
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/README.md +202 -7
- package/dist/src/agent-dispatch.d.ts +18 -0
- package/dist/src/agent-dispatch.js +178 -0
- package/dist/src/agent-loop.d.ts +94 -0
- package/dist/src/agent-loop.js +95 -0
- package/dist/src/agent-tools.d.ts +24 -0
- package/dist/src/agent-tools.js +147 -0
- package/dist/src/apps.d.ts +224 -0
- package/dist/src/apps.js +432 -0
- package/dist/src/assets.d.ts +225 -0
- package/dist/src/assets.js +534 -0
- package/dist/src/auth-api.d.ts +219 -0
- package/dist/src/auth-api.js +248 -0
- package/dist/src/auth.d.ts +591 -0
- package/dist/src/auth.js +947 -31
- package/dist/src/compute.d.ts +674 -6
- package/dist/src/compute.js +968 -20
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +368 -0
- package/dist/src/data.js +1124 -0
- package/dist/src/endpoints.d.ts +43 -0
- package/dist/src/endpoints.js +23 -0
- package/dist/src/ethos.d.ts +85 -0
- package/dist/src/ethos.js +463 -7
- package/dist/src/index.d.ts +22 -4
- package/dist/src/index.js +47 -2
- package/dist/src/internal/cmk-wrap.d.ts +41 -0
- package/dist/src/internal/cmk-wrap.js +132 -0
- package/dist/src/internal/delegate-bundle.js +7 -2
- package/dist/src/internal/envelope.d.ts +93 -0
- package/dist/src/internal/envelope.js +59 -0
- package/dist/src/internal/owner-signers.d.ts +5 -2
- package/dist/src/internal/owner-signers.js +22 -1
- package/dist/src/internal/recovery-file.d.ts +2 -0
- package/dist/src/internal/recovery-file.js +7 -0
- package/dist/src/key-store.d.ts +10 -0
- package/dist/src/key-store.js +6 -0
- package/dist/src/mandates.d.ts +58 -1
- package/dist/src/mandates.js +46 -3
- package/dist/src/migrate.d.ts +105 -0
- package/dist/src/migrate.js +367 -0
- package/dist/src/react/AithosAsset.d.ts +66 -0
- package/dist/src/react/AithosAsset.js +67 -0
- package/dist/src/react/context.d.ts +29 -0
- package/dist/src/react/context.js +31 -0
- package/dist/src/react/index.d.ts +29 -0
- package/dist/src/react/index.js +31 -0
- package/dist/src/react/use-aithos-asset.d.ts +39 -0
- package/dist/src/react/use-aithos-asset.js +118 -0
- package/dist/src/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/rotate.d.ts +94 -0
- package/dist/src/rotate.js +298 -0
- package/dist/src/sdk.d.ts +36 -2
- package/dist/src/sdk.js +72 -1
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/agent-dispatch.test.d.ts +2 -0
- package/dist/test/agent-dispatch.test.js +222 -0
- package/dist/test/agent-loop.test.d.ts +2 -0
- package/dist/test/agent-loop.test.js +117 -0
- package/dist/test/agent-tools.test.d.ts +2 -0
- package/dist/test/agent-tools.test.js +50 -0
- package/dist/test/auth-j3.test.js +32 -1
- package/dist/test/canonical-conformance.test.d.ts +2 -0
- package/dist/test/canonical-conformance.test.js +86 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +4 -0
- package/dist/test/converse.test.d.ts +2 -0
- package/dist/test/converse.test.js +162 -0
- package/dist/test/data-sphere.test.d.ts +2 -0
- package/dist/test/data-sphere.test.js +57 -0
- package/dist/test/endpoints.test.js +40 -1
- package/dist/test/envelope-core-conformance.test.d.ts +2 -0
- package/dist/test/envelope-core-conformance.test.js +75 -0
- package/dist/test/envelope.test.d.ts +2 -0
- package/dist/test/envelope.test.js +318 -0
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +371 -0
- package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
- package/dist/test/invoke-turn-sdk.test.js +177 -0
- package/dist/test/migrate.test.d.ts +2 -0
- package/dist/test/migrate.test.js +340 -0
- package/dist/test/owner-data-client.test.d.ts +2 -0
- package/dist/test/owner-data-client.test.js +88 -0
- package/dist/test/rotate-ethos.test.d.ts +2 -0
- package/dist/test/rotate-ethos.test.js +151 -0
- package/dist/test/rotate.test.d.ts +2 -0
- package/dist/test/rotate.test.js +63 -0
- package/dist/test/schema-autoresolve.test.d.ts +2 -0
- package/dist/test/schema-autoresolve.test.js +146 -0
- package/dist/test/sdk.test.js +11 -2
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +311 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +20 -3
package/README.md
CHANGED
|
@@ -55,6 +55,40 @@ const reply = await sdk.compute.invokeBedrock({
|
|
|
55
55
|
console.log(reply.content);
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Transcribing audio → text
|
|
59
|
+
|
|
60
|
+
`sdk.compute.invokeTranscribe` turns an audio `Blob` into text through AWS
|
|
61
|
+
Transcribe. It does one thing — audio → text — and **stores nothing**: it
|
|
62
|
+
returns the transcript and you decide what to do with it (write it to an
|
|
63
|
+
ethos, a PDS, your own database, email it, or throw it away).
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// Browser: a Blob from MediaRecorder; backend: a Blob from a Buffer.
|
|
67
|
+
const result = await sdk.compute.invokeTranscribe({
|
|
68
|
+
audio: blob, // Blob/File (Node 18+ has global Blob)
|
|
69
|
+
model: "transcribe:aws-fr-standard", // default; also aws-en-standard
|
|
70
|
+
languageCode: "fr-FR", // optional
|
|
71
|
+
// durationSecOverride: 127, // REQUIRED on backends (no DOM probe)
|
|
72
|
+
onProgress: (s) => console.log(s.phase), // uploading → starting → processing → completed
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log(result.text); // "Bonjour, je voulais te dire que…"
|
|
76
|
+
console.log(result.segments); // [{ start_sec, end_sec, text }]
|
|
77
|
+
console.log(result.creditsCharged);
|
|
78
|
+
|
|
79
|
+
// Then YOU choose where it goes — the compute has no opinion:
|
|
80
|
+
await myEthos.addRevision(result.text); // or PDS, DB, email, nothing…
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The core is isomorphic (Node + browser) and depends only on `Blob`, `fetch`
|
|
84
|
+
and timers. Browser-only resilience is opt-in and framework-agnostic:
|
|
85
|
+
`sdk.compute.transcribeDraft` (IndexedDB queue of recordings) and
|
|
86
|
+
`sdk.compute.listLocalPendingTranscribes()` /
|
|
87
|
+
`subscribeLocalPendingTranscribes()` / `resumeTranscribe(jobId)` recover jobs
|
|
88
|
+
across reloads. React users get `useAithosTranscribePendingJobs(sdk.compute)`
|
|
89
|
+
from `@aithos/sdk/react`. Advanced callers can drive the flow manually with
|
|
90
|
+
`prepareTranscribe` / `startTranscribe` / `getTranscribeStatus`.
|
|
91
|
+
|
|
58
92
|
## Delegating compute to an agent — opt-in token spending
|
|
59
93
|
|
|
60
94
|
To let an agent (or another user, or a third-party app) invoke Bedrock
|
|
@@ -100,15 +134,176 @@ network — they fail fast with a precise `AithosSDKError`:
|
|
|
100
134
|
set — useful for agents that only consume tokens (e.g. creative
|
|
101
135
|
assistants) without seeing any of your data.
|
|
102
136
|
|
|
137
|
+
## Custodial auth — onboarding users without a recovery file
|
|
138
|
+
|
|
139
|
+
Three new methods on `AithosAuth` let an app create and authenticate
|
|
140
|
+
its end-users via a server-managed custody flow — the user only needs
|
|
141
|
+
an email address and a password sent by mail. No recovery file, no
|
|
142
|
+
Google account, no client-side cryptography to handle.
|
|
143
|
+
|
|
144
|
+
The model is honest custody: Aithos KMS-wraps the user's Ed25519
|
|
145
|
+
identity seeds, and unwraps them on every sign-in after password
|
|
146
|
+
verification. Equivalent to how Coinbase or any hosted SaaS keeps your
|
|
147
|
+
private key. Annunciated to the user in the welcome email.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { AithosSDK } from "@aithos/sdk";
|
|
151
|
+
|
|
152
|
+
// ─── Server-side: sign-up ───────────────────────────────────────────
|
|
153
|
+
// MUST run on your backend. The API key is a server secret —
|
|
154
|
+
// provisioned by Aithos via the operator runbook.
|
|
155
|
+
const sdk = new AithosSDK({ identity });
|
|
156
|
+
const result = await sdk.auth.signUpCustodial({
|
|
157
|
+
apiKey: process.env.AITHOS_API_KEY!,
|
|
158
|
+
email: "alice@example.com",
|
|
159
|
+
displayName: "Alice",
|
|
160
|
+
});
|
|
161
|
+
// → { userId, did, handle, email, mailSent }
|
|
162
|
+
// The user receives an email with their password and a sign-in link.
|
|
163
|
+
|
|
164
|
+
// ─── Browser-side: sign-in ──────────────────────────────────────────
|
|
165
|
+
// User pastes the password from their mail into your sign-in form,
|
|
166
|
+
// then your frontend calls this. No API key needed — the password
|
|
167
|
+
// is the credential.
|
|
168
|
+
const { session, passwordMustChange } = await sdk.auth.signInCustodial({
|
|
169
|
+
email: "alice@example.com",
|
|
170
|
+
password: "MyTempPass32chars",
|
|
171
|
+
});
|
|
172
|
+
// Local KeyStore is now hydrated with the 5 Ed25519 sphere seeds
|
|
173
|
+
// (root, public, circle, self, #data) — the user can publish ethos
|
|
174
|
+
// editions, mint mandates, invoke compute, and own PDS data/asset
|
|
175
|
+
// collections (signed under the dedicated #data sphere), exactly as if
|
|
176
|
+
// they had signed in via a recovery file or Google SSO.
|
|
177
|
+
if (passwordMustChange) {
|
|
178
|
+
// Optional: nudge the user to set their own password via the
|
|
179
|
+
// standard reset flow.
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Browser-side: request password reset ───────────────────────────
|
|
183
|
+
// The backend always returns silently (anti-enumeration). If the email
|
|
184
|
+
// is registered AND in custodial mode AND not in cooldown AND under the
|
|
185
|
+
// daily cap, a magic-link email is sent to the address.
|
|
186
|
+
await sdk.auth.requestPasswordReset({ email: "alice@example.com" });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The reset finalization (collecting the new password from the user) is
|
|
190
|
+
done on a small web page hosted by Aithos at `https://app.aithos.be/reset`
|
|
191
|
+
(or your app's own `reset_base_url` if you've registered one — see the
|
|
192
|
+
operator runbook). The page POSTs to `/auth/custodial/reset/finalize`
|
|
193
|
+
and returns the user to your sign-in page on success.
|
|
194
|
+
|
|
195
|
+
### Getting an API key
|
|
196
|
+
|
|
197
|
+
API keys are provisioned out-of-band by Aithos. Contact the maintainer
|
|
198
|
+
(or use the self-service console at `aithos.be/console` when it ships
|
|
199
|
+
in V2). The pattern is `aithos_<env>_<32 chars b58>`. Keep it in your
|
|
200
|
+
backend's secrets manager — never in browser code.
|
|
201
|
+
|
|
202
|
+
### Trade-offs vs. the zk and Google SSO flows
|
|
203
|
+
|
|
204
|
+
| | zk (recovery file) | Google SSO (KMS) | **Custodial** |
|
|
205
|
+
|----------------|----------------------------|----------------------|---------------|
|
|
206
|
+
| User burden | downloads `recovery.json` | Google consent | email only |
|
|
207
|
+
| Password reset | requires recovery file | re-auth via Google | magic-link mail |
|
|
208
|
+
| Trust model | zero-knowledge (you only) | Aithos + Google | Aithos only |
|
|
209
|
+
| Multi-device | re-import recovery | re-Google | email + password |
|
|
210
|
+
| SDK signing capability | full | full | full |
|
|
211
|
+
|
|
212
|
+
Custodial is the right default for SDK-integrated apps that want
|
|
213
|
+
SaaS-grade UX. zk is the right default for power users who want
|
|
214
|
+
sovereign custody. SSO is the right default for users already invested
|
|
215
|
+
in the Google ecosystem.
|
|
216
|
+
|
|
217
|
+
## Extracting webpages without an LLM
|
|
218
|
+
|
|
219
|
+
`sdk.web` is a token-priced primitive that lets your agent read a
|
|
220
|
+
public webpage and get back cleaned HTML, purged CSS and a
|
|
221
|
+
deterministic visual signature — all computed server-side without an
|
|
222
|
+
LLM in the loop. Pricing is a flat **1 microcredit** per successful
|
|
223
|
+
extraction (refunded on failure), versus ~30 mc for a comparable
|
|
224
|
+
LLM-based extraction.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { AithosSDK } from "@aithos/sdk";
|
|
228
|
+
|
|
229
|
+
const sdk = new AithosSDK({ auth, appDid });
|
|
230
|
+
|
|
231
|
+
const { data, creditsCharged } = await sdk.web.extract({
|
|
232
|
+
url: "https://example.com",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
console.log(data.meta.title); // "Example Domain"
|
|
236
|
+
console.log(data.visual_signature.colors.primary); // "#0078d4"
|
|
237
|
+
console.log(data.styles.css.length); // purged + minified CSS
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Owners can mint a mandate for delegate-only extraction:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { WEB_EXTRACT_SCOPE } from "@aithos/sdk";
|
|
244
|
+
|
|
245
|
+
await sdk.mandates.create({
|
|
246
|
+
appDid: "did:aithos:app:my-agent",
|
|
247
|
+
scopes: [WEB_EXTRACT_SCOPE],
|
|
248
|
+
// ...
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Calling a third-party Aithos-aware backend
|
|
253
|
+
|
|
254
|
+
If your app talks to its own backend (a service you built that verifies
|
|
255
|
+
Aithos envelopes per spec §11.2 using
|
|
256
|
+
`@aithos/protocol-core/envelope`), use `sdk.auth.signEnvelope` to sign
|
|
257
|
+
the request with the same primitive that SDK namespaces use internally
|
|
258
|
+
for `api.aithos.be`. No JWT, no shadow session — the user's DID in the
|
|
259
|
+
envelope's `iss` field is the identity.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { AithosSDK, type SignedEnvelope } from "@aithos/sdk";
|
|
263
|
+
|
|
264
|
+
// Sign a request to your own backend with the active owner's
|
|
265
|
+
// public-sphere key. Default TTL is 60 s.
|
|
266
|
+
const envelope: SignedEnvelope = await sdk.auth.signEnvelope({
|
|
267
|
+
aud: "https://api.example.com/v1/widgets",
|
|
268
|
+
method: "myapp.widgets.create",
|
|
269
|
+
params: { name: "Widget #1" },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await fetch("https://api.example.com/v1/widgets", {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "content-type": "application/json" },
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
jsonrpc: "2.0",
|
|
277
|
+
id: crypto.randomUUID(),
|
|
278
|
+
method: "myapp.widgets.create",
|
|
279
|
+
params: { name: "Widget #1", _envelope: envelope },
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The envelope binds the signature to `(iss, aud, method, params_hash,
|
|
285
|
+
nonce, iat, exp)`, so a single envelope cannot be replayed against a
|
|
286
|
+
different endpoint, method, or payload. Throws
|
|
287
|
+
`AithosSDKError("auth_not_signed_in")` if no owner is loaded; throws
|
|
288
|
+
`AithosSDKError("auth_invalid_sphere")` if you pass a sphere outside
|
|
289
|
+
`"root" | "public" | "circle" | "self"` (default is `"public"`).
|
|
290
|
+
|
|
291
|
+
Server-side, your backend verifies the envelope with
|
|
292
|
+
`@aithos/protocol-core`'s `verifyEnvelope` (the 9-step check from spec
|
|
293
|
+
§11.4) — same algorithm that `api.aithos.be` uses, no re-implementation
|
|
294
|
+
needed.
|
|
295
|
+
|
|
103
296
|
## What lives where
|
|
104
297
|
|
|
105
|
-
| Namespace
|
|
106
|
-
|
|
|
107
|
-
| `sdk.
|
|
108
|
-
| `sdk.
|
|
109
|
-
| `sdk.
|
|
110
|
-
| `sdk.
|
|
111
|
-
| `sdk.
|
|
298
|
+
| Namespace | Purpose |
|
|
299
|
+
| -------------------------- | ------------------------------------------------------------------------------------------ |
|
|
300
|
+
| `sdk.auth` | Sign-in, sign-up, key custody — and `signEnvelope` for calls to your own Aithos-aware backend. |
|
|
301
|
+
| `sdk.compute` | Bedrock invocation through the Aithos compute proxy (signed envelope, wallet enforcement). |
|
|
302
|
+
| `sdk.web` | Webpage extraction without an LLM through the web extractor proxy (1 mc / call). |
|
|
303
|
+
| `sdk.wallet` | Stripe Checkout sessions for credit-pack top-ups, balance helpers. |
|
|
304
|
+
| `sdk.ethos` | Ethos-zone composition / parsing — re-exported from `@aithos/protocol-client`. |
|
|
305
|
+
| `sdk.onboarding` | First-run identity / DID flows — re-exported. |
|
|
306
|
+
| `sdk.mandates` | Mint / verify mandates — re-exported. |
|
|
112
307
|
|
|
113
308
|
## License
|
|
114
309
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DispatchOutcome } from "./agent-loop.js";
|
|
2
|
+
import type { EthosClient } from "./ethos.js";
|
|
3
|
+
/** Optional structured-data reader (gamma). Wire `sdk.data` here if available. */
|
|
4
|
+
export type DataProvider = (collection: string, limit: number) => Promise<readonly Record<string, unknown>[]>;
|
|
5
|
+
export interface AgentDispatchContext {
|
|
6
|
+
/** Ethos client for the conversation's subject (resolved via ethos.of(did)). */
|
|
7
|
+
readonly ethos: EthosClient;
|
|
8
|
+
/** Scopes carried by the active delegate mandate. Empty/owner → see `mode`. */
|
|
9
|
+
readonly delegateScopes: readonly string[];
|
|
10
|
+
/** Optional gamma reader for `data_query`. Absent → tool returns is_error. */
|
|
11
|
+
readonly dataProvider?: DataProvider;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execute one tool LOCALLY. Never throws on a tool-level problem — converts it
|
|
15
|
+
* into an `is_error` outcome the model can read and recover from.
|
|
16
|
+
*/
|
|
17
|
+
export declare function dispatchAgentToolLocal(ctx: AgentDispatchContext, name: string, input: Record<string, unknown>): Promise<DispatchOutcome>;
|
|
18
|
+
//# sourceMappingURL=agent-dispatch.d.ts.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
import { ZONE_NAMES } from "./ethos.js";
|
|
4
|
+
import { AithosSDKError } from "./types.js";
|
|
5
|
+
import { TOOL_ETHOS_LIST_SECTIONS, TOOL_ETHOS_READ_SECTION, TOOL_DATA_QUERY, TOOL_ETHOS_ADD_SECTION, TOOL_ETHOS_UPDATE_SECTION, TOOL_ETHOS_DELETE_SECTION, } from "./agent-tools.js";
|
|
6
|
+
function ok(payload) {
|
|
7
|
+
return { payload: typeof payload === "string" ? payload : JSON.stringify(payload), isError: false };
|
|
8
|
+
}
|
|
9
|
+
function err(message) {
|
|
10
|
+
return { payload: JSON.stringify({ error: message }), isError: true };
|
|
11
|
+
}
|
|
12
|
+
/** Whether the caller may WRITE the given zone. */
|
|
13
|
+
function canWriteZone(ctx, zone) {
|
|
14
|
+
if (ctx.ethos.mode === "owner")
|
|
15
|
+
return true;
|
|
16
|
+
if (ctx.ethos.mode === "anonymous")
|
|
17
|
+
return false;
|
|
18
|
+
return ctx.delegateScopes.includes(`ethos.write.${zone}`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read every zone the caller can see, returning the zone + its sections.
|
|
22
|
+
* Zones the caller cannot read (ungranted scope / undecryptable) are skipped
|
|
23
|
+
* silently — same bounding rule as `AithosSDK.buildWorkingSet`.
|
|
24
|
+
*/
|
|
25
|
+
async function readableZones(ctx) {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const zone of ZONE_NAMES) {
|
|
28
|
+
try {
|
|
29
|
+
const sections = await ctx.ethos.zone(zone).sections();
|
|
30
|
+
out.push({
|
|
31
|
+
zone,
|
|
32
|
+
sections: sections.map((s) => ({ id: s.id, title: s.title, body: s.body })),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (e instanceof AithosSDKError)
|
|
37
|
+
continue; // not granted / not decryptable
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Execute one tool LOCALLY. Never throws on a tool-level problem — converts it
|
|
45
|
+
* into an `is_error` outcome the model can read and recover from.
|
|
46
|
+
*/
|
|
47
|
+
export async function dispatchAgentToolLocal(ctx, name, input) {
|
|
48
|
+
try {
|
|
49
|
+
switch (name) {
|
|
50
|
+
/* ------------------------------- reads ------------------------------ */
|
|
51
|
+
case TOOL_ETHOS_LIST_SECTIONS: {
|
|
52
|
+
const zones = await readableZones(ctx);
|
|
53
|
+
const sections = zones.flatMap(({ zone, sections }) => sections.map((s) => ({ zone, id: s.id, title: s.title })));
|
|
54
|
+
return ok({ sections });
|
|
55
|
+
}
|
|
56
|
+
case TOOL_ETHOS_READ_SECTION: {
|
|
57
|
+
const sectionId = input.section_id;
|
|
58
|
+
if (typeof sectionId !== "string" || !sectionId) {
|
|
59
|
+
return err("section_id (string) is required");
|
|
60
|
+
}
|
|
61
|
+
const zones = await readableZones(ctx);
|
|
62
|
+
for (const { zone, sections } of zones) {
|
|
63
|
+
const found = sections.find((s) => s.id === sectionId);
|
|
64
|
+
if (found) {
|
|
65
|
+
return ok({ zone, title: found.title, body: found.body });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return err(`no readable section with id '${sectionId}'`);
|
|
69
|
+
}
|
|
70
|
+
case TOOL_DATA_QUERY: {
|
|
71
|
+
const collection = input.collection;
|
|
72
|
+
if (typeof collection !== "string" || !collection) {
|
|
73
|
+
return err("collection (string) is required");
|
|
74
|
+
}
|
|
75
|
+
if (!ctx.dataProvider) {
|
|
76
|
+
return err("structured data (gamma) is not available in this session");
|
|
77
|
+
}
|
|
78
|
+
const limit = typeof input.limit === "number" && Number.isInteger(input.limit)
|
|
79
|
+
? Math.max(1, Math.min(100, input.limit))
|
|
80
|
+
: 20;
|
|
81
|
+
const records = await ctx.dataProvider(collection, limit);
|
|
82
|
+
return ok({ collection, records });
|
|
83
|
+
}
|
|
84
|
+
/* ------------------------------ writes ------------------------------ */
|
|
85
|
+
case TOOL_ETHOS_ADD_SECTION: {
|
|
86
|
+
const zone = input.zone;
|
|
87
|
+
const title = input.title;
|
|
88
|
+
const body = input.body;
|
|
89
|
+
if (!isZone(zone))
|
|
90
|
+
return err("zone must be one of public|circle|self");
|
|
91
|
+
if (typeof title !== "string" || !title)
|
|
92
|
+
return err("title (string) is required");
|
|
93
|
+
if (typeof body !== "string")
|
|
94
|
+
return err("body (string) is required");
|
|
95
|
+
if (!canWriteZone(ctx, zone)) {
|
|
96
|
+
return err(`not authorized to write the '${zone}' zone (missing ethos.write.${zone})`);
|
|
97
|
+
}
|
|
98
|
+
ctx.ethos.zone(zone).addSection({ title, body });
|
|
99
|
+
const result = await ctx.ethos.publish();
|
|
100
|
+
return ok({ published: true, zone, editionHeight: result.editionHeight });
|
|
101
|
+
}
|
|
102
|
+
case TOOL_ETHOS_UPDATE_SECTION: {
|
|
103
|
+
const sectionId = input.section_id;
|
|
104
|
+
if (typeof sectionId !== "string" || !sectionId) {
|
|
105
|
+
return err("section_id (string) is required");
|
|
106
|
+
}
|
|
107
|
+
const patch = {};
|
|
108
|
+
if (input.title !== undefined) {
|
|
109
|
+
if (typeof input.title !== "string")
|
|
110
|
+
return err("title must be a string");
|
|
111
|
+
patch.title = input.title;
|
|
112
|
+
}
|
|
113
|
+
if (input.body !== undefined) {
|
|
114
|
+
if (typeof input.body !== "string")
|
|
115
|
+
return err("body must be a string");
|
|
116
|
+
patch.body = input.body;
|
|
117
|
+
}
|
|
118
|
+
if (patch.title === undefined && patch.body === undefined) {
|
|
119
|
+
return err("nothing to update: provide title and/or body");
|
|
120
|
+
}
|
|
121
|
+
const located = await locateSection(ctx, sectionId);
|
|
122
|
+
if (!located)
|
|
123
|
+
return err(`no readable section with id '${sectionId}'`);
|
|
124
|
+
if (!canWriteZone(ctx, located)) {
|
|
125
|
+
return err(`not authorized to write the '${located}' zone (missing ethos.write.${located})`);
|
|
126
|
+
}
|
|
127
|
+
ctx.ethos.zone(located).updateSection(sectionId, patch);
|
|
128
|
+
const result = await ctx.ethos.publish();
|
|
129
|
+
return ok({ published: true, zone: located, editionHeight: result.editionHeight });
|
|
130
|
+
}
|
|
131
|
+
case TOOL_ETHOS_DELETE_SECTION: {
|
|
132
|
+
const sectionId = input.section_id;
|
|
133
|
+
if (typeof sectionId !== "string" || !sectionId) {
|
|
134
|
+
return err("section_id (string) is required");
|
|
135
|
+
}
|
|
136
|
+
const located = await locateSection(ctx, sectionId);
|
|
137
|
+
if (!located)
|
|
138
|
+
return err(`no readable section with id '${sectionId}'`);
|
|
139
|
+
if (!canWriteZone(ctx, located)) {
|
|
140
|
+
return err(`not authorized to write the '${located}' zone (missing ethos.write.${located})`);
|
|
141
|
+
}
|
|
142
|
+
ctx.ethos.zone(located).deleteSection(sectionId);
|
|
143
|
+
const result = await ctx.ethos.publish();
|
|
144
|
+
return ok({ published: true, zone: located, editionHeight: result.editionHeight });
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
return err(`unknown tool '${name}'`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
// Any failure (scope refusal inside publish(), network, decrypt, …) →
|
|
152
|
+
// is_error so the model can recover; we never let it break the loop.
|
|
153
|
+
const message = e instanceof AithosSDKError
|
|
154
|
+
? `${e.code}: ${e.message}`
|
|
155
|
+
: e?.message ?? String(e);
|
|
156
|
+
return err(message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function isZone(v) {
|
|
160
|
+
return v === "public" || v === "circle" || v === "self";
|
|
161
|
+
}
|
|
162
|
+
/** Find which readable zone holds the section id, or null. */
|
|
163
|
+
async function locateSection(ctx, sectionId) {
|
|
164
|
+
for (const zone of ZONE_NAMES) {
|
|
165
|
+
try {
|
|
166
|
+
const sections = await ctx.ethos.zone(zone).sections();
|
|
167
|
+
if (sections.some((s) => s.id === sectionId))
|
|
168
|
+
return zone;
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
if (e instanceof AithosSDKError)
|
|
172
|
+
continue;
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=agent-dispatch.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface TextBlock {
|
|
2
|
+
readonly type: "text";
|
|
3
|
+
readonly text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolUseBlock {
|
|
6
|
+
readonly type: "tool_use";
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly input: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolResultBlock {
|
|
12
|
+
readonly type: "tool_result";
|
|
13
|
+
readonly tool_use_id: string;
|
|
14
|
+
/** Stringified result payload (JSON or plain text). */
|
|
15
|
+
readonly content: string;
|
|
16
|
+
/** Set when the tool failed — lets the model recover instead of us failing. */
|
|
17
|
+
readonly is_error?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | Record<string, unknown>;
|
|
20
|
+
/** A message in the running conversation. `content` is a string or blocks. */
|
|
21
|
+
export interface AgentMessage {
|
|
22
|
+
readonly role: "user" | "assistant";
|
|
23
|
+
readonly content: string | readonly ContentBlock[];
|
|
24
|
+
}
|
|
25
|
+
/** Anthropic tool definition (forwarded to the proxy in `tools`). */
|
|
26
|
+
export interface AgentToolSpec {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly description: string;
|
|
29
|
+
readonly input_schema: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export type AgentTurnStopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use";
|
|
32
|
+
/** One per-turn result from the proxy (`aithos.compute_invoke_turn`). */
|
|
33
|
+
export interface AgentTurnResult {
|
|
34
|
+
readonly content: readonly ContentBlock[];
|
|
35
|
+
readonly stopReason: AgentTurnStopReason;
|
|
36
|
+
readonly usage: {
|
|
37
|
+
readonly inputTokens: number;
|
|
38
|
+
readonly outputTokens: number;
|
|
39
|
+
};
|
|
40
|
+
/** Microcredits charged for THIS turn (cumulative billing — HANDOFF §1). */
|
|
41
|
+
readonly creditsCharged?: number;
|
|
42
|
+
readonly walletBalance?: number;
|
|
43
|
+
}
|
|
44
|
+
/** Extract the tool_use blocks from a turn's content (in order). */
|
|
45
|
+
export declare function extractToolUseBlocks(content: readonly ContentBlock[]): readonly ToolUseBlock[];
|
|
46
|
+
/** Concatenate the text blocks of a turn into a single string. */
|
|
47
|
+
export declare function extractText(content: readonly ContentBlock[]): string;
|
|
48
|
+
/** Build a single tool_result block. */
|
|
49
|
+
export declare function toolResult(toolUseId: string, payload: string, isError?: boolean): ToolResultBlock;
|
|
50
|
+
/** Build the user message carrying one or more tool_result blocks. */
|
|
51
|
+
export declare function buildToolResultMessage(results: readonly ToolResultBlock[]): AgentMessage;
|
|
52
|
+
export type LoopStopReason = "end_turn" | "max_tokens" | "stop_sequence" | "max_iterations";
|
|
53
|
+
export interface ToolCallTrace {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly ok: boolean;
|
|
56
|
+
readonly turn: number;
|
|
57
|
+
}
|
|
58
|
+
export interface AggregateUsage {
|
|
59
|
+
readonly inputTokens: number;
|
|
60
|
+
readonly outputTokens: number;
|
|
61
|
+
}
|
|
62
|
+
export interface DispatchOutcome {
|
|
63
|
+
readonly payload: string;
|
|
64
|
+
readonly isError: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface LocalAgenticLoopResult {
|
|
67
|
+
readonly finalContent: string;
|
|
68
|
+
readonly stopReason: LoopStopReason;
|
|
69
|
+
/** Number of proxy turns performed (each is a billed call). */
|
|
70
|
+
readonly iterations: number;
|
|
71
|
+
readonly usage: AggregateUsage;
|
|
72
|
+
readonly toolCalls: readonly ToolCallTrace[];
|
|
73
|
+
/** Sum of per-turn `creditsCharged` (HANDOFF §1: per-turn cumulative billing). */
|
|
74
|
+
readonly creditsCharged: number;
|
|
75
|
+
/** Wallet balance reported by the LAST turn (0 if none reported). */
|
|
76
|
+
readonly walletBalance: number;
|
|
77
|
+
}
|
|
78
|
+
export interface LocalAgenticLoopArgs {
|
|
79
|
+
/** Initial conversation (copied internally; not mutated by the caller). */
|
|
80
|
+
readonly messages: readonly AgentMessage[];
|
|
81
|
+
/** Hard cap on proxy turns. */
|
|
82
|
+
readonly maxIterations: number;
|
|
83
|
+
/** One signed proxy turn. Receives the running message list. */
|
|
84
|
+
readonly invokeTurn: (messages: readonly AgentMessage[]) => Promise<AgentTurnResult>;
|
|
85
|
+
/** Execute one tool call LOCALLY (read/write the user's ethos). Async. */
|
|
86
|
+
readonly dispatch: (name: string, input: Record<string, unknown>) => Promise<DispatchOutcome>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run the client-side agentic loop. Never throws on tool errors (they become
|
|
90
|
+
* `tool_result.is_error` so the model can recover); only `invokeTurn`
|
|
91
|
+
* rejections propagate to the caller (the proxy already refunded that turn).
|
|
92
|
+
*/
|
|
93
|
+
export declare function runAgenticLoopLocal(args: LocalAgenticLoopArgs): Promise<LocalAgenticLoopResult>;
|
|
94
|
+
//# sourceMappingURL=agent-loop.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* Pure helpers */
|
|
5
|
+
/* -------------------------------------------------------------------------- */
|
|
6
|
+
/** Extract the tool_use blocks from a turn's content (in order). */
|
|
7
|
+
export function extractToolUseBlocks(content) {
|
|
8
|
+
return content.filter((b) => typeof b === "object" &&
|
|
9
|
+
b !== null &&
|
|
10
|
+
b.type === "tool_use");
|
|
11
|
+
}
|
|
12
|
+
/** Concatenate the text blocks of a turn into a single string. */
|
|
13
|
+
export function extractText(content) {
|
|
14
|
+
return content
|
|
15
|
+
.filter((b) => typeof b === "object" &&
|
|
16
|
+
b !== null &&
|
|
17
|
+
b.type === "text" &&
|
|
18
|
+
typeof b.text === "string")
|
|
19
|
+
.map((b) => b.text)
|
|
20
|
+
.join("");
|
|
21
|
+
}
|
|
22
|
+
/** Build a single tool_result block. */
|
|
23
|
+
export function toolResult(toolUseId, payload, isError = false) {
|
|
24
|
+
return {
|
|
25
|
+
type: "tool_result",
|
|
26
|
+
tool_use_id: toolUseId,
|
|
27
|
+
content: payload,
|
|
28
|
+
...(isError ? { is_error: true } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Build the user message carrying one or more tool_result blocks. */
|
|
32
|
+
export function buildToolResultMessage(results) {
|
|
33
|
+
return { role: "user", content: results };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run the client-side agentic loop. Never throws on tool errors (they become
|
|
37
|
+
* `tool_result.is_error` so the model can recover); only `invokeTurn`
|
|
38
|
+
* rejections propagate to the caller (the proxy already refunded that turn).
|
|
39
|
+
*/
|
|
40
|
+
export async function runAgenticLoopLocal(args) {
|
|
41
|
+
const messages = [...args.messages];
|
|
42
|
+
const trace = [];
|
|
43
|
+
let inputTokens = 0;
|
|
44
|
+
let outputTokens = 0;
|
|
45
|
+
let creditsCharged = 0;
|
|
46
|
+
let walletBalance = 0;
|
|
47
|
+
let lastText = "";
|
|
48
|
+
const usage = () => ({ inputTokens, outputTokens });
|
|
49
|
+
for (let turn = 1; turn <= args.maxIterations; turn++) {
|
|
50
|
+
const r = await args.invokeTurn(messages);
|
|
51
|
+
inputTokens += r.usage.inputTokens;
|
|
52
|
+
outputTokens += r.usage.outputTokens;
|
|
53
|
+
if (typeof r.creditsCharged === "number")
|
|
54
|
+
creditsCharged += r.creditsCharged;
|
|
55
|
+
if (typeof r.walletBalance === "number")
|
|
56
|
+
walletBalance = r.walletBalance;
|
|
57
|
+
const text = extractText(r.content);
|
|
58
|
+
if (text)
|
|
59
|
+
lastText = text;
|
|
60
|
+
const toolUses = extractToolUseBlocks(r.content);
|
|
61
|
+
// Terminal: the model stopped without (valid) tool calls.
|
|
62
|
+
if (r.stopReason !== "tool_use" || toolUses.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
finalContent: text || lastText,
|
|
65
|
+
stopReason: r.stopReason === "tool_use" ? "end_turn" : r.stopReason,
|
|
66
|
+
iterations: turn,
|
|
67
|
+
usage: usage(),
|
|
68
|
+
toolCalls: trace,
|
|
69
|
+
creditsCharged,
|
|
70
|
+
walletBalance,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Append the assistant turn (carries the tool_use blocks), then dispatch
|
|
74
|
+
// each tool LOCALLY and feed the results back as a single user turn.
|
|
75
|
+
messages.push({ role: "assistant", content: r.content });
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const tu of toolUses) {
|
|
78
|
+
const out = await args.dispatch(tu.name, tu.input);
|
|
79
|
+
trace.push({ name: tu.name, ok: !out.isError, turn });
|
|
80
|
+
results.push(toolResult(tu.id, out.payload, out.isError));
|
|
81
|
+
}
|
|
82
|
+
messages.push(buildToolResultMessage(results));
|
|
83
|
+
}
|
|
84
|
+
// Cap reached while still mid-tool-loop: return the last text we saw.
|
|
85
|
+
return {
|
|
86
|
+
finalContent: lastText,
|
|
87
|
+
stopReason: "max_iterations",
|
|
88
|
+
iterations: args.maxIterations,
|
|
89
|
+
usage: usage(),
|
|
90
|
+
toolCalls: trace,
|
|
91
|
+
creditsCharged,
|
|
92
|
+
walletBalance,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=agent-loop.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentToolSpec } from "./agent-loop.js";
|
|
2
|
+
export declare const TOOL_ETHOS_LIST_SECTIONS = "ethos_list_sections";
|
|
3
|
+
export declare const TOOL_ETHOS_READ_SECTION = "ethos_read_section";
|
|
4
|
+
export declare const TOOL_DATA_QUERY = "data_query";
|
|
5
|
+
export declare const TOOL_ETHOS_ADD_SECTION = "ethos_add_section";
|
|
6
|
+
export declare const TOOL_ETHOS_UPDATE_SECTION = "ethos_update_section";
|
|
7
|
+
export declare const TOOL_ETHOS_DELETE_SECTION = "ethos_delete_section";
|
|
8
|
+
export declare const AITHOS_AGENT_READ_TOOLS: readonly AgentToolSpec[];
|
|
9
|
+
export declare const AITHOS_AGENT_WRITE_TOOLS: readonly AgentToolSpec[];
|
|
10
|
+
export declare const AITHOS_AGENT_TOOLS: readonly AgentToolSpec[];
|
|
11
|
+
export declare function isWriteTool(name: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the tool specs to forward to the proxy.
|
|
14
|
+
*
|
|
15
|
+
* - `undefined` / empty → the full catalogue (read + write).
|
|
16
|
+
* - a list of names → exactly those (unknown names ignored).
|
|
17
|
+
* - `{ readOnly: true }` → only the read family (a caller that wants a
|
|
18
|
+
* strictly non-mutating agent).
|
|
19
|
+
*/
|
|
20
|
+
export declare function selectAgentTools(opts?: {
|
|
21
|
+
readonly tools?: readonly string[];
|
|
22
|
+
readonly readOnly?: boolean;
|
|
23
|
+
}): readonly AgentToolSpec[];
|
|
24
|
+
//# sourceMappingURL=agent-tools.d.ts.map
|