| ... | @@ -1,7 +1,10 @@ | ... | @@ -1,7 +1,10 @@ |
| 1 | import { | 1 | import { |
| | 2 | type AssistantDeliveryMode, |
| 2 | CommandId, | 3 | CommandId, |
| 3 | MessageId, | 4 | MessageId, |
| | 5 | type OrchestrationEvent, |
| 4 | ProviderThreadId, | 6 | ProviderThreadId, |
| | 7 | type ThreadId, |
| 5 | TurnId, | 8 | TurnId, |
| 6 | type OrchestrationThreadActivity, | 9 | type OrchestrationThreadActivity, |
| 7 | type ProviderRuntimeEvent, | 10 | type ProviderRuntimeEvent, |
| ... | @@ -19,8 +22,32 @@ import { | ... | @@ -19,8 +22,32 @@ import { |
| 19 | const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`; | 22 | const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`; |
| 20 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => | 23 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => |
| 21 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); | 24 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); |
| | 25 | const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered"; |
| 22 | const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; | 26 | const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; |
| 23 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); | 27 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); |
| | 28 | const TURN_DELIVERY_MODE_BY_TURN_CACHE_CAPACITY = 10_000; |
| | 29 | const TURN_DELIVERY_MODE_BY_TURN_TTL = Duration.minutes(120); |
| | 30 | const PENDING_DELIVERY_MODE_BY_THREAD_CACHE_CAPACITY = 10_000; |
| | 31 | const PENDING_DELIVERY_MODE_BY_THREAD_TTL = Duration.minutes(30); |
| | 32 | const MESSAGE_DELIVERY_MODE_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; |
| | 33 | const MESSAGE_DELIVERY_MODE_BY_MESSAGE_ID_TTL = Duration.minutes(120); |
| | 34 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; |
| | 35 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120); |
| | 36 | |
| | 37 | type TurnStartRequestedDomainEvent = Extract< |
| | 38 | OrchestrationEvent, |
| | 39 | { type: "thread.turn-start-requested" } |
| | 40 | >; |
| | 41 | |
| | 42 | type RuntimeIngestionInput = |
| | 43 | | { |
| | 44 | source: "runtime"; |
| | 45 | event: ProviderRuntimeEvent; |
| | 46 | } |
| | 47 | | { |
| | 48 | source: "domain"; |
| | 49 | event: TurnStartRequestedDomainEvent; |
| | 50 | }; |
| 24 | | 51 | |
| 25 | function toTurnId(value: string | undefined): TurnId | undefined { | 52 | function toTurnId(value: string | undefined): TurnId | undefined { |
| 26 | return value === undefined ? undefined : TurnId.makeUnsafe(value); | 53 | return value === undefined ? undefined : TurnId.makeUnsafe(value); |
| ... | @@ -135,6 +162,26 @@ const make = Effect.gen(function* () { | ... | @@ -135,6 +162,26 @@ const make = Effect.gen(function* () { |
| 135 | timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL, | 162 | timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL, |
| 136 | lookup: () => Effect.succeed(new Set<MessageId>()), | 163 | lookup: () => Effect.succeed(new Set<MessageId>()), |
| 137 | }); | 164 | }); |
| | 165 | const assistantDeliveryModeByTurnKey = yield* Cache.make<string, AssistantDeliveryMode>({ |
| | 166 | capacity: TURN_DELIVERY_MODE_BY_TURN_CACHE_CAPACITY, |
| | 167 | timeToLive: TURN_DELIVERY_MODE_BY_TURN_TTL, |
| | 168 | lookup: () => Effect.succeed(DEFAULT_ASSISTANT_DELIVERY_MODE), |
| | 169 | }); |
| | 170 | const pendingAssistantDeliveryModeByThreadId = yield* Cache.make<ThreadId, AssistantDeliveryMode>({ |
| | 171 | capacity: PENDING_DELIVERY_MODE_BY_THREAD_CACHE_CAPACITY, |
| | 172 | timeToLive: PENDING_DELIVERY_MODE_BY_THREAD_TTL, |
| | 173 | lookup: () => Effect.succeed(DEFAULT_ASSISTANT_DELIVERY_MODE), |
| | 174 | }); |
| | 175 | const assistantDeliveryModeByMessageId = yield* Cache.make<MessageId, AssistantDeliveryMode>({ |
| | 176 | capacity: MESSAGE_DELIVERY_MODE_BY_MESSAGE_ID_CACHE_CAPACITY, |
| | 177 | timeToLive: MESSAGE_DELIVERY_MODE_BY_MESSAGE_ID_TTL, |
| | 178 | lookup: () => Effect.succeed(DEFAULT_ASSISTANT_DELIVERY_MODE), |
| | 179 | }); |
| | 180 | const bufferedAssistantTextByMessageId = yield* Cache.make<MessageId, string>({ |
| | 181 | capacity: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY, |
| | 182 | timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, |
| | 183 | lookup: () => Effect.succeed(""), |
| | 184 | }); |
| 138 | | 185 | |
| 139 | const rememberAssistantMessageId = ( | 186 | const rememberAssistantMessageId = ( |
| 140 | sessionId: ProviderSessionId, | 187 | sessionId: ProviderSessionId, |
| ... | @@ -189,6 +236,144 @@ const make = Effect.gen(function* () { | ... | @@ -189,6 +236,144 @@ const make = Effect.gen(function* () { |
| 189 | const clearAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => | 236 | const clearAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => |
| 190 | Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)); | 237 | Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)); |
| 191 | | 238 | |
| | 239 | const rememberAssistantDeliveryModeForTurn = ( |
| | 240 | sessionId: ProviderSessionId, |
| | 241 | turnId: TurnId, |
| | 242 | assistantDeliveryMode: AssistantDeliveryMode, |
| | 243 | ) => |
| | 244 | Cache.set( |
| | 245 | assistantDeliveryModeByTurnKey, |
| | 246 | providerTurnKey(sessionId, turnId), |
| | 247 | assistantDeliveryMode, |
| | 248 | ); |
| | 249 | |
| | 250 | const clearAssistantDeliveryModeForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => |
| | 251 | Cache.invalidate(assistantDeliveryModeByTurnKey, providerTurnKey(sessionId, turnId)); |
| | 252 | |
| | 253 | const rememberPendingAssistantDeliveryModeForThread = ( |
| | 254 | threadId: ThreadId, |
| | 255 | assistantDeliveryMode: AssistantDeliveryMode, |
| | 256 | ) => Cache.set(pendingAssistantDeliveryModeByThreadId, threadId, assistantDeliveryMode); |
| | 257 | |
| | 258 | const takePendingAssistantDeliveryModeForThread = (threadId: ThreadId) => |
| | 259 | Cache.getOption(pendingAssistantDeliveryModeByThreadId, threadId).pipe( |
| | 260 | Effect.flatMap((assistantDeliveryMode) => |
| | 261 | Cache.invalidate(pendingAssistantDeliveryModeByThreadId, threadId).pipe( |
| | 262 | Effect.as(assistantDeliveryMode), |
| | 263 | ), |
| | 264 | ), |
| | 265 | ); |
| | 266 | |
| | 267 | const clearPendingAssistantDeliveryModeForThread = (threadId: ThreadId) => |
| | 268 | Cache.invalidate(pendingAssistantDeliveryModeByThreadId, threadId); |
| | 269 | |
| | 270 | const rememberAssistantDeliveryModeForMessage = ( |
| | 271 | messageId: MessageId, |
| | 272 | assistantDeliveryMode: AssistantDeliveryMode, |
| | 273 | ) => Cache.set(assistantDeliveryModeByMessageId, messageId, assistantDeliveryMode); |
| | 274 | |
| | 275 | const clearAssistantDeliveryModeForMessage = (messageId: MessageId) => |
| | 276 | Cache.invalidate(assistantDeliveryModeByMessageId, messageId); |
| | 277 | |
| | 278 | const appendBufferedAssistantText = (messageId: MessageId, delta: string) => |
| | 279 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( |
| | 280 | Effect.flatMap((existingText) => |
| | 281 | Cache.set( |
| | 282 | bufferedAssistantTextByMessageId, |
| | 283 | messageId, |
| | 284 | Option.match(existingText, { |
| | 285 | onNone: () => delta, |
| | 286 | onSome: (text) => `${text}${delta}`, |
| | 287 | }), |
| | 288 | ), |
| | 289 | ), |
| | 290 | ); |
| | 291 | |
| | 292 | const takeBufferedAssistantText = (messageId: MessageId) => |
| | 293 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( |
| | 294 | Effect.flatMap((existingText) => |
| | 295 | Cache.invalidate(bufferedAssistantTextByMessageId, messageId).pipe( |
| | 296 | Effect.as(Option.getOrElse(existingText, () => "")), |
| | 297 | ), |
| | 298 | ), |
| | 299 | ); |
| | 300 | |
| | 301 | const clearBufferedAssistantText = (messageId: MessageId) => |
| | 302 | Cache.invalidate(bufferedAssistantTextByMessageId, messageId); |
| | 303 | |
| | 304 | const clearAssistantMessageState = (messageId: MessageId) => |
| | 305 | Effect.all( |
| | 306 | [ |
| | 307 | clearAssistantDeliveryModeForMessage(messageId), |
| | 308 | clearBufferedAssistantText(messageId), |
| | 309 | ], |
| | 310 | { concurrency: 1 }, |
| | 311 | ).pipe(Effect.asVoid); |
| | 312 | |
| | 313 | const resolveAssistantDeliveryModeForMessage = (input: { |
| | 314 | threadId: ThreadId; |
| | 315 | sessionId: ProviderSessionId; |
| | 316 | turnId?: TurnId; |
| | 317 | messageId: MessageId; |
| | 318 | }) => |
| | 319 | Effect.gen(function* () { |
| | 320 | const messageMode = yield* Cache.getOption(assistantDeliveryModeByMessageId, input.messageId); |
| | 321 | if (Option.isSome(messageMode)) { |
| | 322 | return messageMode.value; |
| | 323 | } |
| | 324 | |
| | 325 | if (input.turnId) { |
| | 326 | const turnMode = yield* Cache.getOption( |
| | 327 | assistantDeliveryModeByTurnKey, |
| | 328 | providerTurnKey(input.sessionId, input.turnId), |
| | 329 | ); |
| | 330 | if (Option.isSome(turnMode)) { |
| | 331 | return turnMode.value; |
| | 332 | } |
| | 333 | } |
| | 334 | |
| | 335 | const pendingMode = yield* Cache.getOption( |
| | 336 | pendingAssistantDeliveryModeByThreadId, |
| | 337 | input.threadId, |
| | 338 | ); |
| | 339 | if (Option.isSome(pendingMode)) { |
| | 340 | if (input.turnId) { |
| | 341 | yield* rememberAssistantDeliveryModeForTurn(input.sessionId, input.turnId, pendingMode.value); |
| | 342 | yield* clearPendingAssistantDeliveryModeForThread(input.threadId); |
| | 343 | } |
| | 344 | return pendingMode.value; |
| | 345 | } |
| | 346 | |
| | 347 | return DEFAULT_ASSISTANT_DELIVERY_MODE; |
| | 348 | }); |
| | 349 | |
| | 350 | const dispatchAssistantCompletion = (input: { |
| | 351 | event: ProviderRuntimeEvent; |
| | 352 | threadId: ThreadId; |
| | 353 | messageId: MessageId; |
| | 354 | turnId?: TurnId; |
| | 355 | assistantDeliveryMode: AssistantDeliveryMode; |
| | 356 | createdAt: string; |
| | 357 | commandTag: string; |
| | 358 | }) => |
| | 359 | Effect.gen(function* () { |
| | 360 | const text = |
| | 361 | input.assistantDeliveryMode === "buffered" |
| | 362 | ? yield* takeBufferedAssistantText(input.messageId) |
| | 363 | : undefined; |
| | 364 | |
| | 365 | yield* orchestrationEngine.dispatch({ |
| | 366 | type: "thread.message.assistant.complete", |
| | 367 | commandId: providerCommandId(input.event, input.commandTag), |
| | 368 | threadId: input.threadId, |
| | 369 | messageId: input.messageId, |
| | 370 | ...(input.turnId ? { turnId: input.turnId } : {}), |
| | 371 | ...(text !== undefined ? { text } : {}), |
| | 372 | createdAt: input.createdAt, |
| | 373 | }); |
| | 374 | yield* clearAssistantMessageState(input.messageId); |
| | 375 | }); |
| | 376 | |
| 192 | const clearTurnStateForSession = (sessionId: ProviderSessionId) => | 377 | const clearTurnStateForSession = (sessionId: ProviderSessionId) => |
| 193 | Effect.gen(function* () { | 378 | Effect.gen(function* () { |
| 194 | const prefix = `${sessionId}:`; | 379 | const prefix = `${sessionId}:`; |
| ... | @@ -196,12 +381,35 @@ const make = Effect.gen(function* () { | ... | @@ -196,12 +381,35 @@ const make = Effect.gen(function* () { |
| 196 | yield* Effect.forEach( | 381 | yield* Effect.forEach( |
| 197 | turnKeys, | 382 | turnKeys, |
| 198 | (key) => | 383 | (key) => |
| 199 | key.startsWith(prefix) ? Cache.invalidate(turnMessageIdsByTurnKey, key) : Effect.void, | 384 | Effect.gen(function* () { |
| | 385 | if (!key.startsWith(prefix)) { |
| | 386 | return; |
| | 387 | } |
| | 388 | |
| | 389 | const messageIds = yield* Cache.getOption(turnMessageIdsByTurnKey, key); |
| | 390 | if (Option.isSome(messageIds)) { |
| | 391 | yield* Effect.forEach(messageIds.value, clearAssistantMessageState, { |
| | 392 | concurrency: 1, |
| | 393 | }).pipe(Effect.asVoid); |
| | 394 | } |
| | 395 | |
| | 396 | yield* Cache.invalidate(turnMessageIdsByTurnKey, key); |
| | 397 | }), |
| | 398 | { concurrency: 1 }, |
| | 399 | ).pipe(Effect.asVoid); |
| | 400 | |
| | 401 | const deliveryModeKeys = Array.from(yield* Cache.keys(assistantDeliveryModeByTurnKey)); |
| | 402 | yield* Effect.forEach( |
| | 403 | deliveryModeKeys, |
| | 404 | (key) => |
| | 405 | key.startsWith(prefix) |
| | 406 | ? Cache.invalidate(assistantDeliveryModeByTurnKey, key) |
| | 407 | : Effect.void, |
| 200 | { concurrency: 1 }, | 408 | { concurrency: 1 }, |
| 201 | ).pipe(Effect.asVoid); | 409 | ).pipe(Effect.asVoid); |
| 202 | }); | 410 | }); |
| 203 | | 411 | |
| 204 | const processEvent = (event: ProviderRuntimeEvent) => | 412 | const processRuntimeEvent = (event: ProviderRuntimeEvent) => |
| 205 | Effect.gen(function* () { | 413 | Effect.gen(function* () { |
| 206 | const readModel = yield* orchestrationEngine.getReadModel(); | 414 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 207 | const thread = readModel.threads.find( | 415 | const thread = readModel.threads.find( |
| ... | @@ -218,8 +426,19 @@ const make = Effect.gen(function* () { | ... | @@ -218,8 +426,19 @@ const make = Effect.gen(function* () { |
| 218 | event.type === "turn.started" || | 426 | event.type === "turn.started" || |
| 219 | event.type === "turn.completed" | 427 | event.type === "turn.completed" |
| 220 | ) { | 428 | ) { |
| 221 | const activeTurnId = | 429 | const activeTurnId = event.type === "turn.started" ? (toTurnId(event.turnId) ?? null) : null; |
| 222 | event.type === "turn.started" ? (toTurnId(event.turnId) ?? null) : null; | 430 | if (event.type === "turn.started" && activeTurnId) { |
| | 431 | const pendingAssistantDeliveryMode = yield* takePendingAssistantDeliveryModeForThread( |
| | 432 | thread.id, |
| | 433 | ); |
| | 434 | if (Option.isSome(pendingAssistantDeliveryMode)) { |
| | 435 | yield* rememberAssistantDeliveryModeForTurn( |
| | 436 | event.sessionId, |
| | 437 | activeTurnId, |
| | 438 | pendingAssistantDeliveryMode.value, |
| | 439 | ); |
| | 440 | } |
| | 441 | } |
| 223 | const providerThreadIdFromEvent = | 442 | const providerThreadIdFromEvent = |
| 224 | event.type === "thread.started" | 443 | event.type === "thread.started" |
| 225 | ? ProviderThreadId.makeUnsafe(event.threadId) | 444 | ? ProviderThreadId.makeUnsafe(event.threadId) |
| ... | @@ -269,16 +488,27 @@ const make = Effect.gen(function* () { | ... | @@ -269,16 +488,27 @@ const make = Effect.gen(function* () { |
| 269 | if (turnId) { | 488 | if (turnId) { |
| 270 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 489 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 271 | } | 490 | } |
| 272 | | 491 | const assistantDeliveryMode = yield* resolveAssistantDeliveryModeForMessage({ |
| 273 | yield* orchestrationEngine.dispatch({ | | |
| 274 | type: "thread.message.assistant.delta", | | |
| 275 | commandId: providerCommandId(event, "assistant-delta"), | | |
| 276 | threadId: thread.id, | 492 | threadId: thread.id, |
| | 493 | sessionId: event.sessionId, |
| | 494 | turnId, |
| 277 | messageId: assistantMessageId, | 495 | messageId: assistantMessageId, |
| 278 | delta: event.delta, | | |
| 279 | ...(turnId ? { turnId } : {}), | | |
| 280 | createdAt: now, | | |
| 281 | }); | 496 | }); |
| | 497 | yield* rememberAssistantDeliveryModeForMessage(assistantMessageId, assistantDeliveryMode); |
| | 498 | |
| | 499 | if (assistantDeliveryMode === "buffered") { |
| | 500 | yield* appendBufferedAssistantText(assistantMessageId, event.delta); |
| | 501 | } else { |
| | 502 | yield* orchestrationEngine.dispatch({ |
| | 503 | type: "thread.message.assistant.delta", |
| | 504 | commandId: providerCommandId(event, "assistant-delta"), |
| | 505 | threadId: thread.id, |
| | 506 | messageId: assistantMessageId, |
| | 507 | delta: event.delta, |
| | 508 | ...(turnId ? { turnId } : {}), |
| | 509 | createdAt: now, |
| | 510 | }); |
| | 511 | } |
| 282 | } | 512 | } |
| 283 | | 513 | |
| 284 | if (event.type === "message.completed") { | 514 | if (event.type === "message.completed") { |
| ... | @@ -287,14 +517,22 @@ const make = Effect.gen(function* () { | ... | @@ -287,14 +517,22 @@ const make = Effect.gen(function* () { |
| 287 | if (turnId) { | 517 | if (turnId) { |
| 288 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 518 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 289 | } | 519 | } |
| | 520 | const assistantDeliveryMode = yield* resolveAssistantDeliveryModeForMessage({ |
| | 521 | threadId: thread.id, |
| | 522 | sessionId: event.sessionId, |
| | 523 | turnId, |
| | 524 | messageId: assistantMessageId, |
| | 525 | }); |
| | 526 | yield* rememberAssistantDeliveryModeForMessage(assistantMessageId, assistantDeliveryMode); |
| 290 | | 527 | |
| 291 | yield* orchestrationEngine.dispatch({ | 528 | yield* dispatchAssistantCompletion({ |
| 292 | type: "thread.message.assistant.complete", | 529 | event, |
| 293 | commandId: providerCommandId(event, "assistant-complete"), | | |
| 294 | threadId: thread.id, | 530 | threadId: thread.id, |
| 295 | messageId: assistantMessageId, | 531 | messageId: assistantMessageId, |
| 296 | ...(turnId ? { turnId } : {}), | 532 | ...(turnId ? { turnId } : {}), |
| | 533 | assistantDeliveryMode, |
| 297 | createdAt: now, | 534 | createdAt: now, |
| | 535 | commandTag: "assistant-complete", |
| 298 | }); | 536 | }); |
| 299 | | 537 | |
| 300 | if (turnId) { | 538 | if (turnId) { |
| ... | @@ -306,22 +544,37 @@ const make = Effect.gen(function* () { | ... | @@ -306,22 +544,37 @@ const make = Effect.gen(function* () { |
| 306 | const turnId = toTurnId(event.turnId); | 544 | const turnId = toTurnId(event.turnId); |
| 307 | if (turnId) { | 545 | if (turnId) { |
| 308 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(event.sessionId, turnId); | 546 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(event.sessionId, turnId); |
| 309 | yield* Effect.forEach(assistantMessageIds, (assistantMessageId) => | 547 | yield* Effect.forEach( |
| 310 | orchestrationEngine.dispatch({ | 548 | assistantMessageIds, |
| 311 | type: "thread.message.assistant.complete", | 549 | (assistantMessageId) => |
| 312 | commandId: providerCommandId(event, "assistant-complete-finalize"), | 550 | Effect.gen(function* () { |
| 313 | threadId: thread.id, | 551 | const assistantDeliveryMode = |
| 314 | messageId: assistantMessageId, | 552 | yield* resolveAssistantDeliveryModeForMessage({ |
| 315 | turnId, | 553 | threadId: thread.id, |
| 316 | createdAt: now, | 554 | sessionId: event.sessionId, |
| 317 | }), | 555 | turnId, |
| | 556 | messageId: assistantMessageId, |
| | 557 | }); |
| | 558 | yield* dispatchAssistantCompletion({ |
| | 559 | event, |
| | 560 | threadId: thread.id, |
| | 561 | messageId: assistantMessageId, |
| | 562 | turnId, |
| | 563 | assistantDeliveryMode, |
| | 564 | createdAt: now, |
| | 565 | commandTag: "assistant-complete-finalize", |
| | 566 | }); |
| | 567 | }), |
| | 568 | { concurrency: 1 }, |
| 318 | ).pipe(Effect.asVoid); | 569 | ).pipe(Effect.asVoid); |
| 319 | yield* clearAssistantMessageIdsForTurn(event.sessionId, turnId); | 570 | yield* clearAssistantMessageIdsForTurn(event.sessionId, turnId); |
| | 571 | yield* clearAssistantDeliveryModeForTurn(event.sessionId, turnId); |
| 320 | } | 572 | } |
| 321 | } | 573 | } |
| 322 | | 574 | |
| 323 | if (event.type === "session.exited") { | 575 | if (event.type === "session.exited") { |
| 324 | yield* clearTurnStateForSession(event.sessionId); | 576 | yield* clearTurnStateForSession(event.sessionId); |
| | 577 | yield* clearPendingAssistantDeliveryModeForThread(thread.id); |
| 325 | } | 578 | } |
| 326 | | 579 | |
| 327 | if (event.type === "runtime.error") { | 580 | if (event.type === "runtime.error") { |
| ... | @@ -360,33 +613,50 @@ const make = Effect.gen(function* () { | ... | @@ -360,33 +613,50 @@ const make = Effect.gen(function* () { |
| 360 | ).pipe(Effect.asVoid); | 613 | ).pipe(Effect.asVoid); |
| 361 | }); | 614 | }); |
| 362 | | 615 | |
| 363 | const processEventSafely = (event: ProviderRuntimeEvent) => | 616 | const processDomainEvent = (event: TurnStartRequestedDomainEvent) => |
| 364 | processEvent(event).pipe( | 617 | rememberPendingAssistantDeliveryModeForThread( |
| | 618 | event.payload.threadId, |
| | 619 | event.payload.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, |
| | 620 | ); |
| | 621 | |
| | 622 | const processInput = (input: RuntimeIngestionInput) => |
| | 623 | input.source === "runtime" ? processRuntimeEvent(input.event) : processDomainEvent(input.event); |
| | 624 | |
| | 625 | const processInputSafely = (input: RuntimeIngestionInput) => |
| | 626 | processInput(input).pipe( |
| 365 | Effect.catchCause((cause) => { | 627 | Effect.catchCause((cause) => { |
| 366 | if (Cause.hasInterruptsOnly(cause)) { | 628 | if (Cause.hasInterruptsOnly(cause)) { |
| 367 | return Effect.failCause(cause); | 629 | return Effect.failCause(cause); |
| 368 | } | 630 | } |
| 369 | return Effect.logWarning("provider runtime ingestion failed to process event", { | 631 | return Effect.logWarning("provider runtime ingestion failed to process event", { |
| 370 | eventId: event.eventId, | 632 | source: input.source, |
| 371 | eventType: event.type, | 633 | eventId: input.event.eventId, |
| | 634 | eventType: input.event.type, |
| 372 | cause: Cause.pretty(cause), | 635 | cause: Cause.pretty(cause), |
| 373 | }); | 636 | }); |
| 374 | }), | 637 | }), |
| 375 | ); | 638 | ); |
| 376 | | 639 | |
| 377 | const start: ProviderRuntimeIngestionShape["start"] = Effect.gen(function* () { | 640 | const start: ProviderRuntimeIngestionShape["start"] = Effect.gen(function* () { |
| 378 | const providerEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 641 | const inputQueue = yield* Queue.unbounded<RuntimeIngestionInput>(); |
| 379 | | 642 | yield* Effect.addFinalizer(() => Queue.shutdown(inputQueue).pipe(Effect.asVoid)); |
| 380 | yield* Effect.addFinalizer(() => Queue.shutdown(providerEventQueue).pipe(Effect.asVoid)); | | |
| 381 | | 643 | |
| 382 | yield* Effect.forkScoped( | 644 | yield* Effect.forkScoped( |
| 383 | Effect.forever(Queue.take(providerEventQueue).pipe(Effect.flatMap(processEventSafely))), | 645 | Effect.forever(Queue.take(inputQueue).pipe(Effect.flatMap(processInputSafely))), |
| 384 | ); | 646 | ); |
| 385 | yield* Effect.forkScoped( | 647 | yield* Effect.forkScoped( |
| 386 | Stream.runForEach(providerService.streamEvents, (event) => | 648 | Stream.runForEach(providerService.streamEvents, (event) => |
| 387 | Queue.offer(providerEventQueue, event).pipe(Effect.asVoid), | 649 | Queue.offer(inputQueue, { source: "runtime", event }).pipe(Effect.asVoid), |
| 388 | ), | 650 | ), |
| 389 | ); | 651 | ); |
| | 652 | yield* Effect.forkScoped( |
| | 653 | Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { |
| | 654 | if (event.type !== "thread.turn-start-requested") { |
| | 655 | return Effect.void; |
| | 656 | } |
| | 657 | return Queue.offer(inputQueue, { source: "domain", event }).pipe(Effect.asVoid); |
| | 658 | }), |
| | 659 | ); |
| 390 | }); | 660 | }); |
| 391 | | 661 | |
| 392 | return { | 662 | return { |