@llm4ts/flow 2.23.0 → 2.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.
@@ -0,0 +1,158 @@
1
+ import * as Clock from "effect/Clock";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Exit from "effect/Exit";
4
+ import * as Ref from "effect/Ref";
5
+ import * as Schema from "effect/Schema";
6
+ import * as Stream from "effect/Stream";
7
+ import { redactText } from "@llm4ts/core/observability/Redaction";
8
+ import { roleOr } from "./Timing.js";
9
+ /**
10
+ * What the model was told and what it answered, per seat call — an opt-in
11
+ * record (`llm4ts run --transcript`) that `llm4ts watch` tails. It lives in
12
+ * its own files, never in the trace, so the trace and `llm4ts profile` stay
13
+ * content-free. Secrets are redacted and every field is capped.
14
+ */
15
+ const Call = Schema.TaggedStruct("Call", {
16
+ at: Schema.Number,
17
+ call: Schema.String,
18
+ role: Schema.String,
19
+ executor: Schema.optionalKey(Schema.String),
20
+ /** The system prompt, on a chat's first call only. */
21
+ system: Schema.optionalKey(Schema.String),
22
+ /** The input: a chat's newest message, or a one-shot prompt. */
23
+ input: Schema.String,
24
+ /** How many messages came before the input (a chat resends them all). */
25
+ earlier: Schema.optionalKey(Schema.Int)
26
+ });
27
+ const Reply = Schema.TaggedStruct("Reply", {
28
+ at: Schema.Number,
29
+ call: Schema.String,
30
+ text: Schema.String
31
+ });
32
+ const Tool = Schema.TaggedStruct("Tool", {
33
+ at: Schema.Number,
34
+ call: Schema.String,
35
+ tool: Schema.String,
36
+ args: Schema.String
37
+ });
38
+ const ToolResult = Schema.TaggedStruct("ToolResult", {
39
+ at: Schema.Number,
40
+ call: Schema.String,
41
+ output: Schema.String,
42
+ failed: Schema.optionalKey(Schema.Boolean)
43
+ });
44
+ const End = Schema.TaggedStruct("End", {
45
+ at: Schema.Number,
46
+ call: Schema.String,
47
+ ms: Schema.Number,
48
+ failed: Schema.optionalKey(Schema.Boolean)
49
+ });
50
+ export const TranscriptEntry = Schema.Union([Call, Reply, Tool, ToolResult, End]);
51
+ export const makeMemoryTranscriptSink = Effect.map(Ref.make([]), (entries) => ({
52
+ sink: { write: (lane, entry) => Ref.update(entries, (all) => [...all, { lane, entry }]) },
53
+ entries: Ref.get(entries)
54
+ }));
55
+ const inputChars = 64_000;
56
+ const partChars = 4_000;
57
+ const clean = (text, limit) => redactText(text, { maxLength: limit });
58
+ let calls = 0;
59
+ const nextCall = Effect.sync(() => {
60
+ calls += 1;
61
+ return `call-${calls}`;
62
+ });
63
+ /** A seat whose every call is written to `sink`, as it happens. */
64
+ export const transcriptSeat = (service, sink, options) => {
65
+ const write = (entry) => sink.write(options.lane, entry);
66
+ const opened = (input, extra) => Effect.gen(function* () {
67
+ const call = yield* nextCall;
68
+ const start = yield* Clock.currentTimeMillis;
69
+ const role = yield* roleOr(options.role);
70
+ const executor = options.executor === undefined ? undefined : yield* options.executor;
71
+ yield* write({
72
+ _tag: "Call",
73
+ at: start,
74
+ call,
75
+ role,
76
+ ...(executor === undefined ? {} : { executor }),
77
+ ...(extra.system === undefined ? {} : { system: clean(extra.system, inputChars) }),
78
+ input: clean(input, inputChars),
79
+ ...(extra.earlier === undefined ? {} : { earlier: extra.earlier })
80
+ });
81
+ return { call, start };
82
+ });
83
+ const closed = (call, start, failed) => Effect.flatMap(Clock.currentTimeMillis, (at) => write({ _tag: "End", at, call, ms: at - start, ...(failed ? { failed } : {}) }));
84
+ const chunkEntry = (call, chunk) => Effect.flatMap(Clock.currentTimeMillis, (at) => {
85
+ switch (chunk.metadata.event) {
86
+ case "tool_use":
87
+ return write({
88
+ _tag: "Tool",
89
+ at,
90
+ call,
91
+ tool: chunk.metadata.tool_name ?? chunk.metadata.toolName ?? "",
92
+ args: clean(chunk.metadata.tool_input ?? chunk.metadata.toolInput ?? "", partChars)
93
+ });
94
+ case "tool_result": {
95
+ const failed = chunk.metadata.tool_failed === "true" || chunk.metadata.tool_status === "error";
96
+ return write({
97
+ _tag: "ToolResult",
98
+ at,
99
+ call,
100
+ output: clean(chunk.metadata.tool_content ?? "", partChars),
101
+ ...(failed ? { failed } : {})
102
+ });
103
+ }
104
+ default:
105
+ return chunk.delta.length === 0
106
+ ? Effect.void
107
+ : write({
108
+ _tag: "Reply",
109
+ at,
110
+ call,
111
+ text: redactText(chunk.delta, { maxLength: inputChars })
112
+ });
113
+ }
114
+ });
115
+ const recorded = (open, stream) => Stream.unwrap(Effect.map(Effect.all([open, Ref.make(false)]), ([{ call, start }, failed]) => stream.pipe(Stream.tap((chunk) => chunkEntry(call, chunk)), Stream.tapError(() => Ref.set(failed, true)), Stream.ensuring(Effect.flatMap(Ref.get(failed), (did) => closed(call, start, did))))));
116
+ const once = (input, effect) => Effect.flatMap(opened(input, {}), ({ call, start }) => Effect.onExit(effect, (exit) => Effect.andThen(Exit.isSuccess(exit)
117
+ ? Effect.flatMap(Clock.currentTimeMillis, (at) => write({
118
+ _tag: "Reply",
119
+ at,
120
+ call,
121
+ text: clean(JSON.stringify(exit.value) ?? "", inputChars)
122
+ }))
123
+ : Effect.void, closed(call, start, Exit.isFailure(exit)))));
124
+ /** A chat's first call is told everything; a later one only what is new. */
125
+ const chatInput = (messages) => {
126
+ const system = messages.filter((message) => message.role === "System");
127
+ const rest = messages.filter((message) => message.role !== "System");
128
+ const first = !rest.some((message) => message.role === "Assistant");
129
+ const newest = rest.at(-1)?.content ?? "";
130
+ return first
131
+ ? {
132
+ input: rest.map((message) => message.content).join("\n\n"),
133
+ extra: system.length === 0
134
+ ? {}
135
+ : { system: system.map((message) => message.content).join("\n\n") }
136
+ }
137
+ : { input: newest, extra: { earlier: rest.length - 1 } };
138
+ };
139
+ const batched = service.scoreLabelSequence;
140
+ return {
141
+ executeStream: (prompt) => recorded(opened(prompt, {}), service.executeStream(prompt)),
142
+ executeStreamWithHistory: (messages) => {
143
+ const { input, extra } = chatInput(messages);
144
+ return recorded(opened(input, extra), service.executeStreamWithHistory(messages));
145
+ },
146
+ executeWithTools: (prompt, tools) => once(prompt, service.executeWithTools(prompt, tools)),
147
+ executeStructured: (prompt, schema, jsonSchema) => once(prompt, service.executeStructured(prompt, schema, jsonSchema)),
148
+ executeStructuredWithUsage: (prompt, schema, jsonSchema) => once(prompt, service.executeStructuredWithUsage(prompt, schema, jsonSchema)),
149
+ scoreLabels: (prompt, labels) => once(prompt, service.scoreLabels(prompt, labels)),
150
+ ...(batched === undefined
151
+ ? {}
152
+ : {
153
+ scoreLabelSequence: (prompt, labelSets) => once(prompt, batched(prompt, labelSets))
154
+ }),
155
+ isAvailable: service.isAvailable
156
+ };
157
+ };
158
+ //# sourceMappingURL=Transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Transcript.js","sourceRoot":"","sources":["../src/Transcript.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AACnC,OAAO,KAAK,GAAG,MAAM,YAAY,CAAA;AACjC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAA;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC;;;;;GAKG;AAEH,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE;IACvC,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;IAC3C,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;IACzC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC,MAAM;IACpB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC;CACxC,CAAC,CAAA;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;IACzC,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC,CAAA;AACF,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE;IACvC,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC,CAAA;AACF,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,YAAY,EAAE;IACnD,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC;CAC3C,CAAC,CAAA;AACF,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE;IACrC,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC;CAC3C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAA;AAejF,MAAM,CAAC,MAAM,wBAAwB,GAAwC,MAAM,CAAC,GAAG,CACrF,GAAG,CAAC,IAAI,CACN,EAAE,CACH,EACD,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACZ,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE;IACzF,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC;CAC1B,CAAC,CACH,CAAA;AAED,MAAM,UAAU,GAAG,MAAM,CAAA;AACzB,MAAM,SAAS,GAAG,KAAK,CAAA;AAEvB,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;AAE7F,IAAI,KAAK,GAAG,CAAC,CAAA;AACb,MAAM,QAAQ,GAA0B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;IACvD,KAAK,IAAI,CAAC,CAAA;IACV,OAAO,QAAQ,KAAK,EAAE,CAAA;AACxB,CAAC,CAAC,CAAA;AAUF,mEAAmE;AACnE,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,OAAwB,EACxB,IAAoB,EACpB,OAA8B,EACb,EAAE;IACnB,MAAM,KAAK,GAAG,CAAC,KAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAEzE,MAAM,MAAM,GAAG,CACb,KAAa,EACb,KAA8D,EACI,EAAE,CACpE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAA;QAC5B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAA;QAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACxC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAA;QACrF,KAAK,CAAC,CAAC,KAAK,CAAC;YACX,IAAI,EAAE,MAAM;YACZ,EAAE,EAAE,KAAK;YACT,IAAI;YACJ,IAAI;YACJ,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;YAC/C,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;YAClF,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC;YAC/B,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;SACnE,CAAC,CAAA;QACF,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IACxB,CAAC,CAAC,CAAA;IAEJ,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,MAAe,EAAuB,EAAE,CACnF,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,EAAE,CAC7C,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAChF,CAAA;IAEH,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,KAAe,EAAuB,EAAE,CACxE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,EAAE;QAC7C,QAAQ,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YAC7B,KAAK,UAAU;gBACb,OAAO,KAAK,CAAC;oBACX,IAAI,EAAE,MAAM;oBACZ,EAAE;oBACF,IAAI;oBACJ,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,IAAI,EAAE;oBAC/D,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,IAAI,EAAE,EAAE,SAAS,CAAC;iBACpF,CAAC,CAAA;YACJ,KAAK,aAAa,CAAC,CAAC,CAAC;gBACnB,MAAM,MAAM,GACV,KAAK,CAAC,QAAQ,CAAC,WAAW,KAAK,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,KAAK,OAAO,CAAA;gBACjF,OAAO,KAAK,CAAC;oBACX,IAAI,EAAE,YAAY;oBAClB,EAAE;oBACF,IAAI;oBACJ,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,YAAY,IAAI,EAAE,EAAE,SAAS,CAAC;oBAC3D,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC9B,CAAC,CAAA;YACJ,CAAC;YACD;gBACE,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;oBAC7B,CAAC,CAAC,MAAM,CAAC,IAAI;oBACb,CAAC,CAAC,KAAK,CAAC;wBACJ,IAAI,EAAE,OAAO;wBACb,EAAE;wBACF,IAAI;wBACJ,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;qBACzD,CAAC,CAAA;QACV,CAAC;IACH,CAAC,CAAC,CAAA;IAEJ,MAAM,QAAQ,GAAG,CACf,IAAsE,EACtE,MAAkC,EACN,EAAE,CAC9B,MAAM,CAAC,MAAM,CACX,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAC5E,MAAM,CAAC,IAAI,CACT,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,EAC9C,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAC5C,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CACpF,CACF,CACF,CAAA;IAEH,MAAM,IAAI,GAAG,CAAU,KAAa,EAAE,MAA8B,EAA0B,EAAE,CAC9F,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CACpD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAC7B,MAAM,CAAC,OAAO,CACZ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAClB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,EAAE,CAC7C,KAAK,CAAC;YACJ,IAAI,EAAE,OAAO;YACb,EAAE;YACF,IAAI;YACJ,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC;SAC1D,CAAC,CACH;QACH,CAAC,CAAC,MAAM,CAAC,IAAI,EACf,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAC1C,CACF,CACF,CAAA;IAEH,4EAA4E;IAC5E,MAAM,SAAS,GAAG,CAAC,QAAgC,EAAE,EAAE;QACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAA;QACtE,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAA;QACpE,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,CAAC,CAAA;QACnE,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAA;QACzC,OAAO,KAAK;YACV,CAAC,CAAC;gBACE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC1D,KAAK,EACH,MAAM,CAAC,MAAM,KAAK,CAAC;oBACjB,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;aACxE;YACH,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,CAAA;IAC5D,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,kBAAkB,CAAA;IAC1C,OAAO;QACL,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACtF,wBAAwB,EAAE,CAAC,QAAQ,EAAE,EAAE;YACrC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;YAC5C,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAA;QACnF,CAAC;QACD,gBAAgB,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC1F,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CAChD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QACrE,0BAA0B,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CACzD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QAC9E,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAClF,GAAG,CAAC,OAAO,KAAK,SAAS;YACvB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE,kBAAkB,EAAE,CAAC,MAAc,EAAE,SAA+C,EAAE,EAAE,CACtF,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;aAC3C,CAAC;QACN,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAA;AACH,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/flow",
3
- "version": "2.23.0",
3
+ "version": "2.25.0",
4
4
  "description": "Effect-native LLM workflow, persistence, review, and repository automation",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,6 +56,7 @@
