@intentius/chant-lexicon-fountain 0.102.0 → 0.104.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/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { observeResourcesDeepFountain } from "./deep-observe.js";
4
4
  export type { FountainDeepObserveOptions } from "./deep-observe.js";
5
5
  export { fountainDeepNormalizationHooks, FOUNTAIN_SERVER_FIELDS, FOUNTAIN_DEFAULTS, } from "./deep-observe-hooks.js";
6
6
  export * from "./generated/index.js";
7
- export { fountainApply, fountainRun, DEFAULT_FOUNTAIN_BASE_URL } from "./op/activities/index.js";
7
+ export { fountainApply, fountainRun, fountainPrompt, DEFAULT_FOUNTAIN_BASE_URL } from "./op/activities/index.js";
8
8
  export type { FountainApplyArgs, FountainApplySummary, FountainRunArgs, FountainRunResult } from "./op/activities/index.js";
9
9
  export { fountainConfigSchema, resolveProfile } from "./config.js";
10
10
  export type { FountainConfig, FountainProfile } from "./config.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIlD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EACL,8BAA8B,EAC9B,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAG9B,cAAc,mBAAmB,CAAC;AAIlC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACxF,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAInH,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhE,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,wBAAwB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAChG,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC7G,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACzG,OAAO,EACL,GAAG,EACH,qBAAqB,EACrB,+BAA+B,EAC/B,mCAAmC,EACnC,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAIxI,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIlD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EACL,8BAA8B,EAC9B,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAG9B,cAAc,mBAAmB,CAAC;AAIlC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACxG,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAInH,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhE,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,wBAAwB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAChG,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC7G,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACzG,OAAO,EACL,GAAG,EACH,qBAAqB,EACrB,+BAA+B,EAC/B,mCAAmC,EACnC,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAIxI,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "6a31ea7fecb3b65dfff50c618e0c913128caf2a9ffc6ae2c88e302faffc527bf",
4
+ "manifest.json": "76b78ea255b90a278d3669a41c53507ddc92b2489a7b2a2c36b2bca47ee41561",
5
5
  "meta.json": "3301437a2a56eaf1a5cc8f6eabb18a40767f916e09701b2d9bf20aba4066738c",
6
6
  "types/index.d.ts": "cf6b634fe0a8e9e96f1d9d07e3df53838def2683dc93dbe3c7fd971c6d3908ae",
7
7
  "rules/ftn001-no-secret-literals.ts": "a417b194cd7039911cca9a0f14c49b042dfa6b1ee48517f7c2e22b16590a8599",
@@ -21,7 +21,7 @@
21
21
  "skills/chant-fountain.md": "906a312756a02c88de634915f63ab4dfe1b1078ff4d8cfb4b6dc97eec14bc237",
22
22
  "skills/chant-fountain-secrets.md": "40aa847d62547581bebdde630b54796c4e1cbf498869392e0d396d96a75c0ab6",
23
23
  "skills/chant-fountain-ops.md": "4e72f0a0c7477a9e98f85ea45e49ba69e8a699f9c13ba0f0cf1cb20a88151373",
24
- "skills/chant-fountain-locked-sandboxes.md": "14def4c68b13c2bd561f75819b188968cec85073a5dd8b59e91708e1af26daae"
24
+ "skills/chant-fountain-locked-sandboxes.md": "48f4840a891e6b1cc7b5ac3319aebaec8c4e934f452370e55d60cb02a05812ae"
25
25
  },
26
- "composite": "6ddf244e344acbe1d2f46049ba25cd65b6a8e2c115a069ab0a9457cf3aeb5af9"
26
+ "composite": "6b479490227e45d1639ae76d30a4915eba2b4dcfd062e766f92b0c01428b47e7"
27
27
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fountain",
3
- "version": "0.102.0",
3
+ "version": "0.104.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Fountain",
6
6
  "specVersion": "v0.21.0"
