@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as Context from "effect/Context"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Layer from "effect/Layer"
|
|
4
|
+
import * as PubSub from "effect/PubSub"
|
|
5
|
+
import * as Ref from "effect/Ref"
|
|
6
|
+
import * as Schema from "effect/Schema"
|
|
7
|
+
import type * as Scope from "effect/Scope"
|
|
8
|
+
import * as Stream from "effect/Stream"
|
|
9
|
+
import { TokenUsage } from "@llm4ts/core/Models"
|
|
10
|
+
|
|
11
|
+
export class StageStarted extends Schema.TaggedClass<StageStarted>()("StageStarted", {
|
|
12
|
+
stage: Schema.String
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export class StageCompleted extends Schema.TaggedClass<StageCompleted>()("StageCompleted", {
|
|
16
|
+
stage: Schema.String
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
export class StageFailed extends Schema.TaggedClass<StageFailed>()("StageFailed", {
|
|
20
|
+
stage: Schema.String,
|
|
21
|
+
message: Schema.String
|
|
22
|
+
}) {}
|
|
23
|
+
|
|
24
|
+
export class Aborted extends Schema.TaggedClass<Aborted>()("Aborted", {
|
|
25
|
+
message: Schema.String
|
|
26
|
+
}) {}
|
|
27
|
+
|
|
28
|
+
export class Info extends Schema.TaggedClass<Info>()("Info", {
|
|
29
|
+
message: Schema.String
|
|
30
|
+
}) {}
|
|
31
|
+
|
|
32
|
+
export class ToolUse extends Schema.TaggedClass<ToolUse>()("ToolUse", {
|
|
33
|
+
tool: Schema.String,
|
|
34
|
+
args: Schema.String
|
|
35
|
+
}) {}
|
|
36
|
+
|
|
37
|
+
export class AssistantMessage extends Schema.TaggedClass<AssistantMessage>()("AssistantMessage", {
|
|
38
|
+
text: Schema.String
|
|
39
|
+
}) {}
|
|
40
|
+
|
|
41
|
+
export class TokensUsed extends Schema.TaggedClass<TokensUsed>()("TokensUsed", {
|
|
42
|
+
agent: Schema.String,
|
|
43
|
+
model: Schema.optionalKey(Schema.String),
|
|
44
|
+
usage: TokenUsage
|
|
45
|
+
}) {}
|
|
46
|
+
|
|
47
|
+
export class CapabilityUsedEvent extends Schema.TaggedClass<CapabilityUsedEvent>()(
|
|
48
|
+
"CapabilityUsed",
|
|
49
|
+
{
|
|
50
|
+
capability: Schema.String,
|
|
51
|
+
operation: Schema.String
|
|
52
|
+
}
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
export class CapabilityDeniedEvent extends Schema.TaggedClass<CapabilityDeniedEvent>()(
|
|
56
|
+
"CapabilityDenied",
|
|
57
|
+
{
|
|
58
|
+
capability: Schema.String,
|
|
59
|
+
operation: Schema.String
|
|
60
|
+
}
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
export class CapabilityUnenforceable extends Schema.TaggedClass<CapabilityUnenforceable>()(
|
|
64
|
+
"CapabilityUnenforceable",
|
|
65
|
+
{
|
|
66
|
+
detail: Schema.String
|
|
67
|
+
}
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
export class Declassified extends Schema.TaggedClass<Declassified>()("Declassified", {
|
|
71
|
+
label: Schema.String
|
|
72
|
+
}) {}
|
|
73
|
+
|
|
74
|
+
export const FlowEvent = Schema.Union([
|
|
75
|
+
StageStarted,
|
|
76
|
+
StageCompleted,
|
|
77
|
+
StageFailed,
|
|
78
|
+
Aborted,
|
|
79
|
+
Info,
|
|
80
|
+
ToolUse,
|
|
81
|
+
AssistantMessage,
|
|
82
|
+
TokensUsed,
|
|
83
|
+
CapabilityUsedEvent,
|
|
84
|
+
CapabilityDeniedEvent,
|
|
85
|
+
CapabilityUnenforceable,
|
|
86
|
+
Declassified
|
|
87
|
+
])
|
|
88
|
+
export type FlowEvent = typeof FlowEvent.Type
|
|
89
|
+
|
|
90
|
+
export const FlowEventsValues = Object.freeze({
|
|
91
|
+
CapabilityUsed: (capability: string, operation: string): CapabilityUsedEvent =>
|
|
92
|
+
new CapabilityUsedEvent({ capability, operation }),
|
|
93
|
+
CapabilityDenied: (capability: string, operation: string): CapabilityDeniedEvent =>
|
|
94
|
+
new CapabilityDeniedEvent({ capability, operation }),
|
|
95
|
+
CapabilityUnenforceable: (detail: string): CapabilityUnenforceable =>
|
|
96
|
+
new CapabilityUnenforceable({ detail }),
|
|
97
|
+
Declassified: (label: string): Declassified => new Declassified({ label })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export interface FlowEventsShape {
|
|
101
|
+
readonly publish: (event: FlowEvent) => Effect.Effect<void>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class FlowEvents extends Context.Service<FlowEvents, FlowEventsShape>()(
|
|
105
|
+
"@llm4ts/flow/FlowEvents"
|
|
106
|
+
) {}
|
|
107
|
+
|
|
108
|
+
const noopFlowEvents = FlowEvents.of({
|
|
109
|
+
publish: (_event) => Effect.void
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
export const FlowEventsNoop = Layer.succeed(FlowEvents)(noopFlowEvents)
|
|
113
|
+
|
|
114
|
+
export interface CollectingFlowEvents extends FlowEventsShape {
|
|
115
|
+
readonly recorded: Effect.Effect<ReadonlyArray<FlowEvent>>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const makeCollectingFlowEvents: Effect.Effect<CollectingFlowEvents> = Effect.map(
|
|
119
|
+
Ref.make<ReadonlyArray<FlowEvent>>([]),
|
|
120
|
+
(events) => ({
|
|
121
|
+
publish: (event) => Ref.update(events, (recorded) => [...recorded, event]),
|
|
122
|
+
recorded: Ref.get(events)
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
export interface FlowEventHub extends FlowEventsShape {
|
|
127
|
+
readonly subscribe: Effect.Effect<PubSub.Subscription<FlowEvent>, never, Scope.Scope>
|
|
128
|
+
readonly stream: Stream.Stream<FlowEvent>
|
|
129
|
+
readonly publishedCount: Effect.Effect<number>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const makeFlowEventHub = Effect.fn("@llm4ts/flow/FlowEvents.makeHub")(function* (
|
|
133
|
+
capacity = 64
|
|
134
|
+
): Effect.fn.Return<FlowEventHub> {
|
|
135
|
+
const events = yield* PubSub.bounded<FlowEvent>(capacity)
|
|
136
|
+
const published = yield* Ref.make(0)
|
|
137
|
+
return {
|
|
138
|
+
publish: (event) =>
|
|
139
|
+
Ref.update(published, (count) => count + 1).pipe(
|
|
140
|
+
Effect.andThen(PubSub.publish(events, event)),
|
|
141
|
+
Effect.asVoid
|
|
142
|
+
),
|
|
143
|
+
subscribe: PubSub.subscribe(events),
|
|
144
|
+
stream: Stream.fromPubSub(events),
|
|
145
|
+
publishedCount: Ref.get(published)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as Clock from "effect/Clock"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Ref from "effect/Ref"
|
|
4
|
+
import * as Schema from "effect/Schema"
|
|
5
|
+
import * as Semaphore from "effect/Semaphore"
|
|
6
|
+
import type * as Scope from "effect/Scope"
|
|
7
|
+
import * as Stream from "effect/Stream"
|
|
8
|
+
import type { StreamRecorder } from "@llm4ts/core/observability/StreamRecorder"
|
|
9
|
+
import type { JsonValue } from "@llm4ts/core/providers/CliSupport"
|
|
10
|
+
import type { FlowEvent, FlowEventHub } from "./FlowEvents.ts"
|
|
11
|
+
import type { PlainFileStoreShape } from "./Persistence.ts"
|
|
12
|
+
|
|
13
|
+
export class TraceLine extends Schema.Class<TraceLine>("TraceLine")({
|
|
14
|
+
schemaVersion: Schema.optionalKey(Schema.Int),
|
|
15
|
+
seq: Schema.Int,
|
|
16
|
+
timestamp: Schema.Number,
|
|
17
|
+
runId: Schema.String,
|
|
18
|
+
kind: Schema.String,
|
|
19
|
+
fields: Schema.Record(Schema.String, Schema.Json)
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
export interface FlowRecorderShape extends StreamRecorder {
|
|
23
|
+
readonly runId: string
|
|
24
|
+
readonly tracePath: string
|
|
25
|
+
readonly record: (event: FlowEvent) => Effect.Effect<void>
|
|
26
|
+
readonly consume: (hub: FlowEventHub) => Effect.Effect<void, never, Scope.Scope>
|
|
27
|
+
readonly awaitDrained: (hub: FlowEventHub) => Effect.Effect<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const encode = (line: TraceLine): string =>
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
schemaVersion: line.schemaVersion,
|
|
33
|
+
seq: line.seq,
|
|
34
|
+
timestamp: line.timestamp,
|
|
35
|
+
runId: line.runId,
|
|
36
|
+
kind: line.kind,
|
|
37
|
+
fields: line.fields
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const makeFlowRecorder = Effect.fn("@llm4ts/flow/FlowRecorder.make")(function* (
|
|
41
|
+
files: PlainFileStoreShape,
|
|
42
|
+
tracePath: string,
|
|
43
|
+
runId: string
|
|
44
|
+
): Effect.fn.Return<FlowRecorderShape> {
|
|
45
|
+
const sequence = yield* Ref.make(0)
|
|
46
|
+
const consumed = yield* Ref.make(0)
|
|
47
|
+
const lock = yield* Semaphore.make(1)
|
|
48
|
+
const degraded = yield* Ref.make(false)
|
|
49
|
+
|
|
50
|
+
const append = (kind: string, fields: Readonly<Record<string, JsonValue>>): Effect.Effect<void> =>
|
|
51
|
+
lock.withPermit(
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
if (yield* Ref.get(degraded)) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
const seq = yield* Ref.getAndUpdate(sequence, (current) => current + 1)
|
|
57
|
+
const timestamp = yield* Clock.currentTimeMillis
|
|
58
|
+
const line = TraceLine.make({
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
seq,
|
|
61
|
+
timestamp,
|
|
62
|
+
runId,
|
|
63
|
+
kind,
|
|
64
|
+
fields
|
|
65
|
+
})
|
|
66
|
+
yield* files
|
|
67
|
+
.append(tracePath, `${encode(line)}\n`)
|
|
68
|
+
.pipe(Effect.catch(() => Ref.set(degraded, true)))
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const record = (event: FlowEvent): Effect.Effect<void> =>
|
|
73
|
+
append(event._tag, { event: JSON.stringify(event) })
|
|
74
|
+
|
|
75
|
+
const recorder: FlowRecorderShape = {
|
|
76
|
+
runId,
|
|
77
|
+
tracePath,
|
|
78
|
+
record,
|
|
79
|
+
rawLine: (provider, model, line) =>
|
|
80
|
+
append("RawLine", {
|
|
81
|
+
provider,
|
|
82
|
+
...(model === undefined ? {} : { model }),
|
|
83
|
+
line
|
|
84
|
+
}),
|
|
85
|
+
streamError: (provider, model, message) =>
|
|
86
|
+
append("StreamError", {
|
|
87
|
+
provider,
|
|
88
|
+
...(model === undefined ? {} : { model }),
|
|
89
|
+
message
|
|
90
|
+
}),
|
|
91
|
+
consume: (hub) =>
|
|
92
|
+
Effect.gen(function* () {
|
|
93
|
+
const subscription = yield* hub.subscribe
|
|
94
|
+
yield* Stream.fromSubscription(subscription).pipe(
|
|
95
|
+
Stream.runForEach((event) =>
|
|
96
|
+
record(event).pipe(Effect.andThen(Ref.update(consumed, (count) => count + 1)))
|
|
97
|
+
),
|
|
98
|
+
Effect.forkScoped,
|
|
99
|
+
Effect.asVoid
|
|
100
|
+
)
|
|
101
|
+
}),
|
|
102
|
+
awaitDrained: (hub) =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const target = yield* hub.publishedCount
|
|
105
|
+
const drain: Effect.Effect<void> = Effect.suspend(() =>
|
|
106
|
+
Ref.get(consumed).pipe(
|
|
107
|
+
Effect.flatMap((count) =>
|
|
108
|
+
count >= target ? Effect.void : Effect.yieldNow.pipe(Effect.andThen(drain))
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
yield* drain
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return recorder
|
|
116
|
+
})
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import { Capabilities } 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 IssueRef extends Schema.Class<IssueRef>("IssueRef")({
|
|
10
|
+
owner: Schema.String,
|
|
11
|
+
repo: Schema.String,
|
|
12
|
+
number: Schema.Int
|
|
13
|
+
}) {
|
|
14
|
+
get shortRef(): string {
|
|
15
|
+
return `${this.owner}/${this.repo}#${this.number}`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const parseIssueRef = (input: string): IssueRef | undefined => {
|
|
20
|
+
const match = /^([^/\s]+)\/([^#\s]+)#(\d+)$/.exec(input.trim())
|
|
21
|
+
const owner = match?.[1]
|
|
22
|
+
const repo = match?.[2]
|
|
23
|
+
const number = Number.parseInt(match?.[3] ?? "", 10)
|
|
24
|
+
return owner === undefined || repo === undefined || !Number.isInteger(number) || number <= 0
|
|
25
|
+
? undefined
|
|
26
|
+
: IssueRef.make({ owner, repo, number })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Issue extends Schema.Class<Issue>("Issue")({
|
|
30
|
+
title: Schema.String,
|
|
31
|
+
body: Schema.String,
|
|
32
|
+
author: Schema.String
|
|
33
|
+
}) {}
|
|
34
|
+
|
|
35
|
+
export class PullRequest extends Schema.Class<PullRequest>("PullRequest")({
|
|
36
|
+
owner: Schema.String,
|
|
37
|
+
repo: Schema.String,
|
|
38
|
+
number: Schema.Int,
|
|
39
|
+
url: Schema.String
|
|
40
|
+
}) {
|
|
41
|
+
get shortRef(): string {
|
|
42
|
+
return `${this.owner}/${this.repo}#${this.number}`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const BuildOutcome = Schema.Literals(["Success", "Failure", "Pending", "TimedOut"])
|
|
47
|
+
export type BuildOutcome = typeof BuildOutcome.Type
|
|
48
|
+
|
|
49
|
+
export const parsePullRequestUrl = (url: string): PullRequest | undefined => {
|
|
50
|
+
const match = /^https?:\/\/[^/]+\/([^/]+)\/([^/]+)\/pull\/(\d+).*$/.exec(url.trim())
|
|
51
|
+
const owner = match?.[1]
|
|
52
|
+
const repo = match?.[2]
|
|
53
|
+
const number = Number.parseInt(match?.[3] ?? "", 10)
|
|
54
|
+
return owner === undefined || repo === undefined || !Number.isInteger(number)
|
|
55
|
+
? undefined
|
|
56
|
+
: PullRequest.make({
|
|
57
|
+
owner,
|
|
58
|
+
repo,
|
|
59
|
+
number,
|
|
60
|
+
url: url.trim()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const prViewArgs = ["pr", "view", "--json", "url", "--jq", ".url"]
|
|
65
|
+
|
|
66
|
+
export const prCreateArgs = (
|
|
67
|
+
title: string,
|
|
68
|
+
body: string,
|
|
69
|
+
base: string | undefined,
|
|
70
|
+
draft: boolean
|
|
71
|
+
): ReadonlyArray<string> => [
|
|
72
|
+
"pr",
|
|
73
|
+
"create",
|
|
74
|
+
"--title",
|
|
75
|
+
title,
|
|
76
|
+
"--body",
|
|
77
|
+
body,
|
|
78
|
+
...(base === undefined ? [] : ["--base", base]),
|
|
79
|
+
...(draft ? ["--draft"] : [])
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
export const issueViewArgs = (ref: IssueRef): ReadonlyArray<string> => [
|
|
83
|
+
"issue",
|
|
84
|
+
"view",
|
|
85
|
+
String(ref.number),
|
|
86
|
+
"--repo",
|
|
87
|
+
`${ref.owner}/${ref.repo}`,
|
|
88
|
+
"--json",
|
|
89
|
+
"title,body,author"
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
export const issueCommentArgs = (ref: IssueRef, body: string): ReadonlyArray<string> => [
|
|
93
|
+
"issue",
|
|
94
|
+
"comment",
|
|
95
|
+
String(ref.number),
|
|
96
|
+
"--repo",
|
|
97
|
+
`${ref.owner}/${ref.repo}`,
|
|
98
|
+
"--body",
|
|
99
|
+
body
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
export const prCommentArgs = (pr: PullRequest, body: string): ReadonlyArray<string> => [
|
|
103
|
+
"pr",
|
|
104
|
+
"comment",
|
|
105
|
+
String(pr.number),
|
|
106
|
+
"--repo",
|
|
107
|
+
`${pr.owner}/${pr.repo}`,
|
|
108
|
+
"--body",
|
|
109
|
+
body
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
export const prPatchArgs = (
|
|
113
|
+
pr: PullRequest,
|
|
114
|
+
title: string,
|
|
115
|
+
body: string
|
|
116
|
+
): ReadonlyArray<string> => [
|
|
117
|
+
"api",
|
|
118
|
+
"--method",
|
|
119
|
+
"PATCH",
|
|
120
|
+
`repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`,
|
|
121
|
+
"-f",
|
|
122
|
+
`title=${title}`,
|
|
123
|
+
"-f",
|
|
124
|
+
`body=${body}`
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
export const prChecksArgs = (pr: PullRequest): ReadonlyArray<string> => [
|
|
128
|
+
"pr",
|
|
129
|
+
"view",
|
|
130
|
+
String(pr.number),
|
|
131
|
+
"--repo",
|
|
132
|
+
`${pr.owner}/${pr.repo}`,
|
|
133
|
+
"--json",
|
|
134
|
+
"statusCheckRollup"
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
class GhAuthor extends Schema.Class<GhAuthor>("GhAuthor")({
|
|
138
|
+
login: Schema.String
|
|
139
|
+
}) {}
|
|
140
|
+
|
|
141
|
+
class GhIssue extends Schema.Class<GhIssue>("GhIssue")({
|
|
142
|
+
title: Schema.String,
|
|
143
|
+
body: Schema.String,
|
|
144
|
+
author: GhAuthor
|
|
145
|
+
}) {}
|
|
146
|
+
|
|
147
|
+
class GhCheck extends Schema.Class<GhCheck>("GhCheck")({
|
|
148
|
+
status: Schema.optionalKey(Schema.String),
|
|
149
|
+
conclusion: Schema.optionalKey(Schema.String),
|
|
150
|
+
state: Schema.optionalKey(Schema.String)
|
|
151
|
+
}) {}
|
|
152
|
+
|
|
153
|
+
class GhChecks extends Schema.Class<GhChecks>("GhChecks")({
|
|
154
|
+
statusCheckRollup: Schema.Array(GhCheck).pipe(
|
|
155
|
+
Schema.withConstructorDefault(Effect.succeed(Object.freeze([])))
|
|
156
|
+
)
|
|
157
|
+
}) {}
|
|
158
|
+
|
|
159
|
+
export const parseIssue = (json: string): Effect.Effect<Issue, ProcessError> =>
|
|
160
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(GhIssue))(json).pipe(
|
|
161
|
+
Effect.map(
|
|
162
|
+
(issue) =>
|
|
163
|
+
new Issue({
|
|
164
|
+
title: issue.title,
|
|
165
|
+
body: issue.body,
|
|
166
|
+
author: issue.author.login
|
|
167
|
+
})
|
|
168
|
+
),
|
|
169
|
+
Effect.mapError((error) =>
|
|
170
|
+
ProcessError.make({
|
|
171
|
+
message: "gh issue view",
|
|
172
|
+
detail: `invalid issue JSON: ${String(error)}`
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
export const outcomeFromChecksJson = (json: string): Effect.Effect<BuildOutcome, ProcessError> =>
|
|
178
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(GhChecks))(json).pipe(
|
|
179
|
+
Effect.map((parsed) => {
|
|
180
|
+
const pending = parsed.statusCheckRollup.some(
|
|
181
|
+
(check) =>
|
|
182
|
+
(check.status !== undefined && check.status.toUpperCase() !== "COMPLETED") ||
|
|
183
|
+
["PENDING", "EXPECTED"].includes(check.state?.toUpperCase() ?? "")
|
|
184
|
+
)
|
|
185
|
+
const failed = parsed.statusCheckRollup.some(
|
|
186
|
+
(check) =>
|
|
187
|
+
["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE"].includes(
|
|
188
|
+
check.conclusion?.toUpperCase() ?? ""
|
|
189
|
+
) || ["FAILURE", "ERROR"].includes(check.state?.toUpperCase() ?? "")
|
|
190
|
+
)
|
|
191
|
+
return pending ? "Pending" : failed ? "Failure" : "Success"
|
|
192
|
+
}),
|
|
193
|
+
Effect.mapError((error) =>
|
|
194
|
+
ProcessError.make({
|
|
195
|
+
message: "gh pr checks",
|
|
196
|
+
detail: `invalid checks JSON: ${String(error)}`
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
export interface GitHubToolShape {
|
|
202
|
+
readonly createPr: (
|
|
203
|
+
title: string,
|
|
204
|
+
body: string,
|
|
205
|
+
base?: string,
|
|
206
|
+
draft?: boolean
|
|
207
|
+
) => Effect.Effect<PullRequest, FlowError>
|
|
208
|
+
readonly readIssue: (ref: IssueRef) => Effect.Effect<Issue, FlowError>
|
|
209
|
+
readonly writeIssueComment: (ref: IssueRef, body: string) => Effect.Effect<void, FlowError>
|
|
210
|
+
readonly writePrComment: (pr: PullRequest, body: string) => Effect.Effect<void, FlowError>
|
|
211
|
+
readonly updatePr: (
|
|
212
|
+
pr: PullRequest,
|
|
213
|
+
title: string,
|
|
214
|
+
body: string
|
|
215
|
+
) => Effect.Effect<void, FlowError>
|
|
216
|
+
readonly prChecks: (pr: PullRequest) => Effect.Effect<BuildOutcome, FlowError>
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const output = (result: ProcessResult): string => result.stdout.join("\n").trim()
|
|
220
|
+
|
|
221
|
+
export const makeGitHubTool = (
|
|
222
|
+
process: ProcessExecutorShape,
|
|
223
|
+
workDir: string,
|
|
224
|
+
events: FlowEventsShape
|
|
225
|
+
): GitHubToolShape => {
|
|
226
|
+
const run = (
|
|
227
|
+
args: ReadonlyArray<string>,
|
|
228
|
+
allowFailure = false
|
|
229
|
+
): Effect.Effect<ProcessResult, FlowError> =>
|
|
230
|
+
process.run(["gh", ...args], workDir, {}).pipe(
|
|
231
|
+
Effect.mapError((error) =>
|
|
232
|
+
ProcessError.make({
|
|
233
|
+
message: `gh ${args.join(" ")}`,
|
|
234
|
+
detail: error.message
|
|
235
|
+
})
|
|
236
|
+
),
|
|
237
|
+
Effect.flatMap((result) =>
|
|
238
|
+
result.exitCode === 0 || allowFailure
|
|
239
|
+
? Effect.succeed(result)
|
|
240
|
+
: Effect.fail(
|
|
241
|
+
ProcessError.make({
|
|
242
|
+
message: `gh ${args.join(" ")}`,
|
|
243
|
+
detail:
|
|
244
|
+
[...result.stdout, ...result.stderr].join("\n").trim() ||
|
|
245
|
+
`exit code ${result.exitCode}`
|
|
246
|
+
})
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const read = <A>(
|
|
252
|
+
operation: string,
|
|
253
|
+
effect: Effect.Effect<A, FlowError>
|
|
254
|
+
): Effect.Effect<A, FlowError> => guarded(Capabilities.GhRead, operation, events, effect)
|
|
255
|
+
const write = <A>(
|
|
256
|
+
operation: string,
|
|
257
|
+
effect: Effect.Effect<A, FlowError>
|
|
258
|
+
): Effect.Effect<A, FlowError> => guarded(Capabilities.GhWrite, operation, events, effect)
|
|
259
|
+
|
|
260
|
+
const findOpenPr = run(prViewArgs, true).pipe(
|
|
261
|
+
Effect.map((result) =>
|
|
262
|
+
result.exitCode === 0
|
|
263
|
+
? output(result)
|
|
264
|
+
.split(/\r?\n/)
|
|
265
|
+
.map(parsePullRequestUrl)
|
|
266
|
+
.find((item) => item !== undefined)
|
|
267
|
+
: undefined
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
createPr: (title, body, base, draft = false) =>
|
|
273
|
+
write(
|
|
274
|
+
"gh pr create",
|
|
275
|
+
Effect.flatMap(findOpenPr, (existing) =>
|
|
276
|
+
existing === undefined
|
|
277
|
+
? run(prCreateArgs(title, body, base, draft)).pipe(
|
|
278
|
+
Effect.flatMap((result) => {
|
|
279
|
+
const pr = output(result)
|
|
280
|
+
.split(/\r?\n/)
|
|
281
|
+
.map(parsePullRequestUrl)
|
|
282
|
+
.find((item) => item !== undefined)
|
|
283
|
+
return pr === undefined
|
|
284
|
+
? Effect.fail(
|
|
285
|
+
ProcessError.make({
|
|
286
|
+
message: "gh pr create",
|
|
287
|
+
detail: `could not parse a PR URL from: ${output(result)}`
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
: Effect.succeed(pr)
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
: Effect.succeed(existing)
|
|
294
|
+
)
|
|
295
|
+
),
|
|
296
|
+
readIssue: (ref) =>
|
|
297
|
+
read(
|
|
298
|
+
"gh issue view",
|
|
299
|
+
run(issueViewArgs(ref)).pipe(Effect.flatMap((result) => parseIssue(output(result))))
|
|
300
|
+
),
|
|
301
|
+
writeIssueComment: (ref, body) =>
|
|
302
|
+
write("gh issue comment", run(issueCommentArgs(ref, body)).pipe(Effect.asVoid)),
|
|
303
|
+
writePrComment: (pr, body) =>
|
|
304
|
+
write("gh pr comment", run(prCommentArgs(pr, body)).pipe(Effect.asVoid)),
|
|
305
|
+
updatePr: (pr, title, body) =>
|
|
306
|
+
write("gh pr edit", run(prPatchArgs(pr, title, body)).pipe(Effect.asVoid)),
|
|
307
|
+
prChecks: (pr) =>
|
|
308
|
+
read(
|
|
309
|
+
"gh pr checks",
|
|
310
|
+
run(prChecksArgs(pr)).pipe(
|
|
311
|
+
Effect.flatMap((result) => outcomeFromChecksJson(output(result)))
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|