@batonfx/foldkit 0.4.4 → 0.6.1

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 CHANGED
@@ -1,5 +1,66 @@
1
1
  # `@batonfx/foldkit`
2
2
 
3
- FoldKit adapter for Baton transport sessions and chat UI.
3
+ Focused composition guide for the FoldKit adapter to Baton transport sessions.
4
4
 
5
- See the [Baton documentation](https://github.com/In-Time-Tec/batonfx#readme) for installation, examples, and API guidance.
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", typeof StringReasonFields>;
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, any, AgentConnection>;
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
- reason: Schema.String;
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
- reason: Schema.String;
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
- reason: Schema.String;
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", StringReasonFields);
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 commandFailed = (error) => FailedAgentCommand({ reason: Schema.is(SendFailed)(error) ? error.reason : String(error) });
178
- const catchCommandFailure = (effect) => effect.pipe(Effect.catchCause((cause) => Effect.succeed(commandFailed(Cause.squash(cause)))));
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 Effect.succeed(connection
601
- .frames(afterSeq < 0 ? { sessionId } : { sessionId, afterSeq })
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
  }),
@@ -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, SendFailed>;
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;
@@ -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", { reason: Schema.String });
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 reasonFrom = (error) => {
27
- if (Schema.is(SendFailed)(error))
28
- return error.reason;
29
- if (error instanceof Error)
30
- return error.message;
31
- return String(error);
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 "Open":
45
+ case "Connected":
36
46
  return Option.some(ConnectionOpened());
37
- case "Reconnecting":
38
- case "Closed":
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) => Layer.succeed(AgentConnection, AgentConnection.of(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(Option.none());
50
- const clearActive = (connection) => Ref.update(active, (current) => Option.isSome(current) && current.value.connection === connection ? Option.none() : current);
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
- frames: ({ sessionId }) => Stream.unwrap(Effect.gen(function* () {
53
- const connection = yield* client.connect({ url: options.url, sessionId });
54
- yield* Ref.set(active, Option.some({ sessionId, connection }));
55
- yield* Effect.addFinalizer(() => clearActive(connection));
56
- const statuses = connection.status.pipe(Stream.filterMap((status) => Option.match(statusIncoming(status), {
57
- onNone: () => Result.fail(undefined),
58
- onSome: Result.succeed,
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.4",
4
+ "version": "0.6.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -17,16 +17,16 @@
17
17
  "typecheck": "bun tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "@batonfx/transport": "0.4.4"
20
+ "@batonfx/transport": "0.6.1"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "effect": ">=4.0.0-beta.88 <4.0.1",
24
24
  "foldkit": ">=0.122.0 <1"
25
25
  },
26
26
  "devDependencies": {
27
- "@effect/vitest": "4.0.0-beta.93",
27
+ "@effect/vitest": "4.0.0-beta.98",
28
28
  "@types/bun": "1.3.13",
29
- "effect": "4.0.0-beta.93",
29
+ "effect": "4.0.0-beta.98",
30
30
  "foldkit": "0.122.0",
31
31
  "typescript": "7.0.2",
32
32
  "vitest": "4.0.16"