@argentic/chest-sdk 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Chest SDK
2
2
 
3
3
  `@argentic/chest-sdk` is what a server tool (tool contract v2) embeds to talk
4
- with its Chest: the member the Chest asserts on a request, the other members
4
+ with its Chest: the member the Chest asserts on a request, the Chest itself
5
+ (its organization, time zone and language), the other members
5
6
  who have the tool, the address of the tool's own database, its private
6
7
  files, the badges and notifications it shows members inside the Chest, the
7
- events of its members' lifecycle — and, for the tool's tests, a fake Chest. The SDK has no dependency: it
8
+ events of its members' lifecycle, AI models through the Chest — and, for the
9
+ tool's tests, a fake Chest. The SDK has no dependency: it
8
10
  only imports `node:*`.
9
11
 
10
12
  ```sh
@@ -16,31 +18,35 @@ Node 22 or later. ESM only, compiled JavaScript with its type declarations.
16
18
  ## Imports
17
19
 
18
20
  Each module is its own subpath and pulls in nothing else; the root gives them
19
- all, with the files, members, notifications and events APIs as the
20
- namespaces `files`, `members`, `notifications` and `events` (the testing
21
- module is not in the root).
21
+ all, with the files, members, notifications, events and ai APIs as the
22
+ namespaces `files`, `members`, `notifications`, `events` and `ai` (the
23
+ testing module is not in the root).
22
24
 
23
25
  | Import | Gives |
24
26
  |---|---|
25
- | `@argentic/chest-sdk/member` | `member(request)`, type `Member`: the member of a request on the team host of a server tool, read from the `Chest-Member` assertion and verified; `null` without a valid assertion. `memberIdPattern`, `groupIdPattern`: the grammars of the identifiers (`mbr_…`, `grp_…`) |
27
+ | `@argentic/chest-sdk/member` | `member(request)`, type `Member`: the member of a request on the team host of a server tool, with the language the Chest speaks to them and the zone they work in, read from the `Chest-Member` assertion and verified; `null` without a valid assertion. `memberIdPattern`, `groupIdPattern`, `languagePattern`, `timeZonePattern`: the grammars of the identifiers (`mbr_…`, `grp_…`), of a language and of a zone |
28
+ | `@argentic/chest-sdk/chest` | `chest`, type `Chest`: the Chest the tool runs in — `chest.organization.name`, `chest.timeZone`, `chest.language`, `chest.today()` —, the same for every member, on a request or outside one |
26
29
  | `@argentic/chest-sdk/members` | `list`, `get`, `lookup`, `groups.list`, `forget`, types `MemberPage`, `Lookup`, `FormerMember`, `Group`: the members who have the tool (capability `members`, their addresses with `members.email`) |
27
30
  | `@argentic/chest-sdk/notifications` | `notify`, `withdraw`, `badge.set`, `badge.setMany`, types `Notice`, `Delivery`, `BadgeCount`, `BadgeWrite`: counters on the tool's tile and items in members' inboxes, inside the Chest (capability `notifications`) |
28
31
  | `@argentic/chest-sdk/events` | `handle`, `verify`, `acknowledgeErasure`, `memorySeen`, `erasureIdPattern`, types `ChestEvent`, `MemberUpdated`, `AccessRevoked`, `MemberRemoved`, `MemberErased`, `MemberChange`, `Handlers`, `Seen`: the events of the members' lifecycle the Chest posts to the tool's `/chest-events` (`"receives": ["member.*"]`), verified, deduplicated by id, and the acknowledgment of an erasure |
32
+ | `@argentic/chest-sdk/ai` | `chat`, `embed`, `models`, `usage`, types `Alias`, `Provider`, `ChatMessage`, `ChatTool`, `ToolChoice`, `ResponseFormat`, `ChatOptions`, `ChatResult`, `ChatChunk`, `ToolCall`, `ToolCallDelta`, `Usage`, `EmbedOptions`, `Embeddings`, `AiModel`, `AiUsage`: AI models through the Chest, on the owner's connectors, metered against the tool's monthly cap (capability `ai`) |
29
33
  | `@argentic/chest-sdk/database` | `databaseUrl()`: the address of the tool's own PostgreSQL database (capability `database`) |
30
34
  | `@argentic/chest-sdk/files` | `put`, `get`, `stat`, `list`, `move`, `delete`, `url`, `uploadUrl`, types `FileObject`, `FileData`, `FilePage`: the tool's private files (capability `files`), kept by the Chest, a 15-minute signed link to one (or to its thumbnail), and uploads straight from a member's browser |
31
- | `@argentic/chest-sdk/errors` | `ChestError` (`code`, `status`), `CapabilityNotGranted` (403), `TooLarge` (413), `QuotaExceeded` (429), `RateLimited` (429), `Unavailable` (503): what the SDK throws when the Chest does not give what a tool asks |
32
- | `@argentic/chest-sdk/testing` | `signAssertion`, `withMember`, `fakeChest`, types `FakeChest`, `FakeChestOptions`, `FakeGroup`, `FakeFile`, `FakeNotification`, `FakeEvent`: for the tool's own tests only |
33
- | `@argentic/chest-sdk` | all of the above but `testing`; `files`, `members`, `notifications` and `events` as namespaces |
35
+ | `@argentic/chest-sdk/errors` | `ChestError` (`code`, `status`), `CapabilityNotGranted` (403), `TooLarge` (413), `QuotaExceeded` (429), `RateLimited` (429), `Unavailable` (503), and for AI `AiCapReached` (402), `AiModelNotAllowed` (403), `AiRefused` (422), `AiUnavailable` (502, 503), type `AiUnavailableReason`: what the SDK throws when the Chest does not give what a tool asks |
36
+ | `@argentic/chest-sdk/testing` | `signAssertion`, `withMember`, `fakeChest`, types `AssertionOptions`, `FakeChest`, `FakeChestOptions`, `FakeGroup`, `FakeFile`, `FakeNotification`, `FakeEvent`, `FakeAi`, `FakeAiModel`, `FakeAiReply`, `FakeAiCall`: for the tool's own tests only |
37
+ | `@argentic/chest-sdk` | all of the above but `testing`; `files`, `members`, `notifications`, `events` and `ai` as namespaces |
34
38
 
35
39
  ```ts
