authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-28 10:51:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-28 10:51:01-07:00
log3e2df5a7823d180f634370998d58497993b94ee7
tree4b49a45007e9438bb6600d37a97d958de39d5cdd
parentf0cdaf94c2b69a0f6a55f7ca2e3d13782b4272a7
signature Signed by PGP key B5690EEEBB952194

Normalize typed provider runtime ingestion (#1475)


2 files changed, 73 insertions(+), 41 deletions(-)

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+65-3
......@@ -4,7 +4,6 @@ import path from "node:path";
44
55import type {
66 OrchestrationReadModel,
7 ProviderKind,
87 ProviderRuntimeEvent,
98 ProviderSession,
109} from "@t3tools/contracts";
......@@ -56,7 +55,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
5655type LegacyProviderRuntimeEvent = {
5756 readonly type: string;
5857 readonly eventId: EventId;
59 readonly provider: ProviderKind;
58 readonly provider: ProviderRuntimeEvent["provider"];
6059 readonly createdAt: string;
6160 readonly threadId: ThreadId;
6261 readonly turnId?: string | undefined;
......@@ -66,6 +65,23 @@ type LegacyProviderRuntimeEvent = {
6665 readonly [key: string]: unknown;
6766};
6867
68type LegacyTurnCompletedEvent = LegacyProviderRuntimeEvent & {
69 readonly type: "turn.completed";
70 readonly payload?: undefined;
71 readonly status: "completed" | "failed" | "interrupted" | "cancelled";
72 readonly errorMessage?: string | undefined;
73};
74
75function isLegacyTurnCompletedEvent(
76 event: LegacyProviderRuntimeEvent,
77): event is LegacyTurnCompletedEvent {
78 return (
79 event.type === "turn.completed" &&
80 event.payload === undefined &&
81 typeof event.status === "string"
82 );
83}
84
6985function createProviderServiceHarness() {
7086 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
7187 const runtimeSessions: ProviderSession[] = [];
......@@ -93,8 +109,23 @@ function createProviderServiceHarness() {
93109 runtimeSessions.push(session);
94110 };
95111
112 const normalizeLegacyEvent = (event: LegacyProviderRuntimeEvent): ProviderRuntimeEvent => {
113 if (isLegacyTurnCompletedEvent(event)) {
114 const normalized: Extract<ProviderRuntimeEvent, { type: "turn.completed" }> = {
115 ...(event as Omit<Extract<ProviderRuntimeEvent, { type: "turn.completed" }>, "payload">),
116 payload: {
117 state: event.status,
118 ...(typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}),
119 },
120 };
121 return normalized;
122 }
123
124 return event as ProviderRuntimeEvent;
125 };
126
96127 const emit = (event: LegacyProviderRuntimeEvent): void => {
97 Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent));
128 Effect.runSync(PubSub.publish(runtimeEventPubSub, normalizeLegacyEvent(event)));
98129 };
99130
100131 return {
......@@ -1695,6 +1726,37 @@ describe("ProviderRuntimeIngestion", () => {
16951726 expect(thread.session?.lastError).toBe("runtime exploded");
16961727 });
16971728
1729 it("records runtime.error activities from the typed payload message", async () => {
1730 const harness = await createHarness();
1731 const now = new Date().toISOString();
1732
1733 harness.emit({
1734 type: "runtime.error",
1735 eventId: asEventId("evt-runtime-error-activity"),
1736 provider: "codex",
1737 createdAt: now,
1738 threadId: asThreadId("thread-1"),
1739 turnId: asTurnId("turn-runtime-error-activity"),
1740 payload: {
1741 message: "runtime activity exploded",
1742 },
1743 });
1744
1745 const thread = await waitForThread(harness.engine, (entry) =>
1746 entry.activities.some((activity) => activity.id === "evt-runtime-error-activity"),
1747 );
1748 const activity = thread.activities.find(
1749 (entry: ProviderRuntimeTestActivity) => entry.id === "evt-runtime-error-activity",
1750 );
1751 const activityPayload =
1752 activity?.payload && typeof activity.payload === "object"
1753 ? (activity.payload as Record<string, unknown>)
1754 : undefined;
1755
1756 expect(activity?.kind).toBe("runtime.error");
1757 expect(activityPayload?.message).toBe("runtime activity exploded");
1758 });
1759
16981760 it("keeps the session running when a runtime.warning arrives during an active turn", async () => {
16991761 const harness = await createHarness();
17001762 const now = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+8-38
......@@ -98,10 +98,6 @@ function proposedPlanIdFromEvent(event: ProviderRuntimeEvent, threadId: ThreadId
9898 return `plan:${threadId}:event:${event.eventId}`;
9999}
100100
101function asString(value: unknown): string | undefined {
102 return typeof value === "string" ? value : undefined;
103}
104
105101function buildContextWindowActivityPayload(
106102 event: ProviderRuntimeEvent,
107103): ThreadTokenUsageSnapshot | undefined {
......@@ -111,14 +107,6 @@ function buildContextWindowActivityPayload(
111107 return event.payload.usage;
112108}
113109
114function runtimePayloadRecord(event: ProviderRuntimeEvent): Record<string, unknown> | undefined {
115 const payload = (event as { payload?: unknown }).payload;
116 if (!payload || typeof payload !== "object") {
117 return undefined;
118 }
119 return payload as Record<string, unknown>;
120}
121
122110function normalizeRuntimeTurnState(
123111 value: string | undefined,
124112): "completed" | "failed" | "interrupted" | "cancelled" {
......@@ -133,23 +121,6 @@ function normalizeRuntimeTurnState(
133121 }
134122}
135123
136function runtimeTurnState(
137 event: ProviderRuntimeEvent,
138): "completed" | "failed" | "interrupted" | "cancelled" {
139 const payloadState = asString(runtimePayloadRecord(event)?.state);
140 return normalizeRuntimeTurnState(payloadState);
141}
142
143function runtimeTurnErrorMessage(event: ProviderRuntimeEvent): string | undefined {
144 const payloadErrorMessage = asString(runtimePayloadRecord(event)?.errorMessage);
145 return payloadErrorMessage;
146}
147
148function runtimeErrorMessageFromEvent(event: ProviderRuntimeEvent): string | undefined {
149 const payloadMessage = asString(runtimePayloadRecord(event)?.message);
150 return payloadMessage;
151}
152
153124function orchestrationSessionStatusFromRuntimeState(
154125 state: "starting" | "running" | "waiting" | "ready" | "interrupted" | "stopped" | "error",
155126): "starting" | "running" | "ready" | "interrupted" | "stopped" | "error" {
......@@ -253,10 +224,6 @@ function runtimeEventToActivities(
253224 }
254225
255226 case "runtime.error": {
256 const message = runtimeErrorMessageFromEvent(event);
257 if (!message) {
258 return [];
259 }
260227 return [
261228 {
262229 id: event.eventId,
......@@ -265,7 +232,7 @@ function runtimeEventToActivities(
265232 kind: "runtime.error",
266233 summary: "Runtime error",
267234 payload: {
268 message: truncateDetail(message),
235 message: truncateDetail(event.payload.message),
269236 },
270237 turnId: toTurnId(event.turnId) ?? null,
271238 ...maybeSequence,
......@@ -973,7 +940,9 @@ const make = Effect.gen(function* () {
973940 case "session.exited":
974941 return "stopped";
975942 case "turn.completed":
976 return runtimeTurnState(event) === "failed" ? "error" : "ready";
943 return normalizeRuntimeTurnState(event.payload.state) === "failed"
944 ? "error"
945 : "ready";
977946 case "session.started":
978947 case "thread.started":
979948 // Provider thread/session start notifications can arrive during an
......@@ -984,8 +953,9 @@ const make = Effect.gen(function* () {
984953 const lastError =
985954 event.type === "session.state.changed" && event.payload.state === "error"
986955 ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error")
987 : event.type === "turn.completed" && runtimeTurnState(event) === "failed"
988 ? (runtimeTurnErrorMessage(event) ?? thread.session?.lastError ?? "Turn failed")
956 : event.type === "turn.completed" &&
957 normalizeRuntimeTurnState(event.payload.state) === "failed"
958 ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed")
989959 : status === "ready"
990960 ? null
991961 : (thread.session?.lastError ?? null);
......@@ -1176,7 +1146,7 @@ const make = Effect.gen(function* () {
11761146 }
11771147
11781148 if (event.type === "runtime.error") {
1179 const runtimeErrorMessage = runtimeErrorMessageFromEvent(event) ?? "Provider runtime error";
1149 const runtimeErrorMessage = event.payload.message;
11801150
11811151 const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD
11821152 ? true