Ops
Three op activities ship with the lexicon, resolvable by name via loadActivities(["fountain"]).
fountainApply
Section titled “fountainApply”The native applier: compiles the serializer’s manifest YAML into fountain’s bulk POST /api/apply request and sends it in one call.
| Behavior | Detail |
|---|---|
| Create / update | By name, reconciled server-side. An agent’s environment reference resolves by name, against the manifest or the tenant’s existing environments, with no client-side id lookup. |
| Order | Environment → Vault → Agent, fixed server-side regardless of manifest order. |
| Prune | Off by default. With prune: true, chant-owned resources absent from the manifest are deleted, in reverse order. Bulk apply does not prune, so this step still lists live state per kind. |
| Secrets | spec.secrets converts from chant’s authored {key, value}[] to the wire’s {KEY: value} map; the server upserts them through the encrypted envelope path in the same request. Always upserted, since values are write-only upstream. |
| Failure | Best-effort per resource. Every result is collected before this throws, so the error covers every failed resource in the manifest. |
| === |
await fountainApply({ manifestPath: "build/fountain.yaml", prune: true });Endpoint and token resolution: explicit args win, then the named fountain.profiles entry (profile arg, falling back to defaultProfile), then FOUNTAIN_ENDPOINT / FOUNTAIN_TOKEN, then the hosted default. See Profiles.
fountainRun
Section titled “fountainRun”Conversations are runs, not resources, so they are started rather than declared. fountainRun resolves the agent by name, starts a conversation (optionally with a prompt and an allowlisted vault), and polls until the launch is done: the conversation settles on idle with its latest turn ended, or reaches completed, failed, timed_out or terminated. status in the result is the turn’s own outcome (completed, failed or interrupted), or the conversation’s status.
Every conversation gets a sandbox of its own: Managoat removed persistent sandboxes (managoat/managoat#2793), so an agent with sandbox_mode: "persistent" fails the build with FTN025. To keep a machine across turns, run the first with terminate: "never" and send the later ones with fountainPrompt.
An empty prompt counts as no prompt. The result also carries sandboxId and spriteName (fountain’s sandbox.sprite_name), so a caller can reach the machine without listing /api/sandboxes. spriteName in the args passes through as fountain’s sprite_name, the suffix of the machine’s name, so a box’s runtime can predict it.
| Arg | Effect |
|---|---|
timeoutMs | Deadline for the wait. Default 10 minutes. |
pollMs | Poll interval. Default 5 seconds. |
sandboxApiAccess | fountain’s sandbox_api_access. owner, fountain’s default, gives the sandbox a callback token (FOUNTAIN_TOKEN) scoped to the owner. none gives it none. Managoat accepts none only for a fresh sandbox, never on a channel_id resume (managoat/fountain#2497). |
terminate | What happens to the conversation’s machine when the wait ends: always (the default) terminates it whether the turn ended or the deadline fired, never leaves it, on-deadline terminates only a run whose turn never ended. Pass never on an ephemeral run to send it a later turn with fountainPrompt. |
Under chant run the executor calls these activities as fn(args, signal). When the signal fires (the step’s timeout, or Ctrl-C) fountainRun stops polling, applies the terminate policy as it does at its deadline, and the step fails.
phase("Start", [activity("fountainRun", { agent: "studio-box", spriteName: "studio-box" })]);fountainPrompt
Section titled “fountainPrompt”fountainPrompt adds a turn to a conversation that already exists, such as one fountainRun started, and waits for that turn to end (#3356). It sends fountain’s POST /api/conversations/{id}/prompts with a client_request_id, which puts the turn on the same runtime session, so the agent has the earlier turns in front of it. fountain wakes a conversation whose server has gone, on a fresh machine that resumes the session. The turn is found by that client_request_id on GET .../turns and polled until it is completed, failed or interrupted.
const run = await fountainRun({ agent: "builder", prompt: build, terminate: "never" });const debrief = await fountainPrompt({ conversation: run.conversationId, prompt: ask, timeoutMs: 120_000, terminate: "always" });| Arg | Effect |
|---|---|
conversation | The conversation’s id: fountainRun’s conversationId. |
prompt | The turn’s prompt. A blank one is refused before anything is sent. |
clientRequestId | fountain’s client_request_id, the name the turn is found by. Defaults to chant-<uuid>. fountain does not deduplicate on it, so keep it unique within the conversation. |
timeoutMs | The turn’s cap. Past it the turn is interrupted (POST .../interrupt), which ends the turn and keeps the conversation. Default 10 minutes. |
pollMs | Poll interval. Default 5 seconds. |
terminate | What happens to the conversation when the wait ends: never (the default) leaves it up for another turn, on-deadline terminates it only when the turn was interrupted at its cap, always terminates it either way. |
The result is { conversationId, clientRequestId, turnId, turnNumber, status, interruptedByDeadline, limitReason, terminated }. limitReason is fountain’s limit_reason, a service limit that ended the turn, which makes even a completed turn incomplete. When the signal fires, fountainPrompt interrupts the turn, applies terminate as at its cap, and the step fails.
What Managoat does not allow decides what this activity leaves out:
| Not possible | Why, and what to do instead |
|---|---|
| A turn on a terminated conversation | The prompts route answers 410 once a conversation is terminated, and fountainPrompt fails saying so. fountainRun terminates the conversation at its deadline by default, so run the earlier turn with terminate: "never" and end the conversation after the last turn, with terminate: "always" here. |
| A tool allowlist for one turn | The prompts route takes the prompt, images and client_request_id, nothing else. The tools a conversation may use are its agent’s permission_policy, narrowed once by the launch’s, and no call narrows them for one turn. Narrow the agent’s policy, or launch a conversation with the policy the turn needs. |
| A cap on the agent loop’s iterations | fountain has no per-turn iteration cap. The cap here is time, timeoutMs. |
Interrupting a running turn on its own, answering a permission request, and reading the transcript stay with fountain’s own conversations API.