36
40
  import { member } from "@argentic/chest-sdk/member";
41
+ import { chest } from "@argentic/chest-sdk/chest";
37
42
  import { databaseUrl } from "@argentic/chest-sdk/database";
38
43
  import * as files from "@argentic/chest-sdk/files";
39
44
  import * as members from "@argentic/chest-sdk/members";
40
45
  import * as notifications from "@argentic/chest-sdk/notifications";
41
46
  import * as events from "@argentic/chest-sdk/events";
47
+ import * as ai from "@argentic/chest-sdk/ai";
42
48
  import { CapabilityNotGranted } from "@argentic/chest-sdk/errors";
43
- // or: import { member, databaseUrl, files, members, notifications, events } from "@argentic/chest-sdk";
49
+ // or: import { member, chest, databaseUrl, files, members, notifications, events, ai } from "@argentic/chest-sdk";
44
50
  ```
45
51
 
46
52
  Types refer to `node:http` (`IncomingMessage`): a TypeScript project needs
@@ -50,7 +56,7 @@ Types refer to `node:http` (`IncomingMessage`): a TypeScript project needs
50
56
  ### Next.js
51
57
 
52
58
  The SDK runs on the server only — it reads the tool's environment
53
- (`CHEST_TOKEN`, `DATABASE_URL`, `CHEST_API`) and uses Node built-ins. Import it
59
+ (`CHEST_TOKEN`, `DATABASE_URL`, `CHEST_API`, `CHEST_TIME_ZONE`…) and uses Node built-ins. Import it
54
60
  in route handlers, server components or server actions, never in a
55
61
  `"use client"` module. Webpack and Turbopack resolve the
56
62
  compiled package with no configuration (no `transpilePackages`):
@@ -70,7 +76,7 @@ export function GET(request: Request) {
70
76
  A v2 tool is an ordinary web server in a container without network, run by
71
77
  its Chest. The Chest's front is the only one to reach it; the tool reaches only
72
78
  what its launcher gives it on `127.0.0.1` (its database, the Chest's API for
73
- its files, its members and its notifications), and the Chest posts it the
79
+ its files, its members, its notifications and AI), and the Chest posts it the
74
80
  events it receives on `/chest-events`, through the same launcher. Rights come
75
81
  from the Chest — the signed member, the capabilities approved for the
76
82
  version — and the Chest enforces them even outside the SDK:
@@ -83,7 +89,8 @@ is described in the Chest repository, `docs/architecture.md`.
83
89
  A v2 tool is an ordinary web server; on its team host, the Chest relays
84
90
  `/chest` and everything below it with the `Chest-Member` header of the
85
91
  signed-in member. `member(request)` accepts a Node request (`IncomingMessage`)
86
- or a Web `Request` and returns:
92
+ or a Web `Request` and returns a `Member`, the type the `members` API
93
+ answers too:
87
94
 
88
95
  ```ts
