@jpowersdev/effect-pi 0.1.0 → 0.2.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/CHANGELOG.md +5 -0
- package/README.md +9 -1
- package/dist/Session.d.ts +1 -1
- package/dist/Session.d.ts.map +1 -1
- package/dist/Session.js +1 -1
- package/dist/Session.js.map +1 -1
- package/dist/internal/ClusterSessions.d.ts +10 -1
- package/dist/internal/ClusterSessions.d.ts.map +1 -1
- package/dist/internal/ClusterSessions.js +2 -2
- package/dist/internal/ClusterSessions.js.map +1 -1
- package/dist/internal/Pi.d.ts +1 -18
- package/dist/internal/Pi.d.ts.map +1 -1
- package/dist/internal/Pi.js +28 -3
- package/dist/internal/Pi.js.map +1 -1
- package/dist/internal/Protocol.d.ts +20 -2
- package/dist/internal/Protocol.d.ts.map +1 -1
- package/dist/internal/Protocol.js +1 -1
- package/dist/internal/Protocol.js.map +1 -1
- package/dist/internal/Session.d.ts +83 -7
- package/dist/internal/Session.d.ts.map +1 -1
- package/dist/internal/Session.js +108 -7
- package/dist/internal/Session.js.map +1 -1
- package/dist-examples/cluster-session.js +4 -2
- package/dist-examples/cluster-session.js.map +1 -1
- package/dist-examples/local-session-pool.js +4 -2
- package/dist-examples/local-session-pool.js.map +1 -1
- package/dist-examples/single-session.js +4 -2
- package/dist-examples/single-session.js.map +1 -1
- package/docs/reference.md +14 -4
- package/examples/cluster-session.ts +4 -2
- package/examples/local-session-pool.ts +4 -2
- package/examples/single-session.ts +4 -2
- package/package.json +16 -24
- package/src/Session.ts +10 -2
- package/src/internal/ClusterSessions.ts +6 -3
- package/src/internal/Pi.ts +33 -5
- package/src/internal/Protocol.ts +1 -1
- package/src/internal/Session.ts +165 -11
package/src/internal/Session.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type * as Pi from "@earendil-works/pi-coding-agent"
|
|
2
|
-
import
|
|
2
|
+
import * as Deferred from "effect/Deferred"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Exit from "effect/Exit"
|
|
5
|
+
import * as PubSub from "effect/PubSub"
|
|
6
|
+
import * as Queue from "effect/Queue"
|
|
3
7
|
import * as Schema from "effect/Schema"
|
|
4
|
-
import
|
|
8
|
+
import * as Stream from "effect/Stream"
|
|
9
|
+
import * as Take from "effect/Take"
|
|
5
10
|
|
|
6
11
|
/** A Pi session id accepted by Pi's SessionManager. */
|
|
7
12
|
export const Id = Schema.String.check(
|
|
@@ -30,32 +35,71 @@ export class PromptResult extends Schema.Class<PromptResult>("@jpowersdev/effect
|
|
|
30
35
|
costUsd: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))
|
|
31
36
|
}) {}
|
|
32
37
|
|
|
33
|
-
const StatusEvent = Schema.TaggedStruct("Status", {
|
|
38
|
+
export const StatusEvent = Schema.TaggedStruct("Status", {
|
|
34
39
|
sequence: Schema.Natural,
|
|
35
40
|
status: Status
|
|
36
41
|
})
|
|
37
|
-
|
|
42
|
+
export type StatusEvent = typeof StatusEvent.Type
|
|
43
|
+
|
|
44
|
+
export const MessageStart = Schema.TaggedStruct("MessageStart", {
|
|
45
|
+
sequence: Schema.Natural,
|
|
46
|
+
messageSequence: Schema.Natural
|
|
47
|
+
})
|
|
48
|
+
export type MessageStart = typeof MessageStart.Type
|
|
49
|
+
|
|
50
|
+
export const MessageDelta = Schema.TaggedStruct("MessageDelta", {
|
|
38
51
|
sequence: Schema.Natural,
|
|
52
|
+
messageSequence: Schema.Natural,
|
|
39
53
|
delta: Schema.String
|
|
40
54
|
})
|
|
41
|
-
|
|
55
|
+
export type MessageDelta = typeof MessageDelta.Type
|
|
56
|
+
|
|
57
|
+
export const MessageEnd = Schema.TaggedStruct("MessageEnd", {
|
|
58
|
+
sequence: Schema.Natural,
|
|
59
|
+
messageSequence: Schema.Natural,
|
|
60
|
+
content: Schema.String,
|
|
61
|
+
stopReason: Schema.String
|
|
62
|
+
})
|
|
63
|
+
export type MessageEnd = typeof MessageEnd.Type
|
|
64
|
+
|
|
65
|
+
export const MessagePart = Schema.Union([MessageStart, MessageDelta, MessageEnd])
|
|
66
|
+
export type MessagePart = typeof MessagePart.Type
|
|
67
|
+
|
|
68
|
+
export const ToolStartedEvent = Schema.TaggedStruct("ToolStarted", {
|
|
42
69
|
sequence: Schema.Natural,
|
|
43
70
|
toolName: Schema.String
|
|
44
71
|
})
|
|
45
|
-
|
|
72
|
+
export type ToolStartedEvent = typeof ToolStartedEvent.Type
|
|
73
|
+
|
|
74
|
+
export const ToolFinishedEvent = Schema.TaggedStruct("ToolFinished", {
|
|
46
75
|
sequence: Schema.Natural,
|
|
47
76
|
toolName: Schema.String,
|
|
48
77
|
isError: Schema.Boolean
|
|
49
78
|
})
|
|
79
|
+
export type ToolFinishedEvent = typeof ToolFinishedEvent.Type
|
|
50
80
|
|
|
51
|
-
/**
|
|
52
|
-
export const
|
|
81
|
+
/** Flat, serializable event protocol used across Cluster transport. */
|
|
82
|
+
export const WireEvent = Schema.Union([
|
|
53
83
|
StatusEvent,
|
|
54
|
-
|
|
84
|
+
MessageStart,
|
|
85
|
+
MessageDelta,
|
|
86
|
+
MessageEnd,
|
|
55
87
|
ToolStartedEvent,
|
|
56
88
|
ToolFinishedEvent
|
|
57
89
|
])
|
|
58
|
-
export type
|
|
90
|
+
export type WireEvent = typeof WireEvent.Type
|
|
91
|
+
|
|
92
|
+
/** One finite assistant response within a possibly multi-turn tool-using prompt. */
|
|
93
|
+
export interface AssistantMessage {
|
|
94
|
+
readonly _tag: "AssistantMessage"
|
|
95
|
+
readonly sequence: number
|
|
96
|
+
readonly stream: Stream.Stream<MessagePart, Error>
|
|
97
|
+
/** Await the authoritative text carried by MessageEnd. */
|
|
98
|
+
readonly content: Effect.Effect<string, Error>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** High-level live event stream exposed by a Session. */
|
|
102
|
+
export type Event = StatusEvent | AssistantMessage | ToolStartedEvent | ToolFinishedEvent
|
|
59
103
|
|
|
60
104
|
export const Operation = Schema.Literals([
|
|
61
105
|
"abort",
|
|
@@ -75,6 +119,111 @@ export class Error extends Schema.TaggedError<Error>()("SessionError", {
|
|
|
75
119
|
message: Schema.String
|
|
76
120
|
}) {}
|
|
77
121
|
|
|
122
|
+
interface ActiveMessage {
|
|
123
|
+
readonly messageSequence: number
|
|
124
|
+
readonly parts: Queue.Queue<Take.Take<MessagePart, Error>>
|
|
125
|
+
readonly content: Deferred.Deferred<string, Error>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Reconstruct high-level assistant-message resources from serializable event frames. */
|
|
129
|
+
export const eventsFromWire = (
|
|
130
|
+
id: Id,
|
|
131
|
+
wireEvents: Stream.Stream<WireEvent, Error>
|
|
132
|
+
): Stream.Stream<Event, Error> => Stream.unwrap(Effect.gen(function* () {
|
|
133
|
+
const output = yield* Effect.acquireRelease(
|
|
134
|
+
PubSub.sliding<Take.Take<Event, Error>>({ capacity: 1024, replay: 1024 }),
|
|
135
|
+
PubSub.shutdown
|
|
136
|
+
)
|
|
137
|
+
let active: ActiveMessage | undefined
|
|
138
|
+
|
|
139
|
+
const finishActive = (error: Error) => {
|
|
140
|
+
const current = active
|
|
141
|
+
active = undefined
|
|
142
|
+
if (current === undefined) return Effect.void
|
|
143
|
+
return Effect.all([
|
|
144
|
+
Queue.offer(current.parts, Exit.fail(error)),
|
|
145
|
+
Deferred.fail(current.content, error)
|
|
146
|
+
], { discard: true })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const consumeFrames: Effect.Effect<void, Error> = wireEvents.pipe(
|
|
150
|
+
Stream.runForEach((event) => Effect.gen(function* () {
|
|
151
|
+
switch (event._tag) {
|
|
152
|
+
case "MessageStart": {
|
|
153
|
+
yield* finishActive(new Error({
|
|
154
|
+
sessionId: id,
|
|
155
|
+
operation: "events",
|
|
156
|
+
message: "A new assistant message started before the previous message ended"
|
|
157
|
+
}))
|
|
158
|
+
const parts = yield* Queue.unbounded<Take.Take<MessagePart, Error>>()
|
|
159
|
+
const content = yield* Deferred.make<string, Error>()
|
|
160
|
+
active = { messageSequence: event.messageSequence, parts, content }
|
|
161
|
+
yield* PubSub.publish(output, [{
|
|
162
|
+
_tag: "AssistantMessage",
|
|
163
|
+
sequence: event.sequence,
|
|
164
|
+
stream: Stream.fromQueue(parts).pipe(Stream.flattenTake),
|
|
165
|
+
content: Deferred.await(content)
|
|
166
|
+
}])
|
|
167
|
+
yield* Queue.offer(parts, [event])
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
case "MessageDelta":
|
|
171
|
+
if (active?.messageSequence === event.messageSequence) {
|
|
172
|
+
yield* Queue.offer(active.parts, [event])
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
case "MessageEnd":
|
|
176
|
+
if (active?.messageSequence === event.messageSequence) {
|
|
177
|
+
const completed = active
|
|
178
|
+
active = undefined
|
|
179
|
+
yield* Queue.offer(completed.parts, [event])
|
|
180
|
+
yield* Deferred.succeed(completed.content, event.content)
|
|
181
|
+
yield* Queue.offer(completed.parts, Exit.void)
|
|
182
|
+
}
|
|
183
|
+
return
|
|
184
|
+
default:
|
|
185
|
+
if (event._tag === "Status" && event.status === "idle" && active !== undefined) {
|
|
186
|
+
yield* finishActive(new Error({
|
|
187
|
+
sessionId: id,
|
|
188
|
+
operation: "events",
|
|
189
|
+
message: "The assistant message ended with missing event frames"
|
|
190
|
+
}))
|
|
191
|
+
}
|
|
192
|
+
yield* PubSub.publish(output, [event])
|
|
193
|
+
}
|
|
194
|
+
}))
|
|
195
|
+
)
|
|
196
|
+
const consume = consumeFrames.pipe(
|
|
197
|
+
Effect.onExit((exit) => Effect.gen(function* () {
|
|
198
|
+
if (active !== undefined) {
|
|
199
|
+
if (Exit.isFailure(exit)) {
|
|
200
|
+
yield* Queue.offer(active.parts, exit)
|
|
201
|
+
yield* Deferred.failCause(active.content, exit.cause)
|
|
202
|
+
} else {
|
|
203
|
+
yield* finishActive(new Error({
|
|
204
|
+
sessionId: id,
|
|
205
|
+
operation: "events",
|
|
206
|
+
message: "The event stream ended before the assistant message completed"
|
|
207
|
+
}))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
yield* PubSub.publish(output, exit)
|
|
211
|
+
}))
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
yield* Effect.forkScoped(consume, { startImmediately: true })
|
|
215
|
+
|
|
216
|
+
return Stream.fromPubSubTake(output)
|
|
217
|
+
}))
|
|
218
|
+
|
|
219
|
+
/** Flatten high-level events into the serializable Cluster wire protocol. */
|
|
220
|
+
export const eventsToWire = (
|
|
221
|
+
events: Stream.Stream<Event, Error>
|
|
222
|
+
): Stream.Stream<WireEvent, Error> => events.pipe(
|
|
223
|
+
Stream.flatMap((event): Stream.Stream<WireEvent, Error> =>
|
|
224
|
+
event._tag === "AssistantMessage" ? event.stream : Stream.fromIterable<WireEvent>([event]))
|
|
225
|
+
)
|
|
226
|
+
|
|
78
227
|
/** One scoped Pi session, whether local or represented by a cluster client. */
|
|
79
228
|
export interface Session {
|
|
80
229
|
readonly id: Id
|
|
@@ -83,7 +232,12 @@ export interface Session {
|
|
|
83
232
|
readonly prompt: (text: string) => Effect.Effect<PromptResult, Error>
|
|
84
233
|
/** Interrupt the active prompt; queued prompts remain eligible to run. */
|
|
85
234
|
readonly abort: Effect.Effect<void, Error>
|
|
86
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Ephemeral, bounded activity stream. Each AssistantMessage owns one finite,
|
|
237
|
+
* single-consumer part stream and an independently awaitable final content effect.
|
|
238
|
+
* Sequence numbers are shared by top-level events and nested message parts;
|
|
239
|
+
* restoration resets them.
|
|
240
|
+
*/
|
|
87
241
|
readonly events: Stream.Stream<Event, Error>
|
|
88
242
|
/** Current serialization, not an acknowledgement that the backing store has been flushed. */
|
|
89
243
|
readonly jsonl: Effect.Effect<string, Error>
|