Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/server/src/orchestration-v2/RunExecutionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,20 @@ import * as ProviderEventIngestor from "./ProviderEventIngestor.ts";
import * as McpAppModelContext from "../mcpApps/McpAppModelContext.ts";
import * as RunExecutionService from "./RunExecutionService.ts";
import * as RunFinalizationService from "./RunFinalizationService.ts";
import * as TurnItemPositionStore from "./TurnItemPositionStore.ts";

const driver = ProviderDriverKind.make("codex");

const layerTurnItemPositionsTest = Layer.mock(TurnItemPositionStore.TurnItemPositionStoreV2)({
allocate: () => Effect.succeed(1),
});

const layerRunExecutionTest = RunExecutionService.layer.pipe(
Layer.provide(
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({}),
IdAllocator.layer,
Layer.mock(ProviderEventIngestor.ProviderEventIngestorV2)({
Expand Down Expand Up @@ -755,6 +761,7 @@ it.effect("passes the thread's MCP app context to the provider under a safe key"
Layer.mock(CheckpointService.CheckpointServiceV2)({
captureBaseline: () => Effect.void,
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({}),
IdAllocator.layer,
Layer.mock(ProviderEventIngestor.ProviderEventIngestorV2)({
Expand Down Expand Up @@ -782,6 +789,7 @@ it.effect("fails the run when its ownership check cannot be read before calling
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
writeIfRunCurrent: (input) =>
Ref.update(writes, (current) => [...current, ...input.events]).pipe(
Expand Down Expand Up @@ -1065,6 +1073,7 @@ it.effect("starts the provider when checkpoint baseline capture fails", () =>
}),
),
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
writeWithEffects: (input) =>
Ref.update(writes, (current) => [
Expand Down Expand Up @@ -1177,6 +1186,7 @@ it.effect.each(["failure", "interruption", "stale-attempt", "start-guard"] as co
captureBaseline: () =>
scenario === "start-guard" ? Effect.void : Effect.die("not reached"),
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
writeIfRunCurrent: (input) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -1346,6 +1356,7 @@ it.effect("ingests new member threads of an owned child after the root turn term
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -1575,6 +1586,7 @@ it.effect("ends a nested native subagent and its thread when the root run is int
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -1932,6 +1944,7 @@ it.effect(
Layer.mock(CheckpointService.CheckpointServiceV2)({
captureBaseline: () => Effect.void,
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -2148,6 +2161,7 @@ it.effect("drops late root provider-thread writes from a superseded attempt", ()
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -2355,6 +2369,7 @@ it.effect(
Layer.mock(CheckpointService.CheckpointServiceV2)({
captureBaseline: () => Effect.void,
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -2527,6 +2542,7 @@ it.effect(
Layer.mock(CheckpointService.CheckpointServiceV2)({
captureBaseline: () => Effect.void,
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -2697,6 +2713,7 @@ it.effect(
Layer.mock(CheckpointService.CheckpointServiceV2)({
captureBaseline: () => Effect.void,
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -3138,6 +3155,7 @@ it.effect(
Layer.mock(CheckpointService.CheckpointServiceV2)({
captureBaseline: () => Effect.void,
}),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down Expand Up @@ -3862,6 +3880,7 @@ function captureRootRunTermination(input: {
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: (payload) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -4343,6 +4362,7 @@ function runBackgroundItemScenario(
Layer.mergeAll(
McpAppModelContext.layerEmpty,
Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
layerTurnItemPositionsTest,
Layer.mock(EventSink.EventSinkV2)({
write: () => Effect.succeed([]),
writeWithEffects: (input) =>
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/orchestration-v2/RunExecutionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
makeProviderFailureTurnItem,
} from "@t3tools/provider-core/server/failure";
import * as RunFinalizationService from "./RunFinalizationService.ts";
import * as TurnItemPositionStore from "./TurnItemPositionStore.ts";

export interface ProviderEventRoutingState {
readonly ownedThreadIds: ReadonlySet<ThreadId>;
Expand Down Expand Up @@ -569,6 +570,7 @@ export const layer: Layer.Layer<
| ProviderEventIngestor.ProviderEventIngestorV2
| ServerSettings.ServerSettingsService
| McpAppModelContext.McpAppModelContext
| TurnItemPositionStore.TurnItemPositionStoreV2
> = Layer.effect(
RunExecutionServiceV2,
Effect.gen(function* () {
Expand All @@ -578,6 +580,7 @@ export const layer: Layer.Layer<
const providerEventIngestor = yield* ProviderEventIngestor.ProviderEventIngestorV2;
const serverSettings = yield* ServerSettings.ServerSettingsService;
const mcpAppModelContext = yield* McpAppModelContext.McpAppModelContext;
const turnItemPositions = yield* TurnItemPositionStore.TurnItemPositionStoreV2;
const finalizationObserver = yield* RunFinalizationService.RunFinalizationObserver;

const writeFinalRunEvents = (input: {
Expand Down Expand Up @@ -1224,6 +1227,9 @@ export const layer: Layer.Layer<
return true;
});
const filterAssistantEvent = makeAssistantStreamingFilter(responseStreamingMode);
// Held text writes nothing, so its item would take a position only
// when finally written, behind every item that began after it.
const reservedHeldTurnItemIds = new Set<TurnItemId>();
const providerEventFiber = yield* eventSubscription.events.pipe(
Stream.filterEffect((event) =>
Ref.modify(eventRouting, (state) => routeProviderEvent(event, routeIdentity, state)),
Expand Down Expand Up @@ -1286,6 +1292,19 @@ export const layer: Layer.Layer<
// stream on this run's background probe.
yield* Ref.set(providerThreadOwnerLost, true);
}
} else if (
event.type === "turn_item.updated" &&
(event.turnItem.type === "assistant_message" ||
event.turnItem.type === "reasoning") &&
event.turnItem.streaming &&
!reservedHeldTurnItemIds.has(event.turnItem.id)
) {
yield* turnItemPositions.allocate({
threadId: event.turnItem.threadId,
turnItemId: event.turnItem.id,
runId: event.turnItem.runId,
});
reservedHeldTurnItemIds.add(event.turnItem.id);
}
if (event.type === "provider_thread.updated") {
if (event.providerThread.id === input.providerThread.id && storedEventCount > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ProviderThreadId,
ProviderTurnId,
ThreadId,
TurnItemId,
type ModelSelection,
type OrchestrationV2DomainEvent,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -702,3 +703,120 @@ it.effect("starts a steer that missed the turn on the saved next-turn selection"
}),
),
);

// A draft that never reaches a paragraph boundary is held back while it
// streams. Codex only flushes it at turn end, after the final answer is written,
// so the draft must already own its place from before the steer.
it.effect("keeps a held commentary draft before the steer when turn end flushes it", () =>
Effect.scoped(
Effect.gen(function* () {
const { events, started, layer, startFirstTurn } =
yield* nextTurnSelectionHarness("steering-held-draft");
yield* Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
const threadId = yield* startFirstTurn;
const first = started[0]!;
const turn = (yield* orchestrator.getThreadProjection(threadId)).providerTurns[0]!;
const now = yield* DateTime.now;
const assistantItem = (id: string, text: string, streaming: boolean) =>
({
type: "turn_item.updated",
driver,
turnItem: {
id: TurnItemId.make(id),
threadId,
runId: first.runId,
nodeId: first.rootNodeId,
providerThreadId: turn.providerThreadId,
providerTurnId: turn.id,
nativeItemRef: null,
parentItemId: null,
ordinal: 0,
status: streaming ? "running" : "completed",
title: null,
startedAt: now,
completedAt: streaming ? null : now,
updatedAt: now,
type: "assistant_message",
messageId: MessageId.make(`message:${id}`),
text,
streaming,
},
}) satisfies ProviderAdapter.ProviderAdapterV2Event;
const watch = (predicate: (event: OrchestrationV2DomainEvent) => boolean) =>
orchestrator.streamDomainEvents.pipe(
Stream.filter(predicate),
Stream.take(1),
Stream.runDrain,
Effect.forkScoped,
);

// The cut-off draft starts first; the commentary after it is written,
// so the draft has been seen by the time the steer arrives.
yield* Queue.offer(events, assistantItem("draft", "Let me look at the", true));
const commentaryWritten = yield* watch(
(event) => event.type === "turn-item.updated" && event.payload.id === "commentary",
);
yield* Queue.offer(events, assistantItem("commentary", "Checking the popover.", false));
yield* Fiber.join(commentaryWritten);

yield* orchestrator.dispatch({
type: "message.dispatch",
commandId: CommandId.make("steer"),
threadId,
messageId: MessageId.make("message:steer"),
text: "fix the popover",
attachments: [],
dispatchMode: { type: "steer_active", targetRunId: first.runId },
createdBy: "user",
creationSource: "web",
});
yield* worker.drain();

const settled = yield* watch(
(event) =>
event.type === "run.updated" &&
event.payload.id === first.runId &&
event.payload.status === "waiting",
);
yield* Queue.offer(events, assistantItem("answer", "All fixed.", false));
yield* Queue.offer(events, assistantItem("draft", "Let me look at the", false));
yield* Queue.offer(events, {
type: "provider_turn.updated",
driver,
providerTurn: { ...turn, status: "completed", completedAt: yield* DateTime.now },
});
yield* Queue.offer(events, {
type: "turn.terminal",
driver,
providerThreadId: turn.providerThreadId,
providerTurnId: turn.id,
runOrdinal: first.runOrdinal,
status: "completed",
failure: null,
threadDisposition: "reusable",
});
yield* Fiber.join(settled);
yield* worker.drain();

const items = (yield* orchestrator.getThreadProjection(threadId)).turnItems
.toSorted((left, right) => left.ordinal - right.ordinal)
.flatMap((item) =>
item.type === "user_message"
? [String(item.messageId)]
: item.type === "assistant_message"
? [String(item.id)]
: [],
);
assert.deepEqual(items, [
"message:first",
"draft",
"commentary",
"message:steer",
"answer",
]);
}).pipe(Effect.provide(layer));
}),
),
);
1 change: 1 addition & 0 deletions apps/server/src/orchestration-v2/runtimeLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const layerRunExecutionServiceProvided = RunExecutionService.layer.pipe(
layerEventSinkProvided,
IdAllocator.layer,
layerProviderEventIngestorProvided,
TurnItemPositionStore.layer,
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export function layerWithRegistry<Error>(
IdAllocator.layer,
layerProviderEventIngestorProvided,
layerServerSettings,
layerStores,
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ export function assertOpenCode2ToolCallOutput(
assertSemanticProjectionIntegrity(projection);
assertVisibleTurnItemsMirrorLocalTurnItems(projection);
// Buffered delivery keeps streaming text out of the read model until
// `text.ended`, which OpenCode sends after both tool calls started.
// `text.ended`, which OpenCode sends after both tool calls started. The text
// keeps the position it took when it began, ahead of the tool calls.
assertTurnItemTypeSequence(projection, [
"user_message",
"assistant_message",
"dynamic_tool",
"command_execution",
"assistant_message",
"assistant_message",
"checkpoint",
]);
assertUserMessagesInclude(projection, [OPENCODE2_TOOL_CALL_PROMPT]);
Expand Down
Loading