@intentius/chant-lexicon-fountain 0.124.27 → 0.124.28

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "4cd2b1a1515c4d32f4fca0e514dcc212c19dffb8933e41f7c2d680e100fddd1a",
4
+ "manifest.json": "897892e3f675a2e4d5d27da914798743a1159472ac616fc150275551e3e55699",
5
5
  "meta.json": "3301437a2a56eaf1a5cc8f6eabb18a40767f916e09701b2d9bf20aba4066738c",
6
6
  "types/index.d.ts": "cf6b634fe0a8e9e96f1d9d07e3df53838def2683dc93dbe3c7fd971c6d3908ae",
7
7
  "rules/ftn001-no-secret-literals.ts": "dd1b54db3919afadcb5b8261db9a5db7c316a5f263c86f7a551fddb19f6ac532",
@@ -23,5 +23,5 @@
23
23
  "skills/chant-fountain-ops.md": "4e72f0a0c7477a9e98f85ea45e49ba69e8a699f9c13ba0f0cf1cb20a88151373",
24
24
  "skills/chant-fountain-locked-sandboxes.md": "7404ece96f2a3bc9105942fd3b6db156a3537db7e34c8cf2faa9ba87f5512567"
25
25
  },
26
- "composite": "88243a1acbb024b85b4ea3c7e56d704e39c3cc7d81f9483a2e5bf206a90fbcab"
26
+ "composite": "d9a461b89d5f289cba62e4b0941a05cde8865390e9614fad44f985d9feb70bf8"
27
27
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fountain",
3
- "version": "0.124.27",
3
+ "version": "0.124.28",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Fountain",
6
6
  "specVersion": "v0.21.0"
@@ -229,6 +229,15 @@ interface TailOptions {
229
229
  idleTimeoutMs: number;
230
230
  now: () => number;
231
231
  progress?: (record: StepRecord) => void;
232
+ /**
233
+ * Where the run's event stream goes (#4041, `chant run <op> --on fountain
234
+ * --events-json`). The tail writes it as the steward works: a phase for
235
+ * each stage the turn reports, a step for each tool call, started when the
236
+ * call starts and settled when it settles. `runtime` is `steward`, the run
237
+ * id is the conversation's, and the commit is null, since the run builds
238
+ * from whatever the steward's sandbox holds.
239
+ */
240
+ runEvents?: (line: string) => void;
232
241
  signal?: AbortSignal;
233
242
  /**
234
243
  * REST seam for the conversation poll (#2167). Without it the tail waits on
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/op/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/op/runtime.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-fountain",
3
- "version": "0.124.27",
3
+ "version": "0.124.28",
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.124.27",
53
+ "@intentius/chant": "^0.124.28",
54
54
  "zod": "^4.3.6"
55
55
  },
56
56
  "devDependencies": {
@@ -10,6 +10,9 @@
10
10
  */
11
11
 
12
12
  import { describe, expect, it, vi } from "vitest";
13
+ import { createRequire } from "node:module";
14
+ import type { RunEvent } from "@intentius/chant";
15
+ import runEventSchema from "@intentius/chant/workspace/run-event.schema.json";
13
16
  import type { ChantConfig } from "@intentius/chant/config";
14
17
  import type { OpConfig, StepRecord } from "@intentius/chant/op";
15
18
  import {
@@ -1312,3 +1315,83 @@ describe("the plugin registers the provider", () => {
1312
1315
  fetchSpy.mockRestore();
1313
1316
  });
1314
1317
  });