89
96
  type Member = {
@@ -96,6 +103,8 @@ type Member = {
96
103
  isAdmin: boolean; // owner or admin of the Chest
97
104
  isBuilder: boolean; // builder of this tool
98
105
  groups: string[]; // "grp_…": the groups that give the member this tool
106
+ language: string; // "en", "fr"…: the language the Chest speaks to this member
107
+ timeZone: string; // "America/New_York": the zone the member works in
99
108
  email?: string; // only with the capability "members.email"
100
109
  };
101
110
  ```
@@ -105,10 +114,13 @@ assertion there and strips a client's), or for any assertion that is not
105
114
  exactly its own. Checks: compact JWS, header exactly
106
115
  `{"alg":"HS256","typ":"JWT"}`, HMAC-SHA256 signature compared in constant time
107
116
  under the key HMAC-SHA256("Chest-Member v2") of the text of `CHEST_TOKEN` —
108
- the Chest's derivation; the label changes with the shape of the claims, so an
109
- assertion of another shape is refused rather than misread —, `aud` equal to
117
+ the Chest's derivation; the label changes when a claim changes meaning or
118
+ goes, so an assertion of another shape is refused rather than misread, and
119
+ stays when a claim is added —, `aud` equal to
110
120
  `CHEST_TOOL`, `iat` and `exp` within 5 s, the shape of each claim (`sub` an
111
- `mbr_` identifier, `groups` `grp_` identifiers; an unknown claim is ignored). Without
121
+ `mbr_` identifier, `groups` `grp_` identifiers, `language` a primary tag of
122
+ 2 or 3 lowercase letters, `time_zone` a zone of `timeZonePattern`; an
123
+ unknown claim is ignored). Without
112
124
  `CHEST_TOKEN` or `CHEST_TOOL`, nobody is a member. The function never throws
113
125
  for what a request carries.
114
126
 
@@ -127,6 +139,72 @@ member among those the manifest declares. Only the Chest's front reaches the
127
139
  container: the signature is a second defence; business rules (who writes
128
140
  what) remain the tool's.
129
141
 
142
+ `language` is the member's own language in the Chest, else the Chest's
143
+ default: a BCP 47 primary tag among those the product speaks (`en`, `fr`
144
+ today; the SDK accepts any, so a language added to the Chest needs no new
145
+ SDK). The tool's private part (`/chest`) speaks it — to this member, on every
146
+ request — and offers no language switch of its own; only its public parts,
147
+ where nobody is signed in, keep their own switch. The members API answers
148
+ it too: a notification or an email to another member is written in *their*
149
+ language (`members.get(id).language`), not in the sender's. A tool that does not
150
+ speak that language uses its own default. `timeZone` is the zone the member
151
+ works in: the one they chose in their profile, else the one their browser
152
+ is in, else the Chest's. The members API answers it too, so a tool reminds
153
+ each member at their own hour. What is the same for every member — the
154
+ organization, the company's time zone — is not the member's: it is the
155
+ Chest's (below).
156
+
157
+ ### Times: store in UTC, decide in the Chest's zone, show in the member's
158
+
159
+ | What | Zone |
160
+ |---|---|
161
+ | An instant (created, due at, sent at) | stored as UTC: `timestamptz` in PostgreSQL, `Date` in code |
162
+ | “Today”, “this week”, a deadline's day, business hours, working days | the company's: `chest.timeZone`, `chest.today()` (the database's `current_date` is the same) |
163
+ | A time or a date shown to a member, a personal reminder's hour | theirs: `member(request).timeZone`, or `members.get(id).timeZone` outside their request |
164
+
165
+ ```ts
166
+ const who = member(request)!;
167
+ const due = await sql`select * from tasks where due_on = ${chest.today()}`; // the company's day
168
+ const shown = new Intl.DateTimeFormat(who.language, { timeZone: who.timeZone, dateStyle: "medium", timeStyle: "short" }).format(task.remindAt);
169
+ ```
170
+
171
+ ## `chest` — the Chest the tool runs in
172
+
173
+ ```ts
174
+ import { chest } from "@argentic/chest-sdk/chest";
175
+
176
+ chest.organization.name; // "Acme SAS": the organization the Chest is of, as its owner wrote it
177
+ chest.timeZone; // "Europe/Paris": an IANA zone, "UTC" until the owner sets one
178
+ chest.language; // "fr": the Chest's own language (a member's is member(request).language)
179
+ chest.today(); // "2026-09-30": the date now in the Chest's zone (or chest.today(at))
180
+ ```
181
+
182
+ The Chest gives these to every tool in its environment at each start
183
+ (`CHEST_ORGANIZATION`, `CHEST_TIME_ZONE`, `CHEST_LANGUAGE`), and starts every
184
+ tool again when its owner changes one in Settings → General — the tool never
185
+ asks its own admin for the company's name or zone. They are there outside a
186
+ request too: a scheduled job, a start-up task, an export. No capability is
187
+ needed: nothing here is more than what the Chest's pages show its members.
188
+
189
+ - `organization.name` is plain text of 2 to 80 characters: show it in a
190
+ header, a document or an email, never as HTML.
191
+ - `timeZone` is the day of “due today” and the hour of a reminder. The Chest
192
+ also makes it the `TimeZone` of the tool's database sessions: there,
193
+ `current_date`, `now()::date` and a `timestamptz` shown as text are in the
194
+ Chest's zone. A session may set its own (`SET TIME ZONE`), for itself.
195
+ - `language` is the language of what the tool writes for no one in
196
+ particular: a public page before the visitor chooses, an export's default.
197
+ A page of `/chest` speaks `member(request).language` instead.
198
+ - `today(at?)` is `YYYY-MM-DD` in the Chest's zone, for now or for an instant
199
+ (`Date` or milliseconds): compare it with dates your database keeps as
200
+ `date`, never with `new Date().toISOString().slice(0, 10)`, which is UTC's.
201
+
202
+ Each value is read from the environment at each access, and checked: outside a
203
+ Chest (a development server without the variables), or for a value the Chest
204
+ never gives, reading it throws a `ChestError` with the code `not_in_chest` —
205
+ a wrong zone read silently is exactly what this module exists to prevent. In
206
+ tests, `fakeChest({chest: {organization, timeZone, language}})` sets them.
207
+
130
208
  ## `members` — who has the tool
131
209
 
132
210
  A v2 tool that declares `"capabilities": ["members"]` (approved like a
@@ -340,6 +418,115 @@ export async function POST(request: Request) {
340
418
  `ChestError` `erasure_not_found` (404: an erasure this tool was not told of)
341
419
  or `invalid_id` (400), `CapabilityNotGranted` (403), `Unavailable`.
342
420
 
421
+ ## `ai` — AI models through the Chest
422
+
423
+ A v2 tool that declares the `ai` capability calls AI models through its
424
+ Chest. The Chest's owner connects OpenRouter with the company's own key;
425
+ the tool calls models by four **aliases**: `default`, `fast`, `smart`,
426
+ `embedding`, each led by the Chest to a model it chose (the owner may choose
427
+ another for `default`). The tool names an alias, never a provider's model:
428
+ the model changes for the whole Chest without touching code. The tool never
429
+ holds a key; the Chest meters every call against the tool's monthly cap.
430
+
431
+ ```jsonc
432
+ // chest.json
433
+ {
434
+ "capabilities": ["ai"],
435
+ "ai": { "monthly": 20, "models": ["default", "embedding"], "purpose": "Summarises support tickets" }
436
+ }
437
+ ```
438
+
439
+ | Key | Default | |
440
+ |---|---|---|
441
+ | `monthly` | 5 | Whole euros a month, 1 to 1,000: what the tool asks; the owner's cap replaces it and may be changed at any time |
442
+ | `models` | `["default"]` | 1 to 4 of `default`, `fast`, `smart`, `embedding`: the only aliases the tool may call |
443
+ | `purpose` | required | 1 to 120 characters, shown at approval: “Uses AI models through the Chest, up to €20 a month” |
444
+
445
+ ```ts
446
+ import * as ai from "@argentic/chest-sdk/ai";
447
+
448
+ const r = await ai.chat({
449
+ model: "default",
450
+ messages: [{ role: "system", content: "Summarise in two sentences." }, { role: "user", content: ticket.text }],
451
+ maxTokens: 300, // 1 to 128,000; 4,096 when not said
452
+ member: who.id, // optional: attribution in the Chest's usage log
453
+ });
454
+ r.text; // "" when the model only called tools
455
+ r.usage; // { input, output, cached, cost } — cost in estimated euros
456
+
457
+ // Streamed: pieces as they come; breaking out of the loop ends the call.
458
+ for await (const chunk of ai.chat({ model: "fast", messages, stream: true, signal })) {
459
+ write(chunk.text); // chunk.toolCalls, chunk.finishReason, then chunk.usage last
460
+ }
461
+
462
+ // Tools: the model asks, the tool runs them and answers.
463
+ const step = await ai.chat({ model: "smart", messages, tools: [{ type: "function", function: { name: "lookup", parameters: schema } }] });
464
+ messages.push(step.message);
465
+ for (const call of step.toolCalls) messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(await run(call.name, JSON.parse(call.arguments))) });
466
+
467
+ const { embeddings } = await ai.embed({ model: "embedding", input: ["first text", "second text"] }); // 1 to 256 texts
468
+ const mapped = await ai.models(); // [{ alias, model, provider, input, output }] — USD per million tokens
469
+ const month = await ai.usage(); // { month: "2026-09", spent, cap, resetsAt }
470
+ ```
471
+
472
+ - **`chat(options)`** takes the OpenAI Chat Completions request in camelCase:
473
+ `model`, `messages`, `maxTokens`, `temperature`, `topP`, `stop`, `tools`,
474
+ `toolChoice`, `responseFormat`, `parallelToolCalls`, `seed`,
475
+ `reasoningEffort`, plus `member`, `stream` and `signal`. Messages, tools and
476
+ response formats keep the OpenAI shape (images in `content` parts too); the
477
+ Chest translates them for each provider and never runs a tool. Without
478
+ `stream` it returns `Promise<ChatResult>` `{text, message, toolCalls,
479
+ finishReason, model, usage}` — `message` is the assistant's message to add
480
+ to the conversation, `toolCalls` are `{id, name, arguments}` with the
481
+ arguments as JSON text, `model` is the provider's model. With
482
+ `stream: true` it returns an `AsyncIterable<ChatChunk>` `{text, toolCalls?,
483
+ finishReason?, usage?}`: tool calls come in pieces (`{index, id?, name?,
484
+ arguments?}`: join the `arguments` of the same `index`), the usage in the
485
+ last chunk.
486
+ - **Bounds**: a request body of 10 MiB (images included), 16 MiB of answer, a
487
+ call ends after 10 minutes (streamed or not); 60 requests a minute and 8
488
+ streams at once per tool (`RateLimited`). An aborted `signal` throws its
489
+ reason.
490
+ - **The cap never overshoots**: before a call the Chest reserves its worst
491
+ case (input and `maxTokens`) against the tool's cap and the Chest's; a call
492
+ that does not fit is refused before anything is spent. Keep `maxTokens` to
493
+ what the answer needs.
494
+ - **`embed({model, input, dimensions?, member?})`** gives one vector per text,
495
+ in the order given, and the input tokens and cost.
496
+ - **`models()`** gives the aliases the tool declared that the owner mapped,
497
+ with their model, provider and prices; **`usage()`** the tool's month:
498
+ estimated euros spent, the cap in force, and when the month resets.
499
+
500
+ **AI can stop at any time** — the month's budget spent, no connector, the
501
+ provider down. Keep the tool usable without it:
502
+
503
+ ```ts
504
+ import { AiCapReached, AiUnavailable } from "@argentic/chest-sdk/errors";
505
+
506
+ let summary: string | null = null;
507
+ try {
508
+ summary = (await ai.chat({ model: "default", messages, maxTokens: 300 })).text;
509
+ } catch (error) {
510
+ if (!(error instanceof AiCapReached || error instanceof AiUnavailable)) throw error;
511
+ // summary stays null: show "AI features are paused" and keep the page working
512
+ }
513
+ ```
514
+
515
+ | Error | Code, status | When |
516
+ |---|---|---|
517
+ | `AiCapReached` | `cap_reached` 402 | The tool's (`scope: "tool"`) or the Chest's (`scope: "chest"`) monthly cap is spent, until `resetsAt` |
518
+ | `AiUnavailable` | `no_connector` 503, `provider_key_invalid` 502, `provider_unavailable` 503 | No connector behind the alias, the provider refused the connector's key, or failed (`reason`) |
519
+ | `AiModelNotAllowed` | `model_not_allowed` 403 | An alias the tool did not declare in `models` (the SDK refuses any other name before sending) |
520
+ | `CapabilityNotGranted` | `capability_not_granted` 403 | The version does not hold `ai`, or it was not approved |
521
+ | `AiRefused` | `content_refused` 422 | The provider's moderation refused the content |
522
+ | `RateLimited` | `rate_limited` 429 | 60 requests a minute or 8 streams at once |
523
+ | `TooLarge` | `too_large` 413 | A body beyond 10 MiB, or a context beyond the model's |
524
+ | `ChestError` | `invalid_body`, `invalid_request` 400 | A malformed request (the SDK refuses most before sending), or parameters the provider rejected (its message in the error's) |
525
+ | `Unavailable` | `unavailable` 503 | The Chest not reached, or an answer that is not its own; in a stream, the stream cut |
526
+
527
+ An error in the middle of a stream is thrown where it comes, after the chunks
528
+ before it.
529
+
343
530
  ## `databaseUrl()` — database of a server tool
344
531
 
345
532
  A v2 tool that declares `"capabilities": ["database"]` in its `chest.json`
@@ -484,8 +671,8 @@ version keeps them.
484
671
  ```ts
