@batonfx/foldkit 0.4.4 → 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 +16 -9
- package/dist/chat.js +46 -14
- package/dist/connection.d.ts +27 -4
- package/dist/connection.js +78 -31
- package/package.json +2 -2
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
|
};
|
|
@@ -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>>;
|
package/dist/chat.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { Cause, Equivalence, Effect, Option, Schema, Stream } from "effect";
|
|
1
|
+
import { Cause, Equivalence, Effect, Option, Result, Schema, Stream } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
3
|
import { Prompt } from "effect/unstable/ai";
|
|
4
4
|
import { define } from "foldkit/command";
|
|
5
5
|
import { m } from "foldkit/message";
|
|
6
6
|
import { make } from "foldkit/subscription";
|
|
7
7
|
import { Wire } from "@batonfx/transport";
|
|
8
|
-
import { AgentConnection, Incoming, SendFailed } from "./connection.js";
|
|
8
|
+
import { AgentCommandError, AgentConnection, CommandOperation, Incoming, SendFailed } from "./connection.js";
|
|
9
9
|
const CompletedFields = { isFailure: Schema.Boolean, result: Schema.Unknown };
|
|
10
10
|
const UserEntryFields = { text: Schema.String };
|
|
11
11
|
const AssistantEntryFields = { text: Schema.String, reasoning: Schema.NullOr(Schema.String) };
|
|
12
12
|
const RunCompletedFields = { text: Schema.String };
|
|
13
|
-
const StringReasonFields = { reason: Schema.String };
|
|
14
13
|
const OpenedSessionFields = { sessionId: Schema.String };
|
|
15
14
|
/** @experimental */
|
|
16
15
|
export const ToolPendingPhase = Schema.Literals(["called", "executing"]);
|
|
@@ -91,7 +90,11 @@ export const ResolvedApproval = m("ResolvedApproval");
|
|
|
91
90
|
/** @experimental */
|
|
92
91
|
export const CancelledRun = m("CancelledRun");
|
|
93
92
|
/** @experimental */
|
|
94
|
-
export const FailedAgentCommand = m("FailedAgentCommand",
|
|
93
|
+
export const FailedAgentCommand = m("FailedAgentCommand", {
|
|
94
|
+
operation: CommandOperation,
|
|
95
|
+
error: AgentCommandError,
|
|
96
|
+
reason: Schema.String,
|
|
97
|
+
});
|
|
95
98
|
/** @experimental */
|
|
96
99
|
export const Message = Schema.Union([
|
|
97
100
|
ReceivedAgent,
|
|
@@ -174,10 +177,28 @@ export const initialModel = (sessionId = null) => ({
|
|
|
174
177
|
streaming: null,
|
|
175
178
|
draft: "",
|
|
176
179
|
});
|
|
177
|
-
const
|
|
178
|
-
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
|
+
})));
|
|
179
200
|
/** @experimental */
|
|
180
|
-
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())))));
|
|
181
202
|
/** @experimental */
|
|
182
203
|
export const ResolveApproval = define("ResolveApproval", {
|
|
183
204
|
sessionId: Schema.String,
|
|
@@ -190,10 +211,10 @@ export const ResolveApproval = define("ResolveApproval", {
|
|
|
190
211
|
: reason === null
|
|
191
212
|
? { _tag: "Denied" }
|
|
192
213
|
: { _tag: "Denied", reason };
|
|
193
|
-
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()))));
|
|
194
215
|
});
|
|
195
216
|
/** @experimental */
|
|
196
|
-
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())))));
|
|
197
218
|
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
198
219
|
const isToolCall = (value) => isRecord(value) &&
|
|
199
220
|
value.type === "tool-call" &&
|
|
@@ -262,10 +283,20 @@ const failureMessage = (failure) => {
|
|
|
262
283
|
switch (failure._tag) {
|
|
263
284
|
case "@batonfx/core/AgentError":
|
|
264
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;
|
|
265
290
|
case "@batonfx/core/MiddlewareViolation":
|
|
266
291
|
return failure.detail;
|
|
267
292
|
case "@batonfx/core/TurnLimitExceeded":
|
|
268
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}`;
|
|
269
300
|
}
|
|
270
301
|
};
|
|
271
302
|
const applyPart = (model, turn, part) => {
|
|
@@ -390,6 +421,9 @@ const projectPrompt = (prompt) => {
|
|
|
390
421
|
return entries;
|
|
391
422
|
};
|
|
392
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
|
+
}
|
|
393
427
|
if (frame.seq <= model.lastSeq)
|
|
394
428
|
return [model, Option.none()];
|
|
395
429
|
const withSeq = { ...model, lastSeq: frame.seq };
|
|
@@ -404,8 +438,6 @@ const applyFrame = (model, frame) => {
|
|
|
404
438
|
}
|
|
405
439
|
case "Ended":
|
|
406
440
|
return [withSeq, Option.none()];
|
|
407
|
-
case "Snapshot":
|
|
408
|
-
return [{ ...withSeq, entries: projectPrompt(frame.transcript), streaming: null }, Option.none()];
|
|
409
441
|
case "SessionStatus":
|
|
410
442
|
return applyStatus(withSeq, frame.status);
|
|
411
443
|
}
|
|
@@ -597,9 +629,9 @@ export const subscriptions = make()((entry) => ({
|
|
|
597
629
|
return Stream.empty;
|
|
598
630
|
return Stream.unwrap(AgentConnection.use((connection) => {
|
|
599
631
|
const afterSeq = readDependencies().afterSeq;
|
|
600
|
-
return
|
|
601
|
-
.
|
|
602
|
-
.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 })))));
|
|
603
635
|
}));
|
|
604
636
|
},
|
|
605
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
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 */
|
|
28
|
+
export const AgentCommandError = Schema.Union([Errors.TransportError, SendFailed]);
|
|
29
|
+
/** @experimental */
|
|
30
|
+
export const CommandOperation = Schema.Literals(["send", "cancel", "resolveApproval"]);
|
|
31
|
+
/** @experimental */
|
|
24
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(SendFailed.make({ reason: "No active agent connection" })),
|
|
65
|
-
onSome: ({ connection }) => connection
|
|
66
|
-
.send(frame)
|
|
67
|
-
.pipe(Effect.mapError((error) => SendFailed.make({ 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",
|