1318
+
1319
+ // ── the run-event stream (#4041) ──────────────────────────────────────────
1320
+
1321
+ describe("tailConversation runEvents (#4041)", () => {
1322
+ // The draft 2020-12 validator core's own contract tests use, loaded from core:
1323
+ // this package carries no ajv of its own.
1324
+ const { default: Ajv2020 } = createRequire(import.meta.resolve("@intentius/chant"))("ajv/dist/2020") as { default: new (o: object) => { compile(s: object): ((d: unknown) => boolean) & { errors?: unknown } } };
1325
+ const validate = new Ajv2020({ strict: true, allErrors: true }).compile(runEventSchema);
1326
+
1327
+ /** Parse and check a stream: schema-valid, framed, one run, each step started before it settles. */
1328
+ function check(lines: string[]) {
1329
+ const events = lines.map((l) => JSON.parse(l) as RunEvent);
1330
+ for (const e of events) expect(validate(e), JSON.stringify(validate.errors)).toBe(true);
1331
+ expect(events.map((e) => e.seq)).toEqual(events.map((_, i) => i));
1332
+ expect(events[0]?.type).toBe("run-start");
1333
+ expect(events.at(-1)?.type).toBe("run-settle");
1334
+ return events;
1335
+ }
1336
+
1337
+ it("streams the steward's stages as phases and its tool calls as steps, on the steward runtime", async () => {
1338
+ const { sse } = fakeSse([
1339
+ [
1340
+ sseEvent("1", { stream: "stage", stage: "plan" }),
1341
+ sseEvent("2", { stream: "stdout", blocks: [{ kind: "tool_call", id: "t1", title: "plan" }] }),
1342
+ sseEvent("3", { stream: "stdout", blocks: [{ kind: "tool_call_update", tool_call_id: "t1", status: "completed" }] }),
1343
+ sseEvent("4", { stream: "stage", stage: "apply" }),
1344
+ sseEvent("5", { stream: "stdout", blocks: [{ kind: "tool_call", id: "t2", title: "apply" }] }),
1345
+ sseEvent("6", { stream: "stdout", blocks: [{ kind: "tool_call_update", tool_call_id: "t2", status: "failed" }] }),
1346
+ sseEvent("7", { stream: "stdout", blocks: [{ kind: "text", body: JSON.stringify(RECORD) }] }),
1347
+ sseEvent("8", { stream: "stage", stage: "turn", state: "done" }),
1348
+ ],
1349
+ ]);
1350
+ const lines: string[] = [];
1351
+ const status = await tailConversation({
1352
+ sse,
1353
+ conversationId: "conv-1",
1354
+ op: "alb-deploy",
1355
+ startedAt: "2026-03-01T10:00:00.000Z",
1356
+ idleTimeoutMs: 60_000,
1357
+ now: fakeClock(),
1358
+ runEvents: (l) => lines.push(l),
1359
+ });
1360
+
1361
+ expect(status.state).toBe("completed");
1362
+ const events = check(lines);
1363
+ expect(events[0]).toMatchObject({ runId: "conv-1", kind: "op", name: "alb-deploy", gitSha: null, runtime: "steward", runOrigin: { forge: "op" } });
1364
+ expect(events.map((e) => [e.type, "phase" in e ? e.phase : undefined, "step" in e ? e.step : undefined, "status" in e ? e.status : undefined])).toEqual([
1365
+ ["run-start", undefined, undefined, undefined],
1366
+ ["phase-start", "plan", undefined, undefined],
1367
+ ["step-start", "plan", "plan", undefined],
1368
+ ["step-settle", "plan", "plan", "ok"],
1369
+ ["phase-settle", "plan", undefined, "ok"],
1370
+ ["phase-start", "apply", undefined, undefined],
1371
+ ["step-start", "apply", "apply", undefined],
1372
+ ["step-settle", "apply", "apply", "failed"],
1373
+ ["phase-settle", "apply", undefined, "failed"],
1374
+ ["run-settle", undefined, undefined, "ok"],
1375
+ ]);
1376
+ });
1377
+
1378
+ it("a tail that gives up still ends the stream, failed with the reason", async () => {
1379
+ const sse: FountainSse = () => ({
1380
+ [Symbol.asyncIterator]: () => ({ next: () => new Promise<never>(() => {}) }),
1381
+ });
1382
+ const lines: string[] = [];
1383
+ await expect(
1384
+ tailConversation({
1385
+ sse,
1386
+ conversationId: "conv-1",
1387
+ op: "alb-deploy",
1388
+ startedAt: "2026-03-01T10:00:00.000Z",
1389
+ idleTimeoutMs: 5,
1390
+ now: () => Date.now(),
1391
+ runEvents: (l) => lines.push(l),
1392
+ }),
1393
+ ).rejects.toThrow(/nothing arrived/);
1394
+ const events = check(lines);
1395
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed", error: expect.stringMatching(/nothing arrived on conversation conv-1/) });
1396
+ });
1397
+ });
package/src/op/runtime.ts CHANGED
@@ -65,6 +65,8 @@ import { loadChantConfig, type ChantConfig } from "@intentius/chant/config";
65
65
  import type { OpConfig, OpGateResolveOptions, OpRunHandle, OpRunRecord, OpRunStartOptions, OpRunState, OpRunStatus, OpRuntimeProvider, StepRecord } from "@intentius/chant/op";
