@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.
Files changed (105) hide show
  1. package/README.md +202 -7
  2. package/dist/src/agent-dispatch.d.ts +18 -0
  3. package/dist/src/agent-dispatch.js +178 -0
  4. package/dist/src/agent-loop.d.ts +94 -0
  5. package/dist/src/agent-loop.js +95 -0
  6. package/dist/src/agent-tools.d.ts +24 -0
  7. package/dist/src/agent-tools.js +147 -0
  8. package/dist/src/apps.d.ts +224 -0
  9. package/dist/src/apps.js +432 -0
  10. package/dist/src/assets.d.ts +225 -0
  11. package/dist/src/assets.js +534 -0
  12. package/dist/src/auth-api.d.ts +219 -0
  13. package/dist/src/auth-api.js +248 -0
  14. package/dist/src/auth.d.ts +591 -0
  15. package/dist/src/auth.js +947 -31
  16. package/dist/src/compute.d.ts +674 -6
  17. package/dist/src/compute.js +968 -20
  18. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  19. package/dist/src/data-schema-contacts-v1.js +28 -0
  20. package/dist/src/data.d.ts +368 -0
  21. package/dist/src/data.js +1124 -0
  22. package/dist/src/endpoints.d.ts +43 -0
  23. package/dist/src/endpoints.js +23 -0
  24. package/dist/src/ethos.d.ts +85 -0
  25. package/dist/src/ethos.js +463 -7
  26. package/dist/src/index.d.ts +22 -4
  27. package/dist/src/index.js +47 -2
  28. package/dist/src/internal/cmk-wrap.d.ts +41 -0
  29. package/dist/src/internal/cmk-wrap.js +132 -0
  30. package/dist/src/internal/delegate-bundle.js +7 -2
  31. package/dist/src/internal/envelope.d.ts +93 -0
  32. package/dist/src/internal/envelope.js +59 -0
  33. package/dist/src/internal/owner-signers.d.ts +5 -2
  34. package/dist/src/internal/owner-signers.js +22 -1
  35. package/dist/src/internal/recovery-file.d.ts +2 -0
  36. package/dist/src/internal/recovery-file.js +7 -0
  37. package/dist/src/key-store.d.ts +10 -0
  38. package/dist/src/key-store.js +6 -0
  39. package/dist/src/mandates.d.ts +58 -1
  40. package/dist/src/mandates.js +46 -3
  41. package/dist/src/migrate.d.ts +105 -0
  42. package/dist/src/migrate.js +367 -0
  43. package/dist/src/react/AithosAsset.d.ts +66 -0
  44. package/dist/src/react/AithosAsset.js +67 -0
  45. package/dist/src/react/context.d.ts +29 -0
  46. package/dist/src/react/context.js +31 -0
  47. package/dist/src/react/index.d.ts +29 -0
  48. package/dist/src/react/index.js +31 -0
  49. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  50. package/dist/src/react/use-aithos-asset.js +118 -0
  51. package/dist/src/react/use-transcribe-pending.d.ts +21 -0
  52. package/dist/src/react/use-transcribe-pending.js +47 -0
  53. package/dist/src/rotate.d.ts +94 -0
  54. package/dist/src/rotate.js +298 -0
  55. package/dist/src/sdk.d.ts +36 -2
  56. package/dist/src/sdk.js +72 -1
  57. package/dist/src/transcribe-resilience.d.ts +57 -0
  58. package/dist/src/transcribe-resilience.js +203 -0
  59. package/dist/src/web.d.ts +279 -0
  60. package/dist/src/web.js +186 -0
  61. package/dist/test/agent-dispatch.test.d.ts +2 -0
  62. package/dist/test/agent-dispatch.test.js +222 -0
  63. package/dist/test/agent-loop.test.d.ts +2 -0
  64. package/dist/test/agent-loop.test.js +117 -0
  65. package/dist/test/agent-tools.test.d.ts +2 -0
  66. package/dist/test/agent-tools.test.js +50 -0
  67. package/dist/test/auth-j3.test.js +32 -1
  68. package/dist/test/canonical-conformance.test.d.ts +2 -0
  69. package/dist/test/canonical-conformance.test.js +86 -0
  70. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  71. package/dist/test/compute-delegate-path.test.js +183 -0
  72. package/dist/test/compute.test.js +4 -0
  73. package/dist/test/converse.test.d.ts +2 -0
  74. package/dist/test/converse.test.js +162 -0
  75. package/dist/test/data-sphere.test.d.ts +2 -0
  76. package/dist/test/data-sphere.test.js +57 -0
  77. package/dist/test/endpoints.test.js +40 -1
  78. package/dist/test/envelope-core-conformance.test.d.ts +2 -0
  79. package/dist/test/envelope-core-conformance.test.js +75 -0
  80. package/dist/test/envelope.test.d.ts +2 -0
  81. package/dist/test/envelope.test.js +318 -0
  82. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  83. package/dist/test/ethos-first-edition.test.js +371 -0
  84. package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
  85. package/dist/test/invoke-turn-sdk.test.js +177 -0
  86. package/dist/test/migrate.test.d.ts +2 -0
  87. package/dist/test/migrate.test.js +340 -0
  88. package/dist/test/owner-data-client.test.d.ts +2 -0
  89. package/dist/test/owner-data-client.test.js +88 -0
  90. package/dist/test/rotate-ethos.test.d.ts +2 -0
  91. package/dist/test/rotate-ethos.test.js +151 -0
  92. package/dist/test/rotate.test.d.ts +2 -0
  93. package/dist/test/rotate.test.js +63 -0
  94. package/dist/test/schema-autoresolve.test.d.ts +2 -0
  95. package/dist/test/schema-autoresolve.test.js +146 -0
  96. package/dist/test/sdk.test.js +11 -2
  97. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  98. package/dist/test/signup-bootstrap.test.js +311 -0
  99. package/dist/test/transcribe-invoke.test.d.ts +2 -0
  100. package/dist/test/transcribe-invoke.test.js +204 -0
  101. package/dist/test/transcribe.test.d.ts +2 -0
  102. package/dist/test/transcribe.test.js +186 -0
  103. package/dist/test/web.test.d.ts +2 -0
  104. package/dist/test/web.test.js +270 -0
  105. 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 | Purpose |
106
- | ---------------- | ------------------------------------------------------------------------------------------ |
107
- | `sdk.compute` | Bedrock invocation through the Aithos compute proxy (signed envelope, wallet enforcement). |
108
- | `sdk.wallet` | Stripe Checkout sessions for credit-pack top-ups, balance helpers. |
109
- | `sdk.ethos` | Ethos-zone composition / parsing re-exported from `@aithos/protocol-client`. |
110
- | `sdk.onboarding` | First-run identity / DID flows re-exported. |
111
- | `sdk.mandates` | Mint / verify mandates — re-exported. |
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