485
672
  import { fakeChest, signAssertion, withMember } from "@argentic/chest-sdk/testing";
486
673
 
487
- const camille = { id: "mbr_k2qhx4mzc7v3b6nfp5r2t7w4ya", firstName: "Camille", lastName: "Martin", name: "Camille Martin", photo: null, role: "editor", isAdmin: false, isBuilder: false, groups: [] };
488
- const chest = await fakeChest({ members: [camille], capabilities: ["members", "files", "notifications"] });
674
+ const camille = { id: "mbr_k2qhx4mzc7v3b6nfp5r2t7w4ya", firstName: "Camille", lastName: "Martin", name: "Camille Martin", photo: null, role: "editor", isAdmin: false, isBuilder: false, groups: [], language: "fr", timeZone: "Europe/Paris" };
675
+ const chest = await fakeChest({ members: [camille], capabilities: ["members", "files", "notifications", "ai"], ai: { reply: () => "Summary." } });
489
676
  const response = await handler(withMember(new Request("http://tool.test/chest/tasks"), camille));
490
677
  assert.equal(await chest.emit({ type: "member.erased", data: { id: camille.id, erasure: "era_k2qhx4mzc7v3b6nfp5r2t7w4ya", deadline: "2026-10-28T10:00:00Z" } }, request => handler(request)), 204);