@@ -0,0 +1,83 @@
1
+ /**
2
+ * fountainPrompt: add a turn to a conversation that already exists, and
3
+ * follow that turn to its end (#3356).
4
+ *
5
+ * `fountainRun` starts a conversation and waits out its first turn. This is
6
+ * the next turn on the same conversation: fountain's
7
+ * `POST /api/conversations/{id}/prompts`, which queues a turn on the same
8
+ * runtime session (fountain wakes the conversation, provisioning a fresh
9
+ * machine that resumes the session, when its server has gone). The turn is
10
+ * found by the `client_request_id` this sends with the prompt, and polled on
11
+ * `GET .../turns` until it is `completed`, `failed` or `interrupted`.
12
+ *
13
+ * What fountain v0.21.0 can and cannot do here, which decides the arguments:
14
+ *
15
+ * - A conversation that is `terminated` takes no more turns: the prompts route
16
+ * answers 410. `fountainRun` terminates an ephemeral agent's conversation at
17
+ * its deadline by default, so a caller that wants a later turn runs the first
18
+ * one with `terminate: "never"` (a persistent agent's default) and ends the
19
+ * conversation itself, or with this op's `terminate`, after the last turn.
20
+ * - A turn has no tool allowlist of its own. The prompts route takes only the
21
+ * prompt, images and `client_request_id`; the tools a conversation may use are
22
+ * its agent's `permission_policy`, narrowed once at launch, and nothing on the
23
+ * API narrows them for one turn. So this op takes no `tools`: the narrowing
24
+ * has to be on the agent, or on the launch.
25
+ * - A turn has no cap on its agent loop. The cap here is time: past `timeoutMs`
26
+ * the turn is interrupted (`POST .../interrupt`), which ends the turn and
27
+ * leaves the conversation up.
28
+ *
29
+ * Under `chant run` the executor calls this as `fountainPrompt(args, signal)`.
30
+ * When the signal fires, polling stops, the turn is interrupted, the
31
+ * `terminate` policy is applied as at the deadline, and the step fails with
32
+ * the abort's reason.
33
+ */
34
+ import { type FountainHttp, type FountainConnectionDeps } from "./fountain-apply.js";
35
+ import type { TerminatePolicy } from "./fountain-run.js";
36
+ export interface FountainPromptArgs {
37
+ /** The conversation's id, as `fountainRun` returns it in `conversationId`. */
38
+ conversation: string;
39
+ /** The turn's prompt. Must carry words. */
40
+ prompt: string;
41
+ /**
42
+ * fountain's `client_request_id`: the name the turn is found by. Defaults
43
+ * to `chant-<uuid>`. Make it unique within the conversation; fountain does
44
+ * not deduplicate on it.
45
+ */
46
+ clientRequestId?: string;
47
+ endpoint?: string;
48
+ token?: string;
49
+ /** Named `fountain.profiles` entry to resolve endpoint/token from, as for `fountainRun`. */
50
+ profile?: string;
51
+ /** Project root `chant.config.ts` is read from. Default: process.cwd(). */
52
+ cwd?: string;
53
+ /** The turn's cap: past this the turn is interrupted. Default 10 min. */
54
+ timeoutMs?: number;
55
+ /** Poll interval. Default 5s. */
56
+ pollMs?: number;
57
+ /**
58
+ * What happens to the conversation when the wait ends: `never` (the
59
+ * default) leaves it up for another turn, `on-deadline` terminates it only
60
+ * when the turn had to be interrupted, `always` terminates it either way.
61
+ */
62
+ terminate?: TerminatePolicy;
63
+ /** Injectable sleep for tests. */
64
+ sleep?: (ms: number) => Promise<void>;
65
+ }
66
+ export interface FountainPromptResult {
67
+ conversationId: string;
68
+ /** The `client_request_id` the turn was sent and found by. */
69
+ clientRequestId: string;
70
+ /** The turn's id and number, once fountain listed the turn. */
71
+ turnId?: string;
72
+ turnNumber?: number;
73
+ /** The turn's outcome: `completed`, `failed` or `interrupted`. */
74
+ status: string;
75
+ /** True when the turn ran past `timeoutMs` and was interrupted. */
76
+ interruptedByDeadline: boolean;
77
+ /** fountain's `limit_reason`: a service limit that ended the turn, which makes even a `completed` turn incomplete. */
78
+ limitReason?: string;
79
+ /** True when the conversation was terminated after the turn. */
80
+ terminated: boolean;
81
+ }
82
+ export declare function fountainPrompt(args: FountainPromptArgs, signal?: AbortSignal, http?: FountainHttp, deps?: FountainConnectionDeps): Promise<FountainPromptResult>;
83
+ //# sourceMappingURL=fountain-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fountain-prompt.d.ts","sourceRoot":"","sources":["../../../src/op/activities/fountain-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,kCAAkC;IAClC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sHAAsH;IACtH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,UAAU,EAAE,OAAO,CAAC;CACrB;AAuCD,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,EACxB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE,YAAY,EACnB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,oBAAoB,CAAC,CA2D/B"}
@@ -2,10 +2,13 @@
2
2
  * fountain Op activities — resolved by the core activity registry when a
3
3
  * project's `chant.config.ts` lists the `fountain` lexicon. Contributes
