@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.
- package/CHANGELOG.md +653 -0
- package/LICENSE +202 -0
- package/README.md +445 -0
- package/dist/client.d.ts +190 -0
- package/dist/client.js +225 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +87 -0
- package/dist/config.js.map +1 -0
- package/dist/conversation.d.ts +100 -0
- package/dist/conversation.js +189 -0
- package/dist/conversation.js.map +1 -0
- package/dist/errors.d.ts +102 -0
- package/dist/errors.js +197 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/openapi.d.ts +16654 -0
- package/dist/generated/openapi.js +6 -0
- package/dist/generated/openapi.js.map +1 -0
- package/dist/http.d.ts +37 -0
- package/dist/http.js +129 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.js +21 -0
- package/dist/node.js.map +1 -0
- package/dist/queue.d.ts +25 -0
- package/dist/queue.js +64 -0
- package/dist/queue.js.map +1 -0
- package/dist/resolve.d.ts +29 -0
- package/dist/resolve.js +89 -0
- package/dist/resolve.js.map +1 -0
- package/dist/resources.d.ts +126 -0
- package/dist/resources.js +206 -0
- package/dist/resources.js.map +1 -0
- package/dist/run.d.ts +81 -0
- package/dist/run.js +247 -0
- package/dist/run.js.map +1 -0
- package/dist/schemas.d.ts +90 -0
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -0
- package/dist/sse.d.ts +58 -0
- package/dist/sse.js +219 -0
- package/dist/sse.js.map +1 -0
- package/dist/team.d.ts +90 -0
- package/dist/team.js +183 -0
- package/dist/team.js.map +1 -0
- package/dist/turn.d.ts +46 -0
- package/dist/turn.js +205 -0
- package/dist/turn.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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.
|