491
678
  assert.deepEqual(chest.acknowledged, ["era_k2qhx4mzc7v3b6nfp5r2t7w4ya"]);
@@ -493,15 +680,18 @@ assert.deepEqual((await members.list()).members.map(m => m.id), [camille.id]);
493
680
  assert.ok(chest.files.has("reports/2026.pdf"));
494
681
  assert.deepEqual(chest.notifications, [{ member: camille.id, title: "New task", path: "/chest/tasks/42", key: "task:42" }]);
495
682
  assert.equal(chest.badges.get(camille.id), 1);
683
+ assert.equal(chest.ai[0]?.path, "/ai/chat");
496
684
  await chest.close();
497
685
  ```
498
686
 
499
687
  | Function | Gives |
500
688
  |---|---|
501
- | `signAssertion(member, {token?, tool?, now?})` | A `Chest-Member` header value signed like the Chest's (the token and tool of the environment by default) |
502
- | `withMember(request, member, options?)` | The request carrying that assertion: a new Web `Request`, or the same Node request |
503
- | `fakeChest({members?, former?, groups?, capabilities?, receives?, files?})` | An HTTP server on `127.0.0.1` that sets `CHEST_API`, `CHEST_TOKEN`, `CHEST_TOOL` (`tool` unless set) and answers members, groups, files, badges, notifications and erasure acknowledgments with a Chest's bounds, quotas and errors; a capability left out answers 403 (`members`, `files` and `notifications` by default; `members.email` adds the addresses; `receives` is `["member.*"]` by default, `[]` refuses acknowledgments). A former member `{id, name?, erased?}` looks up as `former`, or `erased` |
689
+ | `signAssertion(member, {token?, tool?, now?})` | A `Chest-Member` header value signed like the Chest's for that `Member` (the token and tool of the environment by default), signed as given, so a language or a zone the Chest never sends makes `member()` refuse it |
690
+ | `withMember(request, member, options?)` | The request carrying that assertion (the options of `signAssertion`): a new Web `Request`, or the same Node request |
691
+ | `fakeChest({members?, former?, groups?, capabilities?, receives?, files?, ai?, chest?})` | An HTTP server on `127.0.0.1` that sets `CHEST_API`, `CHEST_TOKEN`, `CHEST_TOOL` (`tool` unless set), the Chest's `CHEST_ORGANIZATION`, `CHEST_TIME_ZONE`, `CHEST_LANGUAGE` (`chest: {organization, timeZone, language}`: `"Test organization"`, `"UTC"`, `"en"` by default) and answers members, groups, files, badges, notifications, AI and erasure acknowledgments with a Chest's bounds, quotas and errors; a capability left out answers 403 (`members`, `files`, `notifications` and `ai` by default; `members.email` adds the addresses; `receives` is `["member.*"]` by default, `[]` refuses acknowledgments). A former member `{id, name?, erased?}` looks up as `former`, or `erased` |
504
692
  | `chest.emit(event, to)` | Delivers an event (`{type, data, id?, occurredAt?}`: a new id and now by default; name an id to deliver the same event twice) signed as the Chest signs it, to `to` — the tool's address (`POST <to>/chest-events`) or a function of a Web `Request` — and says the status it answered. A `member.erased` makes its erasure one the tool may acknowledge |
693
+ | `ai: {models?, reply?, cap?, unavailable?}` | The fake Chest's AI, deterministic and without any provider. `models`: the aliases the tool declared, `{alias, model, provider?, input?, output?}` (all four by default, `fake-default`…`fake-embedding`, provider `openrouter`, 1 and 2 USD per million tokens); another alias answers `model_not_allowed`. `reply(request)`: what a chat answers, given the wire request — a string, or `{text?, toolCalls?: {name, arguments, id?}[]}` (by default the last user message, echoed); streamed, it comes word by word, each tool call's arguments in two pieces, then the finish reason and the usage. Embeddings are unit vectors from a hash of each text (8 dimensions unless `dimensions`). Tokens count one per 4 characters; once the spending reaches `cap` (euros, 5 by default; 0 refuses at once) a call answers `cap_reached`. `unavailable` (`no_connector`, `provider_key_invalid`, `provider_unavailable`) makes chat and embeddings answer it. 60 requests a minute |
694
+ | `chest.ai` | The tool's calls to AI, `{path, body}` in order (`body` null for a `GET`) |
505
695
  | `chest.acknowledged` | The erasures the tool acknowledged, each once |
506
696
  | `chest.members`, `chest.groups`, `chest.files` | What the fake Chest holds, to change or assert on; its `members` are those who have the tool |
507
697
  | `chest.notifications`, `chest.badges` | What the tool sent: the items kept, `{member, title, body?, path, key?}` cleaned as the Chest cleans them, in the order sent (a replaced item removed, the new one last; `withdraw` removes), and each member's badge (`Map` member → count; 0 removes it) |
@@ -535,8 +725,8 @@ npm run check:package # npm pack, install into a temp project, import every sub
535
725
 
536
726
  `client/src` holds the modules, `client/index.ts` the package root,
537
727
  `client/test` the tests. `npm run build` compiles `client/index.ts`, the
538
- seven published modules (`errors`, `member`, `members`, `database`, `files`,
539
- `notifications`, `testing`) and the one they share (`api`, the Chest's API) —
728
+ nine published modules (`errors`, `member`, `members`, `database`, `files`,
729
+ `notifications`, `events`, `ai`, `testing`) and the one they share (`api`, the Chest's API) —
540
730
  TypeScript strict, ES2022, NodeNext — into `dist/`: ESM `.js`, `.d.ts` and
541
731
  their maps. `member.ts` imports nothing but `node:*`, so that a tool may copy
542
732
  it alone.
package/client/index.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  // The package root: every published module a tool's code uses (tool contract
2
- // v2). Each one is also its own subpath (@argentic/chest-sdk/member,
3
- // /database, /files, /members, /notifications, /events, /errors), which
4
- // pulls in nothing else. The files, members, notifications and events APIs
5
- // are namespaces here, as their names (get, list, stat, move, notify,
6
- // verify…) are too plain to stand alone. @argentic/chest-sdk/testing is for a tool's tests only, and
2
+ // v2). Each one is also its own subpath (@argentic/chest-sdk/member, /chest,
3
+ // /database, /files, /members, /notifications, /events, /ai, /errors), which
4
+ // pulls in nothing else. The files, members, notifications, events and ai
5
+ // APIs are namespaces here, as their names (get, list, stat, move, notify,
6
+ // verify, chat…) are too plain to stand alone. @argentic/chest-sdk/testing is for a tool's tests only, and
7
7
  // is not here.
8
8
  export * from "./src/errors.js";
9
9
  export * from "./src/member.js";
10
+ export * from "./src/chest.js";
10
11
  export * from "./src/database.js";
11
12
  export * as files from "./src/files.js";
12
13
  export * as members from "./src/members.js";
13
14
  export * as notifications from "./src/notifications.js";
14
15
  export * as events from "./src/events.js";
16
+ export * as ai from "./src/ai.js";