4
4
  * the native applier (`fountainApply` — direct REST against fountain's
5
- * API, no CLI, no state file) and the conversation runner (`fountainRun`).
5
+ * API, no CLI, no state file), the conversation runner (`fountainRun`) and
6
+ * the next turn on a conversation (`fountainPrompt`, #3356).
6
7
  */
7
8
  export { fountainApply, resolveEndpoint, resolveToken, resolveConnection, parseManifest, toApplyPayload, isChantOwned, defaultFountainHttp, DEFAULT_FOUNTAIN_BASE_URL, OWNERSHIP_KEY, OWNERSHIP_VALUE, } from "./fountain-apply.js";
8
9
  export type { FountainApplyArgs, FountainApplySummary, ManifestResource, FountainHttp, FountainConnectionDeps, } from "./fountain-apply.js";
9
10
  export { fountainRun, resolveAgent, resolveAgentId, TERMINAL_STATUSES, PERSISTENT_DONE_STATUSES, } from "./fountain-run.js";
10
11
  export type { FountainRunArgs, FountainRunResult, ResolvedAgent, TerminatePolicy } from "./fountain-run.js";
12
+ export { fountainPrompt } from "./fountain-prompt.js";
13
+ export type { FountainPromptArgs, FountainPromptResult } from "./fountain-prompt.js";
11
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,aAAa,EACb,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,aAAa,EACb,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEzG,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -31,4 +31,4 @@ The environment's config is the enforcement boundary, so watch it: `chant lifecy
31
31
 
32
32
  ## Running conversations
33
33
 
34
- Conversations are runs, not resources. Use the `fountainRun` activity: resolves the agent by name, starts (optionally with a prompt and an allowlisted vault), and waits for the turn to end. What "end" means, and what happens to the machine, follows the agent's `sandbox_mode`: an `ephemeral` agent's conversation is polled for a terminal status and terminated on deadline, same as before; a `persistent` agent's (a `Steward` or `Box`) conversation settles on `idle` once its turn ends — machine still up for the next turn — so `fountainRun` reads the turn back and returns its outcome (`completed | failed | interrupted`) without terminating anything, by default. Pass `terminate: "on-deadline" | "always"` to opt a persistent agent's run back into ending the machine. Multi-turn interaction (follow-up prompts, interrupt) is fountain's own conversations API — keep chant to the lifecycle edges.
34
+ Conversations are runs, not resources. Use the `fountainRun` activity: resolves the agent by name, starts (optionally with a prompt and an allowlisted vault), and waits for the turn to end. What "end" means, and what happens to the machine, follows the agent's `sandbox_mode`: an `ephemeral` agent's conversation is polled for a terminal status and terminated on deadline, same as before; a `persistent` agent's (a `Steward` or `Box`) conversation settles on `idle` once its turn ends — machine still up for the next turn — so `fountainRun` reads the turn back and returns its outcome (`completed | failed | interrupted`) without terminating anything, by default. Pass `terminate: "on-deadline" | "always"` to opt a persistent agent's run back into ending the machine. To add a turn to a conversation that already exists, use `fountainPrompt` with the conversation's id: it sends the prompt on the same session and waits for that turn, interrupting it at `timeoutMs`. It cannot reach a terminated conversation (fountain answers 410), so run the earlier turn with `terminate: "never"`, and it cannot narrow tools for one turn: that is the agent's `permission_policy`, set at launch. Interrupts and permission answers on their own are fountain's own conversations API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-fountain",
3
- "version": "0.102.0",
3
+ "version": "0.104.0",
4
4
  "type": "module",
5
5
  "description": "Fountain lexicon for chant — sandboxed agent environments, vaults, and agents as typed estate",
6
6
  "license": "Apache-2.0",
@@ -50,7 +50,7 @@
50
50
  "bundle": "tsx src/package-cli.ts"
51
51
  },
52
52
  "peerDependencies": {
53
- "@intentius/chant": "^0.102.0",
53
+ "@intentius/chant": "^0.104.0",
54
54
  "typescript": "^5.9.3",
55
55
  "zod": "^4.3.6"
56
56
  },