66
66
  import { runStateOf } from "@intentius/chant/op";
67
67
  import type { GateResolutionRecord } from "@intentius/chant/lifecycle/gate-ledger";
68
+ import type { RunEventSettleStatus } from "@intentius/chant";
69
+ import { openRunEventStream, runEventRunOf, type RunEventStream } from "@intentius/chant/run-event-stream";
68
70
  import { resolveProfile, type FountainProfile } from "../config";
69
71
  import {
70
72
  defaultFountainHttp,
@@ -794,6 +796,7 @@ export function createFountainOpRuntime(opts: FountainOpRuntimeOptions = {}): Op
794
796
  http,
795
797
  ...(opts.conversationPollMs !== undefined ? { pollIntervalMs: opts.conversationPollMs } : {}),
796
798
  ...(startOpts.progress ? { progress: startOpts.progress } : {}),
799
+ ...(startOpts.runEvents ? { runEvents: startOpts.runEvents } : {}),
797
800
  ...(startOpts.signal ? { signal: startOpts.signal } : {}),
798
801
  });
799
802
 
@@ -1077,6 +1080,15 @@ interface TailOptions {
1077
1080
  idleTimeoutMs: number;
1078
1081
  now: () => number;
1079
1082
  progress?: (record: StepRecord) => void;
1083
+ /**
1084
+ * Where the run's event stream goes (#4041, `chant run <op> --on fountain
1085
+ * --events-json`). The tail writes it as the steward works: a phase for
1086
+ * each stage the turn reports, a step for each tool call, started when the
1087
+ * call starts and settled when it settles. `runtime` is `steward`, the run
1088
+ * id is the conversation's, and the commit is null, since the run builds
1089
+ * from whatever the steward's sandbox holds.
1090
+ */
1091
+ runEvents?: (line: string) => void;
1080
1092
  signal?: AbortSignal;
1081
1093
  /**
1082
1094
  * REST seam for the conversation poll (#2167). Without it the tail waits on
@@ -1164,6 +1176,85 @@ async function readConversation(
1164
1176
  * one connection — ends the wait, and it ends it with an error.
1165
1177
  */
1166
1178
  export async function tailConversation(opts: TailOptions): Promise<OpRunStatus> {
1179
+ const stream = opts.runEvents
1180
+ ? openRunEventStream(
1181
+ runEventRunOf({ kind: "op", name: opts.op, runId: opts.conversationId, runtime: "steward", runOrigin: { forge: "op" } }),
1182
+ opts.runEvents,
1183
+ { now: () => new Date(opts.now()) },
1184
+ )
1185
+ : undefined;
1186
+ stream?.emit({ type: "run-start", waves: null });
1187
+ const steps = stewardSteps(stream);
1188
+ try {
1189
+ const status = await tailTurn(opts, steps);
1190
+ steps.close(status.state === "completed" ? "ok" : "failed");
1191
+ stream?.settle(...settleOfState(status));
1192
+ return status;
1193
+ } catch (err) {
1194
+ steps.close("failed");
1195
+ stream?.settle("failed", err instanceof Error ? err.message : String(err));
1196
+ throw err;
1197
+ }
1198
+ }
1199
+
1200
+ /** A run's final state as its stream's `run-settle`. */
1201
+ function settleOfState(status: OpRunStatus): [RunEventSettleStatus, string?] {
1202
+ switch (status.state) {
1203
+ case "completed":
1204
+ return ["ok"];
1205
+ case "gated":
1206
+ return ["gated"];
1207
+ case "waiting":
1208
+ return ["waiting"];
1209
+ case "cancelled":
1210
+ return ["failed", status.error ?? "the run was cancelled"];
1211
+ default:
1212
+ return ["failed", status.error ?? `the run ended ${status.state}`];
1213
+ }
1214
+ }
1215
+
1216
+ /** The steward's tool calls as run events: a phase per stage, a step per call. */
1217
+ interface StewardSteps {
1218
+ /** The tool call `key` started in `phase`. */
1219
+ start(key: string, phase: string, step: string): void;
1220
+ /** The tool call `key` settled. One never seen to start is started first, so its start still comes before its settle. */
1221
+ settle(key: string, phase: string, step: string, ok: boolean, durationMs: number): void;
1222
+ /** Settle the open phase, if any. */
1223
+ close(status: RunEventSettleStatus): void;
1224
+ }
1225
+
1226
+ function stewardSteps(stream: RunEventStream | undefined): StewardSteps {
1227
+ let open: { phase: string; failed: boolean } | undefined;
1228
+ const started = new Set<string>();
1229
+ const enter = (phase: string): void => {
1230
+ if (!stream || open?.phase === phase) return;
1231
+ if (open) stream.emit({ type: "phase-settle", component: null, phase: open.phase, status: open.failed ? "failed" : "ok" });
1232
+ open = { phase, failed: false };
1233
+ stream.emit({ type: "phase-start", component: null, phase });
1234
+ };
1235
+ return {
1236
+ start(key, phase, step) {
1237
+ if (!stream || started.has(key)) return;
1238
+ started.add(key);
1239
+ enter(phase);
1240
+ stream.emit({ type: "step-start", component: null, phase, step });
1241
+ },
1242
+ settle(key, phase, step, ok, durationMs) {
1243
+ if (!stream) return;
1244
+ this.start(key, phase, step);
1245
+ started.delete(key);
1246
+ if (!ok && open?.phase === phase) open.failed = true;
1247
+ stream.emit({ type: "step-settle", component: null, phase, step, status: ok ? "ok" : "failed", durationMs });
1248
+ },
1249
+ close(status) {
1250
+ if (!stream || !open) return;
1251
+ stream.emit({ type: "phase-settle", component: null, phase: open.phase, status: open.failed ? "failed" : status });
1252
+ open = undefined;
1253
+ },
1254
+ };
1255
+ }
1256
+
1257
+ async function tailTurn(opts: TailOptions, steps: StewardSteps): Promise<OpRunStatus> {
1167
1258
  const { sse, conversationId, op, startedAt, idleTimeoutMs, now } = opts;
1168
1259
  const path = `/api/conversations/${conversationId}/stream?streams=stdout,stderr,stage&blocks=true`;
1169
1260
 
@@ -1195,6 +1286,7 @@ export async function tailConversation(opts: TailOptions): Promise<OpRunStatus>
1195
1286
  const key = toolCallId(block);
1196
1287
  const open = pending.get(key);
1197
1288
  pending.delete(key);
1289
+ steps.settle(key, open?.phase ?? phase, open?.fn ?? toolCallName(block), status === "ok", open ? Math.max(0, now() - open.startedAt) : 0);
1198
1290
  if (!opts.progress) return;
1199
1291
  opts.progress({
1200
1292
  phase: open?.phase ?? phase,
@@ -1219,6 +1311,7 @@ export async function tailConversation(opts: TailOptions): Promise<OpRunStatus>
1219
1311
  const key = toolCallId(block);
1220
1312
  if (!pending.has(key)) {
1221
1313
  pending.set(key, { fn: toolCallName(block), phase, startedAt: now() });
1314
+ steps.start(key, phase, toolCallName(block));
1222
1315
  }
1223
1316
  if (block.status && SETTLED_TOOL_STATUS.has(block.status)) {
1224
1317
  emit(block, block.status === "completed" || block.status === "success" ? "ok" : "fail");