@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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/dist/AzureDevOpsTool.d.ts +60 -0
  4. package/dist/AzureDevOpsTool.d.ts.map +1 -0
  5. package/dist/AzureDevOpsTool.js +163 -0
  6. package/dist/AzureDevOpsTool.js.map +1 -0
  7. package/dist/Bench.d.ts +133 -0
  8. package/dist/Bench.d.ts.map +1 -0
  9. package/dist/Bench.js +272 -0
  10. package/dist/Bench.js.map +1 -0
  11. package/dist/BenchReport.d.ts +9 -0
  12. package/dist/BenchReport.d.ts.map +1 -0
  13. package/dist/BenchReport.js +360 -0
  14. package/dist/BenchReport.js.map +1 -0
  15. package/dist/CapabilityGuard.d.ts +6 -0
  16. package/dist/CapabilityGuard.d.ts.map +1 -0
  17. package/dist/CapabilityGuard.js +25 -0
  18. package/dist/CapabilityGuard.js.map +1 -0
  19. package/dist/Chat.d.ts +15 -0
  20. package/dist/Chat.d.ts.map +1 -0
  21. package/dist/Chat.js +36 -0
  22. package/dist/Chat.js.map +1 -0
  23. package/dist/Classified.d.ts +14 -0
  24. package/dist/Classified.d.ts.map +1 -0
  25. package/dist/Classified.js +48 -0
  26. package/dist/Classified.js.map +1 -0
  27. package/dist/CostLedger.d.ts +55 -0
  28. package/dist/CostLedger.d.ts.map +1 -0
  29. package/dist/CostLedger.js +126 -0
  30. package/dist/CostLedger.js.map +1 -0
  31. package/dist/CostTracker.d.ts +13 -0
  32. package/dist/CostTracker.d.ts.map +1 -0
  33. package/dist/CostTracker.js +116 -0
  34. package/dist/CostTracker.js.map +1 -0
  35. package/dist/Equiv.d.ts +163 -0
  36. package/dist/Equiv.d.ts.map +1 -0
  37. package/dist/Equiv.js +256 -0
  38. package/dist/Equiv.js.map +1 -0
  39. package/dist/EquivReport.d.ts +16 -0
  40. package/dist/EquivReport.d.ts.map +1 -0
  41. package/dist/EquivReport.js +77 -0
  42. package/dist/EquivReport.js.map +1 -0
  43. package/dist/Flow.d.ts +25 -0
  44. package/dist/Flow.d.ts.map +1 -0
  45. package/dist/Flow.js +39 -0
  46. package/dist/Flow.js.map +1 -0
  47. package/dist/FlowContext.d.ts +23 -0
  48. package/dist/FlowContext.d.ts.map +1 -0
  49. package/dist/FlowContext.js +4 -0
  50. package/dist/FlowContext.js.map +1 -0
  51. package/dist/FlowError.d.ts +80 -0
  52. package/dist/FlowError.d.ts.map +1 -0
  53. package/dist/FlowError.js +92 -0
  54. package/dist/FlowError.js.map +1 -0
  55. package/dist/FlowEvents.d.ts +101 -0
  56. package/dist/FlowEvents.d.ts.map +1 -0
  57. package/dist/FlowEvents.js +103 -0
  58. package/dist/FlowEvents.js.map +1 -0
  59. package/dist/FlowRecorder.d.ts +26 -0
  60. package/dist/FlowRecorder.d.ts.map +1 -0
  61. package/dist/FlowRecorder.js +74 -0
  62. package/dist/FlowRecorder.js.map +1 -0
  63. package/dist/GitHubTool.d.ts +53 -0
  64. package/dist/GitHubTool.d.ts.map +1 -0
  65. package/dist/GitHubTool.js +191 -0
  66. package/dist/GitHubTool.js.map +1 -0
  67. package/dist/GitTool.d.ts +46 -0
  68. package/dist/GitTool.d.ts.map +1 -0
  69. package/dist/GitTool.js +97 -0
  70. package/dist/GitTool.js.map +1 -0
  71. package/dist/Interaction.d.ts +21 -0
  72. package/dist/Interaction.d.ts.map +1 -0
  73. package/dist/Interaction.js +9 -0
  74. package/dist/Interaction.js.map +1 -0
  75. package/dist/McpServer.d.ts +18 -0
  76. package/dist/McpServer.d.ts.map +1 -0
  77. package/dist/McpServer.js +122 -0
  78. package/dist/McpServer.js.map +1 -0
  79. package/dist/Mermaid.d.ts +7 -0
  80. package/dist/Mermaid.d.ts.map +1 -0
  81. package/dist/Mermaid.js +98 -0
  82. package/dist/Mermaid.js.map +1 -0
  83. package/dist/Pack.d.ts +40 -0
  84. package/dist/Pack.d.ts.map +1 -0
  85. package/dist/Pack.js +141 -0
  86. package/dist/Pack.js.map +1 -0
  87. package/dist/Package.d.ts +2 -0
  88. package/dist/Package.d.ts.map +1 -0
  89. package/dist/Package.js +2 -0
  90. package/dist/Package.js.map +1 -0
  91. package/dist/Persistence.d.ts +32 -0
  92. package/dist/Persistence.d.ts.map +1 -0
  93. package/dist/Persistence.js +85 -0
  94. package/dist/Persistence.js.map +1 -0
  95. package/dist/Plan.d.ts +26 -0
  96. package/dist/Plan.d.ts.map +1 -0
  97. package/dist/Plan.js +101 -0
  98. package/dist/Plan.js.map +1 -0
  99. package/dist/PlanExecution.d.ts +7 -0
  100. package/dist/PlanExecution.d.ts.map +1 -0
  101. package/dist/PlanExecution.js +19 -0
  102. package/dist/PlanExecution.js.map +1 -0
  103. package/dist/Planner.d.ts +32 -0
  104. package/dist/Planner.d.ts.map +1 -0
  105. package/dist/Planner.js +97 -0
  106. package/dist/Planner.js.map +1 -0
  107. package/dist/PrSummary.d.ts +15 -0
  108. package/dist/PrSummary.d.ts.map +1 -0
  109. package/dist/PrSummary.js +29 -0
  110. package/dist/PrSummary.js.map +1 -0
  111. package/dist/PriceList.d.ts +11 -0
  112. package/dist/PriceList.d.ts.map +1 -0
  113. package/dist/PriceList.js +79 -0
  114. package/dist/PriceList.js.map +1 -0
  115. package/dist/Provenance.d.ts +27 -0
  116. package/dist/Provenance.d.ts.map +1 -0
  117. package/dist/Provenance.js +38 -0
  118. package/dist/Provenance.js.map +1 -0
  119. package/dist/Replay.d.ts +29 -0
  120. package/dist/Replay.d.ts.map +1 -0
  121. package/dist/Replay.js +181 -0
  122. package/dist/Replay.js.map +1 -0
  123. package/dist/Review.d.ts +71 -0
  124. package/dist/Review.d.ts.map +1 -0
  125. package/dist/Review.js +204 -0
  126. package/dist/Review.js.map +1 -0
  127. package/dist/ReviewCache.d.ts +6 -0
  128. package/dist/ReviewCache.d.ts.map +1 -0
  129. package/dist/ReviewCache.js +28 -0
  130. package/dist/ReviewCache.js.map +1 -0
  131. package/dist/Reviewer.d.ts +12 -0
  132. package/dist/Reviewer.d.ts.map +1 -0
  133. package/dist/Reviewer.js +30 -0
  134. package/dist/Reviewer.js.map +1 -0
  135. package/dist/SpecChecks.d.ts +17 -0
  136. package/dist/SpecChecks.d.ts.map +1 -0
  137. package/dist/SpecChecks.js +107 -0
  138. package/dist/SpecChecks.js.map +1 -0
  139. package/dist/Survey.d.ts +32 -0
  140. package/dist/Survey.d.ts.map +1 -0
  141. package/dist/Survey.js +119 -0
  142. package/dist/Survey.js.map +1 -0
  143. package/dist/TransientRetry.d.ts +16 -0
  144. package/dist/TransientRetry.d.ts.map +1 -0
  145. package/dist/TransientRetry.js +102 -0
  146. package/dist/TransientRetry.js.map +1 -0
  147. package/dist/Workspace.d.ts +39 -0
  148. package/dist/Workspace.d.ts.map +1 -0
  149. package/dist/Workspace.js +192 -0
  150. package/dist/Workspace.js.map +1 -0
  151. package/dist/WorkspaceLayout.d.ts +3 -0
  152. package/dist/WorkspaceLayout.d.ts.map +1 -0
  153. package/dist/WorkspaceLayout.js +19 -0
  154. package/dist/WorkspaceLayout.js.map +1 -0
  155. package/dist/WorkspaceTools.d.ts +4 -0
  156. package/dist/WorkspaceTools.d.ts.map +1 -0
  157. package/dist/WorkspaceTools.js +130 -0
  158. package/dist/WorkspaceTools.js.map +1 -0
  159. package/package.json +91 -0
  160. package/src/AzureDevOpsTool.ts +291 -0
  161. package/src/Bench.ts +340 -0
  162. package/src/BenchReport.ts +447 -0
  163. package/src/CapabilityGuard.ts +43 -0
  164. package/src/Chat.ts +60 -0
  165. package/src/Classified.ts +67 -0
  166. package/src/CostLedger.ts +171 -0
  167. package/src/CostTracker.ts +170 -0
  168. package/src/Equiv.ts +378 -0
  169. package/src/EquivReport.ts +92 -0
  170. package/src/Flow.ts +72 -0
  171. package/src/FlowContext.ts +23 -0
  172. package/src/FlowError.ts +112 -0
  173. package/src/FlowEvents.ts +147 -0
  174. package/src/FlowRecorder.ts +116 -0
  175. package/src/GitHubTool.ts +315 -0
  176. package/src/GitTool.ts +214 -0
  177. package/src/Interaction.ts +23 -0
  178. package/src/McpServer.ts +168 -0
  179. package/src/Mermaid.ts +112 -0
  180. package/src/Pack.ts +213 -0
  181. package/src/Package.ts +1 -0
  182. package/src/Persistence.ts +198 -0
  183. package/src/Plan.ts +119 -0
  184. package/src/PlanExecution.ts +46 -0
  185. package/src/Planner.ts +134 -0
  186. package/src/PrSummary.ts +37 -0
  187. package/src/PriceList.ts +91 -0
  188. package/src/Provenance.ts +85 -0
  189. package/src/Replay.ts +265 -0
  190. package/src/Review.ts +337 -0
  191. package/src/ReviewCache.ts +45 -0
  192. package/src/Reviewer.ts +31 -0
  193. package/src/SpecChecks.ts +143 -0
  194. package/src/Survey.ts +144 -0
  195. package/src/TransientRetry.ts +238 -0
  196. package/src/Workspace.ts +255 -0
  197. package/src/WorkspaceLayout.ts +22 -0
  198. 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
+ }
@@ -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`