package/src/coverage.ts CHANGED
@@ -35,7 +35,7 @@ import { parseFountainOpenAPI, fountainShortName, MODELED_REQUEST_SCHEMAS } from
35
35
  export const EXCLUDED_KINDS: Record<string, string> = {
36
36
  // Runs, turns, and the envelope around them.
37
37
  ConversationCreateRequest: "conversations are runs, not declarables — started by the fountainRun op",
38
- PromptRequest: "turn-level input inside a conversation run",
38
+ PromptRequest: "turn-level input inside a conversation run — sent by the fountainPrompt op",
39
39
  PermissionAnswerRequest: "a human's answer to one tool card mid-run — an event on a conversation, not estate",
40
40
  TeamMessageRequest: "one turn addressed to a teammate — the run, not the seat",
41
41
  ConversationLabelsRequest:
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ export * from "./generated/index";
19
19
 
20
20
  // Op activities — the native applier and conversation runner. Also
21
21
  // resolvable by name via loadActivities(["fountain"]).
22
- export { fountainApply, fountainRun, DEFAULT_FOUNTAIN_BASE_URL } from "./op/activities";
22
+ export { fountainApply, fountainRun, fountainPrompt, DEFAULT_FOUNTAIN_BASE_URL } from "./op/activities";
23
23
  export type { FountainApplyArgs, FountainApplySummary, FountainRunArgs, FountainRunResult } from "./op/activities";
24
24
 
25
25
  // Config namespace (#2124) — `fountain.profiles` in chant.config.ts, and the
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fountainPrompt } from "./fountain-prompt";
3
+ import type { FountainHttp } from "./fountain-apply";
4
+
5
+ /** A conversation that takes one prompt and lists the turn it opens through `turns`, one reply per poll. */
6
+ function conversation(opts: { promptStatus?: number; promptJson?: unknown; turns: Array<Array<Record<string, unknown>>> }) {
7
+ const calls: string[] = [];
8
+ const bodies: unknown[] = [];
9
+ let poll = 0;
10
+ let sentId: string | undefined;
11
+ const http: FountainHttp = async (method, path, body) => {
12
+ calls.push(`${method} ${path}`);
13
+ if (method === "POST" && path === "/api/conversations/conv-1/prompts") {
14
+ bodies.push(body);
15
+ sentId = (body as { client_request_id?: string }).client_request_id;
16
+ return { status: opts.promptStatus ?? 200, json: opts.promptJson ?? { status: "queued", client_request_id: sentId } };
17
+ }
18
+ if (method === "GET" && path === "/api/conversations/conv-1/turns") {
19
+ const page = opts.turns[Math.min(poll, opts.turns.length - 1)];
20
+ poll += 1;
21
+ return { status: 200, json: { data: page.map((t) => ({ client_request_id: sentId, ...t })) } };
22
+ }
23
+ if (method === "POST" && (path === "/api/conversations/conv-1/interrupt" || path === "/api/conversations/conv-1/terminate")) {
24
+ return { status: 200, json: null };
25
+ }
26
+ throw new Error(`unrouted: ${method} ${path}`);
27
+ };
28
+ return { http, calls, bodies };
29
+ }
30
+
31
+ const quick = { pollMs: 1, sleep: async () => {} };
32
+ /** The build's own turn, which carries no client_request_id of ours. */
33
+ const build = { id: "t-1", turn_number: 1, status: "completed", client_request_id: null };
34
+
35
+ describe("fountainPrompt (#3356)", () => {
36
+ it("sends the prompt to the conversation, finds its turn by client_request_id, and returns the turn's outcome", async () => {
37
+ const { http, calls, bodies } = conversation({
38
+ turns: [[build], [build, { id: "t-2", turn_number: 2, status: "running" }], [build, { id: "t-2", turn_number: 2, status: "completed" }]],
39
+ });
40
+ const result = await fountainPrompt({ conversation: "conv-1", prompt: "debrief", clientRequestId: "debrief-W-012", ...quick }, undefined, http);
41
+ expect(bodies).toEqual([{ prompt: "debrief", client_request_id: "debrief-W-012" }]);
42
+ expect(result).toEqual({
43
+ conversationId: "conv-1",
44
+ clientRequestId: "debrief-W-012",
45
+ turnId: "t-2",
46
+ turnNumber: 2,
47
+ status: "completed",
48
+ interruptedByDeadline: false,
49
+ terminated: false,
50
+ });
51
+ expect(calls.filter((c) => c.endsWith("/terminate") || c.endsWith("/interrupt"))).toEqual([]);
52
+ });
53
+
54
+ it("names its own client_request_id when given none, and reports a service limit that ended the turn", async () => {
55
+ const { http, bodies } = conversation({ turns: [[{ id: "t-2", turn_number: 2, status: "completed", limit_reason: "turn_deadline" }]] });
56
+ const result = await fountainPrompt({ conversation: "conv-1", prompt: "debrief", ...quick }, undefined, http);
57
+ const sent = (bodies[0] as { client_request_id: string }).client_request_id;
58
+ expect(sent).toMatch(/^chant-[0-9a-f-]{36}$/);
59
+ expect(result).toMatchObject({ clientRequestId: sent, status: "completed", limitReason: "turn_deadline" });
60
+ });
61
+
62
+ it("caps the turn by time: past timeoutMs it interrupts the turn and leaves the conversation up", async () => {
63
+ const running = [{ id: "t-2", turn_number: 2, status: "running" }];
64
+ // Polled at 0, 4 and 8 ms, running each time; read once more after the interrupt.
65
+ const { http, calls } = conversation({ turns: [running, running, running, [{ id: "t-2", turn_number: 2, status: "interrupted" }]] });
66
+ let now = 0;
67
+ const realNow = Date.now;
68
+ Date.now = () => now;
69
+ try {
70
+ const result = await fountainPrompt({ conversation: "conv-1", prompt: "debrief", timeoutMs: 10, pollMs: 4, sleep: async (ms) => void (now += ms) }, undefined, http);
71
+ expect(result).toMatchObject({ status: "interrupted", interruptedByDeadline: true, terminated: false, turnId: "t-2" });
72
+ } finally {
73
+ Date.now = realNow;
74
+ }
75
+ expect(calls).toContain("POST /api/conversations/conv-1/interrupt");
76
+ expect(calls).not.toContain("POST /api/conversations/conv-1/terminate");
77
+ });
78
+
79
+ it("terminates the conversation after the turn when asked, and only on the deadline with on-deadline", async () => {
80
+ const always = conversation({ turns: [[{ id: "t-2", turn_number: 2, status: "completed" }]] });
81
+ expect(await fountainPrompt({ conversation: "conv-1", prompt: "debrief", terminate: "always", ...quick }, undefined, always.http)).toMatchObject({ terminated: true });
82
+ expect(always.calls.at(-1)).toBe("POST /api/conversations/conv-1/terminate");
83
+ const onDeadline = conversation({ turns: [[{ id: "t-2", turn_number: 2, status: "failed" }]] });
84
+ expect(await fountainPrompt({ conversation: "conv-1", prompt: "debrief", terminate: "on-deadline", ...quick }, undefined, onDeadline.http)).toMatchObject({ status: "failed", terminated: false });
85
+ });
86
+
87
+ it("says precisely why fountain refused the turn: a terminated conversation, a busy one, an unknown one", async () => {
88
+ const terminated = conversation({ promptStatus: 410, promptJson: { error: "conversation_terminal" }, turns: [[]] });
89
+ await expect(fountainPrompt({ conversation: "conv-1", prompt: "debrief", ...quick }, undefined, terminated.http)).rejects.toThrow(
90
+ /conv-1 is terminated, and fountain adds no turn to a terminated conversation/,
91
+ );
92
+ const busy = conversation({ promptStatus: 400, turns: [[]] });
93
+ await expect(fountainPrompt({ conversation: "conv-1", prompt: "debrief", ...quick }, undefined, busy.http)).rejects.toThrow(/busy: a turn is running/);
94
+ const missing = conversation({ promptStatus: 404, turns: [[]] });
95
+ await expect(fountainPrompt({ conversation: "conv-1", prompt: "debrief", ...quick }, undefined, missing.http)).rejects.toThrow(/no conversation conv-1/);
96
+ });
97
+
98
+ it("refuses a missing conversation id or an empty prompt before calling fountain", async () => {
99
+ const http: FountainHttp = async () => {
100
+ throw new Error("should not be called");
101
+ };
102
+ await expect(fountainPrompt({ conversation: "", prompt: "hi" }, undefined, http)).rejects.toThrow(/conversation's id/);
103
+ await expect(fountainPrompt({ conversation: "conv-1", prompt: " " }, undefined, http)).rejects.toThrow(/prompt with words/);
104
+ });
105
+
106
+ it("on abort it interrupts the turn, applies the terminate policy, and fails with the abort's reason", async () => {
107
+ const { http, calls } = conversation({ turns: [[{ id: "t-2", turn_number: 2, status: "running" }]] });
108
+ const abort = new AbortController();
109
+ const run = fountainPrompt(
110
+ { conversation: "conv-1", prompt: "debrief", terminate: "on-deadline", pollMs: 1, sleep: async () => abort.abort(new Error("lease-lost")) },
111
+ abort.signal,
112
+ http,
113
+ );
114
+ await expect(run).rejects.toThrow(/lease-lost/);
115
+ expect(calls.slice(-2)).toEqual(["POST /api/conversations/conv-1/interrupt", "POST /api/conversations/conv-1/terminate"]);
116
+ });
117
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * fountainPrompt: add a turn to a conversation that already exists, and
3
+ * follow that turn to its end (#3356).
4
+ *
5
+ * `fountainRun` starts a conversation and waits out its first turn. This is
6
+ * the next turn on the same conversation: fountain's
7
+ * `POST /api/conversations/{id}/prompts`, which queues a turn on the same
8
+ * runtime session (fountain wakes the conversation, provisioning a fresh
9
+ * machine that resumes the session, when its server has gone). The turn is
10
+ * found by the `client_request_id` this sends with the prompt, and polled on
11
+ * `GET .../turns` until it is `completed`, `failed` or `interrupted`.
12
+ *
13
+ * What fountain v0.21.0 can and cannot do here, which decides the arguments:
14
+ *
15
+ * - A conversation that is `terminated` takes no more turns: the prompts route
16
+ * answers 410. `fountainRun` terminates an ephemeral agent's conversation at
17
+ * its deadline by default, so a caller that wants a later turn runs the first
18
+ * one with `terminate: "never"` (a persistent agent's default) and ends the
19
+ * conversation itself, or with this op's `terminate`, after the last turn.
20
+ * - A turn has no tool allowlist of its own. The prompts route takes only the
21
+ * prompt, images and `client_request_id`; the tools a conversation may use are
22
+ * its agent's `permission_policy`, narrowed once at launch, and nothing on the
23
+ * API narrows them for one turn. So this op takes no `tools`: the narrowing
24
+ * has to be on the agent, or on the launch.
25
+ * - A turn has no cap on its agent loop. The cap here is time: past `timeoutMs`
26
+ * the turn is interrupted (`POST .../interrupt`), which ends the turn and
27
+ * leaves the conversation up.
28
+ *
29
+ * Under `chant run` the executor calls this as `fountainPrompt(args, signal)`.
30
+ * When the signal fires, polling stops, the turn is interrupted, the
31
+ * `terminate` policy is applied as at the deadline, and the step fails with
32
+ * the abort's reason.
33
+ */
34
+
35
+ import { randomUUID } from "node:crypto";
36
+ import {
37
+ abortable,
38
+ resolveConnection,
39
+ defaultFountainHttp,
40
+ type FountainHttp,
41
+ type FountainConnectionDeps,
42
+ } from "./fountain-apply";
43
+ import type { TerminatePolicy } from "./fountain-run";
44
+
45
+ /** Turn statuses that mean the turn has ended: fountain's `end_turn`, an error, or an interrupt. */
46
+ const ENDED_TURN_STATUSES = new Set(["completed", "failed", "interrupted"]);
47
+
48
+ export interface FountainPromptArgs {
49
+ /** The conversation's id, as `fountainRun` returns it in `conversationId`. */
50
+ conversation: string;
51
+ /** The turn's prompt. Must carry words. */
52
+ prompt: string;
53
+ /**
54
+ * fountain's `client_request_id`: the name the turn is found by. Defaults
55
+ * to `chant-<uuid>`. Make it unique within the conversation; fountain does
56
+ * not deduplicate on it.
57
+ */
58
+ clientRequestId?: string;
59
+ endpoint?: string;
60
+ token?: string;
61
+ /** Named `fountain.profiles` entry to resolve endpoint/token from, as for `fountainRun`. */
62
+ profile?: string;
63
+ /** Project root `chant.config.ts` is read from. Default: process.cwd(). */
64
+ cwd?: string;
65
+ /** The turn's cap: past this the turn is interrupted. Default 10 min. */
66
+ timeoutMs?: number;
67
+ /** Poll interval. Default 5s. */
68
+ pollMs?: number;
69
+ /**
70
+ * What happens to the conversation when the wait ends: `never` (the
71
+ * default) leaves it up for another turn, `on-deadline` terminates it only
72
+ * when the turn had to be interrupted, `always` terminates it either way.
73
+ */
74
+ terminate?: TerminatePolicy;
75
+ /** Injectable sleep for tests. */
76
+ sleep?: (ms: number) => Promise<void>;
77
+ }
78
+
79
+ export interface FountainPromptResult {
80
+ conversationId: string;
81
+ /** The `client_request_id` the turn was sent and found by. */
82
+ clientRequestId: string;
83
+ /** The turn's id and number, once fountain listed the turn. */
84
+ turnId?: string;
85
+ turnNumber?: number;
86
+ /** The turn's outcome: `completed`, `failed` or `interrupted`. */
87
+ status: string;
88
+ /** True when the turn ran past `timeoutMs` and was interrupted. */
89
+ interruptedByDeadline: boolean;
90
+ /** fountain's `limit_reason`: a service limit that ended the turn, which makes even a `completed` turn incomplete. */
91
+ limitReason?: string;
92
+ /** True when the conversation was terminated after the turn. */
93
+ terminated: boolean;
94
+ }
95
+
96
+ interface TurnView {
97
+ id?: string;
98
+ turn_number?: number;
99
+ status?: string;
100
+ client_request_id?: string | null;
101
+ limit_reason?: string | null;
102
+ }
103
+
104
+ /** The latest turn opened by `clientRequestId`, or undefined. */
105
+ async function turnOf(http: FountainHttp, conversationId: string, clientRequestId: string): Promise<TurnView | undefined> {
106
+ const { status, json } = await http("GET", `/api/conversations/${conversationId}/turns`);
107
+ if (status !== 200) return undefined;
108
+ const turns = ((json as { data?: TurnView[] })?.data ?? []).filter((t) => t.client_request_id === clientRequestId);
109
+ if (turns.length === 0) return undefined;
110
+ return turns.reduce((a, b) => ((b.turn_number ?? 0) > (a.turn_number ?? 0) ? b : a));
111
+ }
112
+
113
+ function refusal(conversationId: string, status: number, json: unknown): Error {
114
+ const detail = (json as { error?: unknown; errors?: unknown } | null)?.error ?? (json as { errors?: unknown } | null)?.errors;
115
+ const why = detail === undefined ? "" : `: ${typeof detail === "string" ? detail : JSON.stringify(detail)}`;
116
+ switch (status) {
117
+ case 410:
118
+ return new Error(
119
+ `fountainPrompt: conversation ${conversationId} is terminated, and fountain adds no turn to a terminated conversation. ` +
120
+ `Run the earlier turn with terminate: "never" and end the conversation after the last turn`,
121
+ );
122
+ case 400:
123
+ return new Error(`fountainPrompt: conversation ${conversationId} is busy: a turn is running (400)${why}`);
124
+ case 404:
125
+ return new Error(`fountainPrompt: no conversation ${conversationId} (404)`);
126
+ case 409:
127
+ return new Error(`fountainPrompt: conversation ${conversationId}'s sandbox is being reset or deleted (409)${why}`);
128
+ default:
129
+ return new Error(`fountainPrompt: the prompt was refused (${status})${why}`);
130
+ }
131
+ }
132
+
133
+ export async function fountainPrompt(
134
+ args: FountainPromptArgs,
135
+ signal?: AbortSignal,
136
+ http?: FountainHttp,
137
+ deps?: FountainConnectionDeps,
138
+ ): Promise<FountainPromptResult> {
139
+ if (typeof args?.conversation !== "string" || args.conversation === "") throw new Error("fountainPrompt: needs the conversation's id");
140
+ if (typeof args.prompt !== "string" || args.prompt.trim() === "") throw new Error("fountainPrompt: needs a prompt with words in it");
141
+ // `raw` reaches fountain after the signal has fired, for the interrupt and terminate.
142
+ let raw = http;
143
+ let client = http;
144
+ if (!raw) {
145
+ const { endpoint, token } = await resolveConnection(args, deps);
146
+ raw = defaultFountainHttp(endpoint, token);
147
+ client = defaultFountainHttp(endpoint, token, signal);
148
+ }
149
+ client = abortable(client!, signal);
150
+ const sleep = args.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
151
+ const timeoutMs = args.timeoutMs ?? 600_000;
152
+ const pollMs = args.pollMs ?? 5_000;
153
+ const policy: TerminatePolicy = args.terminate ?? "never";
154
+ const conversationId = args.conversation;
155
+ const clientRequestId = args.clientRequestId ?? `chant-${randomUUID()}`;
156
+
157
+ const sent = await client("POST", `/api/conversations/${conversationId}/prompts`, { prompt: args.prompt, client_request_id: clientRequestId });
158
+ if (sent.status !== 200 && sent.status !== 201 && sent.status !== 202) throw refusal(conversationId, sent.status, sent.json);
159
+
160
+ let turn: TurnView | undefined;
161
+ const done = async (status: string, interruptedByDeadline: boolean): Promise<FountainPromptResult> => {
162
+ const terminate = policy === "always" || (policy === "on-deadline" && interruptedByDeadline);
163
+ if (terminate) await raw!("POST", `/api/conversations/${conversationId}/terminate`);
164
+ return {
165
+ conversationId,
166
+ clientRequestId,
167
+ ...(turn?.id ? { turnId: turn.id } : {}),
168
+ ...(turn?.turn_number !== undefined ? { turnNumber: turn.turn_number } : {}),
169
+ status,
170
+ interruptedByDeadline,
171
+ ...(turn?.limit_reason ? { limitReason: turn.limit_reason } : {}),
172
+ terminated: terminate,
173
+ };
174
+ };
175
+ const interrupt = () => raw!("POST", `/api/conversations/${conversationId}/interrupt`).catch(() => undefined);
176
+
177
+ const deadline = Date.now() + timeoutMs;
178
+ try {
179
+ while (Date.now() < deadline) {
180
+ turn = (await turnOf(client, conversationId, clientRequestId)) ?? turn;
181
+ if (turn?.status && ENDED_TURN_STATUSES.has(turn.status)) return await done(turn.status, false);
182
+ signal?.throwIfAborted();
183
+ await sleep(pollMs);
184
+ }
185
+ } catch (err) {
186
+ if (signal?.aborted) {
187
+ await interrupt();
188
+ if (policy !== "never") await raw!("POST", `/api/conversations/${conversationId}/terminate`).catch(() => undefined);
189
+ }
190
+ throw err;
191
+ }
192
+
193
+ // The cap: the turn ran past timeoutMs. Interrupting ends the turn and keeps the conversation.
194
+ await interrupt();
195
+ turn = (await turnOf(raw!, conversationId, clientRequestId)) ?? turn;
196
+ return done("interrupted", true);
197
+ }
@@ -2,7 +2,8 @@
2
2
  * fountain Op activities — resolved by the core activity registry when a
3
3
  * project's `chant.config.ts` lists the `fountain` lexicon. Contributes
4
4
  * the native applier (`fountainApply` — direct REST against fountain's
5
- * API, no CLI, no state file) and the conversation runner (`fountainRun`).
5
+ * API, no CLI, no state file), the conversation runner (`fountainRun`) and
6
+ * the next turn on a conversation (`fountainPrompt`, #3356).
6
7
  */
7
8
  export {
8
9
  fountainApply,
@@ -33,3 +34,6 @@ export {
33
34
  PERSISTENT_DONE_STATUSES,
34
35
  } from "./fountain-run";
35
36
  export type { FountainRunArgs, FountainRunResult, ResolvedAgent, TerminatePolicy } from "./fountain-run";
37
+
38
+ export { fountainPrompt } from "./fountain-prompt";
39
+ export type { FountainPromptArgs, FountainPromptResult } from "./fountain-prompt";
@@ -31,4 +31,4 @@ The environment's config is the enforcement boundary, so watch it: `chant lifecy
31
31
 
32
32
  ## Running conversations
33
33
 
34
- Conversations are runs, not resources. Use the `fountainRun` activity: resolves the agent by name, starts (optionally with a prompt and an allowlisted vault), and waits for the turn to end. What "end" means, and what happens to the machine, follows the agent's `sandbox_mode`: an `ephemeral` agent's conversation is polled for a terminal status and terminated on deadline, same as before; a `persistent` agent's (a `Steward` or `Box`) conversation settles on `idle` once its turn ends — machine still up for the next turn — so `fountainRun` reads the turn back and returns its outcome (`completed | failed | interrupted`) without terminating anything, by default. Pass `terminate: "on-deadline" | "always"` to opt a persistent agent's run back into ending the machine. Multi-turn interaction (follow-up prompts, interrupt) is fountain's own conversations API — keep chant to the lifecycle edges.
34
+ Conversations are runs, not resources. Use the `fountainRun` activity: resolves the agent by name, starts (optionally with a prompt and an allowlisted vault), and waits for the turn to end. What "end" means, and what happens to the machine, follows the agent's `sandbox_mode`: an `ephemeral` agent's conversation is polled for a terminal status and terminated on deadline, same as before; a `persistent` agent's (a `Steward` or `Box`) conversation settles on `idle` once its turn ends — machine still up for the next turn — so `fountainRun` reads the turn back and returns its outcome (`completed | failed | interrupted`) without terminating anything, by default. Pass `terminate: "on-deadline" | "always"` to opt a persistent agent's run back into ending the machine. To add a turn to a conversation that already exists, use `fountainPrompt` with the conversation's id: it sends the prompt on the same session and waits for that turn, interrupting it at `timeoutMs`. It cannot reach a terminated conversation (fountain answers 410), so run the earlier turn with `terminate: "never"`, and it cannot narrow tools for one turn: that is the agent's `permission_policy`, set at launch. Interrupts and permission answers on their own are fountain's own conversations API.