@batonfx/foldkit 0.4.0 → 0.4.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/src/connection.ts DELETED
@@ -1,133 +0,0 @@
1
- import { Cause, Context, Effect, Layer, Option, Ref, Result, Schema, Stream } from "effect"
2
- import { Socket } from "effect/unstable/socket"
3
- import { m } from "foldkit/message"
4
- import type { CallableTaggedStruct } from "foldkit/schema"
5
- import { Client, Wire } from "@batonfx/transport"
6
-
7
- /** @experimental */
8
- export const ConnectionOpened: CallableTaggedStruct<"ConnectionOpened", {}> = m("ConnectionOpened")
9
-
10
- /** @experimental */
11
- export const ConnectionLost: CallableTaggedStruct<"ConnectionLost", {}> = m("ConnectionLost")
12
-
13
- /** @experimental */
14
- export const ConnectionFailed: CallableTaggedStruct<"ConnectionFailed", { reason: typeof Schema.String }> = m(
15
- "ConnectionFailed",
16
- { reason: Schema.String },
17
- )
18
-
19
- /** @experimental */
20
- export type Incoming =
21
- | Wire.LooseServerFrameType
22
- | typeof ConnectionOpened.Type
23
- | typeof ConnectionLost.Type
24
- | typeof ConnectionFailed.Type
25
-
26
- /** @experimental */
27
- export const Incoming: Schema.Schema<Incoming> = Schema.Union([
28
- Wire.LooseServerFrame,
29
- ConnectionOpened,
30
- ConnectionLost,
31
- ConnectionFailed,
32
- ])
33
-
34
- /** @experimental */
35
- export class SendFailed extends Schema.TaggedErrorClass<SendFailed>()("@batonfx/foldkit/SendFailed", {
36
- reason: Schema.String,
37
- }) {}
38
-
39
- /** @experimental */
40
- export interface Interface {
41
- readonly frames: (options: {
42
- readonly sessionId: string
43
- readonly afterSeq?: number
44
- }) => Stream.Stream<Incoming, never>
45
- readonly send: (frame: Wire.ClientFrameType) => Effect.Effect<void, SendFailed>
46
- }
47
-
48
- /** @experimental */
49
- export class AgentConnection extends Context.Service<AgentConnection, Interface>()(
50
- "@batonfx/foldkit/AgentConnection",
51
- ) {}
52
-
53
- interface ActiveConnection {
54
- readonly sessionId: string
55
- readonly connection: Client.Connection
56
- }
57
-
58
- const reasonFrom = (error: unknown): string => {
59
- if (error instanceof SendFailed) return error.reason
60
- if (error instanceof Error) return error.message
61
- return String(error)
62
- }
63
-
64
- const statusIncoming = (status: Client.ConnectionStatus): Option.Option<Incoming> => {
65
- switch (status._tag) {
66
- case "Open":
67
- return Option.some(ConnectionOpened())
68
- case "Reconnecting":
69
- case "Closed":
70
- return Option.some(ConnectionLost())
71
- case "Connecting":
72
- return Option.none()
73
- }
74
- }
75
-
76
- /** @experimental */
77
- export const testLayer = (implementation: Interface): Layer.Layer<AgentConnection> =>
78
- Layer.succeed(AgentConnection, AgentConnection.of(implementation))
79
-
80
- /** @experimental */
81
- export const layerWebSocket = (options: {
82
- readonly url: string
83
- }): Layer.Layer<AgentConnection, never, Socket.WebSocketConstructor> =>
84
- Layer.effect(
85
- AgentConnection,
86
- Effect.gen(function* () {
87
- const client = yield* Client.AgentClient
88
- const active = yield* Ref.make<Option.Option<ActiveConnection>>(Option.none())
89
-
90
- const clearActive = (connection: Client.Connection) =>
91
- Ref.update(active, (current) =>
92
- Option.isSome(current) && current.value.connection === connection ? Option.none() : current,
93
- )
94
-
95
- return AgentConnection.of({
96
- frames: ({ sessionId }) =>
97
- Stream.unwrap(
98
- Effect.gen(function* () {
99
- const connection = yield* client.connect({ url: options.url, sessionId })
100
- yield* Ref.set(active, Option.some({ sessionId, connection }))
101
- yield* Effect.addFinalizer(() => clearActive(connection))
102
- const statuses = connection.status.pipe(
103
- Stream.filterMap((status) =>
104
- Option.match(statusIncoming(status), {
105
- onNone: () => Result.fail(undefined),
106
- onSome: Result.succeed,
107
- }),
108
- ),
109
- )
110
- const frames = connection.frames.pipe(
111
- Stream.map((frame): Incoming => frame),
112
- Stream.catchCause((cause) =>
113
- Stream.succeed(ConnectionFailed({ reason: reasonFrom(Cause.squash(cause)) })),
114
- ),
115
- )
116
- return statuses.pipe(Stream.merge(frames))
117
- }),
118
- ),
119
- send: (frame) =>
120
- Ref.get(active).pipe(
121
- Effect.flatMap(
122
- Option.match({
123
- onNone: () => Effect.fail(new SendFailed({ reason: "No active agent connection" })),
124
- onSome: ({ connection }) =>
125
- connection
126
- .send(frame)
127
- .pipe(Effect.mapError((error) => new SendFailed({ reason: reasonFrom(error) }))),
128
- }),
129
- ),
130
- ),
131
- })
132
- }),
133
- ).pipe(Layer.provide(Client.layerWebSocket))
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * as Chat from "./chat"
2
- export * as Connection from "./connection"
@@ -1,152 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import { Chat, Connection } from "../src/index"
4
- import { Wire } from "@batonfx/transport"
5
- import { Response } from "effect/unstable/ai"
6
- import type { Document, Html } from "foldkit/html"
7
- import { html } from "foldkit/html"
8
- import { Command, click, expect, placeholder, role, scene, type } from "foldkit/scene"
9
- import type { SceneSimulation } from "foldkit/scene"
10
- import { describe, test } from "vitest"
11
-
12
- const sessionId = "foldkit-scene-session"
13
-
14
- const eventFrame = (seq: number, event: Wire.EventType): Wire.LooseServerFrameType => ({ _tag: "Event", seq, event })
15
-
16
- const toolCall = Response.makePart("tool-call", {
17
- id: "lookup-1",
18
- name: "lookup",
19
- params: { query: "baton foldkit" },
20
- providerExecuted: false,
21
- })
22
-
23
- const toolResult = Response.makePart("tool-result", {
24
- id: "lookup-1",
25
- name: "lookup",
26
- result: { answer: "transport binding" },
27
- encodedResult: { answer: "transport binding" },
28
- isFailure: false,
29
- providerExecuted: false,
30
- preliminary: false,
31
- })
32
-
33
- const frames: ReadonlyArray<Wire.LooseServerFrameType> = [
34
- eventFrame(0, { _tag: "TurnStarted", turn: 0 }),
35
- eventFrame(1, { _tag: "ModelPart", turn: 0, part: toolCall }),
36
- eventFrame(2, { _tag: "ToolExecutionStarted", turn: 0, call: toolCall }),
37
- eventFrame(3, { _tag: "ToolExecutionCompleted", turn: 0, call: toolCall, result: toolResult }),
38
- eventFrame(4, { _tag: "TurnCompleted", turn: 0 }),
39
- eventFrame(5, { _tag: "TurnStarted", turn: 1 }),
40
- eventFrame(6, {
41
- _tag: "ModelPart",
42
- turn: 1,
43
- part: Response.makePart("reasoning-delta", { id: "reasoning-1", delta: "Check the transport stream." }),
44
- }),
45
- eventFrame(7, {
46
- _tag: "ModelPart",
47
- turn: 1,
48
- part: Response.makePart("text-delta", { id: "answer-1", delta: "Final answer" }),
49
- }),
50
- eventFrame(8, { _tag: "TurnCompleted", turn: 1 }),
51
- eventFrame(9, { _tag: "Completed", turns: 2, text: "Final answer" }),
52
- ]
53
-
54
- const reduceMessage = (model: Chat.Model, message: Chat.Message): Chat.Model => Chat.update(model, message)[0]
55
-
56
- const withModel = <Model>(initialModel: Model) =>
57
- Object.assign(
58
- <M, Message, OutMessage = undefined>(simulation: SceneSimulation<M, Message, OutMessage>) =>
59
- ({ ...simulation, model: initialModel }) as unknown as SceneSimulation<M, Message, OutMessage>,
60
- { _phantomModel: undefined },
61
- )
62
-
63
- const scriptedModel = (): Chat.Model => {
64
- let model = Chat.initialModel(null)
65
- model = reduceMessage(model, Chat.OpenedSession({ sessionId }))
66
- model = reduceMessage(model, Chat.ReceivedAgent({ incoming: Connection.ConnectionOpened() }))
67
- model = reduceMessage(model, Chat.ChangedDraft({ text: "Render this run" }))
68
- model = reduceMessage(model, Chat.SubmittedMessage())
69
- for (const frame of frames) {
70
- model = reduceMessage(model, Chat.ReceivedAgent({ incoming: frame }))
71
- }
72
- return model
73
- }
74
-
75
- const rowView = (item: Chat.ConversationItem): Html => {
76
- const h = html<Chat.Message>()
77
- switch (item._tag) {
78
- case "UserConversationItem":
79
- return h.section([h.Role("article"), h.AriaLabel("User message")], [item.entry.text])
80
- case "AssistantConversationItem":
81
- return h.section(
82
- [h.Role("article"), h.AriaLabel("Assistant message")],
83
- [item.entry.reasoning ?? "", item.entry.text],
84
- )
85
- case "ToolConversationItem":
86
- return h.section(
87
- [h.Role("article"), h.AriaLabel(`Tool ${item.entry.name}`), h.DataAttribute("status", item.status)],
88
- [item.entry.name, item.status, item.input, JSON.stringify(item.entry.outcome)],
89
- )
90
- case "StreamingConversationItem":
91
- return h.section([h.Role("article"), h.AriaLabel("Streaming assistant message")], [item.reasoning, item.text])
92
- case "WaitingConversationItem":
93
- return h.section([h.Role("status")], ["Thinking"])
94
- case "ApprovalConversationItem":
95
- return h.section([h.Role("article"), h.AriaLabel("Approval required")], [item.toolName])
96
- case "FailureConversationItem":
97
- return h.section([h.Role("alert")], [item.message])
98
- }
99
- }
100
-
101
- const view = (model: Chat.Model): Document => {
102
- const h = html<Chat.Message>()
103
- return {
104
- title: "Chat scene",
105
- body: h.main(
106
- [],
107
- [
108
- h.div([h.Role("log")], Chat.conversationItems(model).map(rowView)),
109
- h.form(
110
- [h.OnSubmit(Chat.SubmittedMessage())],
111
- [
112
- h.input([
113
- h.AriaLabel("Message"),
114
- h.Placeholder("Message"),
115
- h.Value(model.draft),
116
- h.OnInput((text) => Chat.ChangedDraft({ text })),
117
- ]),
118
- h.button([h.Type("submit")], ["Send"]),
119
- ],
120
- ),
121
- ],
122
- ),
123
- }
124
- }
125
-
126
- describe("Chat Scene", () => {
127
- test("renders scripted stream rows for user, tool execution, and assistant completion", () => {
128
- scene(
129
- { update: Chat.update, view },
130
- withModel(scriptedModel()),
131
- expect(role("article", { name: "User message" })).toContainText("Render this run"),
132
- expect(role("article", { name: "Tool lookup" })).toContainText("lookup"),
133
- expect(role("article", { name: "Tool lookup" })).toContainText("output-available"),
134
- expect(role("article", { name: "Tool lookup" })).toContainText('"answer":"transport binding"'),
135
- expect(role("article", { name: "Assistant message" })).toContainText("Check the transport stream."),
136
- expect(role("article", { name: "Assistant message" })).toContainText("Final answer"),
137
- )
138
- })
139
-
140
- test("send message dispatches the Baton send command", () => {
141
- const model = reduceMessage(Chat.initialModel(null), Chat.OpenedSession({ sessionId }))
142
- scene(
143
- { update: Chat.update, view },
144
- withModel(model),
145
- type(placeholder("Message"), "hello baton"),
146
- click(role("button", { name: "Send" })),
147
- Command.expectExact(Chat.SendUserMessage({ sessionId, text: "hello baton" })),
148
- Command.resolve(Chat.SendUserMessage({ sessionId, text: "hello baton" }), Chat.SentUserMessage()),
149
- expect(role("article", { name: "User message" })).toContainText("hello baton"),
150
- )
151
- })
152
- })
@@ -1,110 +0,0 @@
1
- import { Chat } from "../src/index"
2
- import { Wire } from "@batonfx/transport"
3
- import { Response } from "effect/unstable/ai"
4
- import { Command, expectOutMessage, message, model, story } from "foldkit/story"
5
- import type { StorySimulation } from "foldkit/story"
6
- import { describe, expect, test } from "vitest"
7
-
8
- const sessionId = "foldkit-story-session"
9
-
10
- const eventFrame = (seq: number, event: Wire.EventType): Wire.LooseServerFrameType => ({ _tag: "Event", seq, event })
11
-
12
- const receivedFrame = (frame: Wire.LooseServerFrameType) => Chat.ReceivedAgent({ incoming: frame })
13
-
14
- const withModel = <Model>(initialModel: Model) =>
15
- Object.assign(
16
- <M, Message, OutMessage = undefined>(simulation: StorySimulation<M, Message, OutMessage>) =>
17
- ({ ...simulation, model: initialModel }) as unknown as StorySimulation<M, Message, OutMessage>,
18
- { _phantomModel: undefined },
19
- )
20
-
21
- const toolCall = Response.makePart("tool-call", {
22
- id: "lookup-1",
23
- name: "lookup",
24
- params: { query: "baton foldkit" },
25
- providerExecuted: false,
26
- })
27
-
28
- const toolResult = Response.makePart("tool-result", {
29
- id: "lookup-1",
30
- name: "lookup",
31
- result: { answer: "transport binding" },
32
- encodedResult: { answer: "transport binding" },
33
- isFailure: false,
34
- providerExecuted: false,
35
- preliminary: false,
36
- })
37
-
38
- const frames: ReadonlyArray<Wire.LooseServerFrameType> = [
39
- eventFrame(0, { _tag: "TurnStarted", turn: 0 }),
40
- eventFrame(1, { _tag: "ModelPart", turn: 0, part: toolCall }),
41
- eventFrame(2, { _tag: "ToolExecutionStarted", turn: 0, call: toolCall }),
42
- eventFrame(3, { _tag: "ToolProgress", turn: 0, toolCallId: "lookup-1", message: "looking up" }),
43
- eventFrame(4, { _tag: "ToolExecutionCompleted", turn: 0, call: toolCall, result: toolResult }),
44
- eventFrame(5, { _tag: "TurnCompleted", turn: 0 }),
45
- eventFrame(6, { _tag: "TurnStarted", turn: 1 }),
46
- eventFrame(7, {
47
- _tag: "ModelPart",
48
- turn: 1,
49
- part: Response.makePart("reasoning-delta", { id: "reasoning-1", delta: "Check the transport stream." }),
50
- }),
51
- eventFrame(8, {
52
- _tag: "ModelPart",
53
- turn: 1,
54
- part: Response.makePart("text-delta", { id: "answer-1", delta: "Final answer" }),
55
- }),
56
- eventFrame(9, { _tag: "TurnCompleted", turn: 1 }),
57
- eventFrame(10, { _tag: "Completed", turns: 2, text: "Final answer" }),
58
- ]
59
-
60
- describe("Chat Story", () => {
61
- test("scripted agent frames drive tool call, execution, and completion state", () => {
62
- story(
63
- Chat.update,
64
- withModel(Chat.initialModel(null)),
65
- message(Chat.OpenedSession({ sessionId })),
66
- model((currentModel) => {
67
- expect(currentModel.sessionId).toBe(sessionId)
68
- expect(currentModel.connection).toBe("connecting")
69
- }),
70
- message(Chat.ChangedDraft({ text: "Render this run" })),
71
- message(Chat.SubmittedMessage()),
72
- Command.expectExact(Chat.SendUserMessage({ sessionId, text: "Render this run" })),
73
- Command.resolve(Chat.SendUserMessage({ sessionId, text: "Render this run" }), Chat.SentUserMessage()),
74
- message(receivedFrame(frames[0]!)),
75
- message(receivedFrame(frames[1]!)),
76
- model((currentModel) => {
77
- const tool = currentModel.entries[1]
78
- if (tool?._tag !== "ToolEntry") throw new Error("expected tool call entry")
79
- expect(Chat.toolStatusOf(tool)).toBe("input-streaming")
80
- }),
81
- message(receivedFrame(frames[2]!)),
82
- model((currentModel) => {
83
- const tool = currentModel.entries[1]
84
- if (tool?._tag !== "ToolEntry") throw new Error("expected executing tool entry")
85
- expect(Chat.toolStatusOf(tool)).toBe("input-available")
86
- }),
87
- ...frames.slice(3).map((frame) => message(receivedFrame(frame))),
88
- expectOutMessage(Chat.RunCompleted({ text: "Final answer" })),
89
- model((currentModel) => {
90
- expect(currentModel.run).toEqual(Chat.Idle())
91
- expect(currentModel.streaming).toBeNull()
92
- expect(Chat.conversationItems(currentModel).map((item) => item._tag)).toEqual([
93
- "UserConversationItem",
94
- "ToolConversationItem",
95
- "AssistantConversationItem",
96
- ])
97
-
98
- const tool = currentModel.entries[1]
99
- if (tool?._tag !== "ToolEntry") throw new Error("expected completed tool entry")
100
- expect(tool.progress).toEqual(["looking up"])
101
- expect(Chat.toolStatusOf(tool)).toBe("output-available")
102
-
103
- const assistant = currentModel.entries[2]
104
- expect(assistant).toEqual(
105
- Chat.AssistantEntry({ text: "Final answer", reasoning: "Check the transport stream." }),
106
- )
107
- }),
108
- )
109
- })
110
- })
package/test/chat.test.ts DELETED
@@ -1,154 +0,0 @@
1
- import { describe, expect, it } from "@effect/vitest"
2
- import { Effect, Option, Schema, Stream } from "effect"
3
- import { Response } from "effect/unstable/ai"
4
- import { Wire } from "@batonfx/transport"
5
- import { Chat, Connection } from "../src/index"
6
-
7
- const eventFrame = (seq: number, event: Wire.EventType): Wire.LooseServerFrameType => ({ _tag: "Event", seq, event })
8
-
9
- const updateWith = (model: Chat.Model, incoming: Connection.Incoming) =>
10
- Chat.update(model, Chat.ReceivedAgent({ incoming }))
11
-
12
- describe("Chat", () => {
13
- it("folds a full run into display entries and a completion out-message", () => {
14
- let model = Chat.initialModel("s-chat")
15
- let out: Option.Option<Chat.OutMessage> = Option.none()
16
-
17
- ;[model, , out] = updateWith(model, eventFrame(0, { _tag: "TurnStarted", turn: 0 }))
18
- expect(model.run).toEqual({ _tag: "Running", turn: 0 })
19
- expect(model.streaming).toEqual({ turn: 0, text: "", reasoning: "" })
20
- expect(Option.isNone(out)).toBe(true)
21
- ;[model] = updateWith(
22
- model,
23
- eventFrame(1, {
24
- _tag: "ModelPart",
25
- turn: 0,
26
- part: Response.makePart("text-delta", { id: "text-1", delta: "Hello " }),
27
- }),
28
- )
29
- ;[model] = updateWith(
30
- model,
31
- eventFrame(2, {
32
- _tag: "ModelPart",
33
- turn: 0,
34
- part: Response.makePart("text-delta", { id: "text-1", delta: "world" }),
35
- }),
36
- )
37
-
38
- const call = Response.makePart("tool-call", {
39
- id: "call-1",
40
- name: "lookup",
41
- params: { q: "baton" },
42
- providerExecuted: false,
43
- })
44
- ;[model] = updateWith(model, eventFrame(3, { _tag: "ModelPart", turn: 0, part: call }))
45
- expect(model.entries).toEqual([
46
- {
47
- _tag: "ToolEntry",
48
- callId: "call-1",
49
- name: "lookup",
50
- params: { q: "baton" },
51
- phase: "called",
52
- outcome: { _tag: "Pending" },
53
- progress: [],
54
- },
55
- ])
56
- const pendingTool = model.entries[0]
57
- if (pendingTool?._tag !== "ToolEntry") throw new Error("expected pending tool entry")
58
- expect(Chat.toolStatusOf(pendingTool)).toBe("input-streaming")
59
-
60
- const result = Response.makePart("tool-result", {
61
- id: "call-1",
62
- name: "lookup",
63
- result: { ok: true },
64
- encodedResult: { ok: true },
65
- isFailure: false,
66
- providerExecuted: false,
67
- preliminary: false,
68
- })
69
- ;[model] = updateWith(model, eventFrame(4, { _tag: "ToolExecutionCompleted", turn: 0, call, result }))
70
- expect(model.entries[0]).toMatchObject({
71
- _tag: "ToolEntry",
72
- phase: "executing",
73
- outcome: { _tag: "Completed", isFailure: false, result: { ok: true } },
74
- })
75
- const completedTool = model.entries[0]
76
- if (completedTool?._tag !== "ToolEntry") throw new Error("expected completed tool entry")
77
- expect(Chat.toolStatusOf(completedTool)).toBe("output-available")
78
- ;[model] = updateWith(model, eventFrame(5, { _tag: "TurnCompleted", turn: 0 }))
79
- expect(model.streaming).toBeNull()
80
- expect(model.entries[1]).toEqual({ _tag: "AssistantEntry", text: "Hello world", reasoning: null })
81
- ;[model, , out] = updateWith(model, eventFrame(6, { _tag: "Completed", turns: 1, text: "Done" }))
82
- expect(model.run).toEqual({ _tag: "Idle" })
83
- expect(Option.getOrUndefined(out)).toEqual({ _tag: "RunCompleted", text: "Done" })
84
- })
85
-
86
- it("drops replayed frames whose seq is not newer than lastSeq", () => {
87
- const model = { ...Chat.initialModel("s-chat"), lastSeq: 5, entries: [Chat.UserEntry({ text: "already" })] }
88
- const [next, commands, out] = updateWith(model, eventFrame(5, { _tag: "TurnStarted", turn: 1 }))
89
-
90
- expect(next).toEqual(model)
91
- expect(commands).toEqual([])
92
- expect(Option.isNone(out)).toBe(true)
93
- })
94
-
95
- it("surfaces approval suspension and emits approval commands", () => {
96
- let model = Chat.initialModel("s-chat")
97
- let out: Option.Option<Chat.OutMessage> = Option.none()
98
- ;[model, , out] = updateWith(
99
- model,
100
- Schema.decodeUnknownSync(Wire.LooseServerFrame)({
101
- _tag: "Suspended",
102
- seq: 0,
103
- suspension: {
104
- _tag: "@batonfx/core/AgentSuspended",
105
- token: "approval-token",
106
- reason: "approval",
107
- tool_call_id: "call-approval",
108
- tool_name: "lookup",
109
- tool_params: { q: "baton" },
110
- },
111
- }),
112
- )
113
-
114
- expect(model.run).toEqual({
115
- _tag: "AwaitingApproval",
116
- token: "approval-token",
117
- toolName: "lookup",
118
- params: { q: "baton" },
119
- })
120
- expect(Option.getOrUndefined(out)).toEqual({ _tag: "ApprovalRequired" })
121
-
122
- const [, commands] = Chat.update(model, Chat.ClickedApprove())
123
- expect(commands).toHaveLength(1)
124
- expect(commands[0]?.name).toBe("ResolveApproval")
125
- expect(commands[0]?.args).toEqual({
126
- sessionId: "s-chat",
127
- token: "approval-token",
128
- approved: true,
129
- reason: null,
130
- })
131
- })
132
-
133
- it.effect("converts command send failures into FailedAgentCommand messages", () =>
134
- Effect.gen(function* () {
135
- let model = Chat.initialModel("s-chat")
136
- ;[model] = Chat.update(model, Chat.ChangedDraft({ text: "hello" }))
137
- const [next, commands] = Chat.update(model, Chat.SubmittedMessage())
138
-
139
- expect(next.draft).toBe("")
140
- expect(next.entries).toEqual([Chat.UserEntry({ text: "hello" })])
141
- expect(commands).toHaveLength(1)
142
- const message = yield* commands[0]!.effect
143
-
144
- expect(message).toEqual({ _tag: "FailedAgentCommand", reason: "offline" })
145
- }).pipe(
146
- Effect.provide(
147
- Connection.testLayer({
148
- frames: () => Stream.empty,
149
- send: () => Effect.fail(new Connection.SendFailed({ reason: "offline" })),
150
- }),
151
- ),
152
- ),
153
- )
154
- })
@@ -1,35 +0,0 @@
1
- import { describe, expect, it } from "@effect/vitest"
2
- import { Effect, Stream } from "effect"
3
- import { Chat, Connection } from "../src/index"
4
-
5
- describe("Connection", () => {
6
- it.effect("testLayer provides AgentConnection frames and send", () =>
7
- Effect.gen(function* () {
8
- const incoming = Connection.ConnectionOpened()
9
- const frames = yield* Connection.AgentConnection.use((connection) =>
10
- connection.frames({ sessionId: "s", afterSeq: 1 }).pipe(Stream.runCollect),
11
- )
12
- yield* Connection.AgentConnection.use((connection) => connection.send({ _tag: "Cancel", sessionId: "s" }))
13
-
14
- expect(frames).toEqual([incoming])
15
- }).pipe(
16
- Effect.provide(
17
- Connection.testLayer({
18
- frames: () => Stream.fromIterable([Connection.ConnectionOpened()]),
19
- send: () => Effect.void,
20
- }),
21
- ),
22
- ),
23
- )
24
-
25
- it("keeps the chat subscription alive across afterSeq-only changes", () => {
26
- const subscription = Chat.subscriptions.agentFrames
27
-
28
- expect(subscription.keepAliveEquivalence?.({ sessionId: "s", afterSeq: 1 }, { sessionId: "s", afterSeq: 2 })).toBe(
29
- true,
30
- )
31
- expect(
32
- subscription.keepAliveEquivalence?.({ sessionId: "s-1", afterSeq: 2 }, { sessionId: "s-2", afterSeq: 2 }),
33
- ).toBe(false)
34
- })
35
- })
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "."
5
- },
6
- "include": ["src/**/*.ts", "test/**/*.ts"]
7
- }