| ... | ... | @@ -28,6 +28,10 @@ const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; |
| 28 | 28 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); |
| 29 | 29 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; |
| 30 | 30 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120); |
| 31 | const BUFFERED_ASSISTANT_MESSAGE_SPILLED_CACHE_CAPACITY = 20_000; |
| 32 | const BUFFERED_ASSISTANT_MESSAGE_SPILLED_TTL = Duration.minutes(120); |
| 33 | const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; |
| 34 | const BUFFERED_ASSISTANT_KEEP_CHARS = 8_000; |
| 31 | 35 | |
| 32 | 36 | type TurnStartRequestedDomainEvent = Extract< |
| 33 | 37 | OrchestrationEvent, |
| ... | ... | @@ -169,6 +173,11 @@ const make = Effect.gen(function* () { |
| 169 | 173 | timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, |
| 170 | 174 | lookup: () => Effect.succeed(""), |
| 171 | 175 | }); |
| 176 | const bufferedAssistantMessageSpilledByMessageId = yield* Cache.make<MessageId, true>({ |
| 177 | capacity: BUFFERED_ASSISTANT_MESSAGE_SPILLED_CACHE_CAPACITY, |
| 178 | timeToLive: BUFFERED_ASSISTANT_MESSAGE_SPILLED_TTL, |
| 179 | lookup: () => Effect.succeed(true), |
| 180 | }); |
| 172 | 181 | |
| 173 | 182 | const rememberAssistantMessageId = ( |
| 174 | 183 | sessionId: ProviderSessionId, |
| ... | ... | @@ -226,14 +235,23 @@ const make = Effect.gen(function* () { |
| 226 | 235 | const appendBufferedAssistantText = (messageId: MessageId, delta: string) => |
| 227 | 236 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( |
| 228 | 237 | Effect.flatMap((existingText) => |
| 229 | | Cache.set( |
| 230 | | bufferedAssistantTextByMessageId, |
| 231 | | messageId, |
| 232 | | Option.match(existingText, { |
| 238 | Effect.gen(function* () { |
| 239 | const nextText = Option.match(existingText, { |
| 233 | 240 | onNone: () => delta, |
| 234 | 241 | onSome: (text) => `${text}${delta}`, |
| 235 | | }), |
| 236 | | ), |
| 242 | }); |
| 243 | if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) { |
| 244 | yield* Cache.set(bufferedAssistantTextByMessageId, messageId, nextText); |
| 245 | return ""; |
| 246 | } |
| 247 | |
| 248 | // Safety valve: spill oldest text as a persisted assistant delta to cap memory. |
| 249 | const spillChars = Math.max(1, nextText.length - BUFFERED_ASSISTANT_KEEP_CHARS); |
| 250 | const spillChunk = nextText.slice(0, spillChars); |
| 251 | const retainedTail = nextText.slice(spillChars); |
| 252 | yield* Cache.set(bufferedAssistantTextByMessageId, messageId, retainedTail); |
| 253 | return spillChunk; |
| 254 | }), |
| 237 | 255 | ), |
| 238 | 256 | ); |
| 239 | 257 | |
| ... | ... | @@ -249,18 +267,51 @@ const make = Effect.gen(function* () { |
| 249 | 267 | const clearBufferedAssistantText = (messageId: MessageId) => |
| 250 | 268 | Cache.invalidate(bufferedAssistantTextByMessageId, messageId); |
| 251 | 269 | |
| 252 | | const clearAssistantMessageState = (messageId: MessageId) => clearBufferedAssistantText(messageId); |
| 270 | const markBufferedAssistantMessageSpilled = (messageId: MessageId) => |
| 271 | Cache.set(bufferedAssistantMessageSpilledByMessageId, messageId, true); |
| 272 | |
| 273 | const takeBufferedAssistantMessageSpilled = (messageId: MessageId) => |
| 274 | Cache.getOption(bufferedAssistantMessageSpilledByMessageId, messageId).pipe( |
| 275 | Effect.flatMap((spilled) => |
| 276 | Cache.invalidate(bufferedAssistantMessageSpilledByMessageId, messageId).pipe( |
| 277 | Effect.as(Option.isSome(spilled)), |
| 278 | ), |
| 279 | ), |
| 280 | ); |
| 281 | |
| 282 | const clearBufferedAssistantMessageSpilled = (messageId: MessageId) => |
| 283 | Cache.invalidate(bufferedAssistantMessageSpilledByMessageId, messageId); |
| 284 | |
| 285 | const clearAssistantMessageState = (messageId: MessageId) => |
| 286 | Effect.all( |
| 287 | [clearBufferedAssistantText(messageId), clearBufferedAssistantMessageSpilled(messageId)], |
| 288 | { concurrency: 1 }, |
| 289 | ).pipe(Effect.asVoid); |
| 253 | 290 | |
| 254 | | const dispatchAssistantCompletion = (input: { |
| 291 | const finalizeAssistantMessage = (input: { |
| 255 | 292 | event: ProviderRuntimeEvent; |
| 256 | 293 | threadId: ThreadId; |
| 257 | 294 | messageId: MessageId; |
| 258 | 295 | turnId?: TurnId; |
| 259 | 296 | createdAt: string; |
| 260 | 297 | commandTag: string; |
| 298 | spillRemainderCommandTag: string; |
| 261 | 299 | }) => |
| 262 | 300 | Effect.gen(function* () { |
| 263 | 301 | const text = yield* takeBufferedAssistantText(input.messageId); |
| 302 | const spilled = yield* takeBufferedAssistantMessageSpilled(input.messageId); |
| 303 | |
| 304 | if (spilled && text.length > 0) { |
| 305 | yield* orchestrationEngine.dispatch({ |
| 306 | type: "thread.message.assistant.delta", |
| 307 | commandId: providerCommandId(input.event, input.spillRemainderCommandTag), |
| 308 | threadId: input.threadId, |
| 309 | messageId: input.messageId, |
| 310 | delta: text, |
| 311 | ...(input.turnId ? { turnId: input.turnId } : {}), |
| 312 | createdAt: input.createdAt, |
| 313 | }); |
| 314 | } |
| 264 | 315 | |
| 265 | 316 | yield* orchestrationEngine.dispatch({ |
| 266 | 317 | type: "thread.message.assistant.complete", |
| ... | ... | @@ -268,7 +319,7 @@ const make = Effect.gen(function* () { |
| 268 | 319 | threadId: input.threadId, |
| 269 | 320 | messageId: input.messageId, |
| 270 | 321 | ...(input.turnId ? { turnId: input.turnId } : {}), |
| 271 | | text, |
| 322 | text: spilled ? "" : text, |
| 272 | 323 | createdAt: input.createdAt, |
| 273 | 324 | }); |
| 274 | 325 | yield* clearAssistantMessageState(input.messageId); |
| ... | ... | @@ -369,7 +420,19 @@ const make = Effect.gen(function* () { |
| 369 | 420 | |
| 370 | 421 | const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef); |
| 371 | 422 | if (assistantDeliveryMode === "buffered") { |
| 372 | | yield* appendBufferedAssistantText(assistantMessageId, event.delta); |
| 423 | const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, event.delta); |
| 424 | if (spillChunk.length > 0) { |
| 425 | yield* markBufferedAssistantMessageSpilled(assistantMessageId); |
| 426 | yield* orchestrationEngine.dispatch({ |
| 427 | type: "thread.message.assistant.delta", |
| 428 | commandId: providerCommandId(event, "assistant-delta-buffer-spill"), |
| 429 | threadId: thread.id, |
| 430 | messageId: assistantMessageId, |
| 431 | delta: spillChunk, |
| 432 | ...(turnId ? { turnId } : {}), |
| 433 | createdAt: now, |
| 434 | }); |
| 435 | } |
| 373 | 436 | } else { |
| 374 | 437 | yield* orchestrationEngine.dispatch({ |
| 375 | 438 | type: "thread.message.assistant.delta", |
| ... | ... | @@ -390,13 +453,14 @@ const make = Effect.gen(function* () { |
| 390 | 453 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 391 | 454 | } |
| 392 | 455 | |
| 393 | | yield* dispatchAssistantCompletion({ |
| 456 | yield* finalizeAssistantMessage({ |
| 394 | 457 | event, |
| 395 | 458 | threadId: thread.id, |
| 396 | 459 | messageId: assistantMessageId, |
| 397 | 460 | ...(turnId ? { turnId } : {}), |
| 398 | 461 | createdAt: now, |
| 399 | 462 | commandTag: "assistant-complete", |
| 463 | spillRemainderCommandTag: "assistant-delta-buffer-remainder", |
| 400 | 464 | }); |
| 401 | 465 | |
| 402 | 466 | if (turnId) { |
| ... | ... | @@ -412,13 +476,14 @@ const make = Effect.gen(function* () { |
| 412 | 476 | assistantMessageIds, |
| 413 | 477 | (assistantMessageId) => |
| 414 | 478 | Effect.gen(function* () { |
| 415 | | yield* dispatchAssistantCompletion({ |
| 479 | yield* finalizeAssistantMessage({ |
| 416 | 480 | event, |
| 417 | 481 | threadId: thread.id, |
| 418 | 482 | messageId: assistantMessageId, |
| 419 | 483 | turnId, |
| 420 | 484 | createdAt: now, |
| 421 | 485 | commandTag: "assistant-complete-finalize", |
| 486 | spillRemainderCommandTag: "assistant-delta-buffer-remainder-finalize", |
| 422 | 487 | }); |
| 423 | 488 | }), |
| 424 | 489 | { concurrency: 1 }, |