@batonfx/foldkit 0.4.3 → 0.6.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/README.md +63 -2
- package/dist/chat.d.ts +27 -17
- package/dist/chat.js +53 -20
- package/dist/connection.d.ts +28 -5
- package/dist/connection.js +79 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# `@batonfx/foldkit`
|
|
2
2
|
|
|
3
|
-
FoldKit adapter
|
|
3
|
+
Focused composition guide for the FoldKit adapter to Baton transport sessions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add effect foldkit @batonfx/foldkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Imports
|
|
12
|
+
|
|
13
|
+
Import the public namespace from the package root:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Connection } from "@batonfx/foldkit"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Layer graph
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
Connection.testLayer
|
|
23
|
+
└─ provides Connection.AgentConnection
|
|
24
|
+
└─ session acquisition requires Scope
|
|
25
|
+
└─ program closes it with Effect.scoped
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`testLayer` is a deterministic adapter for examples and tests. Production applications provide an `AgentConnection` backed by a Baton transport client; they do not use this synthetic frame stream.
|
|
29
|
+
|
|
30
|
+
## Runnable program
|
|
31
|
+
|
|
32
|
+
Checked source: [`../../examples/package-composition-guides/src/foldkit.ts`](../../examples/package-composition-guides/src/foldkit.ts)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Console, Effect, Stream } from "effect"
|
|
36
|
+
import { Connection } from "@batonfx/foldkit"
|
|
37
|
+
|
|
38
|
+
const connectionLayer = Connection.testLayer({
|
|
39
|
+
frames: () => Stream.make(Connection.ConnectionOpened()),
|
|
40
|
+
send: () => Effect.void,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const program = Connection.AgentConnection.use((connection) =>
|
|
44
|
+
Effect.scoped(
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const session = yield* connection.session({ sessionId: "guide-session" })
|
|
47
|
+
const frames = yield* Stream.runCollect(session.frames)
|
|
48
|
+
yield* Console.log(`received ${frames.length} connection event`)
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
).pipe(Effect.provide(connectionLayer))
|
|
52
|
+
|
|
53
|
+
await Effect.runPromise(program)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From the repository root, run `bun examples/package-composition-guides/src/foldkit.ts`.
|
|
57
|
+
|
|
58
|
+
## Errors, requirements, and resources
|
|
59
|
+
|
|
60
|
+
The fully provided program has type-level result `Effect<void, never, never>` and prints one received event. `Effect.scoped` delimits the session lifetime and discharges its `Scope` requirement; this test adapter has no release action, while production scoped connections release their transport resources. Production command writes can fail with schema-backed `Connection.SendFailed`; the test sender cannot fail. This example owns one session, allocates no queue, and creates no timers, detached fibers, or concurrent work.
|
|
61
|
+
|
|
62
|
+
## More
|
|
63
|
+
|
|
64
|
+
- Current behavior: [FoldKit adapter](../../docs/features/foldkit.md)
|
|
65
|
+
- Deeper example: [deep research agent](../../examples/deep-research-agent/)
|
|
66
|
+
- Existing custom `AgentConnection` providers must implement the scoped `session` acquisition; `testLayer` remains a compatibility adapter for legacy test implementations.
|
package/dist/chat.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Equivalence, Effect, Option, Schema, Stream } from "effect";
|
|
2
2
|
import { type Command } from "foldkit/command";
|
|
3
3
|
import type { CallableTaggedStruct } from "foldkit/schema";
|
|
4
|
-
import { AgentConnection, Incoming } from "./connection.js";
|
|
4
|
+
import { AgentCommandError, AgentConnection, CommandOperation, Incoming } from "./connection.js";
|
|
5
5
|
declare const CompletedFields: {
|
|
6
6
|
isFailure: Schema.Boolean;
|
|
7
7
|
result: Schema.Unknown;
|
|
@@ -16,9 +16,6 @@ declare const AssistantEntryFields: {
|
|
|
16
16
|
declare const RunCompletedFields: {
|
|
17
17
|
text: Schema.String;
|
|
18
18
|
};
|
|
19
|
-
declare const StringReasonFields: {
|
|
20
|
-
reason: Schema.String;
|
|
21
|
-
};
|
|
22
19
|
declare const OpenedSessionFields: {
|
|
23
20
|
sessionId: Schema.String;
|
|
24
21
|
};
|
|
@@ -35,7 +32,7 @@ declare const ToolEntryFields: {
|
|
|
35
32
|
progress: Schema.$Array<Schema.String>;
|
|
36
33
|
};
|
|
37
34
|
declare const RunningFields: {
|
|
38
|
-
turn: Schema.
|
|
35
|
+
turn: Schema.Finite;
|
|
39
36
|
};
|
|
40
37
|
declare const AwaitingApprovalFields: {
|
|
41
38
|
token: Schema.String;
|
|
@@ -49,7 +46,7 @@ declare const ReceivedAgentFields: {
|
|
|
49
46
|
incoming: Schema.Schema<Incoming>;
|
|
50
47
|
};
|
|
51
48
|
declare const ModelStreaming: Schema.Struct<{
|
|
52
|
-
readonly turn: Schema.
|
|
49
|
+
readonly turn: Schema.Finite;
|
|
53
50
|
readonly text: Schema.String;
|
|
54
51
|
readonly reasoning: Schema.String;
|
|
55
52
|
}>;
|
|
@@ -117,7 +114,11 @@ export declare const ResolvedApproval: CallableTaggedStruct<"ResolvedApproval",
|
|
|
117
114
|
/** @experimental */
|
|
118
115
|
export declare const CancelledRun: CallableTaggedStruct<"CancelledRun", {}>;
|
|
119
116
|
/** @experimental */
|
|
120
|
-
export declare const FailedAgentCommand: CallableTaggedStruct<"FailedAgentCommand",
|
|
117
|
+
export declare const FailedAgentCommand: CallableTaggedStruct<"FailedAgentCommand", {
|
|
118
|
+
operation: typeof CommandOperation;
|
|
119
|
+
error: typeof AgentCommandError;
|
|
120
|
+
reason: typeof Schema.String;
|
|
121
|
+
}>;
|
|
121
122
|
/** @experimental */
|
|
122
123
|
export type Message = typeof ReceivedAgent.Type | typeof OpenedSession.Type | typeof ChangedDraft.Type | typeof SubmittedMessage.Type | typeof ClickedCancel.Type | typeof ClickedApprove.Type | typeof ClickedDeny.Type | typeof SentUserMessage.Type | typeof ResolvedApproval.Type | typeof CancelledRun.Type | typeof FailedAgentCommand.Type;
|
|
123
124
|
/** @experimental */
|
|
@@ -198,7 +199,7 @@ export type ConversationItem = typeof UserConversationItem.Type | typeof Assista
|
|
|
198
199
|
/** @experimental */
|
|
199
200
|
export declare const ConversationItem: Schema.Schema<ConversationItem>;
|
|
200
201
|
/** @experimental */
|
|
201
|
-
export type ChatCommand = Command<Message,
|
|
202
|
+
export type ChatCommand = Command<Message, AgentCommandError, AgentConnection>;
|
|
202
203
|
/** @experimental */
|
|
203
204
|
export declare const initialModel: (sessionId?: string | null) => Model;
|
|
204
205
|
/** @experimental */
|
|
@@ -207,7 +208,9 @@ export declare const SendUserMessage: import("foldkit/command").CommandDefinitio
|
|
|
207
208
|
text: Schema.String;
|
|
208
209
|
}, Effect.Effect<Schema.Struct.ReadonlySide<{
|
|
209
210
|
readonly _tag: Schema.tag<"FailedAgentCommand">;
|
|
210
|
-
|
|
211
|
+
operation: typeof CommandOperation;
|
|
212
|
+
error: typeof AgentCommandError;
|
|
213
|
+
reason: typeof Schema.String;
|
|
211
214
|
}, "Type"> | {
|
|
212
215
|
readonly _tag: "SentUserMessage";
|
|
213
216
|
}, never, AgentConnection>>;
|
|
@@ -219,7 +222,9 @@ export declare const ResolveApproval: import("foldkit/command").CommandDefinitio
|
|
|
219
222
|
reason: Schema.NullOr<Schema.String>;
|
|
220
223
|
}, Effect.Effect<Schema.Struct.ReadonlySide<{
|
|
221
224
|
readonly _tag: Schema.tag<"FailedAgentCommand">;
|
|
222
|
-
|
|
225
|
+
operation: typeof CommandOperation;
|
|
226
|
+
error: typeof AgentCommandError;
|
|
227
|
+
reason: typeof Schema.String;
|
|
223
228
|
}, "Type"> | {
|
|
224
229
|
readonly _tag: "ResolvedApproval";
|
|
225
230
|
}, never, AgentConnection>>;
|
|
@@ -228,7 +233,9 @@ export declare const CancelRun: import("foldkit/command").CommandDefinitionWithA
|
|
|
228
233
|
sessionId: Schema.String;
|
|
229
234
|
}, Effect.Effect<Schema.Struct.ReadonlySide<{
|
|
230
235
|
readonly _tag: Schema.tag<"FailedAgentCommand">;
|
|
231
|
-
|
|
236
|
+
operation: typeof CommandOperation;
|
|
237
|
+
error: typeof AgentCommandError;
|
|
238
|
+
reason: typeof Schema.String;
|
|
232
239
|
}, "Type"> | {
|
|
233
240
|
readonly _tag: "CancelledRun";
|
|
234
241
|
}, never, AgentConnection>>;
|
|
@@ -239,30 +246,33 @@ export declare const toolStatusOf: (entry: typeof ToolEntry.Type) => ToolStatus;
|
|
|
239
246
|
/** @experimental */
|
|
240
247
|
export declare const conversationItems: (model: Model) => ReadonlyArray<ConversationItem>;
|
|
241
248
|
/** @experimental */
|
|
242
|
-
export declare const update:
|
|
249
|
+
export declare const update: {
|
|
250
|
+
(message: Message): (model: Model) => readonly [Model, ReadonlyArray<ChatCommand>, Option.Option<OutMessage>];
|
|
251
|
+
(model: Model, message: Message): readonly [Model, ReadonlyArray<ChatCommand>, Option.Option<OutMessage>];
|
|
252
|
+
};
|
|
243
253
|
/** @experimental */
|
|
244
254
|
export declare const subscriptions: {
|
|
245
255
|
readonly agentFrames: {
|
|
246
256
|
readonly dependenciesSchema: Schema.Schema<Schema.Struct.ReadonlySide<{
|
|
247
257
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
248
|
-
readonly afterSeq: Schema.
|
|
258
|
+
readonly afterSeq: Schema.Finite;
|
|
249
259
|
}, "Type">> & {
|
|
250
260
|
readonly fields: Schema.Struct.Fields;
|
|
251
261
|
};
|
|
252
262
|
readonly modelToDependencies: (model: Model) => Schema.Struct.ReadonlySide<{
|
|
253
263
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
254
|
-
readonly afterSeq: Schema.
|
|
264
|
+
readonly afterSeq: Schema.Finite;
|
|
255
265
|
}, "Type">;
|
|
256
266
|
readonly keepAliveEquivalence: Equivalence.Equivalence<Schema.Struct.ReadonlySide<{
|
|
257
267
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
258
|
-
readonly afterSeq: Schema.
|
|
268
|
+
readonly afterSeq: Schema.Finite;
|
|
259
269
|
}, "Type">>;
|
|
260
270
|
readonly dependenciesToStream: (dependencies: Schema.Struct.ReadonlySide<{
|
|
261
271
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
262
|
-
readonly afterSeq: Schema.
|
|
272
|
+
readonly afterSeq: Schema.Finite;
|
|
263
273
|
}, "Type">, readDependencies: () => Schema.Struct.ReadonlySide<{
|
|
264
274
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
265
|
-
readonly afterSeq: Schema.
|
|
275
|
+
readonly afterSeq: Schema.Finite;
|
|
266
276
|
}, "Type">) => Stream.Stream<Message, never, AgentConnection>;
|
|
267
277
|
} & {
|
|
268
278
|
readonly __subscription: never;
|
package/dist/chat.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { Cause, Equivalence, Effect, Option, Schema, Stream } from "effect";
|
|
1
|
+
import { Cause, Equivalence, Effect, Option, Result, Schema, Stream } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
2
3
|
import { Prompt } from "effect/unstable/ai";
|
|
3
4
|
import { define } from "foldkit/command";
|
|
4
5
|
import { m } from "foldkit/message";
|
|
5
6
|
import { make } from "foldkit/subscription";
|
|
6
7
|
import { Wire } from "@batonfx/transport";
|
|
7
|
-
import { AgentConnection, Incoming, SendFailed } from "./connection.js";
|
|
8
|
+
import { AgentCommandError, AgentConnection, CommandOperation, Incoming, SendFailed } from "./connection.js";
|
|
8
9
|
const CompletedFields = { isFailure: Schema.Boolean, result: Schema.Unknown };
|
|
9
10
|
const UserEntryFields = { text: Schema.String };
|
|
10
11
|
const AssistantEntryFields = { text: Schema.String, reasoning: Schema.NullOr(Schema.String) };
|
|
11
12
|
const RunCompletedFields = { text: Schema.String };
|
|
12
|
-
const StringReasonFields = { reason: Schema.String };
|
|
13
13
|
const OpenedSessionFields = { sessionId: Schema.String };
|
|
14
14
|
/** @experimental */
|
|
15
15
|
export const ToolPendingPhase = Schema.Literals(["called", "executing"]);
|
|
@@ -21,7 +21,7 @@ const ToolEntryFields = {
|
|
|
21
21
|
outcome: Schema.suspend(() => ToolOutcome),
|
|
22
22
|
progress: Schema.Array(Schema.String),
|
|
23
23
|
};
|
|
24
|
-
const RunningFields = { turn: Schema.
|
|
24
|
+
const RunningFields = { turn: Schema.Finite };
|
|
25
25
|
const AwaitingApprovalFields = {
|
|
26
26
|
token: Schema.String,
|
|
27
27
|
toolName: Schema.String,
|
|
@@ -30,7 +30,7 @@ const AwaitingApprovalFields = {
|
|
|
30
30
|
const ClickedDenyFields = { reason: Schema.NullOr(Schema.String) };
|
|
31
31
|
const ReceivedAgentFields = { incoming: Incoming };
|
|
32
32
|
const ModelStreaming = Schema.Struct({
|
|
33
|
-
turn: Schema.
|
|
33
|
+
turn: Schema.Finite,
|
|
34
34
|
text: Schema.String,
|
|
35
35
|
reasoning: Schema.String,
|
|
36
36
|
});
|
|
@@ -63,7 +63,7 @@ export const RunState = Schema.Union([Idle, Running, AwaitingApproval, Failed]);
|
|
|
63
63
|
export const Model = Schema.Struct({
|
|
64
64
|
sessionId: Schema.NullOr(Schema.String),
|
|
65
65
|
connection: ModelConnection,
|
|
66
|
-
lastSeq: Schema.
|
|
66
|
+
lastSeq: Schema.Finite,
|
|
67
67
|
run: RunState,
|
|
68
68
|
entries: Schema.Array(ChatEntry),
|
|
69
69
|
streaming: Schema.NullOr(ModelStreaming),
|
|
@@ -90,7 +90,11 @@ export const ResolvedApproval = m("ResolvedApproval");
|
|
|
90
90
|
/** @experimental */
|
|
91
91
|
export const CancelledRun = m("CancelledRun");
|
|
92
92
|
/** @experimental */
|
|
93
|
-
export const FailedAgentCommand = m("FailedAgentCommand",
|
|
93
|
+
export const FailedAgentCommand = m("FailedAgentCommand", {
|
|
94
|
+
operation: CommandOperation,
|
|
95
|
+
error: AgentCommandError,
|
|
96
|
+
reason: Schema.String,
|
|
97
|
+
});
|
|
94
98
|
/** @experimental */
|
|
95
99
|
export const Message = Schema.Union([
|
|
96
100
|
ReceivedAgent,
|
|
@@ -173,10 +177,28 @@ export const initialModel = (sessionId = null) => ({
|
|
|
173
177
|
streaming: null,
|
|
174
178
|
draft: "",
|
|
175
179
|
});
|
|
176
|
-
const
|
|
177
|
-
const
|
|
180
|
+
const unexpectedCause = (cause) => {
|
|
181
|
+
const reasons = [];
|
|
182
|
+
for (const reason of cause.reasons) {
|
|
183
|
+
if (Cause.isDieReason(reason) || Cause.isInterruptReason(reason))
|
|
184
|
+
reasons.push(reason);
|
|
185
|
+
}
|
|
186
|
+
return reasons.length === 0 ? Option.none() : Option.some(Cause.fromReasons(reasons));
|
|
187
|
+
};
|
|
188
|
+
const commandFailed = (operation, error) => FailedAgentCommand({
|
|
189
|
+
operation,
|
|
190
|
+
error,
|
|
191
|
+
reason: Schema.is(SendFailed)(error) ? error.reason : error.message,
|
|
192
|
+
});
|
|
193
|
+
const catchCommandFailure = (operation, effect) => effect.pipe(Effect.catchCause((cause) => Option.match(unexpectedCause(cause), {
|
|
194
|
+
onNone: () => Result.match(Cause.findError(cause), {
|
|
195
|
+
onFailure: Effect.failCause,
|
|
196
|
+
onSuccess: (error) => Effect.succeed(commandFailed(operation, error)),
|
|
197
|
+
}),
|
|
198
|
+
onSome: Effect.failCause,
|
|
199
|
+
})));
|
|
178
200
|
/** @experimental */
|
|
179
|
-
export const SendUserMessage = define("SendUserMessage", { sessionId: Schema.String, text: Schema.String }, SentUserMessage, FailedAgentCommand)(({ sessionId, text }) => AgentConnection.use((connection) => catchCommandFailure(connection.send({ _tag: "SendMessage", sessionId, prompt: text }).pipe(Effect.as(SentUserMessage())))));
|
|
201
|
+
export const SendUserMessage = define("SendUserMessage", { sessionId: Schema.String, text: Schema.String }, SentUserMessage, FailedAgentCommand)(({ sessionId, text }) => AgentConnection.use((connection) => catchCommandFailure("send", connection.send({ _tag: "SendMessage", sessionId, prompt: text }).pipe(Effect.as(SentUserMessage())))));
|
|
180
202
|
/** @experimental */
|
|
181
203
|
export const ResolveApproval = define("ResolveApproval", {
|
|
182
204
|
sessionId: Schema.String,
|
|
@@ -189,10 +211,10 @@ export const ResolveApproval = define("ResolveApproval", {
|
|
|
189
211
|
: reason === null
|
|
190
212
|
? { _tag: "Denied" }
|
|
191
213
|
: { _tag: "Denied", reason };
|
|
192
|
-
return AgentConnection.use((connection) => catchCommandFailure(connection.send({ _tag: "ResolveApproval", sessionId, token, decision }).pipe(Effect.as(ResolvedApproval()))));
|
|
214
|
+
return AgentConnection.use((connection) => catchCommandFailure("resolveApproval", connection.send({ _tag: "ResolveApproval", sessionId, token, decision }).pipe(Effect.as(ResolvedApproval()))));
|
|
193
215
|
});
|
|
194
216
|
/** @experimental */
|
|
195
|
-
export const CancelRun = define("CancelRun", { sessionId: Schema.String }, CancelledRun, FailedAgentCommand)(({ sessionId }) => AgentConnection.use((connection) => catchCommandFailure(connection.send({ _tag: "Cancel", sessionId }).pipe(Effect.as(CancelledRun())))));
|
|
217
|
+
export const CancelRun = define("CancelRun", { sessionId: Schema.String }, CancelledRun, FailedAgentCommand)(({ sessionId }) => AgentConnection.use((connection) => catchCommandFailure("cancel", connection.send({ _tag: "Cancel", sessionId }).pipe(Effect.as(CancelledRun())))));
|
|
196
218
|
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
197
219
|
const isToolCall = (value) => isRecord(value) &&
|
|
198
220
|
value.type === "tool-call" &&
|
|
@@ -261,10 +283,20 @@ const failureMessage = (failure) => {
|
|
|
261
283
|
switch (failure._tag) {
|
|
262
284
|
case "@batonfx/core/AgentError":
|
|
263
285
|
return failure.message;
|
|
286
|
+
case "@batonfx/core/TurnPolicyError":
|
|
287
|
+
return failure.message;
|
|
288
|
+
case "@batonfx/core/TurnPolicyStopped":
|
|
289
|
+
return failure.reason._tag === "Policy" ? failure.reason.detail : failure.reason._tag;
|
|
264
290
|
case "@batonfx/core/MiddlewareViolation":
|
|
265
291
|
return failure.detail;
|
|
266
292
|
case "@batonfx/core/TurnLimitExceeded":
|
|
267
293
|
return `Turn limit exceeded at turn ${failure.turn}`;
|
|
294
|
+
case "@batonfx/core/ResumeMismatch":
|
|
295
|
+
return failure.reason === "identity-mismatch"
|
|
296
|
+
? "Resume suspension does not match the current checkpoint"
|
|
297
|
+
: "Resume checkpoint not found";
|
|
298
|
+
case "@batonfx/core/FrameworkFailure":
|
|
299
|
+
return `${failure.tool} ${failure.stage}: ${failure.message}`;
|
|
268
300
|
}
|
|
269
301
|
};
|
|
270
302
|
const applyPart = (model, turn, part) => {
|
|
@@ -389,6 +421,9 @@ const projectPrompt = (prompt) => {
|
|
|
389
421
|
return entries;
|
|
390
422
|
};
|
|
391
423
|
const applyFrame = (model, frame) => {
|
|
424
|
+
if (frame._tag === "Snapshot") {
|
|
425
|
+
return [{ ...model, lastSeq: frame.seq, entries: projectPrompt(frame.transcript), streaming: null }, Option.none()];
|
|
426
|
+
}
|
|
392
427
|
if (frame.seq <= model.lastSeq)
|
|
393
428
|
return [model, Option.none()];
|
|
394
429
|
const withSeq = { ...model, lastSeq: frame.seq };
|
|
@@ -403,8 +438,6 @@ const applyFrame = (model, frame) => {
|
|
|
403
438
|
}
|
|
404
439
|
case "Ended":
|
|
405
440
|
return [withSeq, Option.none()];
|
|
406
|
-
case "Snapshot":
|
|
407
|
-
return [{ ...withSeq, entries: projectPrompt(frame.transcript), streaming: null }, Option.none()];
|
|
408
441
|
case "SessionStatus":
|
|
409
442
|
return applyStatus(withSeq, frame.status);
|
|
410
443
|
}
|
|
@@ -495,7 +528,7 @@ const isServerFrame = (incoming) => incoming._tag === "Event" ||
|
|
|
495
528
|
incoming._tag === "Snapshot" ||
|
|
496
529
|
incoming._tag === "SessionStatus";
|
|
497
530
|
/** @experimental */
|
|
498
|
-
export const update = (model, message) => {
|
|
531
|
+
export const update = dual(2, (model, message) => {
|
|
499
532
|
switch (message._tag) {
|
|
500
533
|
case "ReceivedAgent":
|
|
501
534
|
if (isServerFrame(message.incoming)) {
|
|
@@ -585,10 +618,10 @@ export const update = (model, message) => {
|
|
|
585
618
|
case "CancelledRun":
|
|
586
619
|
return [model, [], Option.none()];
|
|
587
620
|
}
|
|
588
|
-
};
|
|
621
|
+
});
|
|
589
622
|
/** @experimental */
|
|
590
623
|
export const subscriptions = make()((entry) => ({
|
|
591
|
-
agentFrames: entry({ sessionId: Schema.NullOr(Schema.String), afterSeq: Schema.
|
|
624
|
+
agentFrames: entry({ sessionId: Schema.NullOr(Schema.String), afterSeq: Schema.Finite }, {
|
|
592
625
|
modelToDependencies: (model) => ({ sessionId: model.sessionId, afterSeq: model.lastSeq }),
|
|
593
626
|
keepAliveEquivalence: Equivalence.make((left, right) => left.sessionId === right.sessionId),
|
|
594
627
|
dependenciesToStream: ({ sessionId }, readDependencies) => {
|
|
@@ -596,9 +629,9 @@ export const subscriptions = make()((entry) => ({
|
|
|
596
629
|
return Stream.empty;
|
|
597
630
|
return Stream.unwrap(AgentConnection.use((connection) => {
|
|
598
631
|
const afterSeq = readDependencies().afterSeq;
|
|
599
|
-
return
|
|
600
|
-
.
|
|
601
|
-
.pipe(Stream.map((incoming) => ReceivedAgent({ incoming }))));
|
|
632
|
+
return connection
|
|
633
|
+
.session(afterSeq < 0 ? { sessionId } : { sessionId, afterSeq })
|
|
634
|
+
.pipe(Effect.map((sessionConnection) => sessionConnection.frames.pipe(Stream.map((incoming) => ReceivedAgent({ incoming })))));
|
|
602
635
|
}));
|
|
603
636
|
},
|
|
604
637
|
}),
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { Cause, Context, Effect, Layer, Schema, Stream } from "effect";
|
|
1
|
+
import { Cause, Context, Effect, Layer, Schema, Scope, Stream } from "effect";
|
|
2
2
|
import { Socket } from "effect/unstable/socket";
|
|
3
3
|
import type { CallableTaggedStruct } from "foldkit/schema";
|
|
4
|
-
import { Wire } from "@batonfx/transport";
|
|
4
|
+
import { Errors, Wire } from "@batonfx/transport";
|
|
5
5
|
/** @experimental */
|
|
6
6
|
export declare const ConnectionOpened: CallableTaggedStruct<"ConnectionOpened", {}>;
|
|
7
7
|
/** @experimental */
|
|
8
8
|
export declare const ConnectionLost: CallableTaggedStruct<"ConnectionLost", {}>;
|
|
9
9
|
/** @experimental */
|
|
10
10
|
export declare const ConnectionFailed: CallableTaggedStruct<"ConnectionFailed", {
|
|
11
|
+
operation: Schema.Literal<"connect">;
|
|
12
|
+
error: typeof Errors.TransportError;
|
|
11
13
|
reason: typeof Schema.String;
|
|
12
14
|
}>;
|
|
13
15
|
/** @experimental */
|
|
@@ -21,19 +23,40 @@ declare const SendFailed_base: Schema.Class<SendFailed, Schema.TaggedStruct<"@ba
|
|
|
21
23
|
export declare class SendFailed extends SendFailed_base {
|
|
22
24
|
}
|
|
23
25
|
/** @experimental */
|
|
26
|
+
export declare const AgentCommandError: Schema.Union<readonly [typeof Errors.TransportError, typeof SendFailed]>;
|
|
27
|
+
/** @experimental */
|
|
28
|
+
export type AgentCommandError = typeof AgentCommandError.Type;
|
|
29
|
+
/** @experimental */
|
|
30
|
+
export declare const CommandOperation: Schema.Literals<readonly ["send", "cancel", "resolveApproval"]>;
|
|
31
|
+
/** @experimental */
|
|
32
|
+
export type CommandOperation = typeof CommandOperation.Type;
|
|
33
|
+
/** @experimental */
|
|
34
|
+
export interface SessionConnection {
|
|
35
|
+
readonly sessionId: string;
|
|
36
|
+
readonly frames: Stream.Stream<Incoming, never>;
|
|
37
|
+
readonly send: (frame: Exclude<Wire.ClientFrameType, {
|
|
38
|
+
readonly _tag: "Attach";
|
|
39
|
+
}>) => Effect.Effect<void, AgentCommandError>;
|
|
40
|
+
}
|
|
41
|
+
/** @experimental */
|
|
24
42
|
export interface Interface {
|
|
43
|
+
readonly session: (options: {
|
|
44
|
+
readonly sessionId: string;
|
|
45
|
+
readonly afterSeq?: number;
|
|
46
|
+
}) => Effect.Effect<SessionConnection, never, Scope.Scope>;
|
|
25
47
|
readonly frames: (options: {
|
|
26
48
|
readonly sessionId: string;
|
|
27
49
|
readonly afterSeq?: number;
|
|
28
50
|
}) => Stream.Stream<Incoming, never>;
|
|
29
|
-
readonly send: (frame: Wire.ClientFrameType) => Effect.Effect<void,
|
|
51
|
+
readonly send: (frame: Wire.ClientFrameType) => Effect.Effect<void, AgentCommandError>;
|
|
30
52
|
}
|
|
31
|
-
declare const AgentConnection_base: Context.ServiceClass<AgentConnection, "@batonfx/foldkit/AgentConnection", Interface>;
|
|
53
|
+
declare const AgentConnection_base: Context.ServiceClass<AgentConnection, "@batonfx/foldkit/connection/AgentConnection", Interface>;
|
|
32
54
|
/** @experimental */
|
|
33
55
|
export declare class AgentConnection extends AgentConnection_base {
|
|
34
56
|
}
|
|
57
|
+
type LegacyInterface = Omit<Interface, "session">;
|
|
35
58
|
/** @experimental */
|
|
36
|
-
export declare const testLayer: (implementation: Interface) => Layer.Layer<AgentConnection>;
|
|
59
|
+
export declare const testLayer: (implementation: Interface | LegacyInterface) => Layer.Layer<AgentConnection>;
|
|
37
60
|
/** @experimental */
|
|
38
61
|
export declare const layerWebSocket: (options: {
|
|
39
62
|
readonly url: string;
|
package/dist/connection.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { Cause, Context, Effect, Layer, Option, Ref, Result, Schema, Stream } from "effect";
|
|
1
|
+
import { Cause, Context, Effect, Layer, Option, Ref, Result, Schema, Scope, Stream } from "effect";
|
|
2
2
|
import { Socket } from "effect/unstable/socket";
|
|
3
3
|
import { m } from "foldkit/message";
|
|
4
|
-
import { Client, Wire } from "@batonfx/transport";
|
|
4
|
+
import { Client, Errors, Wire } from "@batonfx/transport";
|
|
5
5
|
/** @experimental */
|
|
6
6
|
export const ConnectionOpened = m("ConnectionOpened");
|
|
7
7
|
/** @experimental */
|
|
8
8
|
export const ConnectionLost = m("ConnectionLost");
|
|
9
9
|
/** @experimental */
|
|
10
|
-
export const ConnectionFailed = m("ConnectionFailed", {
|
|
10
|
+
export const ConnectionFailed = m("ConnectionFailed", {
|
|
11
|
+
operation: Schema.Literal("connect"),
|
|
12
|
+
error: Errors.TransportError,
|
|
13
|
+
reason: Schema.String,
|
|
14
|
+
});
|
|
11
15
|
/** @experimental */
|
|
12
16
|
export const Incoming = Schema.Union([
|
|
13
17
|
Wire.LooseServerFrame,
|
|
@@ -21,50 +25,93 @@ export class SendFailed extends Schema.TaggedErrorClass()("@batonfx/foldkit/Send
|
|
|
21
25
|
}) {
|
|
22
26
|
}
|
|
23
27
|
/** @experimental */
|
|
24
|
-
export
|
|
28
|
+
export const AgentCommandError = Schema.Union([Errors.TransportError, SendFailed]);
|
|
29
|
+
/** @experimental */
|
|
30
|
+
export const CommandOperation = Schema.Literals(["send", "cancel", "resolveApproval"]);
|
|
31
|
+
/** @experimental */
|
|
32
|
+
export class AgentConnection extends Context.Service()("@batonfx/foldkit/connection/AgentConnection") {
|
|
25
33
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const sendThrough = (connection, frame) => connection.send(frame);
|
|
35
|
+
const unexpectedCause = (cause) => {
|
|
36
|
+
const reasons = [];
|
|
37
|
+
for (const reason of cause.reasons) {
|
|
38
|
+
if (Cause.isDieReason(reason) || Cause.isInterruptReason(reason))
|
|
39
|
+
reasons.push(reason);
|
|
40
|
+
}
|
|
41
|
+
return reasons.length === 0 ? Option.none() : Option.some(Cause.fromReasons(reasons));
|
|
32
42
|
};
|
|
33
43
|
const statusIncoming = (status) => {
|
|
34
44
|
switch (status._tag) {
|
|
35
|
-
case "
|
|
45
|
+
case "Connected":
|
|
36
46
|
return Option.some(ConnectionOpened());
|
|
37
|
-
case "
|
|
38
|
-
case "
|
|
47
|
+
case "Disconnected":
|
|
48
|
+
case "Retrying":
|
|
39
49
|
return Option.some(ConnectionLost());
|
|
40
50
|
case "Connecting":
|
|
41
51
|
return Option.none();
|
|
42
52
|
}
|
|
43
53
|
};
|
|
44
54
|
/** @experimental */
|
|
45
|
-
export const testLayer = (implementation) =>
|
|
55
|
+
export const testLayer = (implementation) => {
|
|
56
|
+
const session = "session" in implementation
|
|
57
|
+
? implementation.session
|
|
58
|
+
: (options) => Effect.succeed({
|
|
59
|
+
sessionId: options.sessionId,
|
|
60
|
+
frames: implementation.frames(options),
|
|
61
|
+
send: (frame) => frame.sessionId === options.sessionId
|
|
62
|
+
? implementation.send(frame)
|
|
63
|
+
: Effect.fail(SendFailed.make({
|
|
64
|
+
reason: `Session ${options.sessionId} cannot send a command for ${frame.sessionId}`,
|
|
65
|
+
})),
|
|
66
|
+
});
|
|
67
|
+
return Layer.succeed(AgentConnection, AgentConnection.of({ ...implementation, session }));
|
|
68
|
+
};
|
|
46
69
|
/** @experimental */
|
|
47
70
|
export const layerWebSocket = (options) => Layer.effect(AgentConnection, Effect.gen(function* () {
|
|
48
71
|
const client = yield* Client.AgentClient;
|
|
49
|
-
const active = yield* Ref.make(
|
|
50
|
-
const clearActive = (
|
|
72
|
+
const active = yield* Ref.make(new Map());
|
|
73
|
+
const clearActive = (owner) => Ref.update(active, (current) => {
|
|
74
|
+
if (current.get(owner.sessionId) !== owner)
|
|
75
|
+
return current;
|
|
76
|
+
const updated = new Map(current);
|
|
77
|
+
updated.delete(owner.sessionId);
|
|
78
|
+
return updated;
|
|
79
|
+
});
|
|
80
|
+
const session = ({ sessionId }) => Effect.gen(function* () {
|
|
81
|
+
const connection = yield* client.connect({ url: options.url, sessionId });
|
|
82
|
+
const owner = { sessionId, connection };
|
|
83
|
+
yield* Effect.acquireRelease(Ref.update(active, (current) => {
|
|
84
|
+
const updated = new Map(current);
|
|
85
|
+
updated.set(sessionId, owner);
|
|
86
|
+
return updated;
|
|
87
|
+
}), () => clearActive(owner));
|
|
88
|
+
const statuses = connection.status.pipe(Stream.filterMap((status) => Option.match(statusIncoming(status), {
|
|
89
|
+
onNone: () => Result.fail(undefined),
|
|
90
|
+
onSome: Result.succeed,
|
|
91
|
+
})));
|
|
92
|
+
const frames = connection.frames.pipe(Stream.map((frame) => frame), Stream.catchCause((cause) => Option.match(unexpectedCause(cause), {
|
|
93
|
+
onNone: () => Result.match(Cause.findError(cause), {
|
|
94
|
+
onFailure: Stream.failCause,
|
|
95
|
+
onSuccess: (error) => Stream.succeed(ConnectionFailed({ operation: "connect", error, reason: error.message })),
|
|
96
|
+
}),
|
|
97
|
+
onSome: Stream.failCause,
|
|
98
|
+
})));
|
|
99
|
+
return {
|
|
100
|
+
sessionId,
|
|
101
|
+
frames: statuses.pipe(Stream.merge(frames)),
|
|
102
|
+
send: (frame) => frame.sessionId === sessionId
|
|
103
|
+
? sendThrough(connection, frame)
|
|
104
|
+
: Effect.fail(SendFailed.make({ reason: `Session ${sessionId} cannot send a command for ${frame.sessionId}` })),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
51
107
|
return AgentConnection.of({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})));
|
|
60
|
-
const frames = connection.frames.pipe(Stream.map((frame) => frame), Stream.catchCause((cause) => Stream.succeed(ConnectionFailed({ reason: reasonFrom(Cause.squash(cause)) }))));
|
|
61
|
-
return statuses.pipe(Stream.merge(frames));
|
|
108
|
+
session,
|
|
109
|
+
frames: (sessionOptions) => Stream.unwrap(session(sessionOptions).pipe(Effect.map((owned) => owned.frames))),
|
|
110
|
+
send: (frame) => Ref.get(active).pipe(Effect.flatMap((current) => {
|
|
111
|
+
const owner = current.get(frame.sessionId);
|
|
112
|
+
return owner === undefined
|
|
113
|
+
? Effect.fail(SendFailed.make({ reason: `No active agent connection for session ${frame.sessionId}` }))
|
|
114
|
+
: sendThrough(owner.connection, frame);
|
|
62
115
|
})),
|
|
63
|
-
send: (frame) => Ref.get(active).pipe(Effect.flatMap(Option.match({
|
|
64
|
-
onNone: () => Effect.fail(new SendFailed({ reason: "No active agent connection" })),
|
|
65
|
-
onSome: ({ connection }) => connection
|
|
66
|
-
.send(frame)
|
|
67
|
-
.pipe(Effect.mapError((error) => new SendFailed({ reason: reasonFrom(error) }))),
|
|
68
|
-
}))),
|
|
69
116
|
});
|
|
70
117
|
})).pipe(Layer.provide(Client.layerWebSocket));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@batonfx/foldkit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typecheck": "bun tsc --noEmit"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@batonfx/transport": "0.
|
|
20
|
+
"@batonfx/transport": "0.6.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"effect": ">=4.0.0-beta.88 <4.0.1",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/bun": "1.3.13",
|
|
29
29
|
"effect": "4.0.0-beta.93",
|
|
30
30
|
"foldkit": "0.122.0",
|
|
31
|
-
"typescript": "
|
|
31
|
+
"typescript": "7.0.2",
|
|
32
32
|
"vitest": "4.0.16"
|
|
33
33
|
},
|
|
34
34
|
"description": "FoldKit adapter for Baton transport sessions and chat UI",
|