@llm4ts/flow 0.1.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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/AzureDevOpsTool.d.ts +60 -0
- package/dist/AzureDevOpsTool.d.ts.map +1 -0
- package/dist/AzureDevOpsTool.js +163 -0
- package/dist/AzureDevOpsTool.js.map +1 -0
- package/dist/Bench.d.ts +133 -0
- package/dist/Bench.d.ts.map +1 -0
- package/dist/Bench.js +272 -0
- package/dist/Bench.js.map +1 -0
- package/dist/BenchReport.d.ts +9 -0
- package/dist/BenchReport.d.ts.map +1 -0
- package/dist/BenchReport.js +360 -0
- package/dist/BenchReport.js.map +1 -0
- package/dist/CapabilityGuard.d.ts +6 -0
- package/dist/CapabilityGuard.d.ts.map +1 -0
- package/dist/CapabilityGuard.js +25 -0
- package/dist/CapabilityGuard.js.map +1 -0
- package/dist/Chat.d.ts +15 -0
- package/dist/Chat.d.ts.map +1 -0
- package/dist/Chat.js +36 -0
- package/dist/Chat.js.map +1 -0
- package/dist/Classified.d.ts +14 -0
- package/dist/Classified.d.ts.map +1 -0
- package/dist/Classified.js +48 -0
- package/dist/Classified.js.map +1 -0
- package/dist/CostLedger.d.ts +55 -0
- package/dist/CostLedger.d.ts.map +1 -0
- package/dist/CostLedger.js +126 -0
- package/dist/CostLedger.js.map +1 -0
- package/dist/CostTracker.d.ts +13 -0
- package/dist/CostTracker.d.ts.map +1 -0
- package/dist/CostTracker.js +116 -0
- package/dist/CostTracker.js.map +1 -0
- package/dist/Equiv.d.ts +163 -0
- package/dist/Equiv.d.ts.map +1 -0
- package/dist/Equiv.js +256 -0
- package/dist/Equiv.js.map +1 -0
- package/dist/EquivReport.d.ts +16 -0
- package/dist/EquivReport.d.ts.map +1 -0
- package/dist/EquivReport.js +77 -0
- package/dist/EquivReport.js.map +1 -0
- package/dist/Flow.d.ts +25 -0
- package/dist/Flow.d.ts.map +1 -0
- package/dist/Flow.js +39 -0
- package/dist/Flow.js.map +1 -0
- package/dist/FlowContext.d.ts +23 -0
- package/dist/FlowContext.d.ts.map +1 -0
- package/dist/FlowContext.js +4 -0
- package/dist/FlowContext.js.map +1 -0
- package/dist/FlowError.d.ts +80 -0
- package/dist/FlowError.d.ts.map +1 -0
- package/dist/FlowError.js +92 -0
- package/dist/FlowError.js.map +1 -0
- package/dist/FlowEvents.d.ts +101 -0
- package/dist/FlowEvents.d.ts.map +1 -0
- package/dist/FlowEvents.js +103 -0
- package/dist/FlowEvents.js.map +1 -0
- package/dist/FlowRecorder.d.ts +26 -0
- package/dist/FlowRecorder.d.ts.map +1 -0
- package/dist/FlowRecorder.js +74 -0
- package/dist/FlowRecorder.js.map +1 -0
- package/dist/GitHubTool.d.ts +53 -0
- package/dist/GitHubTool.d.ts.map +1 -0
- package/dist/GitHubTool.js +191 -0
- package/dist/GitHubTool.js.map +1 -0
- package/dist/GitTool.d.ts +46 -0
- package/dist/GitTool.d.ts.map +1 -0
- package/dist/GitTool.js +97 -0
- package/dist/GitTool.js.map +1 -0
- package/dist/Interaction.d.ts +21 -0
- package/dist/Interaction.d.ts.map +1 -0
- package/dist/Interaction.js +9 -0
- package/dist/Interaction.js.map +1 -0
- package/dist/McpServer.d.ts +18 -0
- package/dist/McpServer.d.ts.map +1 -0
- package/dist/McpServer.js +122 -0
- package/dist/McpServer.js.map +1 -0
- package/dist/Mermaid.d.ts +7 -0
- package/dist/Mermaid.d.ts.map +1 -0
- package/dist/Mermaid.js +98 -0
- package/dist/Mermaid.js.map +1 -0
- package/dist/Pack.d.ts +40 -0
- package/dist/Pack.d.ts.map +1 -0
- package/dist/Pack.js +141 -0
- package/dist/Pack.js.map +1 -0
- package/dist/Package.d.ts +2 -0
- package/dist/Package.d.ts.map +1 -0
- package/dist/Package.js +2 -0
- package/dist/Package.js.map +1 -0
- package/dist/Persistence.d.ts +32 -0
- package/dist/Persistence.d.ts.map +1 -0
- package/dist/Persistence.js +85 -0
- package/dist/Persistence.js.map +1 -0
- package/dist/Plan.d.ts +26 -0
- package/dist/Plan.d.ts.map +1 -0
- package/dist/Plan.js +101 -0
- package/dist/Plan.js.map +1 -0
- package/dist/PlanExecution.d.ts +7 -0
- package/dist/PlanExecution.d.ts.map +1 -0
- package/dist/PlanExecution.js +19 -0
- package/dist/PlanExecution.js.map +1 -0
- package/dist/Planner.d.ts +32 -0
- package/dist/Planner.d.ts.map +1 -0
- package/dist/Planner.js +97 -0
- package/dist/Planner.js.map +1 -0
- package/dist/PrSummary.d.ts +15 -0
- package/dist/PrSummary.d.ts.map +1 -0
- package/dist/PrSummary.js +29 -0
- package/dist/PrSummary.js.map +1 -0
- package/dist/PriceList.d.ts +11 -0
- package/dist/PriceList.d.ts.map +1 -0
- package/dist/PriceList.js +79 -0
- package/dist/PriceList.js.map +1 -0
- package/dist/Provenance.d.ts +27 -0
- package/dist/Provenance.d.ts.map +1 -0
- package/dist/Provenance.js +38 -0
- package/dist/Provenance.js.map +1 -0
- package/dist/Replay.d.ts +29 -0
- package/dist/Replay.d.ts.map +1 -0
- package/dist/Replay.js +181 -0
- package/dist/Replay.js.map +1 -0
- package/dist/Review.d.ts +71 -0
- package/dist/Review.d.ts.map +1 -0
- package/dist/Review.js +204 -0
- package/dist/Review.js.map +1 -0
- package/dist/ReviewCache.d.ts +6 -0
- package/dist/ReviewCache.d.ts.map +1 -0
- package/dist/ReviewCache.js +28 -0
- package/dist/ReviewCache.js.map +1 -0
- package/dist/Reviewer.d.ts +12 -0
- package/dist/Reviewer.d.ts.map +1 -0
- package/dist/Reviewer.js +30 -0
- package/dist/Reviewer.js.map +1 -0
- package/dist/SpecChecks.d.ts +17 -0
- package/dist/SpecChecks.d.ts.map +1 -0
- package/dist/SpecChecks.js +107 -0
- package/dist/SpecChecks.js.map +1 -0
- package/dist/Survey.d.ts +32 -0
- package/dist/Survey.d.ts.map +1 -0
- package/dist/Survey.js +119 -0
- package/dist/Survey.js.map +1 -0
- package/dist/TransientRetry.d.ts +16 -0
- package/dist/TransientRetry.d.ts.map +1 -0
- package/dist/TransientRetry.js +102 -0
- package/dist/TransientRetry.js.map +1 -0
- package/dist/Workspace.d.ts +39 -0
- package/dist/Workspace.d.ts.map +1 -0
- package/dist/Workspace.js +192 -0
- package/dist/Workspace.js.map +1 -0
- package/dist/WorkspaceLayout.d.ts +3 -0
- package/dist/WorkspaceLayout.d.ts.map +1 -0
- package/dist/WorkspaceLayout.js +19 -0
- package/dist/WorkspaceLayout.js.map +1 -0
- package/dist/WorkspaceTools.d.ts +4 -0
- package/dist/WorkspaceTools.d.ts.map +1 -0
- package/dist/WorkspaceTools.js +130 -0
- package/dist/WorkspaceTools.js.map +1 -0
- package/package.json +91 -0
- package/src/AzureDevOpsTool.ts +291 -0
- package/src/Bench.ts +340 -0
- package/src/BenchReport.ts +447 -0
- package/src/CapabilityGuard.ts +43 -0
- package/src/Chat.ts +60 -0
- package/src/Classified.ts +67 -0
- package/src/CostLedger.ts +171 -0
- package/src/CostTracker.ts +170 -0
- package/src/Equiv.ts +378 -0
- package/src/EquivReport.ts +92 -0
- package/src/Flow.ts +72 -0
- package/src/FlowContext.ts +23 -0
- package/src/FlowError.ts +112 -0
- package/src/FlowEvents.ts +147 -0
- package/src/FlowRecorder.ts +116 -0
- package/src/GitHubTool.ts +315 -0
- package/src/GitTool.ts +214 -0
- package/src/Interaction.ts +23 -0
- package/src/McpServer.ts +168 -0
- package/src/Mermaid.ts +112 -0
- package/src/Pack.ts +213 -0
- package/src/Package.ts +1 -0
- package/src/Persistence.ts +198 -0
- package/src/Plan.ts +119 -0
- package/src/PlanExecution.ts +46 -0
- package/src/Planner.ts +134 -0
- package/src/PrSummary.ts +37 -0
- package/src/PriceList.ts +91 -0
- package/src/Provenance.ts +85 -0
- package/src/Replay.ts +265 -0
- package/src/Review.ts +337 -0
- package/src/ReviewCache.ts +45 -0
- package/src/Reviewer.ts +31 -0
- package/src/SpecChecks.ts +143 -0
- package/src/Survey.ts +144 -0
- package/src/TransientRetry.ts +238 -0
- package/src/Workspace.ts +255 -0
- package/src/WorkspaceLayout.ts +22 -0
- package/src/WorkspaceTools.ts +188 -0
package/src/Planner.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
4
|
+
import type { JsonSchema } from "@llm4ts/core/Models"
|
|
5
|
+
import { collect } from "@llm4ts/core/Streaming"
|
|
6
|
+
import { FlowLlmError } from "./FlowError.ts"
|
|
7
|
+
import { Plan } from "./Plan.ts"
|
|
8
|
+
|
|
9
|
+
export class Proceed extends Schema.Class<Proceed>("Proceed")({
|
|
10
|
+
kind: Schema.Literal("Proceed"),
|
|
11
|
+
value: Plan
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
export class Blocked extends Schema.Class<Blocked>("Blocked")({
|
|
15
|
+
kind: Schema.Literal("Blocked"),
|
|
16
|
+
reason: Schema.String
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
export const Verdict = Schema.Union([Proceed, Blocked])
|
|
20
|
+
export type Verdict = typeof Verdict.Type
|
|
21
|
+
|
|
22
|
+
export const planJsonSchema: JsonSchema = {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
epicId: { type: "string" },
|
|
26
|
+
tasks: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
title: { type: "string" },
|
|
32
|
+
description: { type: "string" },
|
|
33
|
+
completed: { type: "boolean" }
|
|
34
|
+
},
|
|
35
|
+
required: ["title", "description", "completed"]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
brief: { type: "string" }
|
|
39
|
+
},
|
|
40
|
+
required: ["epicId", "tasks"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const verdictJsonSchema: JsonSchema = {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
kind: { type: "string", enum: ["Proceed", "Blocked"] },
|
|
47
|
+
value: planJsonSchema,
|
|
48
|
+
reason: { type: "string" }
|
|
49
|
+
},
|
|
50
|
+
required: ["kind"]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const defaultPlanInstructions = [
|
|
54
|
+
"You are a planning assistant. Break the user's request into an ordered list of small,",
|
|
55
|
+
"independently implementable tasks. Each task must be a thin vertical slice with an observable",
|
|
56
|
+
"outcome, described in terms of behavior rather than mechanism. Choose a short kebab-case epicId.",
|
|
57
|
+
'Respond only with JSON: {"epicId":"kebab-case-id","tasks":',
|
|
58
|
+
'[{"title":"...","description":"...","completed":false}]}'
|
|
59
|
+
].join("\n")
|
|
60
|
+
|
|
61
|
+
export const planFrom = Effect.fn("@llm4ts/flow/Planner.from")(function* (
|
|
62
|
+
reasoning: LlmServiceShape,
|
|
63
|
+
prompt: string,
|
|
64
|
+
instructions = defaultPlanInstructions
|
|
65
|
+
): Effect.fn.Return<Plan, FlowLlmError> {
|
|
66
|
+
return yield* reasoning
|
|
67
|
+
.executeStructured(`${instructions}\n\nRequest:\n${prompt}`, Plan, planJsonSchema)
|
|
68
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const reviewPlanInstructions = [
|
|
72
|
+
"Review the draft implementation plan for correctness, completeness, simplicity, and concision.",
|
|
73
|
+
"Keep the same epicId unless it is clearly wrong. Return the complete improved plan, not a patch.",
|
|
74
|
+
'Respond only with JSON: {"epicId":"kebab-case-id","tasks":',
|
|
75
|
+
'[{"title":"...","description":"...","completed":false}]}'
|
|
76
|
+
].join("\n")
|
|
77
|
+
|
|
78
|
+
export const reviewPlan = Effect.fn("@llm4ts/flow/Planner.review")(function* (
|
|
79
|
+
reasoning: LlmServiceShape,
|
|
80
|
+
plan: Plan,
|
|
81
|
+
instructions = reviewPlanInstructions
|
|
82
|
+
): Effect.fn.Return<Plan, FlowLlmError> {
|
|
83
|
+
const improved = yield* reasoning
|
|
84
|
+
.executeStructured(`${instructions}\n\nDraft plan:\n${plan.render}`, Plan, planJsonSchema)
|
|
85
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
86
|
+
return Plan.make({
|
|
87
|
+
...improved,
|
|
88
|
+
...(plan.brief === undefined ? {} : { brief: plan.brief })
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const briefInstructions = [
|
|
93
|
+
"Write a concise codebase brief for an engineer about to implement this change:",
|
|
94
|
+
"relevant modules and file paths, important APIs and types, conventions, and build/test commands.",
|
|
95
|
+
"Explore the repository as needed. Do not restate the task list. Return plain prose."
|
|
96
|
+
].join("\n")
|
|
97
|
+
|
|
98
|
+
export const writeBrief = Effect.fn("@llm4ts/flow/Planner.brief")(function* (
|
|
99
|
+
reasoning: LlmServiceShape,
|
|
100
|
+
prompt: string,
|
|
101
|
+
instructions = briefInstructions
|
|
102
|
+
): Effect.fn.Return<string, FlowLlmError> {
|
|
103
|
+
const response = yield* collect(
|
|
104
|
+
reasoning.executeStream(`${instructions}\n\nChange request:\n${prompt}`)
|
|
105
|
+
).pipe(Effect.mapError(FlowLlmError.from))
|
|
106
|
+
return response.content.trim()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
export const briefPlan = Effect.fn("@llm4ts/flow/Planner.briefPlan")(function* (
|
|
110
|
+
reasoning: LlmServiceShape,
|
|
111
|
+
plan: Plan,
|
|
112
|
+
prompt: string,
|
|
113
|
+
instructions = briefInstructions
|
|
114
|
+
): Effect.fn.Return<Plan, FlowLlmError> {
|
|
115
|
+
const brief = yield* writeBrief(reasoning, prompt, instructions)
|
|
116
|
+
return Plan.make({ ...plan, brief })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export const assessThenPlanInstructions = [
|
|
120
|
+
"First decide whether this request is ready to implement.",
|
|
121
|
+
'Respond only with JSON using exactly one form: {"kind":"Blocked","reason":"why it cannot proceed"}',
|
|
122
|
+
'or {"kind":"Proceed","value":{"epicId":"kebab-id","tasks":',
|
|
123
|
+
'[{"title":"...","description":"...","completed":false}]}}.'
|
|
124
|
+
].join("\n")
|
|
125
|
+
|
|
126
|
+
export const assessThenPlan = Effect.fn("@llm4ts/flow/Planner.assessThenPlan")(function* (
|
|
127
|
+
reasoning: LlmServiceShape,
|
|
128
|
+
prompt: string,
|
|
129
|
+
instructions = assessThenPlanInstructions
|
|
130
|
+
): Effect.fn.Return<Verdict, FlowLlmError> {
|
|
131
|
+
return yield* reasoning
|
|
132
|
+
.executeStructured(`${instructions}\n\nRequest:\n${prompt}`, Verdict, verdictJsonSchema)
|
|
133
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
134
|
+
})
|
package/src/PrSummary.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
4
|
+
import type { JsonSchema } from "@llm4ts/core/Models"
|
|
5
|
+
import { FlowLlmError } from "./FlowError.ts"
|
|
6
|
+
|
|
7
|
+
export class PrSummary extends Schema.Class<PrSummary>("PrSummary")({
|
|
8
|
+
title: Schema.String,
|
|
9
|
+
body: Schema.String
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export const prSummaryJsonSchema: JsonSchema = {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
title: { type: "string" },
|
|
16
|
+
body: { type: "string" }
|
|
17
|
+
},
|
|
18
|
+
required: ["title", "body"]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const summarisePr = Effect.fn("@llm4ts/flow/PrSummary.summarise")(function* (
|
|
22
|
+
reasoning: LlmServiceShape,
|
|
23
|
+
diff: string,
|
|
24
|
+
context?: string
|
|
25
|
+
): Effect.fn.Return<PrSummary, FlowLlmError> {
|
|
26
|
+
const contextBlock =
|
|
27
|
+
context === undefined || context.length === 0 ? "" : `\nContext:\n${context}\n`
|
|
28
|
+
const prompt = [
|
|
29
|
+
'Summarise this change as a pull request. Respond only with JSON: {"title":"...","body":"..."}.',
|
|
30
|
+
contextBlock,
|
|
31
|
+
"Diff:",
|
|
32
|
+
diff
|
|
33
|
+
].join("\n")
|
|
34
|
+
return yield* reasoning
|
|
35
|
+
.executeStructured(prompt, PrSummary, prSummaryJsonSchema)
|
|
36
|
+
.pipe(Effect.mapError(FlowLlmError.from))
|
|
37
|
+
})
|
package/src/PriceList.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { TokenUsage } from "@llm4ts/core/Models"
|
|
2
|
+
|
|
3
|
+
export interface ModelRate {
|
|
4
|
+
readonly prefix: string
|
|
5
|
+
readonly inputUsdPerMillion: number
|
|
6
|
+
readonly outputUsdPerMillion: number
|
|
7
|
+
readonly cachedInputMultiplier: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const PricesAsOf = "2026-07"
|
|
11
|
+
|
|
12
|
+
export const ModelRates: ReadonlyArray<ModelRate> = Object.freeze([
|
|
13
|
+
{
|
|
14
|
+
prefix: "claude-fable-5",
|
|
15
|
+
inputUsdPerMillion: 10,
|
|
16
|
+
outputUsdPerMillion: 50,
|
|
17
|
+
cachedInputMultiplier: 0.1
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
prefix: "claude-opus-4",
|
|
21
|
+
inputUsdPerMillion: 15,
|
|
22
|
+
outputUsdPerMillion: 75,
|
|
23
|
+
cachedInputMultiplier: 0.1
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
prefix: "claude-sonnet-4",
|
|
27
|
+
inputUsdPerMillion: 3,
|
|
28
|
+
outputUsdPerMillion: 15,
|
|
29
|
+
cachedInputMultiplier: 0.1
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
prefix: "claude-haiku-4",
|
|
33
|
+
inputUsdPerMillion: 1,
|
|
34
|
+
outputUsdPerMillion: 5,
|
|
35
|
+
cachedInputMultiplier: 0.1
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
prefix: "gemini-2.5-pro",
|
|
39
|
+
inputUsdPerMillion: 1.25,
|
|
40
|
+
outputUsdPerMillion: 10,
|
|
41
|
+
cachedInputMultiplier: 0.1
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
prefix: "gemini-2.5-flash",
|
|
45
|
+
inputUsdPerMillion: 0.3,
|
|
46
|
+
outputUsdPerMillion: 2.5,
|
|
47
|
+
cachedInputMultiplier: 0.1
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
prefix: "gpt-5.5",
|
|
51
|
+
inputUsdPerMillion: 5,
|
|
52
|
+
outputUsdPerMillion: 30,
|
|
53
|
+
cachedInputMultiplier: 0.1
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
prefix: "grok-4.5",
|
|
57
|
+
inputUsdPerMillion: 2,
|
|
58
|
+
outputUsdPerMillion: 6,
|
|
59
|
+
cachedInputMultiplier: 0.1
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
prefix: "grok-4.3",
|
|
63
|
+
inputUsdPerMillion: 1.25,
|
|
64
|
+
outputUsdPerMillion: 2.5,
|
|
65
|
+
cachedInputMultiplier: 0.1
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
prefix: "grok-4.20",
|
|
69
|
+
inputUsdPerMillion: 1.25,
|
|
70
|
+
outputUsdPerMillion: 2.5,
|
|
71
|
+
cachedInputMultiplier: 0.1
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
prefix: "grok-build-0.1",
|
|
75
|
+
inputUsdPerMillion: 1,
|
|
76
|
+
outputUsdPerMillion: 2,
|
|
77
|
+
cachedInputMultiplier: 0.1
|
|
78
|
+
}
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
export const estimateCostUsd = (model: string, usage: TokenUsage): number | undefined => {
|
|
82
|
+
const rate = ModelRates.find(({ prefix }) => model.startsWith(prefix))
|
|
83
|
+
if (rate === undefined) {
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
(usage.prompt / 1_000_000) * rate.inputUsdPerMillion +
|
|
88
|
+
(usage.completion / 1_000_000) * rate.outputUsdPerMillion +
|
|
89
|
+
((usage.cached ?? 0) / 1_000_000) * rate.inputUsdPerMillion * rate.cachedInputMultiplier
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import { PersistenceError, PlanParseError, type FlowError } from "./FlowError.ts"
|
|
4
|
+
import type { PlainFileStoreShape } from "./Persistence.ts"
|
|
5
|
+
|
|
6
|
+
export class Provenance extends Schema.Class<Provenance>("Provenance")({
|
|
7
|
+
schema: Schema.Int,
|
|
8
|
+
pack: Schema.String,
|
|
9
|
+
llm4tsVersion: Schema.String,
|
|
10
|
+
createdAt: Schema.String,
|
|
11
|
+
approvedBy: Schema.optionalKey(Schema.String),
|
|
12
|
+
seats: Schema.Record(Schema.String, Schema.String),
|
|
13
|
+
specs: Schema.Record(Schema.String, Schema.String),
|
|
14
|
+
gateVerdicts: Schema.Record(Schema.String, Schema.String),
|
|
15
|
+
equivalenceReport: Schema.optionalKey(Schema.String),
|
|
16
|
+
fixSpecs: Schema.Array(Schema.String)
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
const join = (root: string, path: string): string =>
|
|
20
|
+
`${root.replace(/[\\/]+$/, "")}/${path.replace(/^[\\/]+/, "")}`
|
|
21
|
+
|
|
22
|
+
export interface ProvenanceStoreShape {
|
|
23
|
+
readonly write: (path: string, provenance: Provenance) => Effect.Effect<void, FlowError>
|
|
24
|
+
readonly load: (path: string) => Effect.Effect<Provenance, FlowError>
|
|
25
|
+
readonly extend: (
|
|
26
|
+
path: string,
|
|
27
|
+
update: (current: Provenance) => Provenance
|
|
28
|
+
) => Effect.Effect<Provenance, FlowError>
|
|
29
|
+
readonly hashFiles: (
|
|
30
|
+
root: string,
|
|
31
|
+
paths: ReadonlyArray<string>
|
|
32
|
+
) => Effect.Effect<Readonly<Record<string, string>>, PersistenceError>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const makeProvenanceStore = (files: PlainFileStoreShape): ProvenanceStoreShape => {
|
|
36
|
+
const write = (path: string, provenance: Provenance): Effect.Effect<void, FlowError> =>
|
|
37
|
+
Schema.encodeEffect(Schema.fromJsonString(Provenance))(provenance).pipe(
|
|
38
|
+
Effect.mapError((error) =>
|
|
39
|
+
PlanParseError.make({
|
|
40
|
+
message: `failed to encode provenance manifest ${path}: ${String(error)}`
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
Effect.flatMap((contents) => files.writeAtomic(path, `${contents}\n`))
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const load = (path: string): Effect.Effect<Provenance, FlowError> =>
|
|
47
|
+
Effect.flatMap(
|
|
48
|
+
files.read(path),
|
|
49
|
+
(contents): Effect.Effect<Provenance, FlowError> =>
|
|
50
|
+
contents === undefined
|
|
51
|
+
? Effect.fail(
|
|
52
|
+
PersistenceError.make({
|
|
53
|
+
message: `provenance manifest does not exist: ${path}`
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
: Schema.decodeUnknownEffect(Schema.fromJsonString(Provenance))(contents).pipe(
|
|
57
|
+
Effect.mapError((error) =>
|
|
58
|
+
PlanParseError.make({
|
|
59
|
+
message: `malformed provenance manifest ${path}: ${String(error)}`
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const extend = (
|
|
66
|
+
path: string,
|
|
67
|
+
update: (current: Provenance) => Provenance
|
|
68
|
+
): Effect.Effect<Provenance, FlowError> =>
|
|
69
|
+
Effect.flatMap(load(path), (current) => {
|
|
70
|
+
const updated = update(current)
|
|
71
|
+
return write(path, updated).pipe(Effect.as(updated))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const hashFiles = (
|
|
75
|
+
root: string,
|
|
76
|
+
paths: ReadonlyArray<string>
|
|
77
|
+
): Effect.Effect<Readonly<Record<string, string>>, PersistenceError> =>
|
|
78
|
+
Effect.forEach(paths, (path) =>
|
|
79
|
+
files
|
|
80
|
+
.hashSha256(join(root, path))
|
|
81
|
+
.pipe(Effect.map((hash): readonly [path: string, hash: string] => [path, hash]))
|
|
82
|
+
).pipe(Effect.map(Object.fromEntries))
|
|
83
|
+
|
|
84
|
+
return { write, load, extend, hashFiles }
|
|
85
|
+
}
|
package/src/Replay.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Ref from "effect/Ref"
|
|
3
|
+
import * as Schema from "effect/Schema"
|
|
4
|
+
import * as Stream from "effect/Stream"
|
|
5
|
+
import { InvalidRequestError, ParseError, ProviderError } from "@llm4ts/core/Errors"
|
|
6
|
+
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
7
|
+
import { LlmChunk, TokenUsage } from "@llm4ts/core/Models"
|
|
8
|
+
import { collect } from "@llm4ts/core/Streaming"
|
|
9
|
+
import { PlanParseError, UnsupportedSchemaVersion, type FlowError } from "./FlowError.ts"
|
|
10
|
+
import { TraceLine } from "./FlowRecorder.ts"
|
|
11
|
+
import type { PlainFileStoreShape } from "./Persistence.ts"
|
|
12
|
+
|
|
13
|
+
export const CurrentTraceSchema = 1
|
|
14
|
+
|
|
15
|
+
export class ReplaySuccess extends Schema.TaggedClass<ReplaySuccess>()("Success", {
|
|
16
|
+
text: Schema.String,
|
|
17
|
+
usage: Schema.optionalKey(TokenUsage),
|
|
18
|
+
model: Schema.optionalKey(Schema.String)
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
export class ReplayFailure extends Schema.TaggedClass<ReplayFailure>()("Failure", {
|
|
22
|
+
message: Schema.String,
|
|
23
|
+
model: Schema.optionalKey(Schema.String)
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export const ReplayTurn = Schema.Union([ReplaySuccess, ReplayFailure])
|
|
27
|
+
export type ReplayTurn = typeof ReplayTurn.Type
|
|
28
|
+
|
|
29
|
+
const stringField = (
|
|
30
|
+
fields: Readonly<Record<string, unknown>>,
|
|
31
|
+
key: string
|
|
32
|
+
): string | undefined => {
|
|
33
|
+
const value = fields[key]
|
|
34
|
+
return typeof value === "string" ? value : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const integerField = (
|
|
38
|
+
fields: Readonly<Record<string, unknown>>,
|
|
39
|
+
key: string
|
|
40
|
+
): number | undefined => {
|
|
41
|
+
const value = stringField(fields, key)
|
|
42
|
+
if (value === undefined || !/^-?\d+$/.test(value)) {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
const parsed = Number(value)
|
|
46
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isSafeInteger = (value: unknown): value is number =>
|
|
50
|
+
typeof value === "number" && Number.isSafeInteger(value)
|
|
51
|
+
|
|
52
|
+
const flowEventFields = (line: TraceLine): Readonly<Record<string, unknown>> => {
|
|
53
|
+
const event = line.fields.event
|
|
54
|
+
if (typeof event !== "string") {
|
|
55
|
+
return line.fields
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const decoded: unknown = JSON.parse(event)
|
|
59
|
+
if (typeof decoded !== "object" || decoded === null) {
|
|
60
|
+
return line.fields
|
|
61
|
+
}
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.keys(decoded).map((key): readonly [string, unknown] => [
|
|
64
|
+
key,
|
|
65
|
+
Reflect.get(decoded, key)
|
|
66
|
+
])
|
|
67
|
+
)
|
|
68
|
+
} catch {
|
|
69
|
+
return line.fields
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const segmentReplayTurns = (lines: ReadonlyArray<TraceLine>): ReadonlyArray<ReplayTurn> => {
|
|
74
|
+
let pendingUsage: TokenUsage | undefined
|
|
75
|
+
let pendingModel: string | undefined
|
|
76
|
+
const turns: Array<ReplayTurn> = []
|
|
77
|
+
|
|
78
|
+
for (const line of [...lines].sort((left, right) => left.seq - right.seq)) {
|
|
79
|
+
const fields = flowEventFields(line)
|
|
80
|
+
switch (line.kind) {
|
|
81
|
+
case "TokensUsed": {
|
|
82
|
+
const usageValue =
|
|
83
|
+
typeof fields.usage === "object" && fields.usage !== null
|
|
84
|
+
? {
|
|
85
|
+
prompt: Reflect.get(fields.usage, "prompt"),
|
|
86
|
+
completion: Reflect.get(fields.usage, "completion"),
|
|
87
|
+
total: Reflect.get(fields.usage, "total"),
|
|
88
|
+
cached: Reflect.get(fields.usage, "cached")
|
|
89
|
+
}
|
|
90
|
+
: fields
|
|
91
|
+
const prompt =
|
|
92
|
+
typeof usageValue.prompt === "number" ? usageValue.prompt : integerField(fields, "prompt")
|
|
93
|
+
const completion =
|
|
94
|
+
typeof usageValue.completion === "number"
|
|
95
|
+
? usageValue.completion
|
|
96
|
+
: integerField(fields, "completion")
|
|
97
|
+
const total =
|
|
98
|
+
typeof usageValue.total === "number" ? usageValue.total : integerField(fields, "total")
|
|
99
|
+
const cached = typeof usageValue.cached === "number" ? usageValue.cached : undefined
|
|
100
|
+
if (isSafeInteger(prompt) && isSafeInteger(completion) && isSafeInteger(total)) {
|
|
101
|
+
pendingUsage = TokenUsage.make({
|
|
102
|
+
prompt,
|
|
103
|
+
completion,
|
|
104
|
+
total,
|
|
105
|
+
...(cached === undefined ? {} : { cached })
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
pendingModel = stringField(fields, "model") ?? pendingModel
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
case "AssistantMessage":
|
|
112
|
+
turns.push(
|
|
113
|
+
ReplaySuccess.make({
|
|
114
|
+
text: stringField(fields, "text") ?? "",
|
|
115
|
+
...(pendingUsage === undefined ? {} : { usage: pendingUsage }),
|
|
116
|
+
...(pendingModel === undefined ? {} : { model: pendingModel })
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
pendingUsage = undefined
|
|
120
|
+
pendingModel = undefined
|
|
121
|
+
break
|
|
122
|
+
case "StreamError": {
|
|
123
|
+
const model = stringField(fields, "model")
|
|
124
|
+
turns.push(
|
|
125
|
+
ReplayFailure.make({
|
|
126
|
+
message: stringField(fields, "message") ?? "",
|
|
127
|
+
...(model === undefined ? {} : { model })
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
pendingUsage = undefined
|
|
131
|
+
pendingModel = undefined
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return turns
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const traceCodec = Schema.fromJsonString(TraceLine)
|
|
140
|
+
|
|
141
|
+
export const readTrace = Effect.fn("@llm4ts/flow/Replay.readTrace")(function* (
|
|
142
|
+
files: PlainFileStoreShape,
|
|
143
|
+
path: string
|
|
144
|
+
): Effect.fn.Return<ReadonlyArray<TraceLine>, FlowError> {
|
|
145
|
+
const contents = yield* files.read(path)
|
|
146
|
+
if (contents === undefined) {
|
|
147
|
+
return yield* PlanParseError.make({
|
|
148
|
+
message: `trace does not exist: ${path}`
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
const lines = contents
|
|
152
|
+
.split(/\r?\n/u)
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.filter((line) => line.length > 0)
|
|
155
|
+
const decoded = yield* Effect.forEach(
|
|
156
|
+
lines,
|
|
157
|
+
(line, index) =>
|
|
158
|
+
Schema.decodeUnknownEffect(traceCodec)(line).pipe(
|
|
159
|
+
Effect.mapError((error) =>
|
|
160
|
+
PlanParseError.make({
|
|
161
|
+
message: `malformed trace at ${path}:${index + 1}: ${String(error)}`
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
),
|
|
165
|
+
{ concurrency: 1 }
|
|
166
|
+
)
|
|
167
|
+
for (const line of decoded) {
|
|
168
|
+
const actual = line.schemaVersion ?? 1
|
|
169
|
+
if (actual !== CurrentTraceSchema) {
|
|
170
|
+
return yield* UnsupportedSchemaVersion.make({
|
|
171
|
+
path,
|
|
172
|
+
expected: CurrentTraceSchema,
|
|
173
|
+
actual
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return [...decoded].sort((left, right) => left.seq - right.seq)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
export const makeReplayConnector = Effect.fn("@llm4ts/flow/Replay.makeConnector")(function* (
|
|
181
|
+
turns: ReadonlyArray<ReplayTurn>
|
|
182
|
+
): Effect.fn.Return<LlmServiceShape> {
|
|
183
|
+
const cursor = yield* Ref.make(0)
|
|
184
|
+
|
|
185
|
+
const next = Stream.unwrap(
|
|
186
|
+
Ref.getAndUpdate(cursor, (index) => index + 1).pipe(
|
|
187
|
+
Effect.map((index) => {
|
|
188
|
+
const turn = turns[index]
|
|
189
|
+
if (turn === undefined) {
|
|
190
|
+
return Stream.fail(
|
|
191
|
+
ProviderError.make({
|
|
192
|
+
message: `replay trace exhausted at turn ${index}`
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
if (turn._tag === "Failure") {
|
|
197
|
+
return Stream.fail(ProviderError.make({ message: turn.message }))
|
|
198
|
+
}
|
|
199
|
+
return Stream.succeed(
|
|
200
|
+
LlmChunk.make({
|
|
201
|
+
delta: turn.text,
|
|
202
|
+
finishReason: "stop",
|
|
203
|
+
...(turn.usage === undefined ? {} : { usage: turn.usage }),
|
|
204
|
+
metadata: {
|
|
205
|
+
provider: "replay",
|
|
206
|
+
...(turn.model === undefined ? {} : { model: turn.model })
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
executeStream: (_prompt) => next,
|
|
216
|
+
executeStreamWithHistory: (_messages) => next,
|
|
217
|
+
executeWithTools: (_prompt, _tools) =>
|
|
218
|
+
Effect.fail(
|
|
219
|
+
InvalidRequestError.make({
|
|
220
|
+
message: "replay does not support tool calling"
|
|
221
|
+
})
|
|
222
|
+
),
|
|
223
|
+
executeStructured: <A, E, RD, RE>(
|
|
224
|
+
_prompt: string,
|
|
225
|
+
schema: Schema.ConstraintCodec<A, E, RD, RE>
|
|
226
|
+
) =>
|
|
227
|
+
collect(next).pipe(
|
|
228
|
+
Effect.flatMap((response) =>
|
|
229
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(response.content)
|
|
230
|
+
),
|
|
231
|
+
Effect.mapError((error) =>
|
|
232
|
+
ParseError.make({
|
|
233
|
+
message: `replay structured parse error: ${String(error)}`,
|
|
234
|
+
raw: error instanceof ParseError ? error.raw : ""
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
),
|
|
238
|
+
executeStructuredWithUsage: <A, E, RD, RE>(
|
|
239
|
+
_prompt: string,
|
|
240
|
+
schema: Schema.ConstraintCodec<A, E, RD, RE>
|
|
241
|
+
) =>
|
|
242
|
+
collect(next).pipe(
|
|
243
|
+
Effect.flatMap((response) =>
|
|
244
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(response.content).pipe(
|
|
245
|
+
Effect.map((value) => [value, response.usage, response.metadata.model] as const)
|
|
246
|
+
)
|
|
247
|
+
),
|
|
248
|
+
Effect.mapError((error) =>
|
|
249
|
+
ParseError.make({
|
|
250
|
+
message: `replay structured parse error: ${String(error)}`,
|
|
251
|
+
raw: ""
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
),
|
|
255
|
+
isAvailable: Effect.succeed(true)
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
export const replayFromTrace = Effect.fn("@llm4ts/flow/Replay.fromTrace")(function* (
|
|
260
|
+
files: PlainFileStoreShape,
|
|
261
|
+
path: string
|
|
262
|
+
): Effect.fn.Return<LlmServiceShape, FlowError> {
|
|
263
|
+
const lines = yield* readTrace(files, path)
|
|
264
|
+
return yield* makeReplayConnector(segmentReplayTurns(lines))
|
|
265
|
+
})
|