| ... | @@ -9,6 +9,7 @@ import { | ... | @@ -9,6 +9,7 @@ import { |
| 9 | ProviderInstanceId, | 9 | ProviderInstanceId, |
| 10 | ProviderSessionId, | 10 | ProviderSessionId, |
| 11 | ProviderTurnId, | 11 | ProviderTurnId, |
| | 12 | RuntimeRequestId, |
| 12 | ThreadId, | 13 | ThreadId, |
| 13 | TurnItemId, | 14 | TurnItemId, |
| 14 | } from "@t3tools/contracts"; | 15 | } from "@t3tools/contracts"; |
| ... | @@ -539,6 +540,78 @@ it.effect.each(["thread.stop", "run.interrupt"] as const)( | ... | @@ -539,6 +540,78 @@ it.effect.each(["thread.stop", "run.interrupt"] as const)( |
| 539 | }).pipe(Effect.provide(layerTest.pipe(Layer.provideMerge(TestClock.layer())))), | 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 | it.effect("a delegated task that cannot be stopped fails the walk after its siblings stop", () => | 615 | it.effect("a delegated task that cannot be stopped fails the walk after its siblings stop", () => |
| 543 | Effect.gen(function* () { | 616 | Effect.gen(function* () { |
| 544 | const orchestrator = yield* Orchestrator.OrchestratorV2; | 617 | const orchestrator = yield* Orchestrator.OrchestratorV2; |