@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/README.md +5 -0
- package/dist/chat.d.ts +271 -0
- package/dist/chat.js +605 -0
- package/dist/connection.d.ts +41 -0
- package/dist/connection.js +70 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -40015
- package/package.json +29 -4
- package/.turbo/turbo-build.log +0 -5
- package/src/chat.ts +0 -876
- package/src/connection.ts +0 -133
- package/src/index.ts +0 -2
- package/test/chat.scene.test.ts +0 -152
- package/test/chat.story.test.ts +0 -110
- package/test/chat.test.ts +0 -154
- package/test/connection.test.ts +0 -35
- package/tsconfig.json +0 -7
package/dist/chat.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { Cause, Equivalence, Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import { Prompt } from "effect/unstable/ai";
|
|
3
|
+
import { define } from "foldkit/command";
|
|
4
|
+
import { m } from "foldkit/message";
|
|
5
|
+
import { make } from "foldkit/subscription";
|
|
6
|
+
import { Wire } from "@batonfx/transport";
|
|
7
|
+
import { AgentConnection, Incoming, SendFailed } from "./connection.js";
|
|
8
|
+
const CompletedFields = { isFailure: Schema.Boolean, result: Schema.Unknown };
|
|
9
|
+
const UserEntryFields = { text: Schema.String };
|
|
10
|
+
const AssistantEntryFields = { text: Schema.String, reasoning: Schema.NullOr(Schema.String) };
|
|
11
|
+
const RunCompletedFields = { text: Schema.String };
|
|
12
|
+
const StringReasonFields = { reason: Schema.String };
|
|
13
|
+
const OpenedSessionFields = { sessionId: Schema.String };
|
|
14
|
+
/** @experimental */
|
|
15
|
+
export const ToolPendingPhase = Schema.Literals(["called", "executing"]);
|
|
16
|
+
const ToolEntryFields = {
|
|
17
|
+
callId: Schema.String,
|
|
18
|
+
name: Schema.String,
|
|
19
|
+
params: Schema.Unknown,
|
|
20
|
+
phase: ToolPendingPhase,
|
|
21
|
+
outcome: Schema.suspend(() => ToolOutcome),
|
|
22
|
+
progress: Schema.Array(Schema.String),
|
|
23
|
+
};
|
|
24
|
+
const RunningFields = { turn: Schema.Number };
|
|
25
|
+
const AwaitingApprovalFields = {
|
|
26
|
+
token: Schema.String,
|
|
27
|
+
toolName: Schema.String,
|
|
28
|
+
params: Schema.Unknown,
|
|
29
|
+
};
|
|
30
|
+
const ClickedDenyFields = { reason: Schema.NullOr(Schema.String) };
|
|
31
|
+
const ReceivedAgentFields = { incoming: Incoming };
|
|
32
|
+
const ModelStreaming = Schema.Struct({
|
|
33
|
+
turn: Schema.Number,
|
|
34
|
+
text: Schema.String,
|
|
35
|
+
reasoning: Schema.String,
|
|
36
|
+
});
|
|
37
|
+
const ModelConnection = Schema.Literals(["disconnected", "connecting", "open", "reconnecting"]);
|
|
38
|
+
const Pending = m("Pending");
|
|
39
|
+
const Completed = m("Completed", CompletedFields);
|
|
40
|
+
/** @experimental */
|
|
41
|
+
export const ToolOutcome = Schema.Union([Pending, Completed]);
|
|
42
|
+
/** @experimental */
|
|
43
|
+
export const UserEntry = m("UserEntry", UserEntryFields);
|
|
44
|
+
/** @experimental */
|
|
45
|
+
export const AssistantEntry = m("AssistantEntry", AssistantEntryFields);
|
|
46
|
+
/** @experimental */
|
|
47
|
+
export const ToolEntry = m("ToolEntry", ToolEntryFields);
|
|
48
|
+
/** @experimental */
|
|
49
|
+
export const ChatEntry = Schema.Union([UserEntry, AssistantEntry, ToolEntry]);
|
|
50
|
+
/** @experimental */
|
|
51
|
+
export const Idle = m("Idle");
|
|
52
|
+
/** @experimental */
|
|
53
|
+
export const Running = m("Running", RunningFields);
|
|
54
|
+
/** @experimental */
|
|
55
|
+
export const AwaitingApproval = m("AwaitingApproval", AwaitingApprovalFields);
|
|
56
|
+
/** @experimental */
|
|
57
|
+
export const Failed = m("Failed", {
|
|
58
|
+
message: Schema.String,
|
|
59
|
+
});
|
|
60
|
+
/** @experimental */
|
|
61
|
+
export const RunState = Schema.Union([Idle, Running, AwaitingApproval, Failed]);
|
|
62
|
+
/** @experimental */
|
|
63
|
+
export const Model = Schema.Struct({
|
|
64
|
+
sessionId: Schema.NullOr(Schema.String),
|
|
65
|
+
connection: ModelConnection,
|
|
66
|
+
lastSeq: Schema.Number,
|
|
67
|
+
run: RunState,
|
|
68
|
+
entries: Schema.Array(ChatEntry),
|
|
69
|
+
streaming: Schema.NullOr(ModelStreaming),
|
|
70
|
+
draft: Schema.String,
|
|
71
|
+
});
|
|
72
|
+
/** @experimental */
|
|
73
|
+
export const ReceivedAgent = m("ReceivedAgent", ReceivedAgentFields);
|
|
74
|
+
/** @experimental */
|
|
75
|
+
export const OpenedSession = m("OpenedSession", OpenedSessionFields);
|
|
76
|
+
/** @experimental */
|
|
77
|
+
export const ChangedDraft = m("ChangedDraft", UserEntryFields);
|
|
78
|
+
/** @experimental */
|
|
79
|
+
export const SubmittedMessage = m("SubmittedMessage");
|
|
80
|
+
/** @experimental */
|
|
81
|
+
export const ClickedCancel = m("ClickedCancel");
|
|
82
|
+
/** @experimental */
|
|
83
|
+
export const ClickedApprove = m("ClickedApprove");
|
|
84
|
+
/** @experimental */
|
|
85
|
+
export const ClickedDeny = m("ClickedDeny", ClickedDenyFields);
|
|
86
|
+
/** @experimental */
|
|
87
|
+
export const SentUserMessage = m("SentUserMessage");
|
|
88
|
+
/** @experimental */
|
|
89
|
+
export const ResolvedApproval = m("ResolvedApproval");
|
|
90
|
+
/** @experimental */
|
|
91
|
+
export const CancelledRun = m("CancelledRun");
|
|
92
|
+
/** @experimental */
|
|
93
|
+
export const FailedAgentCommand = m("FailedAgentCommand", StringReasonFields);
|
|
94
|
+
/** @experimental */
|
|
95
|
+
export const Message = Schema.Union([
|
|
96
|
+
ReceivedAgent,
|
|
97
|
+
OpenedSession,
|
|
98
|
+
ChangedDraft,
|
|
99
|
+
SubmittedMessage,
|
|
100
|
+
ClickedCancel,
|
|
101
|
+
ClickedApprove,
|
|
102
|
+
ClickedDeny,
|
|
103
|
+
SentUserMessage,
|
|
104
|
+
ResolvedApproval,
|
|
105
|
+
CancelledRun,
|
|
106
|
+
FailedAgentCommand,
|
|
107
|
+
]);
|
|
108
|
+
/** @experimental */
|
|
109
|
+
export const RunCompleted = m("RunCompleted", RunCompletedFields);
|
|
110
|
+
/** @experimental */
|
|
111
|
+
export const ApprovalRequired = m("ApprovalRequired");
|
|
112
|
+
/** @experimental */
|
|
113
|
+
export const RunFailed = m("RunFailed", {
|
|
114
|
+
message: Schema.String,
|
|
115
|
+
});
|
|
116
|
+
/** @experimental */
|
|
117
|
+
export const OutMessage = Schema.Union([RunCompleted, ApprovalRequired, RunFailed]);
|
|
118
|
+
/** @experimental */
|
|
119
|
+
export const MessageAlign = Schema.Literals(["start", "end"]);
|
|
120
|
+
/** @experimental */
|
|
121
|
+
export const PromptInputStatus = Schema.Literals(["idle", "submitted", "streaming", "error"]);
|
|
122
|
+
/** @experimental */
|
|
123
|
+
export const ToolStatus = Schema.Literals(["input-streaming", "input-available", "output-available", "output-error"]);
|
|
124
|
+
/** @experimental */
|
|
125
|
+
export const UserConversationItem = m("UserConversationItem", { key: Schema.String, align: MessageAlign, entry: UserEntry });
|
|
126
|
+
/** @experimental */
|
|
127
|
+
export const AssistantConversationItem = m("AssistantConversationItem", { key: Schema.String, align: MessageAlign, entry: AssistantEntry });
|
|
128
|
+
/** @experimental */
|
|
129
|
+
export const ToolConversationItem = m("ToolConversationItem", {
|
|
130
|
+
key: Schema.String,
|
|
131
|
+
align: MessageAlign,
|
|
132
|
+
entry: ToolEntry,
|
|
133
|
+
status: ToolStatus,
|
|
134
|
+
input: Schema.String,
|
|
135
|
+
});
|
|
136
|
+
/** @experimental */
|
|
137
|
+
export const StreamingConversationItem = m("StreamingConversationItem", {
|
|
138
|
+
key: Schema.String,
|
|
139
|
+
align: MessageAlign,
|
|
140
|
+
text: Schema.String,
|
|
141
|
+
reasoning: Schema.String,
|
|
142
|
+
isStreaming: Schema.Boolean,
|
|
143
|
+
});
|
|
144
|
+
/** @experimental */
|
|
145
|
+
export const WaitingConversationItem = m("WaitingConversationItem", { key: Schema.String, align: MessageAlign });
|
|
146
|
+
/** @experimental */
|
|
147
|
+
export const ApprovalConversationItem = m("ApprovalConversationItem", {
|
|
148
|
+
key: Schema.String,
|
|
149
|
+
align: MessageAlign,
|
|
150
|
+
token: Schema.String,
|
|
151
|
+
toolName: Schema.String,
|
|
152
|
+
params: Schema.Unknown,
|
|
153
|
+
});
|
|
154
|
+
/** @experimental */
|
|
155
|
+
export const FailureConversationItem = m("FailureConversationItem", { key: Schema.String, align: MessageAlign, message: Schema.String });
|
|
156
|
+
/** @experimental */
|
|
157
|
+
export const ConversationItem = Schema.Union([
|
|
158
|
+
UserConversationItem,
|
|
159
|
+
AssistantConversationItem,
|
|
160
|
+
ToolConversationItem,
|
|
161
|
+
StreamingConversationItem,
|
|
162
|
+
WaitingConversationItem,
|
|
163
|
+
ApprovalConversationItem,
|
|
164
|
+
FailureConversationItem,
|
|
165
|
+
]);
|
|
166
|
+
/** @experimental */
|
|
167
|
+
export const initialModel = (sessionId = null) => ({
|
|
168
|
+
sessionId,
|
|
169
|
+
connection: "disconnected",
|
|
170
|
+
lastSeq: -1,
|
|
171
|
+
run: Idle(),
|
|
172
|
+
entries: [],
|
|
173
|
+
streaming: null,
|
|
174
|
+
draft: "",
|
|
175
|
+
});
|
|
176
|
+
const commandFailed = (error) => FailedAgentCommand({ reason: error instanceof SendFailed ? error.reason : String(error) });
|
|
177
|
+
const catchCommandFailure = (effect) => effect.pipe(Effect.catchCause((cause) => Effect.succeed(commandFailed(Cause.squash(cause)))));
|
|
178
|
+
/** @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())))));
|
|
180
|
+
/** @experimental */
|
|
181
|
+
export const ResolveApproval = define("ResolveApproval", {
|
|
182
|
+
sessionId: Schema.String,
|
|
183
|
+
token: Schema.String,
|
|
184
|
+
approved: Schema.Boolean,
|
|
185
|
+
reason: Schema.NullOr(Schema.String),
|
|
186
|
+
}, ResolvedApproval, FailedAgentCommand)(({ sessionId, token, approved, reason }) => {
|
|
187
|
+
const decision = approved
|
|
188
|
+
? { _tag: "Approved" }
|
|
189
|
+
: reason === null
|
|
190
|
+
? { _tag: "Denied" }
|
|
191
|
+
: { _tag: "Denied", reason };
|
|
192
|
+
return AgentConnection.use((connection) => catchCommandFailure(connection.send({ _tag: "ResolveApproval", sessionId, token, decision }).pipe(Effect.as(ResolvedApproval()))));
|
|
193
|
+
});
|
|
194
|
+
/** @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())))));
|
|
196
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
197
|
+
const isToolCall = (value) => isRecord(value) &&
|
|
198
|
+
value.type === "tool-call" &&
|
|
199
|
+
typeof value.id === "string" &&
|
|
200
|
+
typeof value.name === "string" &&
|
|
201
|
+
"params" in value;
|
|
202
|
+
const isToolResult = (value) => isRecord(value) &&
|
|
203
|
+
value.type === "tool-result" &&
|
|
204
|
+
typeof value.id === "string" &&
|
|
205
|
+
typeof value.name === "string" &&
|
|
206
|
+
"result" in value &&
|
|
207
|
+
typeof value.isFailure === "boolean";
|
|
208
|
+
const streamingFor = (model, turn) => model.streaming ?? { turn, text: "", reasoning: "" };
|
|
209
|
+
const appendStreaming = (model, turn, field, delta) => {
|
|
210
|
+
const streaming = streamingFor(model, turn);
|
|
211
|
+
return { ...model, streaming: { ...streaming, [field]: streaming[field] + delta } };
|
|
212
|
+
};
|
|
213
|
+
const flushStreaming = (model) => {
|
|
214
|
+
if (model.streaming === null)
|
|
215
|
+
return model;
|
|
216
|
+
const { text, reasoning } = model.streaming;
|
|
217
|
+
const entry = text.length === 0 && reasoning.length === 0 ? [] : [AssistantEntry({ text, reasoning: reasoning || null })];
|
|
218
|
+
return { ...model, entries: [...model.entries, ...entry], streaming: null };
|
|
219
|
+
};
|
|
220
|
+
const upsertToolCall = (entries, call, phase = "called") => {
|
|
221
|
+
const index = entries.findIndex((entry) => entry._tag === "ToolEntry" && entry.callId === call.id);
|
|
222
|
+
const previous = index >= 0 ? entries[index] : undefined;
|
|
223
|
+
const previousToolEntry = previous?._tag === "ToolEntry" ? previous : undefined;
|
|
224
|
+
const nextPhase = previousToolEntry?.phase === "executing" || phase === "executing" ? "executing" : "called";
|
|
225
|
+
const next = ToolEntry({
|
|
226
|
+
callId: call.id,
|
|
227
|
+
name: call.name,
|
|
228
|
+
params: call.params === undefined ? previousToolEntry?.params : call.params,
|
|
229
|
+
phase: nextPhase,
|
|
230
|
+
outcome: previousToolEntry?.outcome ?? Pending(),
|
|
231
|
+
progress: previousToolEntry?.progress ?? [],
|
|
232
|
+
});
|
|
233
|
+
if (index < 0)
|
|
234
|
+
return [...entries, next];
|
|
235
|
+
return entries.map((entry, entryIndex) => (entryIndex === index ? next : entry));
|
|
236
|
+
};
|
|
237
|
+
const resolveTool = (entries, result) => {
|
|
238
|
+
const withCall = upsertToolCall(entries, { type: "tool-call", id: result.id, name: result.name, params: undefined });
|
|
239
|
+
return withCall.map((entry) => entry._tag === "ToolEntry" && entry.callId === result.id
|
|
240
|
+
? ToolEntry({
|
|
241
|
+
callId: entry.callId,
|
|
242
|
+
name: entry.name,
|
|
243
|
+
params: entry.params,
|
|
244
|
+
phase: entry.phase,
|
|
245
|
+
outcome: Completed({ isFailure: result.isFailure, result: result.result }),
|
|
246
|
+
progress: entry.progress,
|
|
247
|
+
})
|
|
248
|
+
: entry);
|
|
249
|
+
};
|
|
250
|
+
const addProgress = (entries, callId, message) => entries.map((entry) => entry._tag === "ToolEntry" && entry.callId === callId
|
|
251
|
+
? ToolEntry({
|
|
252
|
+
callId: entry.callId,
|
|
253
|
+
name: entry.name,
|
|
254
|
+
params: entry.params,
|
|
255
|
+
phase: entry.phase,
|
|
256
|
+
outcome: entry.outcome,
|
|
257
|
+
progress: entry.progress.concat(message),
|
|
258
|
+
})
|
|
259
|
+
: entry);
|
|
260
|
+
const failureMessage = (failure) => {
|
|
261
|
+
switch (failure._tag) {
|
|
262
|
+
case "@batonfx/core/AgentError":
|
|
263
|
+
return failure.message;
|
|
264
|
+
case "@batonfx/core/MiddlewareViolation":
|
|
265
|
+
return failure.detail;
|
|
266
|
+
case "@batonfx/core/TurnLimitExceeded":
|
|
267
|
+
return `Turn limit exceeded at turn ${failure.turn}`;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const applyPart = (model, turn, part) => {
|
|
271
|
+
if (!isRecord(part) || typeof part.type !== "string")
|
|
272
|
+
return model;
|
|
273
|
+
switch (part.type) {
|
|
274
|
+
case "text-delta":
|
|
275
|
+
return typeof part.delta === "string" ? appendStreaming(model, turn, "text", part.delta) : model;
|
|
276
|
+
case "reasoning-delta":
|
|
277
|
+
return typeof part.delta === "string" ? appendStreaming(model, turn, "reasoning", part.delta) : model;
|
|
278
|
+
case "tool-call":
|
|
279
|
+
return isToolCall(part) ? { ...model, entries: upsertToolCall(model.entries, part) } : model;
|
|
280
|
+
case "tool-result":
|
|
281
|
+
return isToolResult(part) ? { ...model, entries: resolveTool(model.entries, part) } : model;
|
|
282
|
+
default:
|
|
283
|
+
return model;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const applySuspension = (model, suspension) => {
|
|
287
|
+
if (suspension.reason === "approval") {
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
...model,
|
|
291
|
+
run: AwaitingApproval({
|
|
292
|
+
token: suspension.token,
|
|
293
|
+
toolName: suspension.tool_name,
|
|
294
|
+
params: suspension.tool_params,
|
|
295
|
+
}),
|
|
296
|
+
},
|
|
297
|
+
Option.some(ApprovalRequired()),
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
const message = `Tool wait suspension for ${suspension.tool_name} is not resolvable by the FoldKit adapter`;
|
|
301
|
+
return [{ ...model, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
302
|
+
};
|
|
303
|
+
const applyStatus = (model, status) => {
|
|
304
|
+
switch (status._tag) {
|
|
305
|
+
case "Idle":
|
|
306
|
+
return [{ ...model, run: Idle() }, Option.none()];
|
|
307
|
+
case "Running":
|
|
308
|
+
return [{ ...model, run: Running({ turn: status.turn }) }, Option.none()];
|
|
309
|
+
case "Suspended":
|
|
310
|
+
return applySuspension(model, status.suspension);
|
|
311
|
+
case "Failed": {
|
|
312
|
+
const message = failureMessage(status.error);
|
|
313
|
+
return [{ ...model, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const applyEvent = (model, event) => {
|
|
318
|
+
switch (event._tag) {
|
|
319
|
+
case "TurnStarted":
|
|
320
|
+
return [
|
|
321
|
+
{ ...model, run: Running({ turn: event.turn }), streaming: { turn: event.turn, text: "", reasoning: "" } },
|
|
322
|
+
Option.none(),
|
|
323
|
+
];
|
|
324
|
+
case "ModelPart":
|
|
325
|
+
return [applyPart(model, event.turn, event.part), Option.none()];
|
|
326
|
+
case "ToolExecutionStarted":
|
|
327
|
+
return [{ ...model, entries: upsertToolCall(model.entries, event.call, "executing") }, Option.none()];
|
|
328
|
+
case "ApprovalRequested":
|
|
329
|
+
return [{ ...model, entries: upsertToolCall(model.entries, event.call) }, Option.none()];
|
|
330
|
+
case "ToolProgress":
|
|
331
|
+
return event.message === undefined
|
|
332
|
+
? [model, Option.none()]
|
|
333
|
+
: [{ ...model, entries: addProgress(model.entries, event.toolCallId, event.message) }, Option.none()];
|
|
334
|
+
case "ToolExecutionCompleted":
|
|
335
|
+
return [
|
|
336
|
+
{ ...model, entries: resolveTool(upsertToolCall(model.entries, event.call, "executing"), event.result) },
|
|
337
|
+
Option.none(),
|
|
338
|
+
];
|
|
339
|
+
case "SteeringDrained":
|
|
340
|
+
return [model, Option.none()];
|
|
341
|
+
case "TurnCompleted":
|
|
342
|
+
return [flushStreaming(model), Option.none()];
|
|
343
|
+
case "StructuredOutput":
|
|
344
|
+
return [model, Option.none()];
|
|
345
|
+
case "Completed": {
|
|
346
|
+
const flushed = flushStreaming(model);
|
|
347
|
+
return [{ ...flushed, run: Idle() }, Option.some(RunCompleted({ text: event.text }))];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const textFromContent = (content) => content
|
|
352
|
+
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
353
|
+
.map((part) => part.text)
|
|
354
|
+
.join("");
|
|
355
|
+
const reasoningFromContent = (content) => {
|
|
356
|
+
const reasoning = content
|
|
357
|
+
.filter((part) => isRecord(part) && part.type === "reasoning" && typeof part.text === "string")
|
|
358
|
+
.map((part) => part.text)
|
|
359
|
+
.join("");
|
|
360
|
+
return reasoning.length === 0 ? null : reasoning;
|
|
361
|
+
};
|
|
362
|
+
const projectPrompt = (prompt) => {
|
|
363
|
+
let entries = [];
|
|
364
|
+
for (const message of prompt.content) {
|
|
365
|
+
if (message.role === "user") {
|
|
366
|
+
const text = textFromContent(message.content);
|
|
367
|
+
if (text.length > 0)
|
|
368
|
+
entries = entries.concat(UserEntry({ text }));
|
|
369
|
+
}
|
|
370
|
+
else if (message.role === "assistant") {
|
|
371
|
+
const text = textFromContent(message.content);
|
|
372
|
+
const reasoning = reasoningFromContent(message.content);
|
|
373
|
+
if (text.length > 0 || reasoning !== null)
|
|
374
|
+
entries = entries.concat(AssistantEntry({ text, reasoning }));
|
|
375
|
+
for (const part of message.content) {
|
|
376
|
+
if (isToolCall(part))
|
|
377
|
+
entries = upsertToolCall(entries, part);
|
|
378
|
+
if (isToolResult(part))
|
|
379
|
+
entries = resolveTool(entries, part);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else if (message.role === "tool") {
|
|
383
|
+
for (const part of message.content) {
|
|
384
|
+
if (isToolResult(part))
|
|
385
|
+
entries = resolveTool(entries, part);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return entries;
|
|
390
|
+
};
|
|
391
|
+
const applyFrame = (model, frame) => {
|
|
392
|
+
if (frame.seq <= model.lastSeq)
|
|
393
|
+
return [model, Option.none()];
|
|
394
|
+
const withSeq = { ...model, lastSeq: frame.seq };
|
|
395
|
+
switch (frame._tag) {
|
|
396
|
+
case "Event":
|
|
397
|
+
return applyEvent(withSeq, frame.event);
|
|
398
|
+
case "Suspended":
|
|
399
|
+
return applySuspension(withSeq, frame.suspension);
|
|
400
|
+
case "Failed": {
|
|
401
|
+
const message = failureMessage(frame.error);
|
|
402
|
+
return [{ ...withSeq, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
403
|
+
}
|
|
404
|
+
case "Ended":
|
|
405
|
+
return [withSeq, Option.none()];
|
|
406
|
+
case "Snapshot":
|
|
407
|
+
return [{ ...withSeq, entries: projectPrompt(frame.transcript), streaming: null }, Option.none()];
|
|
408
|
+
case "SessionStatus":
|
|
409
|
+
return applyStatus(withSeq, frame.status);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const jsonText = (value) => {
|
|
413
|
+
try {
|
|
414
|
+
return JSON.stringify(value, null, 2) ?? "undefined";
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return String(value);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
/** @experimental */
|
|
421
|
+
export const promptInputStatusOf = (run) => {
|
|
422
|
+
switch (run._tag) {
|
|
423
|
+
case "Idle":
|
|
424
|
+
return "idle";
|
|
425
|
+
case "Running":
|
|
426
|
+
return "streaming";
|
|
427
|
+
case "AwaitingApproval":
|
|
428
|
+
return "submitted";
|
|
429
|
+
case "Failed":
|
|
430
|
+
return "error";
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
/** @experimental */
|
|
434
|
+
export const toolStatusOf = (entry) => {
|
|
435
|
+
switch (entry.outcome._tag) {
|
|
436
|
+
case "Pending":
|
|
437
|
+
return entry.phase === "executing" ? "input-available" : "input-streaming";
|
|
438
|
+
case "Completed":
|
|
439
|
+
return entry.outcome.isFailure ? "output-error" : "output-available";
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const conversationItemFor = (entry, index) => {
|
|
443
|
+
switch (entry._tag) {
|
|
444
|
+
case "UserEntry":
|
|
445
|
+
return UserConversationItem({ key: `entry-${index}-user`, align: "end", entry });
|
|
446
|
+
case "AssistantEntry":
|
|
447
|
+
return AssistantConversationItem({ key: `entry-${index}-assistant`, align: "start", entry });
|
|
448
|
+
case "ToolEntry":
|
|
449
|
+
return ToolConversationItem({
|
|
450
|
+
key: `tool-${entry.callId}`,
|
|
451
|
+
align: "start",
|
|
452
|
+
entry,
|
|
453
|
+
status: toolStatusOf(entry),
|
|
454
|
+
input: jsonText(entry.params),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
/** @experimental */
|
|
459
|
+
export const conversationItems = (model) => {
|
|
460
|
+
const entries = model.entries.map(conversationItemFor);
|
|
461
|
+
const streaming = model.streaming === null
|
|
462
|
+
? []
|
|
463
|
+
: [
|
|
464
|
+
StreamingConversationItem({
|
|
465
|
+
key: "streaming-assistant",
|
|
466
|
+
align: "start",
|
|
467
|
+
text: model.streaming.text,
|
|
468
|
+
reasoning: model.streaming.reasoning,
|
|
469
|
+
isStreaming: true,
|
|
470
|
+
}),
|
|
471
|
+
];
|
|
472
|
+
const waiting = model.run._tag === "Running" && model.streaming === null
|
|
473
|
+
? [WaitingConversationItem({ key: "waiting-assistant", align: "start" })]
|
|
474
|
+
: [];
|
|
475
|
+
const approval = model.run._tag === "AwaitingApproval"
|
|
476
|
+
? [
|
|
477
|
+
ApprovalConversationItem({
|
|
478
|
+
key: `approval-${model.run.token}`,
|
|
479
|
+
align: "start",
|
|
480
|
+
token: model.run.token,
|
|
481
|
+
toolName: model.run.toolName,
|
|
482
|
+
params: model.run.params,
|
|
483
|
+
}),
|
|
484
|
+
]
|
|
485
|
+
: [];
|
|
486
|
+
const failure = model.run._tag === "Failed"
|
|
487
|
+
? [FailureConversationItem({ key: "run-failure", align: "start", message: model.run.message })]
|
|
488
|
+
: [];
|
|
489
|
+
return [...entries, ...streaming, ...waiting, ...approval, ...failure];
|
|
490
|
+
};
|
|
491
|
+
const isServerFrame = (incoming) => incoming._tag === "Event" ||
|
|
492
|
+
incoming._tag === "Suspended" ||
|
|
493
|
+
incoming._tag === "Failed" ||
|
|
494
|
+
incoming._tag === "Ended" ||
|
|
495
|
+
incoming._tag === "Snapshot" ||
|
|
496
|
+
incoming._tag === "SessionStatus";
|
|
497
|
+
/** @experimental */
|
|
498
|
+
export const update = (model, message) => {
|
|
499
|
+
switch (message._tag) {
|
|
500
|
+
case "ReceivedAgent":
|
|
501
|
+
if (isServerFrame(message.incoming)) {
|
|
502
|
+
const [next, out] = applyFrame(model, message.incoming);
|
|
503
|
+
return [next, [], out];
|
|
504
|
+
}
|
|
505
|
+
switch (message.incoming._tag) {
|
|
506
|
+
case "ConnectionOpened":
|
|
507
|
+
return [{ ...model, connection: "open" }, [], Option.none()];
|
|
508
|
+
case "ConnectionLost":
|
|
509
|
+
return [
|
|
510
|
+
{ ...model, connection: model.sessionId === null ? "disconnected" : "reconnecting" },
|
|
511
|
+
[],
|
|
512
|
+
Option.none(),
|
|
513
|
+
];
|
|
514
|
+
case "ConnectionFailed":
|
|
515
|
+
return [
|
|
516
|
+
{ ...model, connection: "disconnected", run: Failed({ message: message.incoming.reason }) },
|
|
517
|
+
[],
|
|
518
|
+
Option.some(RunFailed({ message: message.incoming.reason })),
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
case "OpenedSession":
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
...model,
|
|
525
|
+
sessionId: message.sessionId,
|
|
526
|
+
connection: "connecting",
|
|
527
|
+
lastSeq: -1,
|
|
528
|
+
run: Idle(),
|
|
529
|
+
entries: [],
|
|
530
|
+
streaming: null,
|
|
531
|
+
},
|
|
532
|
+
[],
|
|
533
|
+
Option.none(),
|
|
534
|
+
];
|
|
535
|
+
case "ChangedDraft":
|
|
536
|
+
return [{ ...model, draft: message.text }, [], Option.none()];
|
|
537
|
+
case "SubmittedMessage": {
|
|
538
|
+
const text = model.draft.trim();
|
|
539
|
+
if (model.sessionId === null || text.length === 0)
|
|
540
|
+
return [model, [], Option.none()];
|
|
541
|
+
return [
|
|
542
|
+
{ ...model, draft: "", entries: [...model.entries, UserEntry({ text })] },
|
|
543
|
+
[SendUserMessage({ sessionId: model.sessionId, text })],
|
|
544
|
+
Option.none(),
|
|
545
|
+
];
|
|
546
|
+
}
|
|
547
|
+
case "ClickedApprove":
|
|
548
|
+
return model.sessionId !== null && model.run._tag === "AwaitingApproval"
|
|
549
|
+
? [
|
|
550
|
+
model,
|
|
551
|
+
[
|
|
552
|
+
ResolveApproval({
|
|
553
|
+
sessionId: model.sessionId,
|
|
554
|
+
token: model.run.token,
|
|
555
|
+
approved: true,
|
|
556
|
+
reason: null,
|
|
557
|
+
}),
|
|
558
|
+
],
|
|
559
|
+
Option.none(),
|
|
560
|
+
]
|
|
561
|
+
: [model, [], Option.none()];
|
|
562
|
+
case "ClickedDeny":
|
|
563
|
+
return model.sessionId !== null && model.run._tag === "AwaitingApproval"
|
|
564
|
+
? [
|
|
565
|
+
model,
|
|
566
|
+
[
|
|
567
|
+
ResolveApproval({
|
|
568
|
+
sessionId: model.sessionId,
|
|
569
|
+
token: model.run.token,
|
|
570
|
+
approved: false,
|
|
571
|
+
reason: message.reason,
|
|
572
|
+
}),
|
|
573
|
+
],
|
|
574
|
+
Option.none(),
|
|
575
|
+
]
|
|
576
|
+
: [model, [], Option.none()];
|
|
577
|
+
case "ClickedCancel":
|
|
578
|
+
return model.sessionId === null
|
|
579
|
+
? [model, [], Option.none()]
|
|
580
|
+
: [model, [CancelRun({ sessionId: model.sessionId })], Option.none()];
|
|
581
|
+
case "FailedAgentCommand":
|
|
582
|
+
return [{ ...model, run: Failed({ message: message.reason }) }, [], Option.none()];
|
|
583
|
+
case "SentUserMessage":
|
|
584
|
+
case "ResolvedApproval":
|
|
585
|
+
case "CancelledRun":
|
|
586
|
+
return [model, [], Option.none()];
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
/** @experimental */
|
|
590
|
+
export const subscriptions = make()((entry) => ({
|
|
591
|
+
agentFrames: entry({ sessionId: Schema.NullOr(Schema.String), afterSeq: Schema.Number }, {
|
|
592
|
+
modelToDependencies: (model) => ({ sessionId: model.sessionId, afterSeq: model.lastSeq }),
|
|
593
|
+
keepAliveEquivalence: Equivalence.make((left, right) => left.sessionId === right.sessionId),
|
|
594
|
+
dependenciesToStream: ({ sessionId }, readDependencies) => {
|
|
595
|
+
if (sessionId === null)
|
|
596
|
+
return Stream.empty;
|
|
597
|
+
return Stream.unwrap(AgentConnection.use((connection) => {
|
|
598
|
+
const afterSeq = readDependencies().afterSeq;
|
|
599
|
+
return Effect.succeed(connection
|
|
600
|
+
.frames(afterSeq < 0 ? { sessionId } : { sessionId, afterSeq })
|
|
601
|
+
.pipe(Stream.map((incoming) => ReceivedAgent({ incoming }))));
|
|
602
|
+
}));
|
|
603
|
+
},
|
|
604
|
+
}),
|
|
605
|
+
}));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Cause, Context, Effect, Layer, Schema, Stream } from "effect";
|
|
2
|
+
import { Socket } from "effect/unstable/socket";
|
|
3
|
+
import type { CallableTaggedStruct } from "foldkit/schema";
|
|
4
|
+
import { Wire } from "@batonfx/transport";
|
|
5
|
+
/** @experimental */
|
|
6
|
+
export declare const ConnectionOpened: CallableTaggedStruct<"ConnectionOpened", {}>;
|
|
7
|
+
/** @experimental */
|
|
8
|
+
export declare const ConnectionLost: CallableTaggedStruct<"ConnectionLost", {}>;
|
|
9
|
+
/** @experimental */
|
|
10
|
+
export declare const ConnectionFailed: CallableTaggedStruct<"ConnectionFailed", {
|
|
11
|
+
reason: typeof Schema.String;
|
|
12
|
+
}>;
|
|
13
|
+
/** @experimental */
|
|
14
|
+
export type Incoming = Wire.LooseServerFrameType | typeof ConnectionOpened.Type | typeof ConnectionLost.Type | typeof ConnectionFailed.Type;
|
|
15
|
+
/** @experimental */
|
|
16
|
+
export declare const Incoming: Schema.Schema<Incoming>;
|
|
17
|
+
declare const SendFailed_base: Schema.Class<SendFailed, Schema.TaggedStruct<"@batonfx/foldkit/SendFailed", {
|
|
18
|
+
readonly reason: Schema.String;
|
|
19
|
+
}>, Cause.YieldableError>;
|
|
20
|
+
/** @experimental */
|
|
21
|
+
export declare class SendFailed extends SendFailed_base {
|
|
22
|
+
}
|
|
23
|
+
/** @experimental */
|
|
24
|
+
export interface Interface {
|
|
25
|
+
readonly frames: (options: {
|
|
26
|
+
readonly sessionId: string;
|
|
27
|
+
readonly afterSeq?: number;
|
|
28
|
+
}) => Stream.Stream<Incoming, never>;
|
|
29
|
+
readonly send: (frame: Wire.ClientFrameType) => Effect.Effect<void, SendFailed>;
|
|
30
|
+
}
|
|
31
|
+
declare const AgentConnection_base: Context.ServiceClass<AgentConnection, "@batonfx/foldkit/AgentConnection", Interface>;
|
|
32
|
+
/** @experimental */
|
|
33
|
+
export declare class AgentConnection extends AgentConnection_base {
|
|
34
|
+
}
|
|
35
|
+
/** @experimental */
|
|
36
|
+
export declare const testLayer: (implementation: Interface) => Layer.Layer<AgentConnection>;
|
|
37
|
+
/** @experimental */
|
|
38
|
+
export declare const layerWebSocket: (options: {
|
|
39
|
+
readonly url: string;
|
|
40
|
+
}) => Layer.Layer<AgentConnection, never, Socket.WebSocketConstructor>;
|
|
41
|
+
export {};
|