@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/GitTool.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import { Capabilities, type Capability } from "@llm4ts/core/Capability"
|
|
4
|
+
import type { ProcessExecutorShape, ProcessResult } from "@llm4ts/core/ProcessExecutor"
|
|
5
|
+
import { ProcessError, type FlowError } from "./FlowError.ts"
|
|
6
|
+
import type { FlowEventsShape } from "./FlowEvents.ts"
|
|
7
|
+
import { guarded } from "./CapabilityGuard.ts"
|
|
8
|
+
|
|
9
|
+
export class BranchCreated extends Schema.TaggedClass<BranchCreated>()("Created", {}) {}
|
|
10
|
+
export class BranchAlreadyExists extends Schema.TaggedClass<BranchAlreadyExists>()(
|
|
11
|
+
"AlreadyExists",
|
|
12
|
+
{}
|
|
13
|
+
) {}
|
|
14
|
+
export const CreateBranch = Schema.Union([BranchCreated, BranchAlreadyExists])
|
|
15
|
+
export type CreateBranch = typeof CreateBranch.Type
|
|
16
|
+
|
|
17
|
+
export class Committed extends Schema.TaggedClass<Committed>()("Committed", {}) {}
|
|
18
|
+
export class NothingToCommit extends Schema.TaggedClass<NothingToCommit>()("NothingToCommit", {}) {}
|
|
19
|
+
export const CommitResult = Schema.Union([Committed, NothingToCommit])
|
|
20
|
+
export type CommitResult = typeof CommitResult.Type
|
|
21
|
+
|
|
22
|
+
export interface GitToolShape {
|
|
23
|
+
readonly init: Effect.Effect<void, FlowError>
|
|
24
|
+
readonly initBare: Effect.Effect<void, FlowError>
|
|
25
|
+
readonly config: (key: string, value: string) => Effect.Effect<void, FlowError>
|
|
26
|
+
readonly status: Effect.Effect<string, FlowError>
|
|
27
|
+
readonly currentBranch: Effect.Effect<string, FlowError>
|
|
28
|
+
readonly diff: Effect.Effect<string, FlowError>
|
|
29
|
+
readonly diffAll: Effect.Effect<string, FlowError>
|
|
30
|
+
readonly defaultBase: Effect.Effect<string, FlowError>
|
|
31
|
+
readonly diffVsBase: (base: string, threeDot?: boolean) => Effect.Effect<string, FlowError>
|
|
32
|
+
readonly changedFilesVsBase: (
|
|
33
|
+
base: string,
|
|
34
|
+
threeDot?: boolean
|
|
35
|
+
) => Effect.Effect<ReadonlyArray<string>, FlowError>
|
|
36
|
+
readonly addRemote: (name: string, url: string) => Effect.Effect<void, FlowError>
|
|
37
|
+
readonly checkout: (name: string) => Effect.Effect<void, FlowError>
|
|
38
|
+
readonly checkoutOrCreate: (name: string) => Effect.Effect<void, FlowError>
|
|
39
|
+
readonly createBranch: (name: string) => Effect.Effect<CreateBranch, FlowError>
|
|
40
|
+
readonly commitAll: (message: string) => Effect.Effect<CommitResult, FlowError>
|
|
41
|
+
readonly push: (remote: string, branch: string) => Effect.Effect<void, FlowError>
|
|
42
|
+
readonly checkpoint: Effect.Effect<string, FlowError>
|
|
43
|
+
readonly rollback: (checkpoint: string) => Effect.Effect<void, FlowError>
|
|
44
|
+
readonly addWorktree: (path: string, branch: string) => Effect.Effect<void, FlowError>
|
|
45
|
+
readonly removeWorktree: (path: string) => Effect.Effect<void, FlowError>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nonInteractiveEnvironment = Object.freeze({
|
|
49
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
50
|
+
GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const text = (lines: ReadonlyArray<string>): string => lines.join("\n").trim()
|
|
54
|
+
|
|
55
|
+
const problem = (result: ProcessResult): string => {
|
|
56
|
+
const detail = text([...result.stdout, ...result.stderr])
|
|
57
|
+
return detail.length === 0 ? `process exited with code ${result.exitCode}` : detail
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const makeGitTool = (
|
|
61
|
+
process: ProcessExecutorShape,
|
|
62
|
+
workDir: string,
|
|
63
|
+
events: FlowEventsShape
|
|
64
|
+
): GitToolShape => {
|
|
65
|
+
const run = (args: ReadonlyArray<string>): Effect.Effect<ProcessResult, FlowError> =>
|
|
66
|
+
process.run(["git", ...args], workDir, nonInteractiveEnvironment).pipe(
|
|
67
|
+
Effect.mapError((error) =>
|
|
68
|
+
ProcessError.make({
|
|
69
|
+
message: `git ${args.join(" ")}`,
|
|
70
|
+
detail: error.message
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const runOrFail = (args: ReadonlyArray<string>): Effect.Effect<string, FlowError> =>
|
|
76
|
+
Effect.flatMap(run(args), (result) =>
|
|
77
|
+
result.exitCode === 0
|
|
78
|
+
? Effect.succeed(text(result.stdout))
|
|
79
|
+
: Effect.fail(
|
|
80
|
+
ProcessError.make({
|
|
81
|
+
message: `git ${args.join(" ")}`,
|
|
82
|
+
detail: problem(result)
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const gate = <A>(
|
|
88
|
+
capability: Capability,
|
|
89
|
+
operation: string,
|
|
90
|
+
effect: Effect.Effect<A, FlowError>
|
|
91
|
+
): Effect.Effect<A, FlowError> => guarded(capability, operation, events, effect)
|
|
92
|
+
|
|
93
|
+
const write = <A>(
|
|
94
|
+
operation: string,
|
|
95
|
+
effect: Effect.Effect<A, FlowError>
|
|
96
|
+
): Effect.Effect<A, FlowError> => gate(Capabilities.GitWrite, operation, effect)
|
|
97
|
+
|
|
98
|
+
const read = <A>(
|
|
99
|
+
operation: string,
|
|
100
|
+
effect: Effect.Effect<A, FlowError>
|
|
101
|
+
): Effect.Effect<A, FlowError> => gate(Capabilities.GitRead, operation, effect)
|
|
102
|
+
|
|
103
|
+
const createBranch = (name: string): Effect.Effect<CreateBranch, FlowError> =>
|
|
104
|
+
write(
|
|
105
|
+
"git checkout -b",
|
|
106
|
+
Effect.flatMap(
|
|
107
|
+
run(["checkout", "-b", name]),
|
|
108
|
+
(result): Effect.Effect<CreateBranch, FlowError> =>
|
|
109
|
+
result.exitCode === 0
|
|
110
|
+
? Effect.succeed(new BranchCreated())
|
|
111
|
+
: text(result.stderr).includes("already exists")
|
|
112
|
+
? Effect.succeed(new BranchAlreadyExists())
|
|
113
|
+
: Effect.fail(
|
|
114
|
+
ProcessError.make({
|
|
115
|
+
message: `git checkout -b ${name}`,
|
|
116
|
+
detail: problem(result)
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const checkout = (name: string): Effect.Effect<void, FlowError> =>
|
|
123
|
+
write("git checkout", runOrFail(["checkout", name]).pipe(Effect.asVoid))
|
|
124
|
+
|
|
125
|
+
const addAll = runOrFail(["add", "-A"]).pipe(Effect.asVoid)
|
|
126
|
+
|
|
127
|
+
const commitAllEffect = (message: string): Effect.Effect<CommitResult, FlowError> =>
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
yield* addAll
|
|
130
|
+
const result = yield* run(["commit", "-m", message])
|
|
131
|
+
if (result.exitCode === 0) {
|
|
132
|
+
return new Committed()
|
|
133
|
+
}
|
|
134
|
+
if (problem(result).includes("nothing to commit")) {
|
|
135
|
+
return new NothingToCommit()
|
|
136
|
+
}
|
|
137
|
+
return yield* ProcessError.make({
|
|
138
|
+
message: "git commit",
|
|
139
|
+
detail: problem(result)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
init: write(
|
|
145
|
+
"git init",
|
|
146
|
+
runOrFail(["-c", "init.defaultBranch=main", "init"]).pipe(Effect.asVoid)
|
|
147
|
+
),
|
|
148
|
+
initBare: write(
|
|
149
|
+
"git init --bare",
|
|
150
|
+
runOrFail(["-c", "init.defaultBranch=main", "init", "--bare"]).pipe(Effect.asVoid)
|
|
151
|
+
),
|
|
152
|
+
config: (key, value) =>
|
|
153
|
+
write("git config", runOrFail(["config", key, value]).pipe(Effect.asVoid)),
|
|
154
|
+
status: read("git status", runOrFail(["status", "--short"])),
|
|
155
|
+
currentBranch: read("git currentBranch", runOrFail(["rev-parse", "--abbrev-ref", "HEAD"])),
|
|
156
|
+
diff: read("git diff", runOrFail(["diff"])),
|
|
157
|
+
diffAll: read(
|
|
158
|
+
"git diffAll",
|
|
159
|
+
runOrFail(["add", "--intent-to-add", "-A"]).pipe(Effect.andThen(runOrFail(["diff"])))
|
|
160
|
+
),
|
|
161
|
+
defaultBase: read(
|
|
162
|
+
"git defaultBase",
|
|
163
|
+
Effect.flatMap(run(["symbolic-ref", "refs/remotes/origin/HEAD"]), (symbolic) => {
|
|
164
|
+
const value = text(symbolic.stdout)
|
|
165
|
+
if (symbolic.exitCode === 0 && value.length > 0) {
|
|
166
|
+
return Effect.succeed(value.replace(/^refs\/remotes\//, ""))
|
|
167
|
+
}
|
|
168
|
+
return Effect.flatMap(run(["rev-parse", "--verify", "--quiet", "origin/main"]), (main) =>
|
|
169
|
+
main.exitCode === 0
|
|
170
|
+
? Effect.succeed("origin/main")
|
|
171
|
+
: run(["rev-parse", "--verify", "--quiet", "origin/master"]).pipe(
|
|
172
|
+
Effect.map((master) => (master.exitCode === 0 ? "origin/master" : "main"))
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
),
|
|
177
|
+
diffVsBase: (base, threeDot = true) =>
|
|
178
|
+
read("git diffVsBase", runOrFail(["diff", `${base}${threeDot ? "..." : ".."}HEAD`])),
|
|
179
|
+
changedFilesVsBase: (base, threeDot = true) =>
|
|
180
|
+
read(
|
|
181
|
+
"git changedFilesVsBase",
|
|
182
|
+
runOrFail(["diff", "--name-only", `${base}${threeDot ? "..." : ".."}HEAD`]).pipe(
|
|
183
|
+
Effect.map((output) =>
|
|
184
|
+
output
|
|
185
|
+
.split(/\r?\n/)
|
|
186
|
+
.map((line) => line.trim())
|
|
187
|
+
.filter((line) => line.length > 0)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
),
|
|
191
|
+
addRemote: (name, url) =>
|
|
192
|
+
write("git addRemote", runOrFail(["remote", "add", name, url]).pipe(Effect.asVoid)),
|
|
193
|
+
checkout,
|
|
194
|
+
createBranch,
|
|
195
|
+
checkoutOrCreate: (name) =>
|
|
196
|
+
Effect.flatMap(createBranch(name), (created) =>
|
|
197
|
+
created._tag === "Created" ? Effect.void : checkout(name)
|
|
198
|
+
),
|
|
199
|
+
commitAll: (message) => write("git commitAll", commitAllEffect(message)),
|
|
200
|
+
push: (remote, branch) =>
|
|
201
|
+
gate(
|
|
202
|
+
Capabilities.GitPush,
|
|
203
|
+
"git push",
|
|
204
|
+
runOrFail(["push", "-u", remote, branch]).pipe(Effect.asVoid)
|
|
205
|
+
),
|
|
206
|
+
checkpoint: read("git checkpoint", runOrFail(["rev-parse", "HEAD"])),
|
|
207
|
+
rollback: (checkpoint) =>
|
|
208
|
+
write("git rollback", runOrFail(["reset", "--hard", checkpoint]).pipe(Effect.asVoid)),
|
|
209
|
+
addWorktree: (path, branch) =>
|
|
210
|
+
write("git worktree add", runOrFail(["worktree", "add", path, branch]).pipe(Effect.asVoid)),
|
|
211
|
+
removeWorktree: (path) =>
|
|
212
|
+
write("git worktree remove", runOrFail(["worktree", "remove", path]).pipe(Effect.asVoid))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { FlowError } from "./FlowError.ts"
|
|
4
|
+
|
|
5
|
+
export interface InteractionShape {
|
|
6
|
+
readonly ask: (question: string) => Effect.Effect<string, FlowError>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ApprovalAllowed extends Schema.TaggedClass<ApprovalAllowed>()("Allow", {}) {}
|
|
10
|
+
|
|
11
|
+
export class ApprovalDenied extends Schema.TaggedClass<ApprovalDenied>()("Deny", {
|
|
12
|
+
reason: Schema.String
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export const ApprovalDecision = Schema.Union([ApprovalAllowed, ApprovalDenied])
|
|
16
|
+
export type ApprovalDecision = typeof ApprovalDecision.Type
|
|
17
|
+
|
|
18
|
+
export interface ApprovalPolicy {
|
|
19
|
+
readonly decide: (
|
|
20
|
+
toolName: string,
|
|
21
|
+
input: Schema.Json
|
|
22
|
+
) => Effect.Effect<ApprovalDecision, FlowError>
|
|
23
|
+
}
|
package/src/McpServer.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { FlowError } from "./FlowError.ts"
|
|
4
|
+
import { ApprovalAllowed, type ApprovalPolicy, type InteractionShape } from "./Interaction.ts"
|
|
5
|
+
|
|
6
|
+
export interface McpTool {
|
|
7
|
+
readonly name: string
|
|
8
|
+
readonly description: string
|
|
9
|
+
readonly inputSchema: Schema.Json
|
|
10
|
+
readonly call: (arguments_: Schema.Json) => Effect.Effect<string, FlowError>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const McpProtocolVersion = "2025-06-18"
|
|
14
|
+
export const McpServerName = "llm4ts"
|
|
15
|
+
export const McpServerVersion = "1.0"
|
|
16
|
+
|
|
17
|
+
const objectField = (value: Schema.Json, name: string): Schema.Json | undefined => {
|
|
18
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
const field = Reflect.get(value, name)
|
|
22
|
+
return Schema.is(Schema.Json)(field) ? field : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stringField = (value: Schema.Json, name: string): string | undefined => {
|
|
26
|
+
const field = objectField(value, name)
|
|
27
|
+
return typeof field === "string" ? field : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const params = (request: Schema.Json): Schema.Json => objectField(request, "params") ?? {}
|
|
31
|
+
|
|
32
|
+
const result = (id: Schema.Json, value: Schema.Json): Schema.Json => ({
|
|
33
|
+
jsonrpc: "2.0",
|
|
34
|
+
id,
|
|
35
|
+
result: value
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const error = (id: Schema.Json, code: number, message: string): Schema.Json => ({
|
|
39
|
+
jsonrpc: "2.0",
|
|
40
|
+
id,
|
|
41
|
+
error: { code, message }
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const toolContent = (text: string, isError: boolean): Schema.Json => ({
|
|
45
|
+
content: [{ type: "text", text }],
|
|
46
|
+
isError
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const askUserTool = (interaction: InteractionShape): McpTool => ({
|
|
50
|
+
name: "ask_user",
|
|
51
|
+
description:
|
|
52
|
+
"Ask the human operator a question and wait for their answer. Use when a decision or clarification is required.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
question: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "The question to put to the human operator."
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
required: ["question"]
|
|
62
|
+
},
|
|
63
|
+
call: (arguments_) => interaction.ask(stringField(arguments_, "question") ?? "")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const approvalTool = (policy: ApprovalPolicy): McpTool => ({
|
|
67
|
+
name: "approve",
|
|
68
|
+
description:
|
|
69
|
+
"Decide whether the agent may run a pending tool call and return its permission verdict.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
tool_name: { type: "string" },
|
|
74
|
+
input: { type: "object" }
|
|
75
|
+
},
|
|
76
|
+
required: ["tool_name"]
|
|
77
|
+
},
|
|
78
|
+
call: (arguments_) => {
|
|
79
|
+
const input = objectField(arguments_, "input") ?? {}
|
|
80
|
+
return policy
|
|
81
|
+
.decide(stringField(arguments_, "tool_name") ?? "", input)
|
|
82
|
+
.pipe(
|
|
83
|
+
Effect.map((decision) =>
|
|
84
|
+
JSON.stringify(
|
|
85
|
+
decision instanceof ApprovalAllowed
|
|
86
|
+
? { behavior: "allow", updatedInput: input }
|
|
87
|
+
: { behavior: "deny", message: decision.reason }
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const initializeResult = (request: Schema.Json): Schema.Json => ({
|
|
95
|
+
protocolVersion: stringField(params(request), "protocolVersion") ?? McpProtocolVersion,
|
|
96
|
+
capabilities: { tools: {} },
|
|
97
|
+
serverInfo: {
|
|
98
|
+
name: McpServerName,
|
|
99
|
+
version: McpServerVersion
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const toolsListResult = (tools: ReadonlyArray<McpTool>): Schema.Json => ({
|
|
104
|
+
tools: tools.map((tool) => ({
|
|
105
|
+
name: tool.name,
|
|
106
|
+
description: tool.description,
|
|
107
|
+
inputSchema: tool.inputSchema
|
|
108
|
+
}))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const toolsCall = (
|
|
112
|
+
id: Schema.Json,
|
|
113
|
+
request: Schema.Json,
|
|
114
|
+
tools: ReadonlyArray<McpTool>
|
|
115
|
+
): Effect.Effect<Schema.Json> => {
|
|
116
|
+
const requestParams = params(request)
|
|
117
|
+
const name = stringField(requestParams, "name") ?? ""
|
|
118
|
+
const tool = tools.find((candidate) => candidate.name === name)
|
|
119
|
+
if (tool === undefined) {
|
|
120
|
+
return Effect.succeed(error(id, -32602, `unknown tool: ${name}`))
|
|
121
|
+
}
|
|
122
|
+
return Effect.result(tool.call(objectField(requestParams, "arguments") ?? {})).pipe(
|
|
123
|
+
Effect.map((outcome) =>
|
|
124
|
+
outcome._tag === "Success"
|
|
125
|
+
? result(id, toolContent(outcome.success, false))
|
|
126
|
+
: result(id, toolContent(`tool '${name}' failed: ${outcome.failure.message}`, true))
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const handleMcpRequest = Effect.fn("@llm4ts/flow/McpServer.handle")(function* (
|
|
132
|
+
request: Schema.Json,
|
|
133
|
+
tools: ReadonlyArray<McpTool>
|
|
134
|
+
): Effect.fn.Return<Schema.Json | undefined> {
|
|
135
|
+
const id = objectField(request, "id")
|
|
136
|
+
if (id === undefined) {
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
switch (stringField(request, "method") ?? "") {
|
|
140
|
+
case "initialize":
|
|
141
|
+
return result(id, initializeResult(request))
|
|
142
|
+
case "tools/list":
|
|
143
|
+
return result(id, toolsListResult(tools))
|
|
144
|
+
case "tools/call":
|
|
145
|
+
return yield* toolsCall(id, request, tools)
|
|
146
|
+
default:
|
|
147
|
+
return error(id, -32601, `method not found: ${stringField(request, "method") ?? ""}`)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const parseError = (): Schema.Json => ({
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
id: null,
|
|
154
|
+
error: { code: -32700, message: "Parse error" }
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
export const handleMcpLine = (
|
|
158
|
+
line: string,
|
|
159
|
+
tools: ReadonlyArray<McpTool>
|
|
160
|
+
): Effect.Effect<string | undefined> => {
|
|
161
|
+
const parsed = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json))(line)
|
|
162
|
+
if (parsed._tag === "None") {
|
|
163
|
+
return Effect.succeed(JSON.stringify(parseError()))
|
|
164
|
+
}
|
|
165
|
+
return handleMcpRequest(parsed.value, tools).pipe(
|
|
166
|
+
Effect.map((response) => (response === undefined ? undefined : JSON.stringify(response)))
|
|
167
|
+
)
|
|
168
|
+
}
|
package/src/Mermaid.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { StageCompleted, StageFailed, StageStarted, type FlowEvent } from "./FlowEvents.ts"
|
|
2
|
+
import type { TraceLine } from "./FlowRecorder.ts"
|
|
3
|
+
|
|
4
|
+
const escapeLabel = (label: string): string => label.replaceAll('"', "'")
|
|
5
|
+
|
|
6
|
+
export const renderMermaid = (events: ReadonlyArray<FlowEvent>): string => {
|
|
7
|
+
const starts = events
|
|
8
|
+
.filter((event): event is StageStarted => event._tag === "StageStarted")
|
|
9
|
+
.map((event) => event.stage)
|
|
10
|
+
const failed = new Set(
|
|
11
|
+
events
|
|
12
|
+
.filter((event): event is StageFailed => event._tag === "StageFailed")
|
|
13
|
+
.map((event) => event.stage)
|
|
14
|
+
)
|
|
15
|
+
if (starts.length === 0) {
|
|
16
|
+
return 'flowchart TD\n none["(no stages)"]'
|
|
17
|
+
}
|
|
18
|
+
const order = [...new Set(starts)]
|
|
19
|
+
const counts = new Map<string, number>()
|
|
20
|
+
for (const stage of starts) {
|
|
21
|
+
counts.set(stage, (counts.get(stage) ?? 0) + 1)
|
|
22
|
+
}
|
|
23
|
+
const ids = new Map<string, string>()
|
|
24
|
+
order.forEach((name, index) => ids.set(name, `n${index}`))
|
|
25
|
+
const nodes = order.map((name) => {
|
|
26
|
+
const count = counts.get(name) ?? 1
|
|
27
|
+
const label = count > 1 ? `${name} ×${count}` : name
|
|
28
|
+
return ` ${ids.get(name)}["${escapeLabel(label)}"]${failed.has(name) ? ":::failed" : ""}`
|
|
29
|
+
})
|
|
30
|
+
const edges = order
|
|
31
|
+
.slice(1)
|
|
32
|
+
.map((name, index) => ` ${ids.get(order[index] ?? "")} --> ${ids.get(name)}`)
|
|
33
|
+
const styles = failed.size === 0 ? [] : [" classDef failed fill:#fdd,stroke:#c00,color:#900;"]
|
|
34
|
+
return ["flowchart TD", ...nodes, ...edges, ...styles].join("\n")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const traceStage = (line: TraceLine): FlowEvent | undefined => {
|
|
38
|
+
const field = (key: string): string | undefined => {
|
|
39
|
+
const direct = line.fields[key]
|
|
40
|
+
if (typeof direct === "string") {
|
|
41
|
+
return direct
|
|
42
|
+
}
|
|
43
|
+
const encoded = line.fields.event
|
|
44
|
+
if (typeof encoded !== "string") {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const event: unknown = JSON.parse(encoded)
|
|
49
|
+
if (typeof event !== "object" || event === null) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
const value = Reflect.get(event, key)
|
|
53
|
+
return typeof value === "string" ? value : undefined
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
switch (line.kind) {
|
|
59
|
+
case "StageStarted": {
|
|
60
|
+
const stage = field("stage")
|
|
61
|
+
return stage === undefined ? undefined : StageStarted.make({ stage })
|
|
62
|
+
}
|
|
63
|
+
case "StageCompleted": {
|
|
64
|
+
const stage = field("stage")
|
|
65
|
+
return stage === undefined ? undefined : StageCompleted.make({ stage })
|
|
66
|
+
}
|
|
67
|
+
case "StageFailed": {
|
|
68
|
+
const stage = field("stage")
|
|
69
|
+
return stage === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: StageFailed.make({ stage, message: field("message") ?? "" })
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const renderMermaidTrace = (lines: ReadonlyArray<TraceLine>): string =>
|
|
79
|
+
renderMermaid(
|
|
80
|
+
lines.flatMap((line) => {
|
|
81
|
+
const event = traceStage(line)
|
|
82
|
+
return event === undefined ? [] : [event]
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
87
|
+
|
|
88
|
+
const base64Url = (value: string): string => {
|
|
89
|
+
const bytes = new TextEncoder().encode(value)
|
|
90
|
+
let encoded = ""
|
|
91
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
92
|
+
const first = bytes[index] ?? 0
|
|
93
|
+
const second = bytes[index + 1]
|
|
94
|
+
const third = bytes[index + 2]
|
|
95
|
+
const block = (first << 16) | ((second ?? 0) << 8) | (third ?? 0)
|
|
96
|
+
encoded += alphabet[(block >> 18) & 63]
|
|
97
|
+
encoded += alphabet[(block >> 12) & 63]
|
|
98
|
+
if (second !== undefined) {
|
|
99
|
+
encoded += alphabet[(block >> 6) & 63]
|
|
100
|
+
}
|
|
101
|
+
if (third !== undefined) {
|
|
102
|
+
encoded += alphabet[block & 63]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return encoded
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const mermaidLiveUrl = (diagram: string): string =>
|
|
109
|
+
`https://mermaid.ink/svg/${base64Url(diagram)}`
|
|
110
|
+
|
|
111
|
+
export const mermaidDocument = (diagram: string): string =>
|
|
112
|
+
`\`\`\`mermaid\n${diagram}\n\`\`\`\n\n[View on mermaid.ink](${mermaidLiveUrl(diagram)})\n`
|