|
9 | 9 | ProviderInstanceId, |
10 | 10 | ProviderSessionId, |
11 | 11 | ProviderTurnId, |
| 12 | + RuntimeRequestId, |
12 | 13 | ThreadId, |
13 | 14 | TurnItemId, |
14 | 15 | } from "@t3tools/contracts"; |
@@ -539,6 +540,78 @@ it.effect.each(["thread.stop", "run.interrupt"] as const)( |
539 | 540 | }).pipe(Effect.provide(layerTest.pipe(Layer.provideMerge(TestClock.layer())))), |
540 | 541 | ); |
541 | 542 |
|
| 543 | +it.effect.each([ |
| 544 | + { ending: "cancelled", question: "resolved" }, |
| 545 | + { ending: "interrupted", question: "resolved" }, |
| 546 | + { ending: "completed", question: "pending" }, |
| 547 | + { ending: "failed", question: "pending" }, |
| 548 | +] as const)( |
| 549 | + "a $ending delegated task leaves its async question $question", |
| 550 | + ({ ending, question }) => |
| 551 | + Effect.gen(function* () { |
| 552 | + const orchestrator = yield* Orchestrator.OrchestratorV2; |
| 553 | + const projections = yield* ProjectionStore.ProjectionStoreV2; |
| 554 | + const parentThreadId = ThreadId.make(`thread:stop-question-parent:${ending}`); |
| 555 | + yield* createWatchingThread(parentThreadId, 20); |
| 556 | + yield* send(parentThreadId, "first", "start_immediately"); |
| 557 | + const childThreadId = yield* delegate(parentThreadId, `question task ${ending}`); |
| 558 | + const childRun = (yield* orchestrator.getThreadProjection(childThreadId)).runs[0]!; |
| 559 | + assert.equal( |
| 560 | + (yield* orchestrator.getThreadProjection(childThreadId)).nodes.find( |
| 561 | + (node) => node.id === childRun.rootNodeId, |
| 562 | + )?.runId, |
| 563 | + childRun.id, |
| 564 | + ); |
| 565 | + const now = yield* DateTime.now; |
| 566 | + yield* projections.apply({ |
| 567 | + id: EventId.make("event:stop-question:request"), |
| 568 | + type: "runtime-request.updated", |
| 569 | + threadId: childThreadId, |
| 570 | + runId: childRun.id, |
| 571 | + occurredAt: now, |
| 572 | + payload: { |
| 573 | + id: RuntimeRequestId.make("request:stop-question"), |
| 574 | + nodeId: childRun.rootNodeId!, |
| 575 | + providerTurnId: null, |
| 576 | + nativeRequestRef: null, |
| 577 | + kind: "user_input", |
| 578 | + status: "pending", |
| 579 | + responseCapability: { type: "message" }, |
| 580 | + createdAt: now, |
| 581 | + resolvedAt: null, |
| 582 | + }, |
| 583 | + }); |
| 584 | + const request = Effect.map( |
| 585 | + orchestrator.getThreadProjection(childThreadId), |
| 586 | + (projection) => projection.runtimeRequests[0], |
| 587 | + ); |
| 588 | + |
| 589 | + // While the task runs, its question stays open. |
| 590 | + yield* orchestrator.recoverDelegatedTask(childThreadId, childRun.id); |
| 591 | + assert.equal((yield* request)?.status, "pending"); |
| 592 | + if (ending === "interrupted") { |
| 593 | + // What task_cancel dispatches. |
| 594 | + yield* orchestrator.dispatch({ |
| 595 | + type: "thread.stop", |
| 596 | + commandId: CommandId.make("stop-question-child"), |
| 597 | + threadId: childThreadId, |
| 598 | + }); |
| 599 | + } else { |
| 600 | + yield* projections.apply({ |
| 601 | + id: EventId.make(`event:stop-question:${ending}`), |
| 602 | + type: "run.updated", |
| 603 | + threadId: childThreadId, |
| 604 | + runId: childRun.id, |
| 605 | + occurredAt: now, |
| 606 | + payload: { ...childRun, status: ending, startedAt: now, completedAt: now }, |
| 607 | + }); |
| 608 | + } |
| 609 | + yield* orchestrator.recoverDelegatedTask(childThreadId, childRun.id); |
| 610 | + assert.equal((yield* request)?.status, question); |
| 611 | + assert.equal((yield* request)?.decision, question === "resolved" ? "cancel" : undefined); |
| 612 | + }).pipe(Effect.provide(layerTest)), |
| 613 | +); |
| 614 | + |
542 | 615 | it.effect("a delegated task that cannot be stopped fails the walk after its siblings stop", () => |
543 | 616 | Effect.gen(function* () { |
544 | 617 | const orchestrator = yield* Orchestrator.OrchestratorV2; |
|
0 commit comments