56
56
  "./Judgment": "./dist/Judgment.js",
57
57
  "./JudgmentLog": "./dist/JudgmentLog.js",
58
58
  "./Perimeter": "./dist/Perimeter.js",
59
+ "./NodePreflight": "./dist/NodePreflight.js",
59
60
  "./Persistence": "./dist/Persistence.js",
60
61
  "./Plan": "./dist/Plan.js",
61
62
  "./PlanExecution": "./dist/PlanExecution.js",
@@ -76,6 +77,7 @@
76
77
  "./StoryPlan": "./dist/StoryPlan.js",
77
78
  "./Survey": "./dist/Survey.js",
78
79
  "./Timing": "./dist/Timing.js",
80
+ "./Transcript": "./dist/Transcript.js",
79
81
  "./TransientRetry": "./dist/TransientRetry.js",
80
82
  "./Usage": "./dist/Usage.js",
81
83
  "./Wall": "./dist/Wall.js",
@@ -107,7 +109,7 @@
107
109
  "typescript"
108
110
  ],
109
111
  "dependencies": {
110
- "@llm4ts/core": "2.23.0"
112
+ "@llm4ts/core": "2.25.0"
111
113
  },
112
114
  "peerDependencies": {
113
115
  "effect": "4.0.0"
@@ -25,6 +25,13 @@ export interface FlowContextShape {
25
25
  readonly hosting: GitHubToolShape
26
26
  readonly events: FlowEventsShape
27
27
  readonly reviewers: ReadonlyArray<LlmServiceShape>
28
+ /**
29
+ * The judgment seat as a chat service, for the rubric judges that predate
30
+ * ADR 0017: the reasoner with its tools taken away wherever the harness
31
+ * can (a roster's judge role, or `readOnly` over a CLI reasoner). Absent
32
+ * on a context built by hand; judges then fall back to `reasoning`.
33
+ */
34
+ readonly judge?: LlmServiceShape
28
35
  readonly coderCapabilities: ConnectorCapabilities
29
36
  readonly userPrompt: string
30
37
  readonly workDir: string
package/src/Judgment.ts CHANGED
@@ -108,9 +108,9 @@ export const decide = (
108
108
  return certainty >= bands.act ? "act" : certainty < bands.hold ? "hold" : "caution"
109
109
  }
110
110
 
111
- /** The context's judgment service, or one derived from its reasoning seat. */
111
+ /** The context's judgment service, or one derived from its judge (else reasoning) seat. */
112
112
  export const judgmentOf = (context: FlowContextShape): JudgmentShape =>
113
- context.judgment ?? makeLlmJudgment(context.reasoning)
113
+ context.judgment ?? makeLlmJudgment(context.judge ?? context.reasoning)
114
114
 
115
115
  const backendFailure = (error: JudgmentBackendError): FlowError =>
116
116
  FlowLlmError.from(ProviderError.make({ message: error.message, cause: error }))
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Which Node will the gates run on, and is it the one the application pins?
3
+ *
4
+ * Gates (`pnpm lint`, `pnpm test`, `pnpm build`) are spawned with the
5
+ * environment llm4ts inherited, so `pnpm` finds the `node` on PATH, which is
6
+ * whatever shell launched llm4ts, not what the application asks for in
7
+ * `.nvmrc`, `.node-version` or `package.json#engines`. A mismatch surfaces as
8
+ * a red gate on the first story, in a syntax error or an engine refusal that
9
+ * says nothing about Node. This asks before the first gate, and names both.
10
+ *
11
+ * pnpm's `use-node-version` (`.npmrc`) makes pnpm fetch and run that Node
12
+ * itself, so a target that carries it passes regardless of PATH.
13
+ */
14
+ import { join } from "node:path"
15
+ import * as Effect from "effect/Effect"
16
+ import * as Schema from "effect/Schema"
17
+ import type { ProcessExecutorShape } from "@llm4ts/core/ProcessExecutor"
18
+ import { FlowAborted, type FlowError } from "./FlowError.ts"
19
+ import { Info, type FlowEventsShape } from "./FlowEvents.ts"
20
+ import type { PlainFileStoreShape } from "./Persistence.ts"
21
+
22
+ /** A Node pin as the application wrote it, and where. */
23
+ export class NodePin extends Schema.Class<NodePin>("NodePin")({
24
+ spec: Schema.String,
25
+ source: Schema.String
26
+ }) {}
27
+
28
+ // ---- Version matching ---------------------------------------------------------------
29
+
30
+ type Partial = {
31
+ readonly major: number
32
+ readonly minor?: number
33
+ readonly patch?: number
34
+ }
35
+
36
+ const partialOf = (text: string): Partial | undefined => {
37
+ const match = /^v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/.exec(text.trim())
38
+ if (match === null || match[1] === undefined) {
39
+ return undefined
40
+ }
41
+ const component = (value: string | undefined): number | undefined =>
42
+ value === undefined || value === "x" || value === "*" ? undefined : Number(value)
43
+ const minor = component(match[2])
44
+ const patch = component(match[3])
45
+ return {
46
+ major: Number(match[1]),
47
+ ...(minor === undefined ? {} : { minor }),
48
+ ...(patch === undefined ? {} : { patch })
49
+ }
50
+ }
51
+
52
+ const triple = (version: Partial): ReadonlyArray<number> => [
53
+ version.major,
54
+ version.minor ?? 0,
55
+ version.patch ?? 0
56
+ ]
57
+
58
+ const compare = (left: ReadonlyArray<number>, right: ReadonlyArray<number>): number => {
59
+ for (let index = 0; index < 3; index += 1) {
60
+ const difference = (left[index] ?? 0) - (right[index] ?? 0)
61
+ if (difference !== 0) {
62
+ return difference
63
+ }
64
+ }
65
+ return 0
66
+ }
67
+
68
+ /** The alias forms only a version manager resolves (`lts/*`, `lts/iron`, `node`). */
69
+ const isAlias = (spec: string): boolean =>
70
+ /^(lts\/.*|node|latest|stable|current|system)$/i.test(spec)
71
+
72
+ const tokenHolds = (version: ReadonlyArray<number>, token: string): boolean | undefined => {
73
+ if (token === "*" || token === "x" || token === "latest") {
74
+ return true
75
+ }
76
+ const match = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(token)
77
+ const operator = match?.[1] ?? ""
78
+ const partial = partialOf(match?.[2] ?? "")
79
+ if (partial === undefined) {
80
+ return undefined
81
+ }
82
+ const low = triple(partial)
83
+ switch (operator) {
84
+ case ">=":
85
+ return compare(version, low) >= 0
86
+ case ">":
87
+ return compare(version, low) > 0
88
+ case "<":
89
+ return compare(version, low) < 0
90
+ case "<=":
91
+ return compare(version, low) <= 0
92
+ case "^": {
93
+ const high =
94
+ partial.major > 0
95
+ ? [partial.major + 1, 0, 0]
96
+ : partial.minor !== undefined && partial.minor > 0
97
+ ? [0, partial.minor + 1, 0]
98
+ : [0, 0, (partial.patch ?? 0) + 1]
99
+ return compare(version, low) >= 0 && compare(version, high) < 0
100
+ }
101
+ case "~": {
102
+ const high =
103
+ partial.minor === undefined
104
+ ? [partial.major + 1, 0, 0]
105
+ : [partial.major, partial.minor + 1, 0]
106
+ return compare(version, low) >= 0 && compare(version, high) < 0
107
+ }
108
+ default: {
109
+ // A bare pin matches on the components it names: "20" is any 20.x.y.
110
+ const [major, minor, patch] = version
111
+ return (
112
+ major === partial.major &&
113
+ (partial.minor === undefined || minor === partial.minor) &&
114
+ (partial.patch === undefined || patch === partial.patch)
115
+ )
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Whether a Node version (`v24.12.0`) satisfies a pin as `.nvmrc`, `engines`
122
+ * or corepack write them; `undefined` when the pin is an alias or a form
123
+ * this cannot read, which is a reason to say so, not to fail.
124
+ */
125
+ export const nodeVersionSatisfies = (version: string, spec: string): boolean | undefined => {
126
+ const trimmed = spec.trim()
127
+ const have = partialOf(version)
128
+ if (have === undefined || isAlias(trimmed)) {
129
+ return undefined
130
+ }
131
+ const actual = triple(have)
132
+ let unknown = false
133
+ const clauses = trimmed.split("||").map((clause) => clause.trim())
134
+ for (const clause of clauses) {
135
+ const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(clause)
136
+ const tokens =
137
+ hyphen?.[1] !== undefined && hyphen[2] !== undefined
138
+ ? [`>=${hyphen[1]}`, `<=${hyphen[2]}`]
139
+ : clause.split(/\s+/).filter((token) => token.length > 0)
140
+ const verdicts = tokens.map((token) => tokenHolds(actual, token))
141
+ if (verdicts.some((verdict) => verdict === undefined)) {
142
+ unknown = true
143
+ continue
144
+ }
145
+ if (verdicts.every((verdict) => verdict === true)) {
146
+ return true
147
+ }
148
+ }
149
+ return unknown ? undefined : false
150
+ }
151
+
152
+ // ---- Reading the pins ------------------------------------------------------------------
153
+
154
+ const PackageEngines = Schema.fromJsonString(
155
+ Schema.Struct({
156
+ engines: Schema.optionalKey(Schema.Struct({ node: Schema.optionalKey(Schema.String) }))
157
+ })
158
+ )
159
+
160
+ const readOptional = (
161
+ files: PlainFileStoreShape,
162
+ path: string
163
+ ): Effect.Effect<string | undefined> =>
164
+ files.read(path).pipe(Effect.catch(() => Effect.succeed(undefined)))
165
+
166
+ const firstLine = (text: string): string =>
167
+ text
168
+ .split(/\r?\n/)
169
+ .map((line) => line.replace(/#.*$/, "").trim())
170
+ .find((line) => line.length > 0) ?? ""
171
+
172
+ /** The Node pins in an application directory: `.nvmrc`, `.node-version`, `engines.node`. */
173
+ export const nodePinsOf = (
174
+ files: PlainFileStoreShape,
175
+ appDir: string
176
+ ): Effect.Effect<ReadonlyArray<NodePin>> =>
177
+ Effect.gen(function* () {
178
+ const pins: Array<NodePin> = []
179
+ for (const source of [".nvmrc", ".node-version"]) {
180
+ const text = yield* readOptional(files, join(appDir, source))
181
+ const spec = text === undefined ? "" : firstLine(text)
182
+ if (spec.length > 0) {
183
+ pins.push(NodePin.make({ spec, source }))
184
+ }
185
+ }
186
+ const manifest = yield* readOptional(files, join(appDir, "package.json"))
187
+ if (manifest !== undefined) {
188
+ const engines = yield* Schema.decodeUnknownEffect(PackageEngines)(manifest).pipe(
189
+ Effect.map((parsed) => parsed.engines?.node?.trim()),
190
+ Effect.catch(() => Effect.succeed(undefined))
191
+ )
192
+ if (engines !== undefined && engines.length > 0) {
193
+ pins.push(NodePin.make({ spec: engines, source: "package.json engines.node" }))
194
+ }
195
+ }
196
+ return pins
197
+ })
198
+
199
+ /** pnpm's `use-node-version` from the application's `.npmrc`, when it carries one. */
200
+ export const pnpmNodeVersionOf = (
201
+ files: PlainFileStoreShape,
202
+ appDir: string
203
+ ): Effect.Effect<string | undefined> =>
204
+ Effect.map(readOptional(files, join(appDir, ".npmrc")), (text) => {
205
+ for (const line of (text ?? "").split(/\r?\n/)) {
206
+ const match = /^\s*use-node-version\s*=\s*(\S+)/.exec(line)
207
+ if (match?.[1] !== undefined) {
208
+ return match[1]
209
+ }
210
+ }
211
+ return undefined
212
+ })
213
+
214
+ // ---- The report ---------------------------------------------------------------------------
215
+
216
+ export type NodePreflightReport =
217
+ | { readonly _tag: "Off"; readonly summary: string }
218
+ | { readonly _tag: "NoPin"; readonly summary: string }
219
+ | { readonly _tag: "Managed"; readonly node: string; readonly summary: string }
220
+ | { readonly _tag: "NoNode"; readonly summary: string }
221
+ | {
222
+ readonly _tag: "Mismatch"
223
+ readonly node: string
224
+ readonly unmet: ReadonlyArray<NodePin>
225
+ readonly summary: string
226
+ }
227
+ | { readonly _tag: "Ok"; readonly node: string; readonly summary: string }
228
+
229
+ const switchedOff = (value: string | undefined): boolean =>
230
+ value !== undefined && ["off", "0", "false", "no"].includes(value.trim().toLowerCase())
231
+
232
+ const describePin = (pin: NodePin): string => `${pin.spec} (${pin.source})`
233
+
234
+ /**
235
+ * Compares the Node the gates would run on — `node --version` through the
236
+ * same executor and PATH the gates get — with the application's pins.
237
+ * `LLM4TS_NODE_CHECK=off` skips it.
238
+ */
239
+ export const nodePreflightReport = Effect.fn("@llm4ts/flow/NodePreflight.report")(function* (
240
+ process: ProcessExecutorShape,
241
+ files: PlainFileStoreShape,
242
+ appDir: string,
243
+ environment: Readonly<Record<string, string | undefined>>
244
+ ): Effect.fn.Return<NodePreflightReport> {
245
+ if (switchedOff(environment.LLM4TS_NODE_CHECK)) {
246
+ return { _tag: "Off", summary: "node: check off (LLM4TS_NODE_CHECK)" }
247
+ }
248
+ const pins = yield* nodePinsOf(files, appDir)
249
+ if (pins.length === 0) {
250
+ return {
251
+ _tag: "NoPin",
252
+ summary: `node: ${appDir} pins no version (.nvmrc, .node-version, package.json engines.node)`
253
+ }
254
+ }
255
+ const managed = yield* pnpmNodeVersionOf(files, appDir)
256
+ if (managed !== undefined) {
257
+ return {
258
+ _tag: "Managed",
259
+ node: managed,
260
+ summary: `node: pnpm runs the gates on Node ${managed} (use-node-version in .npmrc)`
261
+ }
262
+ }
263
+ const probed = yield* process.run(["node", "--version"], appDir, {}).pipe(
264
+ Effect.map((result) => (result.exitCode === 0 ? result.stdout.join("").trim() : undefined)),
265
+ Effect.catch(() => Effect.succeed(undefined))
266
+ )
267
+ if (probed === undefined || probed.length === 0) {
268
+ return {
269
+ _tag: "NoNode",
270
+ summary:
271
+ `node: not on the PATH the gates inherit, though ${appDir} pins ` +
272
+ `${pins.map(describePin).join(", ")}; launch llm4ts from a shell with node on PATH`
273
+ }
274
+ }
275
+ const unmet = pins.filter((pin) => nodeVersionSatisfies(probed, pin.spec) === false)
276
+ const unchecked = pins.filter((pin) => nodeVersionSatisfies(probed, pin.spec) === undefined)
277
+ if (unmet.length > 0) {
278
+ return {
279
+ _tag: "Mismatch",
280
+ node: probed,
281
+ unmet,
282
+ summary:
283
+ `node: the gates would run on Node ${probed} (the node on PATH), but ${appDir} pins ` +
284
+ `${unmet.map(describePin).join(", ")}. Launch llm4ts under a matching Node ` +
285
+ "(nvm use / fnm use), or add use-node-version=<x.y.z> to the application's .npmrc " +
286
+ "so pnpm fetches it, or set LLM4TS_NODE_CHECK=off to run anyway"
287
+ }
288
+ }
289
+ return {
290
+ _tag: "Ok",
291
+ node: probed,
292
+ summary:
293
+ `node: ${probed} satisfies ${pins
294
+ .filter((pin) => !unchecked.includes(pin))
295
+ .map(describePin)
296
+ .join(", ")}` +
297
+ (unchecked.length === 0
298
+ ? ""
299
+ : `; ${unchecked.map(describePin).join(", ")} not checked (an alias only a version manager resolves)`)
300
+ }
301
+ })
302
+
303
+ /** The report on the run's events, or the run aborted before its first gate. */
304
+ export const nodePreflight = Effect.fn("@llm4ts/flow/NodePreflight.nodePreflight")(function* (
305
+ process: ProcessExecutorShape,
306
+ files: PlainFileStoreShape,
307
+ events: FlowEventsShape,
308
+ appDir: string,
309
+ environment: Readonly<Record<string, string | undefined>>
310
+ ): Effect.fn.Return<void, FlowError> {
311
+ const report = yield* nodePreflightReport(process, files, appDir, environment)
312
+ if (report._tag === "Mismatch" || report._tag === "NoNode") {
313
+ return yield* FlowAborted.make({ message: report.summary })
314
+ }
315
+ yield* events.publish(Info.make({ message: report.summary }))
316
+ })
package/src/Timing.ts CHANGED
@@ -25,6 +25,10 @@ const TimedRole = Context.Reference<string | undefined>("@llm4ts/flow/Timing/Tim
25
25
  defaultValue: () => undefined
26
26
  })
27
27
 
28
+ /** The role the current call is made for: the caller's, or else `label`. */
29
+ export const roleOr = (label: string): Effect.Effect<string> =>
30
+ Effect.map(TimedRole, (role) => role ?? label)
31
+
28
32
  /** Times the seat calls `effect` makes under `role` instead of the seat's own label. */
29
33
  export const withTimedRole = <A, E, R>(role: string, effect: Effect.Effect<A, E, R>) =>
30
34
  Effect.provideService(effect, TimedRole, role)