@batonfx/foldkit 0.6.4 → 0.7.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.
@@ -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 CompletedFields: {
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 { Cause, Equivalence, Effect, Option, Result, Schema, Stream } from "effect";
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, SendFailed } from "./connection.js";
9
- const CompletedFields = { isFailure: Schema.Boolean, result: Schema.Unknown };
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.6.4",
4
+ "version": "0.7.1",
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.6.4"
20
+ "@batonfx/transport": "0.7.1"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "effect": ">=4.0.0-beta.88 <4.0.1",