@batonfx/foldkit 0.6.3 → 0.7.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/dist/chat-update.d.ts +24 -0
- package/dist/chat-update.js +269 -0
- package/dist/chat.d.ts +2 -4
- package/dist/chat.js +4 -259
- package/package.json +2 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import type { CallableTaggedStruct } from "foldkit/schema";
|
|
3
|
+
import { Wire } from "@batonfx/transport";
|
|
4
|
+
import { AgentCommandError, type AgentConnection, type CommandOperation, type Incoming } from "./connection.js";
|
|
5
|
+
import { type Model, type OutMessage } from "./chat.js";
|
|
6
|
+
declare const catchCommandFailure: <A>(operation: CommandOperation, effect: Effect.Effect<A, AgentCommandError, AgentConnection>) => Effect.Effect<A | Schema.Struct.ReadonlySide<{
|
|
7
|
+
readonly _tag: Schema.tag<"FailedAgentCommand">;
|
|
8
|
+
operation: typeof CommandOperation;
|
|
9
|
+
error: typeof AgentCommandError;
|
|
10
|
+
reason: typeof Schema.String;
|
|
11
|
+
}, "Type">, never, AgentConnection>;
|
|
12
|
+
declare const applyFrame: (model: Model, frame: Wire.LooseServerFrameType) => readonly [Model, Option.Option<OutMessage>];
|
|
13
|
+
declare const isServerFrame: (incoming: Incoming) => incoming is Wire.LooseServerFrameType;
|
|
14
|
+
export declare const chatUpdateRuntime: {
|
|
15
|
+
Pending: CallableTaggedStruct<"Pending", {}>;
|
|
16
|
+
Completed: CallableTaggedStruct<"Completed", {
|
|
17
|
+
isFailure: Schema.Boolean;
|
|
18
|
+
result: Schema.Unknown;
|
|
19
|
+
}>;
|
|
20
|
+
catchCommandFailure: typeof catchCommandFailure;
|
|
21
|
+
applyFrame: typeof applyFrame;
|
|
22
|
+
isServerFrame: typeof isServerFrame;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { Cause, Effect, Option, Result, Schema } from "effect";
|
|
2
|
+
import { Prompt } from "effect/unstable/ai";
|
|
3
|
+
import { m } from "foldkit/message";
|
|
4
|
+
import { Wire } from "@batonfx/transport";
|
|
5
|
+
import { AgentCommandError, SendFailed, } from "./connection.js";
|
|
6
|
+
import { ApprovalRequired, AssistantEntry, AwaitingApproval, Failed, FailedAgentCommand, Idle, RunCompleted, RunFailed, Running, ToolEntry, UserEntry, } from "./chat.js";
|
|
7
|
+
const CompletedFields = { isFailure: Schema.Boolean, result: Schema.Unknown };
|
|
8
|
+
const Pending = m("Pending");
|
|
9
|
+
const Completed = m("Completed", CompletedFields);
|
|
10
|
+
const unexpectedCause = (cause) => {
|
|
11
|
+
const reasons = [];
|
|
12
|
+
for (const reason of cause.reasons) {
|
|
13
|
+
if (Cause.isDieReason(reason) || Cause.isInterruptReason(reason))
|
|
14
|
+
reasons.push(reason);
|
|
15
|
+
}
|
|
16
|
+
return reasons.length === 0 ? Option.none() : Option.some(Cause.fromReasons(reasons));
|
|
17
|
+
};
|
|
18
|
+
const commandFailed = (operation, error) => FailedAgentCommand({
|
|
19
|
+
operation,
|
|
20
|
+
error,
|
|
21
|
+
reason: Schema.is(SendFailed)(error) ? error.reason : error.message,
|
|
22
|
+
});
|
|
23
|
+
const catchCommandFailure = (operation, effect) => effect.pipe(Effect.catchCause((cause) => Option.match(unexpectedCause(cause), {
|
|
24
|
+
onNone: () => Result.match(Cause.findError(cause), {
|
|
25
|
+
onFailure: Effect.failCause,
|
|
26
|
+
onSuccess: (error) => Effect.succeed(commandFailed(operation, error)),
|
|
27
|
+
}),
|
|
28
|
+
onSome: Effect.failCause,
|
|
29
|
+
})));
|
|
30
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
31
|
+
const isToolCall = (value) => isRecord(value) &&
|
|
32
|
+
value.type === "tool-call" &&
|
|
33
|
+
typeof value.id === "string" &&
|
|
34
|
+
typeof value.name === "string" &&
|
|
35
|
+
"params" in value;
|
|
36
|
+
const isToolResult = (value) => isRecord(value) &&
|
|
37
|
+
value.type === "tool-result" &&
|
|
38
|
+
typeof value.id === "string" &&
|
|
39
|
+
typeof value.name === "string" &&
|
|
40
|
+
"result" in value &&
|
|
41
|
+
typeof value.isFailure === "boolean";
|
|
42
|
+
const streamingFor = (model, turn) => model.streaming ?? { turn, text: "", reasoning: "" };
|
|
43
|
+
const appendStreaming = (model, turn, field, delta) => {
|
|
44
|
+
const streaming = streamingFor(model, turn);
|
|
45
|
+
return { ...model, streaming: { ...streaming, [field]: streaming[field] + delta } };
|
|
46
|
+
};
|
|
47
|
+
const flushStreaming = (model) => {
|
|
48
|
+
if (model.streaming === null)
|
|
49
|
+
return model;
|
|
50
|
+
const { text, reasoning } = model.streaming;
|
|
51
|
+
const entry = text.length === 0 && reasoning.length === 0 ? [] : [AssistantEntry({ text, reasoning: reasoning || null })];
|
|
52
|
+
return { ...model, entries: [...model.entries, ...entry], streaming: null };
|
|
53
|
+
};
|
|
54
|
+
const upsertToolCall = (entries, call, phase = "called") => {
|
|
55
|
+
const index = entries.findIndex((entry) => entry._tag === "ToolEntry" && entry.callId === call.id);
|
|
56
|
+
const previous = index >= 0 ? entries[index] : undefined;
|
|
57
|
+
const previousToolEntry = previous?._tag === "ToolEntry" ? previous : undefined;
|
|
58
|
+
const nextPhase = previousToolEntry?.phase === "executing" || phase === "executing" ? "executing" : "called";
|
|
59
|
+
const next = ToolEntry({
|
|
60
|
+
callId: call.id,
|
|
61
|
+
name: call.name,
|
|
62
|
+
params: call.params === undefined ? previousToolEntry?.params : call.params,
|
|
63
|
+
phase: nextPhase,
|
|
64
|
+
outcome: previousToolEntry?.outcome ?? Pending(),
|
|
65
|
+
progress: previousToolEntry?.progress ?? [],
|
|
66
|
+
});
|
|
67
|
+
if (index < 0)
|
|
68
|
+
return [...entries, next];
|
|
69
|
+
return entries.map((entry, entryIndex) => (entryIndex === index ? next : entry));
|
|
70
|
+
};
|
|
71
|
+
const resolveTool = (entries, result) => {
|
|
72
|
+
const withCall = upsertToolCall(entries, { type: "tool-call", id: result.id, name: result.name, params: undefined });
|
|
73
|
+
return withCall.map((entry) => entry._tag === "ToolEntry" && entry.callId === result.id
|
|
74
|
+
? ToolEntry({
|
|
75
|
+
callId: entry.callId,
|
|
76
|
+
name: entry.name,
|
|
77
|
+
params: entry.params,
|
|
78
|
+
phase: entry.phase,
|
|
79
|
+
outcome: Completed({ isFailure: result.isFailure, result: result.result }),
|
|
80
|
+
progress: entry.progress,
|
|
81
|
+
})
|
|
82
|
+
: entry);
|
|
83
|
+
};
|
|
84
|
+
const addProgress = (entries, callId, message) => entries.map((entry) => entry._tag === "ToolEntry" && entry.callId === callId
|
|
85
|
+
? ToolEntry({
|
|
86
|
+
callId: entry.callId,
|
|
87
|
+
name: entry.name,
|
|
88
|
+
params: entry.params,
|
|
89
|
+
phase: entry.phase,
|
|
90
|
+
outcome: entry.outcome,
|
|
91
|
+
progress: entry.progress.concat(message),
|
|
92
|
+
})
|
|
93
|
+
: entry);
|
|
94
|
+
const failureMessage = (failure) => {
|
|
95
|
+
switch (failure._tag) {
|
|
96
|
+
case "@batonfx/core/AgentError":
|
|
97
|
+
return failure.message;
|
|
98
|
+
case "@batonfx/core/TurnPolicyError":
|
|
99
|
+
return failure.message;
|
|
100
|
+
case "@batonfx/core/TurnPolicyStopped":
|
|
101
|
+
return failure.reason._tag === "Policy" ? failure.reason.detail : failure.reason._tag;
|
|
102
|
+
case "@batonfx/core/MiddlewareViolation":
|
|
103
|
+
return failure.detail;
|
|
104
|
+
case "@batonfx/core/TurnLimitExceeded":
|
|
105
|
+
return `Turn limit exceeded at turn ${failure.turn}`;
|
|
106
|
+
case "@batonfx/core/ResumeMismatch":
|
|
107
|
+
return failure.reason === "identity-mismatch"
|
|
108
|
+
? "Resume suspension does not match the current checkpoint"
|
|
109
|
+
: "Resume checkpoint not found";
|
|
110
|
+
case "@batonfx/core/FrameworkFailure":
|
|
111
|
+
return `${failure.tool} ${failure.stage}: ${failure.message}`;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const applyPart = (model, turn, part) => {
|
|
115
|
+
if (!isRecord(part) || typeof part.type !== "string")
|
|
116
|
+
return model;
|
|
117
|
+
switch (part.type) {
|
|
118
|
+
case "text-delta":
|
|
119
|
+
return typeof part.delta === "string" ? appendStreaming(model, turn, "text", part.delta) : model;
|
|
120
|
+
case "reasoning-delta":
|
|
121
|
+
return typeof part.delta === "string" ? appendStreaming(model, turn, "reasoning", part.delta) : model;
|
|
122
|
+
case "tool-call":
|
|
123
|
+
return isToolCall(part) ? { ...model, entries: upsertToolCall(model.entries, part) } : model;
|
|
124
|
+
case "tool-result":
|
|
125
|
+
return isToolResult(part) ? { ...model, entries: resolveTool(model.entries, part) } : model;
|
|
126
|
+
default:
|
|
127
|
+
return model;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const applySuspension = (model, suspension) => {
|
|
131
|
+
if (suspension.reason === "approval") {
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
...model,
|
|
135
|
+
run: AwaitingApproval({
|
|
136
|
+
token: suspension.token,
|
|
137
|
+
toolName: suspension.tool_name,
|
|
138
|
+
params: suspension.tool_params,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
Option.some(ApprovalRequired()),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
const message = `Tool wait suspension for ${suspension.tool_name} is not resolvable by the FoldKit adapter`;
|
|
145
|
+
return [{ ...model, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
146
|
+
};
|
|
147
|
+
const applyStatus = (model, status) => {
|
|
148
|
+
switch (status._tag) {
|
|
149
|
+
case "Idle":
|
|
150
|
+
return [{ ...model, run: Idle() }, Option.none()];
|
|
151
|
+
case "Running":
|
|
152
|
+
return [{ ...model, run: Running({ turn: status.turn }) }, Option.none()];
|
|
153
|
+
case "Suspended":
|
|
154
|
+
return applySuspension(model, status.suspension);
|
|
155
|
+
case "Failed": {
|
|
156
|
+
const message = failureMessage(status.error);
|
|
157
|
+
return [{ ...model, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const applyEvent = (model, event) => {
|
|
162
|
+
switch (event._tag) {
|
|
163
|
+
case "TurnStarted":
|
|
164
|
+
return [
|
|
165
|
+
{ ...model, run: Running({ turn: event.turn }), streaming: { turn: event.turn, text: "", reasoning: "" } },
|
|
166
|
+
Option.none(),
|
|
167
|
+
];
|
|
168
|
+
case "ModelPart":
|
|
169
|
+
return [applyPart(model, event.turn, event.part), Option.none()];
|
|
170
|
+
case "ToolExecutionStarted":
|
|
171
|
+
return [{ ...model, entries: upsertToolCall(model.entries, event.call, "executing") }, Option.none()];
|
|
172
|
+
case "ApprovalRequested":
|
|
173
|
+
return [{ ...model, entries: upsertToolCall(model.entries, event.call) }, Option.none()];
|
|
174
|
+
case "ToolProgress":
|
|
175
|
+
return event.message === undefined
|
|
176
|
+
? [model, Option.none()]
|
|
177
|
+
: [{ ...model, entries: addProgress(model.entries, event.toolCallId, event.message) }, Option.none()];
|
|
178
|
+
case "ToolExecutionCompleted":
|
|
179
|
+
return [
|
|
180
|
+
{ ...model, entries: resolveTool(upsertToolCall(model.entries, event.call, "executing"), event.result) },
|
|
181
|
+
Option.none(),
|
|
182
|
+
];
|
|
183
|
+
case "SteeringDrained":
|
|
184
|
+
return [model, Option.none()];
|
|
185
|
+
case "TurnCompleted":
|
|
186
|
+
return [flushStreaming(model), Option.none()];
|
|
187
|
+
case "StructuredOutput":
|
|
188
|
+
return [model, Option.none()];
|
|
189
|
+
case "Completed": {
|
|
190
|
+
const flushed = flushStreaming(model);
|
|
191
|
+
return [{ ...flushed, run: Idle() }, Option.some(RunCompleted({ text: event.text }))];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const textFromContent = (content) => content
|
|
196
|
+
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
197
|
+
.map((part) => part.text)
|
|
198
|
+
.join("");
|
|
199
|
+
const reasoningFromContent = (content) => {
|
|
200
|
+
const reasoning = content
|
|
201
|
+
.filter((part) => isRecord(part) && part.type === "reasoning" && typeof part.text === "string")
|
|
202
|
+
.map((part) => part.text)
|
|
203
|
+
.join("");
|
|
204
|
+
return reasoning.length === 0 ? null : reasoning;
|
|
205
|
+
};
|
|
206
|
+
const projectPrompt = (prompt) => {
|
|
207
|
+
let entries = [];
|
|
208
|
+
for (const message of prompt.content) {
|
|
209
|
+
if (message.role === "user") {
|
|
210
|
+
const text = textFromContent(message.content);
|
|
211
|
+
if (text.length > 0)
|
|
212
|
+
entries = entries.concat(UserEntry({ text }));
|
|
213
|
+
}
|
|
214
|
+
else if (message.role === "assistant") {
|
|
215
|
+
const text = textFromContent(message.content);
|
|
216
|
+
const reasoning = reasoningFromContent(message.content);
|
|
217
|
+
if (text.length > 0 || reasoning !== null)
|
|
218
|
+
entries = entries.concat(AssistantEntry({ text, reasoning }));
|
|
219
|
+
for (const part of message.content) {
|
|
220
|
+
if (isToolCall(part))
|
|
221
|
+
entries = upsertToolCall(entries, part);
|
|
222
|
+
if (isToolResult(part))
|
|
223
|
+
entries = resolveTool(entries, part);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (message.role === "tool") {
|
|
227
|
+
for (const part of message.content) {
|
|
228
|
+
if (isToolResult(part))
|
|
229
|
+
entries = resolveTool(entries, part);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return entries;
|
|
234
|
+
};
|
|
235
|
+
const applyFrame = (model, frame) => {
|
|
236
|
+
if (frame._tag === "Snapshot") {
|
|
237
|
+
return [{ ...model, lastSeq: frame.seq, entries: projectPrompt(frame.transcript), streaming: null }, Option.none()];
|
|
238
|
+
}
|
|
239
|
+
if (frame.seq <= model.lastSeq)
|
|
240
|
+
return [model, Option.none()];
|
|
241
|
+
const withSeq = { ...model, lastSeq: frame.seq };
|
|
242
|
+
switch (frame._tag) {
|
|
243
|
+
case "Event":
|
|
244
|
+
return applyEvent(withSeq, frame.event);
|
|
245
|
+
case "Suspended":
|
|
246
|
+
return applySuspension(withSeq, frame.suspension);
|
|
247
|
+
case "Failed": {
|
|
248
|
+
const message = failureMessage(frame.error);
|
|
249
|
+
return [{ ...withSeq, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
250
|
+
}
|
|
251
|
+
case "Ended":
|
|
252
|
+
return [withSeq, Option.none()];
|
|
253
|
+
case "SessionStatus":
|
|
254
|
+
return applyStatus(withSeq, frame.status);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const isServerFrame = (incoming) => incoming._tag === "Event" ||
|
|
258
|
+
incoming._tag === "Suspended" ||
|
|
259
|
+
incoming._tag === "Failed" ||
|
|
260
|
+
incoming._tag === "Ended" ||
|
|
261
|
+
incoming._tag === "Snapshot" ||
|
|
262
|
+
incoming._tag === "SessionStatus";
|
|
263
|
+
export const chatUpdateRuntime = {
|
|
264
|
+
Pending,
|
|
265
|
+
Completed,
|
|
266
|
+
catchCommandFailure,
|
|
267
|
+
applyFrame,
|
|
268
|
+
isServerFrame,
|
|
269
|
+
};
|
package/dist/chat.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ 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
4
|
import { AgentCommandError, AgentConnection, CommandOperation, Incoming } from "./connection.js";
|
|
5
|
-
declare const
|
|
5
|
+
declare const Completed: CallableTaggedStruct<"Completed", {
|
|
6
6
|
isFailure: Schema.Boolean;
|
|
7
7
|
result: Schema.Unknown;
|
|
8
|
-
}
|
|
8
|
+
}>, Pending: CallableTaggedStruct<"Pending", {}>;
|
|
9
9
|
declare const UserEntryFields: {
|
|
10
10
|
text: Schema.String;
|
|
11
11
|
};
|
|
@@ -51,8 +51,6 @@ declare const ModelStreaming: Schema.Struct<{
|
|
|
51
51
|
readonly reasoning: Schema.String;
|
|
52
52
|
}>;
|
|
53
53
|
declare const ModelConnection: Schema.Literals<readonly ["disconnected", "connecting", "open", "reconnecting"]>;
|
|
54
|
-
declare const Pending: CallableTaggedStruct<"Pending", {}>;
|
|
55
|
-
declare const Completed: CallableTaggedStruct<"Completed", typeof CompletedFields>;
|
|
56
54
|
/** @experimental */
|
|
57
55
|
export type ToolOutcome = typeof Pending.Type | typeof Completed.Type;
|
|
58
56
|
/** @experimental */
|
package/dist/chat.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Equivalence, Effect, Option, Schema, Stream } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
|
-
import { Prompt } from "effect/unstable/ai";
|
|
4
3
|
import { define } from "foldkit/command";
|
|
5
4
|
import { m } from "foldkit/message";
|
|
6
5
|
import { make } from "foldkit/subscription";
|
|
7
6
|
import { Wire } from "@batonfx/transport";
|
|
8
|
-
import { AgentCommandError, AgentConnection, CommandOperation, Incoming
|
|
9
|
-
|
|
7
|
+
import { AgentCommandError, AgentConnection, CommandOperation, Incoming } from "./connection.js";
|
|
8
|
+
import { chatUpdateRuntime } from "./chat-update.js";
|
|
9
|
+
const { applyFrame, catchCommandFailure, Completed, isServerFrame, Pending } = chatUpdateRuntime;
|
|
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 };
|
|
@@ -35,8 +35,6 @@ const ModelStreaming = Schema.Struct({
|
|
|
35
35
|
reasoning: Schema.String,
|
|
36
36
|
});
|
|
37
37
|
const ModelConnection = Schema.Literals(["disconnected", "connecting", "open", "reconnecting"]);
|
|
38
|
-
const Pending = m("Pending");
|
|
39
|
-
const Completed = m("Completed", CompletedFields);
|
|
40
38
|
/** @experimental */
|
|
41
39
|
export const ToolOutcome = Schema.Union([Pending, Completed]);
|
|
42
40
|
/** @experimental */
|
|
@@ -177,26 +175,6 @@ export const initialModel = (sessionId = null) => ({
|
|
|
177
175
|
streaming: null,
|
|
178
176
|
draft: "",
|
|
179
177
|
});
|
|
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
|
-
})));
|
|
200
178
|
/** @experimental */
|
|
201
179
|
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())))));
|
|
202
180
|
/** @experimental */
|
|
@@ -215,233 +193,6 @@ export const ResolveApproval = define("ResolveApproval", {
|
|
|
215
193
|
});
|
|
216
194
|
/** @experimental */
|
|
217
195
|
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())))));
|
|
218
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
219
|
-
const isToolCall = (value) => isRecord(value) &&
|
|
220
|
-
value.type === "tool-call" &&
|
|
221
|
-
typeof value.id === "string" &&
|
|
222
|
-
typeof value.name === "string" &&
|
|
223
|
-
"params" in value;
|
|
224
|
-
const isToolResult = (value) => isRecord(value) &&
|
|
225
|
-
value.type === "tool-result" &&
|
|
226
|
-
typeof value.id === "string" &&
|
|
227
|
-
typeof value.name === "string" &&
|
|
228
|
-
"result" in value &&
|
|
229
|
-
typeof value.isFailure === "boolean";
|
|
230
|
-
const streamingFor = (model, turn) => model.streaming ?? { turn, text: "", reasoning: "" };
|
|
231
|
-
const appendStreaming = (model, turn, field, delta) => {
|
|
232
|
-
const streaming = streamingFor(model, turn);
|
|
233
|
-
return { ...model, streaming: { ...streaming, [field]: streaming[field] + delta } };
|
|
234
|
-
};
|
|
235
|
-
const flushStreaming = (model) => {
|
|
236
|
-
if (model.streaming === null)
|
|
237
|
-
return model;
|
|
238
|
-
const { text, reasoning } = model.streaming;
|
|
239
|
-
const entry = text.length === 0 && reasoning.length === 0 ? [] : [AssistantEntry({ text, reasoning: reasoning || null })];
|
|
240
|
-
return { ...model, entries: [...model.entries, ...entry], streaming: null };
|
|
241
|
-
};
|
|
242
|
-
const upsertToolCall = (entries, call, phase = "called") => {
|
|
243
|
-
const index = entries.findIndex((entry) => entry._tag === "ToolEntry" && entry.callId === call.id);
|
|
244
|
-
const previous = index >= 0 ? entries[index] : undefined;
|
|
245
|
-
const previousToolEntry = previous?._tag === "ToolEntry" ? previous : undefined;
|
|
246
|
-
const nextPhase = previousToolEntry?.phase === "executing" || phase === "executing" ? "executing" : "called";
|
|
247
|
-
const next = ToolEntry({
|
|
248
|
-
callId: call.id,
|
|
249
|
-
name: call.name,
|
|
250
|
-
params: call.params === undefined ? previousToolEntry?.params : call.params,
|
|
251
|
-
phase: nextPhase,
|
|
252
|
-
outcome: previousToolEntry?.outcome ?? Pending(),
|
|
253
|
-
progress: previousToolEntry?.progress ?? [],
|
|
254
|
-
});
|
|
255
|
-
if (index < 0)
|
|
256
|
-
return [...entries, next];
|
|
257
|
-
return entries.map((entry, entryIndex) => (entryIndex === index ? next : entry));
|
|
258
|
-
};
|
|
259
|
-
const resolveTool = (entries, result) => {
|
|
260
|
-
const withCall = upsertToolCall(entries, { type: "tool-call", id: result.id, name: result.name, params: undefined });
|
|
261
|
-
return withCall.map((entry) => entry._tag === "ToolEntry" && entry.callId === result.id
|
|
262
|
-
? ToolEntry({
|
|
263
|
-
callId: entry.callId,
|
|
264
|
-
name: entry.name,
|
|
265
|
-
params: entry.params,
|
|
266
|
-
phase: entry.phase,
|
|
267
|
-
outcome: Completed({ isFailure: result.isFailure, result: result.result }),
|
|
268
|
-
progress: entry.progress,
|
|
269
|
-
})
|
|
270
|
-
: entry);
|
|
271
|
-
};
|
|
272
|
-
const addProgress = (entries, callId, message) => entries.map((entry) => entry._tag === "ToolEntry" && entry.callId === callId
|
|
273
|
-
? ToolEntry({
|
|
274
|
-
callId: entry.callId,
|
|
275
|
-
name: entry.name,
|
|
276
|
-
params: entry.params,
|
|
277
|
-
phase: entry.phase,
|
|
278
|
-
outcome: entry.outcome,
|
|
279
|
-
progress: entry.progress.concat(message),
|
|
280
|
-
})
|
|
281
|
-
: entry);
|
|
282
|
-
const failureMessage = (failure) => {
|
|
283
|
-
switch (failure._tag) {
|
|
284
|
-
case "@batonfx/core/AgentError":
|
|
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;
|
|
290
|
-
case "@batonfx/core/MiddlewareViolation":
|
|
291
|
-
return failure.detail;
|
|
292
|
-
case "@batonfx/core/TurnLimitExceeded":
|
|
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}`;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
const applyPart = (model, turn, part) => {
|
|
303
|
-
if (!isRecord(part) || typeof part.type !== "string")
|
|
304
|
-
return model;
|
|
305
|
-
switch (part.type) {
|
|
306
|
-
case "text-delta":
|
|
307
|
-
return typeof part.delta === "string" ? appendStreaming(model, turn, "text", part.delta) : model;
|
|
308
|
-
case "reasoning-delta":
|
|
309
|
-
return typeof part.delta === "string" ? appendStreaming(model, turn, "reasoning", part.delta) : model;
|
|
310
|
-
case "tool-call":
|
|
311
|
-
return isToolCall(part) ? { ...model, entries: upsertToolCall(model.entries, part) } : model;
|
|
312
|
-
case "tool-result":
|
|
313
|
-
return isToolResult(part) ? { ...model, entries: resolveTool(model.entries, part) } : model;
|
|
314
|
-
default:
|
|
315
|
-
return model;
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
const applySuspension = (model, suspension) => {
|
|
319
|
-
if (suspension.reason === "approval") {
|
|
320
|
-
return [
|
|
321
|
-
{
|
|
322
|
-
...model,
|
|
323
|
-
run: AwaitingApproval({
|
|
324
|
-
token: suspension.token,
|
|
325
|
-
toolName: suspension.tool_name,
|
|
326
|
-
params: suspension.tool_params,
|
|
327
|
-
}),
|
|
328
|
-
},
|
|
329
|
-
Option.some(ApprovalRequired()),
|
|
330
|
-
];
|
|
331
|
-
}
|
|
332
|
-
const message = `Tool wait suspension for ${suspension.tool_name} is not resolvable by the FoldKit adapter`;
|
|
333
|
-
return [{ ...model, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
334
|
-
};
|
|
335
|
-
const applyStatus = (model, status) => {
|
|
336
|
-
switch (status._tag) {
|
|
337
|
-
case "Idle":
|
|
338
|
-
return [{ ...model, run: Idle() }, Option.none()];
|
|
339
|
-
case "Running":
|
|
340
|
-
return [{ ...model, run: Running({ turn: status.turn }) }, Option.none()];
|
|
341
|
-
case "Suspended":
|
|
342
|
-
return applySuspension(model, status.suspension);
|
|
343
|
-
case "Failed": {
|
|
344
|
-
const message = failureMessage(status.error);
|
|
345
|
-
return [{ ...model, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
const applyEvent = (model, event) => {
|
|
350
|
-
switch (event._tag) {
|
|
351
|
-
case "TurnStarted":
|
|
352
|
-
return [
|
|
353
|
-
{ ...model, run: Running({ turn: event.turn }), streaming: { turn: event.turn, text: "", reasoning: "" } },
|
|
354
|
-
Option.none(),
|
|
355
|
-
];
|
|
356
|
-
case "ModelPart":
|
|
357
|
-
return [applyPart(model, event.turn, event.part), Option.none()];
|
|
358
|
-
case "ToolExecutionStarted":
|
|
359
|
-
return [{ ...model, entries: upsertToolCall(model.entries, event.call, "executing") }, Option.none()];
|
|
360
|
-
case "ApprovalRequested":
|
|
361
|
-
return [{ ...model, entries: upsertToolCall(model.entries, event.call) }, Option.none()];
|
|
362
|
-
case "ToolProgress":
|
|
363
|
-
return event.message === undefined
|
|
364
|
-
? [model, Option.none()]
|
|
365
|
-
: [{ ...model, entries: addProgress(model.entries, event.toolCallId, event.message) }, Option.none()];
|
|
366
|
-
case "ToolExecutionCompleted":
|
|
367
|
-
return [
|
|
368
|
-
{ ...model, entries: resolveTool(upsertToolCall(model.entries, event.call, "executing"), event.result) },
|
|
369
|
-
Option.none(),
|
|
370
|
-
];
|
|
371
|
-
case "SteeringDrained":
|
|
372
|
-
return [model, Option.none()];
|
|
373
|
-
case "TurnCompleted":
|
|
374
|
-
return [flushStreaming(model), Option.none()];
|
|
375
|
-
case "StructuredOutput":
|
|
376
|
-
return [model, Option.none()];
|
|
377
|
-
case "Completed": {
|
|
378
|
-
const flushed = flushStreaming(model);
|
|
379
|
-
return [{ ...flushed, run: Idle() }, Option.some(RunCompleted({ text: event.text }))];
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
const textFromContent = (content) => content
|
|
384
|
-
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
385
|
-
.map((part) => part.text)
|
|
386
|
-
.join("");
|
|
387
|
-
const reasoningFromContent = (content) => {
|
|
388
|
-
const reasoning = content
|
|
389
|
-
.filter((part) => isRecord(part) && part.type === "reasoning" && typeof part.text === "string")
|
|
390
|
-
.map((part) => part.text)
|
|
391
|
-
.join("");
|
|
392
|
-
return reasoning.length === 0 ? null : reasoning;
|
|
393
|
-
};
|
|
394
|
-
const projectPrompt = (prompt) => {
|
|
395
|
-
let entries = [];
|
|
396
|
-
for (const message of prompt.content) {
|
|
397
|
-
if (message.role === "user") {
|
|
398
|
-
const text = textFromContent(message.content);
|
|
399
|
-
if (text.length > 0)
|
|
400
|
-
entries = entries.concat(UserEntry({ text }));
|
|
401
|
-
}
|
|
402
|
-
else if (message.role === "assistant") {
|
|
403
|
-
const text = textFromContent(message.content);
|
|
404
|
-
const reasoning = reasoningFromContent(message.content);
|
|
405
|
-
if (text.length > 0 || reasoning !== null)
|
|
406
|
-
entries = entries.concat(AssistantEntry({ text, reasoning }));
|
|
407
|
-
for (const part of message.content) {
|
|
408
|
-
if (isToolCall(part))
|
|
409
|
-
entries = upsertToolCall(entries, part);
|
|
410
|
-
if (isToolResult(part))
|
|
411
|
-
entries = resolveTool(entries, part);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
else if (message.role === "tool") {
|
|
415
|
-
for (const part of message.content) {
|
|
416
|
-
if (isToolResult(part))
|
|
417
|
-
entries = resolveTool(entries, part);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return entries;
|
|
422
|
-
};
|
|
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
|
-
}
|
|
427
|
-
if (frame.seq <= model.lastSeq)
|
|
428
|
-
return [model, Option.none()];
|
|
429
|
-
const withSeq = { ...model, lastSeq: frame.seq };
|
|
430
|
-
switch (frame._tag) {
|
|
431
|
-
case "Event":
|
|
432
|
-
return applyEvent(withSeq, frame.event);
|
|
433
|
-
case "Suspended":
|
|
434
|
-
return applySuspension(withSeq, frame.suspension);
|
|
435
|
-
case "Failed": {
|
|
436
|
-
const message = failureMessage(frame.error);
|
|
437
|
-
return [{ ...withSeq, run: Failed({ message }) }, Option.some(RunFailed({ message }))];
|
|
438
|
-
}
|
|
439
|
-
case "Ended":
|
|
440
|
-
return [withSeq, Option.none()];
|
|
441
|
-
case "SessionStatus":
|
|
442
|
-
return applyStatus(withSeq, frame.status);
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
196
|
const jsonText = (value) => {
|
|
446
197
|
try {
|
|
447
198
|
return JSON.stringify(value, null, 2) ?? "undefined";
|
|
@@ -521,12 +272,6 @@ export const conversationItems = (model) => {
|
|
|
521
272
|
: [];
|
|
522
273
|
return [...entries, ...streaming, ...waiting, ...approval, ...failure];
|
|
523
274
|
};
|
|
524
|
-
const isServerFrame = (incoming) => incoming._tag === "Event" ||
|
|
525
|
-
incoming._tag === "Suspended" ||
|
|
526
|
-
incoming._tag === "Failed" ||
|
|
527
|
-
incoming._tag === "Ended" ||
|
|
528
|
-
incoming._tag === "Snapshot" ||
|
|
529
|
-
incoming._tag === "SessionStatus";
|
|
530
275
|
/** @experimental */
|
|
531
276
|
export const update = dual(2, (model, message) => {
|
|
532
277
|
switch (message._tag) {
|
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.7.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.7.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"effect": ">=4.0.0-beta.88 <4.0.1",
|