@batonfx/foldkit 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/index.js +452 -23
- package/package.json +2 -2
- package/src/chat.ts +264 -25
- package/test/chat.scene.test.ts +152 -0
- package/test/chat.story.test.ts +110 -0
- package/test/chat.test.ts +13 -5
|
@@ -0,0 +1,110 @@
|
|
|
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
2
|
import { Effect, Option, Schema, Stream } from "effect"
|
|
3
|
-
import
|
|
3
|
+
import { Response } from "effect/unstable/ai"
|
|
4
4
|
import { Wire } from "@batonfx/transport"
|
|
5
5
|
import { Chat, Connection } from "../src/index"
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ describe("Chat", () => {
|
|
|
23
23
|
eventFrame(1, {
|
|
24
24
|
_tag: "ModelPart",
|
|
25
25
|
turn: 0,
|
|
26
|
-
part:
|
|
26
|
+
part: Response.makePart("text-delta", { id: "text-1", delta: "Hello " }),
|
|
27
27
|
}),
|
|
28
28
|
)
|
|
29
29
|
;[model] = updateWith(
|
|
@@ -31,11 +31,11 @@ describe("Chat", () => {
|
|
|
31
31
|
eventFrame(2, {
|
|
32
32
|
_tag: "ModelPart",
|
|
33
33
|
turn: 0,
|
|
34
|
-
part:
|
|
34
|
+
part: Response.makePart("text-delta", { id: "text-1", delta: "world" }),
|
|
35
35
|
}),
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
const call =
|
|
38
|
+
const call = Response.makePart("tool-call", {
|
|
39
39
|
id: "call-1",
|
|
40
40
|
name: "lookup",
|
|
41
41
|
params: { q: "baton" },
|
|
@@ -48,12 +48,16 @@ describe("Chat", () => {
|
|
|
48
48
|
callId: "call-1",
|
|
49
49
|
name: "lookup",
|
|
50
50
|
params: { q: "baton" },
|
|
51
|
+
phase: "called",
|
|
51
52
|
outcome: { _tag: "Pending" },
|
|
52
53
|
progress: [],
|
|
53
54
|
},
|
|
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")
|
|
55
59
|
|
|
56
|
-
const result =
|
|
60
|
+
const result = Response.makePart("tool-result", {
|
|
57
61
|
id: "call-1",
|
|
58
62
|
name: "lookup",
|
|
59
63
|
result: { ok: true },
|
|
@@ -65,8 +69,12 @@ describe("Chat", () => {
|
|
|
65
69
|
;[model] = updateWith(model, eventFrame(4, { _tag: "ToolExecutionCompleted", turn: 0, call, result }))
|
|
66
70
|
expect(model.entries[0]).toMatchObject({
|
|
67
71
|
_tag: "ToolEntry",
|
|
72
|
+
phase: "executing",
|
|
68
73
|
outcome: { _tag: "Completed", isFailure: false, result: { ok: true } },
|
|
69
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")
|
|
70
78
|
;[model] = updateWith(model, eventFrame(5, { _tag: "TurnCompleted", turn: 0 }))
|
|
71
79
|
expect(model.streaming).toBeNull()
|
|
72
80
|
expect(model.entries[1]).toEqual({ _tag: "AssistantEntry", text: "Hello world", reasoning: null })
|