@managoat/fountain-sdk 1.25.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +653 -0
  2. package/LICENSE +202 -0
  3. package/README.md +445 -0
  4. package/dist/client.d.ts +190 -0
  5. package/dist/client.js +225 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/config.d.ts +49 -0
  8. package/dist/config.js +87 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/conversation.d.ts +100 -0
  11. package/dist/conversation.js +189 -0
  12. package/dist/conversation.js.map +1 -0
  13. package/dist/errors.d.ts +102 -0
  14. package/dist/errors.js +197 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/generated/openapi.d.ts +16654 -0
  17. package/dist/generated/openapi.js +6 -0
  18. package/dist/generated/openapi.js.map +1 -0
  19. package/dist/http.d.ts +37 -0
  20. package/dist/http.js +129 -0
  21. package/dist/http.js.map +1 -0
  22. package/dist/index.d.ts +14 -0
  23. package/dist/index.js +13 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/node.d.ts +2 -0
  26. package/dist/node.js +21 -0
  27. package/dist/node.js.map +1 -0
  28. package/dist/queue.d.ts +25 -0
  29. package/dist/queue.js +64 -0
  30. package/dist/queue.js.map +1 -0
  31. package/dist/resolve.d.ts +29 -0
  32. package/dist/resolve.js +89 -0
  33. package/dist/resolve.js.map +1 -0
  34. package/dist/resources.d.ts +126 -0
  35. package/dist/resources.js +206 -0
  36. package/dist/resources.js.map +1 -0
  37. package/dist/run.d.ts +81 -0
  38. package/dist/run.js +247 -0
  39. package/dist/run.js.map +1 -0
  40. package/dist/schemas.d.ts +90 -0
  41. package/dist/schemas.js +2 -0
  42. package/dist/schemas.js.map +1 -0
  43. package/dist/sse.d.ts +58 -0
  44. package/dist/sse.js +219 -0
  45. package/dist/sse.js.map +1 -0
  46. package/dist/team.d.ts +90 -0
  47. package/dist/team.js +183 -0
  48. package/dist/team.js.map +1 -0
  49. package/dist/turn.d.ts +46 -0
  50. package/dist/turn.js +205 -0
  51. package/dist/turn.js.map +1 -0
  52. package/dist/types.d.ts +144 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Jake Gaylor
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,445 @@
1
+ # @managoat/fountain-sdk
2
+
3
+ Give an agent a computer, your repos and your credentials — in one call.
4
+
5
+ ```ts
6
+ import { Fountain } from "@managoat/fountain-sdk";
7
+
8
+ const fountain = new Fountain();
9
+
10
+ const run = await fountain.run("Upgrade us to Phoenix 1.8 and open a PR", {
11
+ agent: "reposage",
12
+ vault: "github-bot", // the token lands in the sandbox, never in the prompt
13
+ });
14
+
15
+ console.log(run.text); // what the agent said
16
+ console.log(run.url); // where a human can watch it happen
17
+ ```
18
+
19
+ That is the whole thing. The agent ran on a real machine with your repository
20
+ cloned and your GitHub token attached at spawn time, and the machine is still
21
+ there when you want to ask it something else.
22
+
23
+ ## Why not just call a model?
24
+
25
+ A model API takes a prompt and returns tokens. To make it do work you supply
26
+ the computer, the checkout, the tools and the secrets — and you keep supplying
27
+ them, on every call, because nothing persists between them.
28
+
29
+ Fountain's unit is not a message. It is **a sandbox with an agent in it**:
30
+
31
+ | | model API | `fountain.run()` |
32
+ |---|---|---|
33
+ | where it runs | your process | an isolated sandbox, provisioned per run |
34
+ | your repo | you paste it in | already cloned, from the **environment** |
35
+ | your secrets | in the prompt, or in your process | attached at spawn from a **vault**, never in the transcript |
36
+ | the next question | resend the whole context | `resume(id).send("...")` — same machine, same session |
37
+ | watching it | your logs | `run.url`, a live transcript |
38
+
39
+ `vault` is the argument to look at. Its values are decrypted into the sandbox's
40
+ environment when the sandbox spawns; they are never part of the prompt, never
41
+ in the model's context, and never in the log feed this SDK reads. Swapping
42
+ `vault: "github-bot"` for `vault: "github-readonly"` changes what the agent can
43
+ do without changing a word of the task.
44
+
45
+ There is a second layer under that, and it is worth knowing about because it
46
+ changes what you can safely let an agent do: Fountain redacts every value of 8
47
+ bytes or more that it placed in the sandbox's environment out of the
48
+ conversation's output, on the single write path every log event goes through.
49
+ An `env`, a `set -x`, a `cat .env`, or an agent simply asked to print its token
50
+ persists as `[REDACTED]`. The secret reaches the process that needs it and not
51
+ the transcript, the database, or this SDK.
52
+
53
+ ## What it replaces
54
+
55
+ Every integration that ever talked to Fountain wrote the same wrapper first —
56
+ open a conversation, send the prompt, follow the log feed, decide when the turn
57
+ is over, glue the text back together. This is that wrapper, once:
58
+
59
+ <details>
60
+ <summary>The same run, by hand</summary>
61
+
62
+ ```bash
63
+ # 1. find the agent, the vault, the environment (three listings, by name)
64
+ curl -sH "$AUTH" $BASE/api/agents | jq -r '.data[] | select(.name=="reposage") | .id'
65
+ curl -sH "$AUTH" $BASE/api/vaults | jq -r '.data[] | select(.name=="github-bot") | .id'
66
+
67
+ # 2. open the conversation
68
+ CONV=$(curl -sH "$AUTH" -H 'Content-Type: application/json' \
69
+ -d '{"agent_id":"...","vault_id":"...","prompt":"Upgrade us to Phoenix 1.8"}' \
70
+ $BASE/api/conversations | jq -r .data.id)
71
+
72
+ # 3. follow the feed — and now the real work starts:
73
+ # - page /events?blocks=true&after=N until has_more is false
74
+ # - keep only events whose turn_id is *your* turn's
75
+ # - keep only `text` blocks; `tool_use` is not the answer, `thinking` is not either
76
+ # - join acp chunks with nothing, stdout rows with a blank line,
77
+ # and start a new paragraph after any tool call
78
+ # - stop on stage/turn/done — or failed, or interrupted
79
+ # - and when the connection drops mid-turn, resume from the last event id
80
+ # or you will either miss output or replay it twice
81
+ curl -sH "$AUTH" "$BASE/api/conversations/$CONV/stream?blocks=true" | ...
82
+ ```
83
+
84
+ </details>
85
+
86
+ Those rules are not incidental complexity you could skip — get the paragraph
87
+ rule wrong and every transcript reads as one run-on sentence; get the cursor
88
+ wrong and a deploy mid-turn silently drops the answer. They are in here, with
89
+ tests.
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ npm install @managoat/fountain-sdk
95
+ ```
96
+
97
+ Node 20.19+ (native `fetch` and ESM). No runtime dependencies.
98
+
99
+ ## Credentials
100
+
101
+ `new Fountain()` resolves the same way the `fountain` CLI does, so a script
102
+ inherits whatever already works in your terminal:
103
+
104
+ ```
105
+ apiKey: option → FOUNTAIN_API_KEY → FOUNTAIN_TOKEN → ~/.fountain/credentials
106
+ baseUrl: option → FOUNTAIN_BASE_URL → ~/.fountain/credentials → hosted
107
+ ```
108
+
109
+ ```ts
110
+ new Fountain({ apiKey: process.env.FOUNTAIN_API_KEY, baseUrl: "https://fountain.internal" });
111
+ new Fountain({ profile: "work" }); // a profile from ~/.fountain/credentials
112
+ ```
113
+
114
+ `FOUNTAIN_TOKEN` is what a Fountain sandbox exports for the agent running
115
+ inside it. An agent that imports this SDK therefore delegates with the token it
116
+ already has, and the conversations it starts are recorded as its children — no
117
+ extra configuration to fan work out.
118
+
119
+ ## Waiting, or not
120
+
121
+ `run()` starts the work and hands back a handle. What you do with the handle
122
+ decides how much of the run you see; there is no second request behind any of
123
+ these.
124
+
125
+ ```ts
126
+ // await it — the finished answer
127
+ const result = await fountain.run(prompt, { agent: "reposage" });
128
+
129
+ // stream the words
130
+ const run = fountain.run(prompt, { agent: "reposage" });
131
+ for await (const chunk of run.textStream) process.stdout.write(chunk);
132
+ const result = await run; // same run, now finished
133
+
134
+ // or watch everything: tools, thinking, lifecycle
135
+ for await (const event of run) {
136
+ if (event.type === "tool") console.log("→", event.name);
137
+ if (event.type === "text") process.stdout.write(event.text);
138
+ }
139
+
140
+ // or don't wait at all — fan out, collect later
141
+ const runs = agents.map((agent) => fountain.run(prompt, { agent }));
142
+ const results = await Promise.all(runs);
143
+ ```
144
+
145
+ A `RunResult` is:
146
+
147
+ ```ts
148
+ {
149
+ conversationId: string; // keep it — the sandbox is still there
150
+ url: string; // where a human watches
151
+ turnNumber: number;
152
+ text: string; // the answer, tool noise removed
153
+ toolsUsed: string[];
154
+ state: "done" | "failed" | "interrupted" | "timeout";
155
+ exitCode: number | null;
156
+ reason: string | null; // stop_reason, or why it failed
157
+ status: string | null; // the conversation's status
158
+ }
159
+ ```
160
+
161
+ A turn that **fails** is a result, not an exception — the agent ran and has
162
+ something to say about it. Check `state`. Only a transport failure, a rejected
163
+ request or a timeout throws.
164
+
165
+ ## When the agent asks first
166
+
167
+ An agent whose `permission_policy` has an `ask` entry stops before the tool
168
+ call and waits to be told. Nothing else in the turn moves until it is answered,
169
+ and an unanswered request is denied when it expires — so an `ask` agent driven
170
+ by a caller that ignores these finishes having quietly skipped the work.
171
+
172
+ ```ts
173
+ for await (const event of run) {
174
+ if (event.type !== "permission") continue;
175
+
176
+ console.log(event.request.summary); // "Run rm -rf build"
177
+ const allow = event.request.options.find((o) => o.kind === "allow_once");
178
+ await run.answer(event.request.requestId, allow!.optionId);
179
+ }
180
+ ```
181
+
182
+ `options` is whatever the agent offered, in its order; `kind` is the part worth
183
+ branching on (`allow_once`, `allow_always`, `reject_once`, `reject_always`).
184
+ Sending an id the agent did not offer is a `ValidationError`, not a forwarded
185
+ answer. `resume(id).answer(...)` is the same call from a process that is not
186
+ the one following the turn.
187
+
188
+ The default policy is `auto_allow` and never asks, so this is opt-in per agent.
189
+ `opencode` never asks at all and refuses anything stricter than `auto_allow`.
190
+
191
+ ## Defining what you run
192
+
193
+ `run()` names an agent; this is where the agent comes from. The whole
194
+ vocabulary fits on one screen:
195
+
196
+ ```ts
197
+ const environment = await fountain.environments.create({
198
+ name: "fountain-ci",
199
+ packages: { apt: ["ripgrep"] },
200
+ env_vars: { MIX_ENV: "test" },
201
+ repositories: [
202
+ { url: "https://github.com/managoat/fountain", mount_path: "/work/fountain" },
203
+ ],
204
+ setup_script: "cd /work/fountain && mix deps.get",
205
+ networking_type: "limited",
206
+ networking_config: { allowed_hosts: ["github.com", "hex.pm", "api.anthropic.com"] },
207
+ });
208
+
209
+ const vault = await fountain.vaults.create({ name: "github-bot" });
210
+ await fountain.vaults.secrets.set("github-bot", "GITHUB_TOKEN", process.env.GITHUB_TOKEN!);
211
+
212
+ const agent = await fountain.agents.create({
213
+ name: "reposage",
214
+ runtime: "claude",
215
+ model: "anthropic/claude-sonnet-5",
216
+ system: "You are a careful reader of other people's code.",
217
+ environment_id: environment.id,
218
+ skills: [
219
+ { source: "obra/superpowers", ref: "v2.1.0" },
220
+ { name: "house-style", content: "# House style\n\nPrefer small diffs." },
221
+ ],
222
+ mcp_servers: { linear: { command: "npx", args: ["-y", "linear-mcp"] } },
223
+ allowed_vault_ids: [vault.id], // this agent may attach that vault, and no other
224
+ });
225
+
226
+ await fountain.run("Find every N+1 query and open a PR", {
227
+ agent: "reposage",
228
+ vault: "github-bot",
229
+ });
230
+ ```
231
+
232
+ `agents`, `environments` and `vaults` all have the same five verbs — `list`,
233
+ `get`, `create`, `update`, `delete` — and take a name or an id:
234
+
235
+ ```ts
236
+ await fountain.agents.update("reposage", { model: "anthropic/claude-opus-5" });
237
+ await fountain.environments.secrets.set("fountain-ci", "HEX_API_KEY", "…");
238
+ await fountain.vaults.secrets.list("github-bot"); // keys only — never values
239
+ await fountain.vaults.secrets.delete("github-bot", "GITHUB_USER");
240
+ ```
241
+
242
+ Secret values are write-only. `list` returns keys and nothing else: the SDK can
243
+ put a credential into a sandbox and cannot read it back out.
244
+
245
+ **Why `environment_id` and not `environmentId`.** Resource payloads use the
246
+ API's own key names, so one definition reads identically in the SDK, in the
247
+ REST API and in a `fountain.yml` manifest. Options that control the SDK's own
248
+ behaviour — `timeoutMs`, `signal` — are camelCase, because those are not data.
249
+
250
+ ## The team
251
+
252
+ Ten of the eleven applications built on Fountain talk to `/api/team`, and some
253
+ never touch `/api/conversations` at all — a teammate is durable (one agent, one
254
+ long-running sandbox, one thread you keep messaging) where a conversation is
255
+ something you open and close.
256
+
257
+ ```ts
258
+ await fountain.team.add("watchtower", { name: "Watchtower" });
259
+
260
+ const reply = await fountain.team.message("watchtower", "Any disks over 80%?");
261
+ console.log(reply.text); // `message()` returns the same Run handle as `run()`
262
+
263
+ for await (const event of fountain.team.stream({ streams: ["stage"] })) {
264
+ if (event.stage === "turn" && event.state === "done") refreshRoster();
265
+ }
266
+ ```
267
+
268
+ `list`, `get`, `rename`, `remove`, `history`, `freshConversation`, and
269
+ `team.schedules.*` for cron routines. The stream reconnects from its last event
270
+ id on its own, and carries server-parsed `blocks` like every other feed — the
271
+ runtime is picked per event from the conversation that produced it — so one
272
+ connection is enough to render a thread.
273
+
274
+ Opening a thread is two calls, and every app wrote both by hand first:
275
+
276
+ ```ts
277
+ const conversation = fountain.resume(id);
278
+ const events = await conversation.history({ streams: ["acp", "stage"] }); // paged to the end
279
+ await conversation.markRead();
280
+ ```
281
+
282
+ ## Errors
283
+
284
+ Branch on `code`, not status — `conversation_busy` is a 400,
285
+ `sandbox_quota_exceeded` a 429, `provisioning` a 503:
286
+
287
+ ```ts
288
+ if (error instanceof ConversationBusyError) … // the turn in flight must finish
289
+ if (error instanceof QuotaExceededError) … // error.activeSandboxes / error.limit
290
+ if (error instanceof NotReadyError) … // error.retryAfter, from the server
291
+ if (error instanceof ValidationError) … // error.fieldErrors
292
+ ```
293
+
294
+ Every error carries `status`, `code`, `body`, `retryAfter` and `retryable`.
295
+
296
+ | class | when |
297
+ |---|---|
298
+ | `AuthError` | 401, or no key configured at all |
299
+ | `SubscriptionRequiredError` | 402 — carries `upgradeUrl` |
300
+ | `NotFoundError` | 404 — wrong id, or it belongs to another account |
301
+ | `ValidationError` | 422 — read `fieldErrors` |
302
+ | `RateLimitError` | 429 |
303
+ | `ConversationBusyError` | the agent is still on the previous prompt |
304
+ | `QuotaExceededError` | at the concurrent-sandbox cap — `activeSandboxes` / `limit` |
305
+ | `NotReadyError` | the sandbox is still coming up — retry after `retryAfter` |
306
+ | `TimeoutError` | the SDK stopped waiting — carries `conversationId` and `partialText` |
307
+ | `ResolutionError` | a name matched no agent/vault/environment, or matched several |
308
+ | `ConnectionError` | the request never reached Fountain — in a browser, usually CORS |
309
+
310
+ A `ResolutionError` names what the account actually has, so a typo is a
311
+ one-line fix rather than a trip to the console.
312
+
313
+ ## In a browser
314
+
315
+ The default entry imports no Node built-in, so it bundles as-is; the
316
+ credentials-file reader lives behind the `node` export condition.
317
+
318
+ ```ts
319
+ const fountain = new Fountain({ baseUrl, apiKey }); // from your own settings UI
320
+ ```
321
+
322
+ Your origin has to be in the server's `API_CORS_ORIGINS`, or every call fails
323
+ before it starts — `ConnectionError` says so, because "Failed to fetch" has
324
+ sent more than one person hunting through their own code.
325
+
326
+ ## Generated underneath
327
+
328
+ `src/generated/openapi.ts` is produced from the same OpenAPI document the
329
+ server serves, and CI regenerates it and fails on a diff — so the types cannot
330
+ drift from the API. `import type { components, paths } from "@managoat/fountain-sdk"` for
331
+ the raw shapes. What is hand-written is what a spec cannot express: that many
332
+ log events fold into one turn, and which of 85 paths are worth a verb.
333
+
334
+ ## Follow-ups
335
+
336
+ ```ts
337
+ const first = await fountain.run("Find every N+1 query in this repo", { agent: "reposage" });
338
+
339
+ const second = await fountain.resume(first.conversationId).send("Fix the worst three.");
340
+ ```
341
+
342
+ The second turn costs one prompt. The sandbox is the same machine, the checkout
343
+ is where the first turn left it, and the agent's session still holds everything
344
+ it learned — this is the part a stateless API cannot do at any price.
345
+
346
+ ## Timeouts and cancellation
347
+
348
+ By default `run()` waits as long as the turn takes; agent work legitimately
349
+ runs for hours.
350
+
351
+ ```ts
352
+ try {
353
+ await fountain.run(prompt, { agent: "reposage", timeoutMs: 5 * 60_000 });
354
+ } catch (error) {
355
+ if (error instanceof TimeoutError) {
356
+ // The turn did not stop — only the waiting did.
357
+ console.log(error.partialText);
358
+ const rest = await fountain.resume(error.conversationId).send("status?");
359
+ }
360
+ }
361
+ ```
362
+
363
+ - `timeoutMs` — stop waiting, throw `TimeoutError`. The agent keeps working.
364
+ - `signal` — an `AbortSignal` that stops the waiting, same deal.
365
+ - `run.interrupt()` — ask the agent to stop the turn. The sandbox stays up.
366
+ - `run.terminate()` — tear the sandbox down. Nothing resumes after this.
367
+
368
+ ## The rest of the API
369
+
370
+ The verbs above are the ones worth wrapping. Everything else Fountain exposes —
371
+ 61 paths and counting: audit, schedules, the team, API keys, conversation trees
372
+ and images — is one call away, with the same auth and error mapping:
373
+
374
+ ```ts
375
+ await fountain.request("GET", "/api/audit", { query: { limit: 50 } });
376
+ await fountain.request("POST", "/api/vaults", { body: { name: "staging" } });
377
+ ```
378
+
379
+ `GET /api/openapi.json` is the generated, always-current spec for those.
380
+
381
+ ## Names, not ids
382
+
383
+ `agent`, `vault` and `environment` all take a name or an id. Names resolve
384
+ against the account (exact match first, then a unique prefix) and the listings
385
+ are memoized per client. A UUID in a script tells the next reader nothing;
386
+ `vault: "github-bot"` tells them everything.
387
+
388
+ ## Development
389
+
390
+ ```bash
391
+ npm install
392
+ npm test # node --test, against an in-process fake Fountain
393
+ npm run typecheck
394
+ npm run build
395
+ ```
396
+
397
+ The tests run a fake Fountain over real HTTP and real SSE, including the parts
398
+ that are easy to get wrong: a connection that dies mid-turn, output belonging
399
+ to another turn, and the two different ways runtimes chunk their text.
400
+
401
+ `npm test` runs the TypeScript sources directly, which needs Node 22.6+ even
402
+ though the published package only needs 20.19 — the tarball is compiled. CI
403
+ runs on Node 24.
404
+
405
+ A route added to `test/server.ts` has to answer in the same envelope the real
406
+ one does. Nearly everything is `{data: …}`; the nine that are not are listed at
407
+ the top of that file. A fake that wraps one of those turns a green suite into a
408
+ lie, which is how `me()` shipped returning `null`.
409
+
410
+ ### Releasing
411
+
412
+ **CI is the only publisher.** `npm publish` from a checkout refuses, and npm is
413
+ configured to accept releases only from this repository's `Publish SDK`
414
+ workflow — so a tarball on the registry always carries provenance tying it to
415
+ a commit and a workflow run.
416
+
417
+ There is no release command and no tag to push. **Merging a version bump is the
418
+ release.**
419
+
420
+ ```bash
421
+ cd sdk/typescript
422
+ npm version patch # or minor / major — edits package.json + the lockfile
423
+ ```
424
+
425
+ Then update two things `npm version` does not touch, and open a PR as usual:
426
+
427
+ - `USER_AGENT` in `src/http.ts`
428
+ - a `## [x.y.z]` section in `CHANGELOG.md`
429
+
430
+ The `SDK release gate` check on the PR fails if either is missing, if the
431
+ version is already on npm, or if you changed what the package ships without
432
+ bumping at all. When the PR merges, `Publish SDK` sees a version the registry
433
+ does not have, publishes it, and tags the merge commit `sdk-v<version>`.
434
+
435
+ A PR that touches only tests, examples or the changelog needs no bump; the
436
+ gate stays quiet, and the publish workflow finds nothing to do. To change the
437
+ published surface deliberately without releasing, label the PR
438
+ `sdk-no-release`.
439
+
440
+ ## License
441
+
442
+ [Apache-2.0](LICENSE). Fountain is not licensed as a single unit: the server is
443
+ AGPL-3.0-or-later, and the clients — this SDK and the CLI — are Apache-2.0 on
444
+ purpose. Talking to the API, or shipping this SDK inside a proprietary
445
+ application, puts no licence obligation on your code.