authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 17:36:41-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 17:36:41-07:00
logd76b24dd15a219666941ab1b4967d8f738adcda0
tree9eddf7c38b4a6c31740a11ec210224df3d427acc
parente3723e06b5b154a3e0ede55c02e2cabc5301d3bf
signature Signed by PGP key B5690EEEBB952194

feat(codex): support async questions (#9512)


26 files changed, 962 insertions(+), 24 deletions(-)

apps/mobile/src/lib/threadActivity.test.ts+23
...@@ -88,6 +88,29 @@ const nativeQuestion = {...@@ -88,6 +88,29 @@ const nativeQuestion = {
88} as const;88} as const;
8989
90describe("pending user input answers", () => {90describe("pending user input answers", () => {
91 it("accepts free-text answers to async questions without options", () => {
92 const question = {
93 id: "0",
94 header: "Question",
95 question: "What should it be named?",
96 options: [],
97 allowCustomAnswer: true,
98 multiSelect: false,
99 };
100 const requested = makeActivity({
101 id: EventId.make("async-question"),
102 kind: "user-input.requested",
103 summary: "User input requested",
104 createdAt: "2026-09-03T00:00:00.000Z",
105 payload: { requestId: "async-1", responseMode: "message", questions: [question] },
106 });
107 const questions = derivePendingUserInputs([requested])[0]?.questions;
108 expect(questions).toEqual([question]);
109 expect(buildPendingUserInputAnswers(questions!, { "0": { customAnswer: "Example" } })).toEqual({
110 "0": "Example",
111 });
112 });
113
91 it("preserves native choice values and custom-answer rules from activities", () => {114 it("preserves native choice values and custom-answer rules from activities", () => {
92 const requested = makeActivity({115 const requested = makeActivity({
93 id: EventId.make("native-question"),116 id: EventId.make("native-question"),
apps/mobile/src/lib/threadActivity.ts+1-1
...@@ -245,7 +245,7 @@ function parseUserInputQuestions(...@@ -245,7 +245,7 @@ function parseUserInputQuestions(
245 };245 };
246 })246 })
247 .filter((option): option is UserInputQuestion["options"][number] => option !== null);247 .filter((option): option is UserInputQuestion["options"][number] => option !== null);
248 if (options.length === 0) {248 if (options.length === 0 && question.allowCustomAnswer === false) {
249 return null;249 return null;
250 }250 }
251 return {251 return {
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+5
...@@ -75,6 +75,7 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -75,6 +75,7 @@ describe("CheckpointDiffQuery.layer", () => {
75 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),75 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
76 Layer.provideMerge(76 Layer.provideMerge(
77 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {77 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
78 getUserInputActivity: () => Effect.die("unused"),
78 getCommandReadModel: () =>79 getCommandReadModel: () =>
79 Effect.die("CheckpointDiffQuery should not request the command read model"),80 Effect.die("CheckpointDiffQuery should not request the command read model"),
80 getSnapshot: () =>81 getSnapshot: () =>
...@@ -185,6 +186,7 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -185,6 +186,7 @@ describe("CheckpointDiffQuery.layer", () => {
185 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),186 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
186 Layer.provideMerge(187 Layer.provideMerge(
187 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {188 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
189 getUserInputActivity: () => Effect.die("unused"),
188 getCommandReadModel: () =>190 getCommandReadModel: () =>
189 Effect.die("CheckpointDiffQuery should not request the command read model"),191 Effect.die("CheckpointDiffQuery should not request the command read model"),
190 getSnapshot: () =>192 getSnapshot: () =>
...@@ -270,6 +272,7 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -270,6 +272,7 @@ describe("CheckpointDiffQuery.layer", () => {
270 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),272 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
271 Layer.provideMerge(273 Layer.provideMerge(
272 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {274 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
275 getUserInputActivity: () => Effect.die("unused"),
273 getCommandReadModel: () =>276 getCommandReadModel: () =>
274 Effect.die("CheckpointDiffQuery should not request the command read model"),277 Effect.die("CheckpointDiffQuery should not request the command read model"),
275 getSnapshot: () =>278 getSnapshot: () =>
...@@ -340,6 +343,7 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -340,6 +343,7 @@ describe("CheckpointDiffQuery.layer", () => {
340 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),343 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
341 Layer.provideMerge(344 Layer.provideMerge(
342 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {345 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
346 getUserInputActivity: () => Effect.die("unused"),
343 getCommandReadModel: () =>347 getCommandReadModel: () =>
344 Effect.die("CheckpointDiffQuery should not request the command read model"),348 Effect.die("CheckpointDiffQuery should not request the command read model"),
345 getSnapshot: () =>349 getSnapshot: () =>
...@@ -395,6 +399,7 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -395,6 +399,7 @@ describe("CheckpointDiffQuery.layer", () => {
395 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),399 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
396 Layer.provideMerge(400 Layer.provideMerge(
397 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {401 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
402 getUserInputActivity: () => Effect.die("unused"),
398 getCommandReadModel: () =>403 getCommandReadModel: () =>
399 Effect.die("CheckpointDiffQuery should not request the command read model"),404 Effect.die("CheckpointDiffQuery should not request the command read model"),
400 getSnapshot: () =>405 getSnapshot: () =>
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+211-5
...@@ -1,4 +1,11 @@...@@ -1,4 +1,11 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeFSP from "node:fs/promises";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";
5
1import {6import {
7 ApprovalRequestId,
8 EventId,
2 CheckpointRef,9 CheckpointRef,
3 CommandId,10 CommandId,
4 DEFAULT_PROVIDER_INTERACTION_MODE,11 DEFAULT_PROVIDER_INTERACTION_MODE,
...@@ -25,7 +32,10 @@ import { PersistenceSqlError } from "../../persistence/Errors.ts";...@@ -25,7 +32,10 @@ import { PersistenceSqlError } from "../../persistence/Errors.ts";
25import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";32import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
26import * as OrchestrationCommandReceipts from "../../persistence/Services/OrchestrationCommandReceipts.ts";33import * as OrchestrationCommandReceipts from "../../persistence/Services/OrchestrationCommandReceipts.ts";
27import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";34import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
28import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";35import {
36 makeSqlitePersistenceLive,
37 SqlitePersistenceMemory,
38} from "../../persistence/Layers/Sqlite.ts";
29import {39import {
30 OrchestrationEventStore,40 OrchestrationEventStore,
31 type OrchestrationEventStoreShape,41 type OrchestrationEventStoreShape,
...@@ -49,7 +59,10 @@ const asMessageId = (value: string): MessageId => MessageId.make(value);...@@ -49,7 +59,10 @@ const asMessageId = (value: string): MessageId => MessageId.make(value);
49const asTurnId = (value: string): TurnId => TurnId.make(value);59const asTurnId = (value: string): TurnId => TurnId.make(value);
50const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(value);60const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(value);
5161
52function makeOrchestrationLayer() {62function makeOrchestrationLayer(databasePath?: string) {
63 const persistence = databasePath
64 ? makeSqlitePersistenceLive(databasePath)
65 : SqlitePersistenceMemory;
53 const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {66 const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
54 prefix: "t3-orchestration-engine-test-",67 prefix: "t3-orchestration-engine-test-",
55 });68 });
...@@ -65,19 +78,21 @@ function makeOrchestrationLayer() {...@@ -65,19 +78,21 @@ function makeOrchestrationLayer() {
65 Layer.provide(OrchestrationEventStoreLive),78 Layer.provide(OrchestrationEventStoreLive),
66 Layer.provideMerge(OrchestrationCommandReceiptRepositoryLive),79 Layer.provideMerge(OrchestrationCommandReceiptRepositoryLive),
67 Layer.provide(RepositoryIdentityResolver.layer),80 Layer.provide(RepositoryIdentityResolver.layer),
68 Layer.provide(SqlitePersistenceMemory),81 Layer.provide(persistence),
69 Layer.provideMerge(ServerConfigLayer),82 Layer.provideMerge(ServerConfigLayer),
70 Layer.provideMerge(NodeServices.layer),83 Layer.provideMerge(NodeServices.layer),
71 );84 );
72}85}
7386
74async function createOrchestrationSystem() {87async function createOrchestrationSystem(databasePath?: string) {
75 const runtime = ManagedRuntime.make(makeOrchestrationLayer());88 const runtime = ManagedRuntime.make(makeOrchestrationLayer(databasePath));
76 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));89 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
77 const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery));90 const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery));
78 return {91 return {
79 engine,92 engine,
80 readModel: () => runtime.runPromise(snapshotQuery.getSnapshot()),93 readModel: () => runtime.runPromise(snapshotQuery.getSnapshot()),
94 readThread: (threadId: ThreadId) =>
95 runtime.runPromise(snapshotQuery.getThreadDetailById(threadId)),
81 run: <A, E>(effect: Effect.Effect<A, E>) => runtime.runPromise(effect),96 run: <A, E>(effect: Effect.Effect<A, E>) => runtime.runPromise(effect),
82 dispose: () => runtime.dispose(),97 dispose: () => runtime.dispose(),
83 };98 };
...@@ -99,6 +114,196 @@ const hasMetricSnapshot = (...@@ -99,6 +114,196 @@ const hasMetricSnapshot = (
99 );114 );
100115
101describe("OrchestrationEngine", () => {116describe("OrchestrationEngine", () => {
117 it.each(["running", "stopped"] as const)(
118 "sends async answers with a %s session and rejects old duplicate replies",
119 async (status) => {
120 const directory = await NodeFSP.mkdtemp(
121 NodePath.join(NodeOS.tmpdir(), "t3-async-questions-"),
122 );
123 const databasePath = NodePath.join(directory, "state.sqlite");
124 let system = await createOrchestrationSystem(databasePath);
125 const threadId = ThreadId.make("async-thread");
126 const projectId = ProjectId.make("async-project");
127 const requestId = ApprovalRequestId.make("codex-async:question-1");
128 try {
129 await system.run(
130 system.engine.dispatch({
131 type: "project.create",
132 commandId: CommandId.make("async-project"),
133 projectId,
134 title: "Async questions",
135 workspaceRoot: "/tmp/async-questions",
136 createdAt: now(),
137 }),
138 );
139 await system.run(
140 system.engine.dispatch({
141 type: "thread.create",
142 commandId: CommandId.make("async-thread"),
143 threadId,
144 projectId,
145 title: "Async questions",
146 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
147 runtimeMode: "full-access",
148 interactionMode: "default",
149 branch: null,
150 worktreePath: null,
151 createdAt: now(),
152 }),
153 );
154 await system.run(
155 system.engine.dispatch({
156 type: "thread.session.set",
157 commandId: CommandId.make("async-session"),
158 threadId,
159 createdAt: now(),
160 session: {
161 threadId,
162 status,
163 providerName: "codex",
164 runtimeMode: "full-access",
165 activeTurnId: status === "running" ? TurnId.make("turn-1") : null,
166 lastError: null,
167 updatedAt: now(),
168 },
169 }),
170 );
171 await system.run(
172 system.engine.dispatch({
173 type: "thread.activity.append",
174 commandId: CommandId.make("async-question"),
175 threadId,
176 createdAt: now(),
177 activity: {
178 id: EventId.make("async-question"),
179 kind: "user-input.requested",
180 summary: "User input requested",
181 tone: "info",
182 turnId: TurnId.make("turn-1"),
183 createdAt: now(),
184 payload: {
185 requestId,
186 responseMode: "message",
187 questions: [
188 {
189 id: "0",
190 header: "Question",
191 question: "Which package manager?",
192 options: [{ label: "pnpm", description: "" }],
193 },
194 {
195 id: "1",
196 header: "Question",
197 question: "What should it be named?",
198 options: [],
199 },
200 ],
201 },
202 },
203 }),
204 );
205 const appendWork = async (prefix: string, createdAt: string) => {
206 for (let index = 0; index < 501; index += 1) {
207 await system.run(
208 system.engine.dispatch({
209 type: "thread.activity.append",
210 commandId: CommandId.make(`${prefix}-${index}`),
211 threadId,
212 createdAt,
213 activity: {
214 id: EventId.make(`${prefix}-${index}`),
215 kind: "tool.completed",
216 summary: "Work continued",
217 payload: {},
218 tone: "info",
219 turnId: TurnId.make("turn-1"),
220 createdAt,
221 },
222 }),
223 );
224 }
225 };
226 await appendWork("work", "2026-01-01T00:00:01.000Z");
227 const before = await system.readModel();
228 expect(
229 before.threads[0]?.activities.some((activity) => activity.id === "async-question"),
230 ).toBe(true);
231 if (status === "stopped") {
232 await system.dispose();
233 system = await createOrchestrationSystem(databasePath);
234 }
235 const response = {
236 type: "thread.user-input.respond" as const,
237 commandId: CommandId.make("async-response"),
238 threadId,
239 requestId,
240 answers: { "0": "pnpm", "1": "Example" },
241 createdAt: "2026-01-01T00:00:02.000Z",
242 };
243 await expect(
244 system.run(
245 system.engine.dispatch({
246 ...response,
247 commandId: CommandId.make("incomplete-answer"),
248 answers: { "0": "pnpm" },
249 }),
250 ),
251 ).rejects.toThrow("Answer each question before sending.");
252 await system.run(system.engine.dispatch(response));
253 const after = await system.readModel();
254 const userMessages = after.threads[0]?.messages.filter(
255 (message) => message.role === "user",
256 );
257 expect(userMessages).toHaveLength(1);
258 expect(userMessages?.[0]?.text).toBe(
259 "Which package manager?\npnpm\n\nWhat should it be named?\nExample",
260 );
261 expect(
262 after.threads[0]?.activities.find((activity) => activity.kind === "user-input.resolved")
263 ?.payload,
264 ).toMatchObject({ requestId, responseMode: "message", answers: response.answers });
265 const events = await system.run(Stream.runCollect(system.engine.readEvents(0)));
266 expect(
267 Array.from(events)
268 .filter((event) => event.commandId === response.commandId)
269 .map((event) => event.type),
270 ).toEqual([
271 "thread.activity-appended",
272 "thread.message-sent",
273 "thread.turn-start-requested",
274 ]);
275 await expect(
276 system.run(
277 system.engine.dispatch({
278 ...response,
279 commandId: CommandId.make("second-client-reply"),
280 }),
281 ),
282 ).rejects.toThrow("This question has already been answered.");
283 await appendWork("later-work", "2026-01-01T00:00:03.000Z");
284 const afterEviction = Option.getOrThrow(await system.readThread(threadId));
285 expect(
286 afterEviction.activities.some((activity) => activity.kind === "user-input.resolved"),
287 ).toBe(false);
288 if (status === "stopped") {
289 await system.dispose();
290 system = await createOrchestrationSystem(databasePath);
291 }
292 await expect(
293 system.run(
294 system.engine.dispatch({
295 ...response,
296 commandId: CommandId.make("reply-after-eviction"),
297 }),
298 ),
299 ).rejects.toThrow("This question has already been answered.");
300 } finally {
301 await system.dispose();
302 await NodeFSP.rm(directory, { recursive: true, force: true });
303 }
304 },
305 );
306
102 it("bootstraps command handling from persisted projections without reading the full snapshot", async () => {307 it("bootstraps command handling from persisted projections without reading the full snapshot", async () => {
103 let nextSequence = 8;308 let nextSequence = 8;
104 const eventStore: OrchestrationEventStoreShape = {309 const eventStore: OrchestrationEventStoreShape = {
...@@ -183,6 +388,7 @@ describe("OrchestrationEngine", () => {...@@ -183,6 +388,7 @@ describe("OrchestrationEngine", () => {
183 const layer = OrchestrationEngineLive.pipe(388 const layer = OrchestrationEngineLive.pipe(
184 Layer.provide(389 Layer.provide(
185 Layer.succeed(ProjectionSnapshotQuery, {390 Layer.succeed(ProjectionSnapshotQuery, {
391 getUserInputActivity: () => Effect.die("unused"),
186 getCommandReadModel: () => Effect.succeed(commandReadModel),392 getCommandReadModel: () => Effect.succeed(commandReadModel),
187 getSnapshot: () =>393 getSnapshot: () =>
188 Effect.sync(() => {394 Effect.sync(() => {
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+9
...@@ -195,9 +195,18 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -195,9 +195,18 @@ const makeOrchestrationEngine = Effect.gen(function* () {
195 });195 });
196 }196 }
197197
198 // Command snapshots omit activities at startup and cap them while running.
199 // Read this request's durable state before deciding how to send the answer.
200 const userInputActivity =
201 envelope.command.type === "thread.user-input.respond"
202 ? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command)
203 : Option.none();
198 const eventBase = yield* decideOrchestrationCommand({204 const eventBase = yield* decideOrchestrationCommand({
199 command: envelope.command,205 command: envelope.command,
200 readModel: commandReadModel,206 readModel: commandReadModel,
207 ...(Option.isSome(userInputActivity)
208 ? { userInputActivity: userInputActivity.value }
209 : {}),
201 }).pipe(210 }).pipe(
202 Effect.provideService(Crypto.Crypto, crypto),211 Effect.provideService(Crypto.Crypto, crypto),
203 Effect.mapError((cause) =>212 Effect.mapError((cause) =>
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+36
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import {1import {
2 ApprovalRequestId,
2 ChatAttachment,3 ChatAttachment,
3 CheckpointRef,4 CheckpointRef,
4 IsoDateTime,5 IsoDateTime,
...@@ -1119,6 +1120,40 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -1119,6 +1120,40 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
1119 `,1120 `,
1120 });1121 });
11211122
1123 const getUserInputActivityRow = SqlSchema.findOneOption({
1124 Request: Schema.Struct({ threadId: ThreadId, requestId: ApprovalRequestId }),
1125 Result: ProjectionThreadActivityDbRowSchema,
1126 execute: ({ threadId, requestId }) => sql`
1127 SELECT
1128 activity_id AS "activityId",
1129 thread_id AS "threadId",
1130 turn_id AS "turnId",
1131 tone,
1132 kind,
1133 summary,
1134 payload_json AS "payload",
1135 sequence,
1136 created_at AS "createdAt"
1137 FROM projection_thread_activities
1138 WHERE thread_id = ${threadId}
1139 AND kind IN ('user-input.requested', 'user-input.resolved')
1140 AND json_extract(payload_json, '$.requestId') = ${requestId}
1141 ORDER BY sequence DESC, created_at DESC, activity_id DESC
1142 LIMIT 1
1143 `,
1144 });
1145
1146 const getUserInputActivity: ProjectionSnapshotQueryShape["getUserInputActivity"] = (input) =>
1147 getUserInputActivityRow(input).pipe(
1148 Effect.map(Option.map(mapThreadActivityRow)),
1149 Effect.mapError(
1150 toPersistenceSqlOrDecodeError(
1151 "ProjectionSnapshotQuery.getUserInputActivity:query",
1152 "ProjectionSnapshotQuery.getUserInputActivity:decodeRow",
1153 ),
1154 ),
1155 );
1156
1122 const listThreadActivityIdsByThread = SqlSchema.findAll({1157 const listThreadActivityIdsByThread = SqlSchema.findAll({
1123 Request: ThreadIdLookupInput,1158 Request: ThreadIdLookupInput,
1124 Result: ProjectionThreadActivityIdRowSchema,1159 Result: ProjectionThreadActivityIdRowSchema,
...@@ -3151,6 +3186,7 @@ pending_approval_requests AS (...@@ -3151,6 +3186,7 @@ pending_approval_requests AS (
31513186
3152 return {3187 return {
3153 getCommandReadModel,3188 getCommandReadModel,
3189 getUserInputActivity,
3154 getSnapshot,3190 getSnapshot,
3155 getShellSnapshot,3191 getShellSnapshot,
3156 getArchivedShellSnapshot,3192 getArchivedShellSnapshot,
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+50
...@@ -2108,6 +2108,56 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2108,6 +2108,56 @@ describe("ProviderRuntimeIngestion", () => {
2108 expect(message?.streaming).toBe(false);2108 expect(message?.streaming).toBe(false);
2109 });2109 });
21102110
2111 it("keeps streaming while an async question is pending", async () => {
2112 const harness = await createHarness({ serverSettings: { enableLegacyTokenStreaming: true } });
2113 const base = {
2114 provider: ProviderDriverKind.make("codex"),
2115 createdAt: "2026-01-01T00:00:00.000Z",
2116 threadId: asThreadId("thread-1"),
2117 turnId: asTurnId("turn-async"),
2118 };
2119 harness.emit({ ...base, type: "turn.started", eventId: asEventId("async-start") });
2120 harness.emit({
2121 ...base,
2122 type: "content.delta",
2123 eventId: asEventId("async-before"),
2124 itemId: asItemId("message-1"),
2125 payload: { streamKind: "assistant_text", delta: "Before. " },
2126 });
2127 harness.emit({
2128 ...base,
2129 type: "user-input.requested",
2130 eventId: asEventId("async-request"),
2131 requestId: ApprovalRequestId.make("codex-async:question-1"),
2132 payload: {
2133 responseMode: "message",
2134 questions: [
2135 {
2136 id: "0",
2137 header: "Question",
2138 question: "Which name?",
2139 options: [],
2140 allowCustomAnswer: true,
2141 },
2142 ],
2143 },
2144 });
2145 harness.emit({
2146 ...base,
2147 type: "content.delta",
2148 eventId: asEventId("async-after"),
2149 itemId: asItemId("message-1"),
2150 payload: { streamKind: "assistant_text", delta: "After." },
2151 });
2152 await harness.drain();
2153 const thread = (await harness.readModel()).threads[0];
2154 expect(thread?.session?.status).toBe("running");
2155 expect(thread?.messages).toMatchObject([{ text: "Before. After.", streaming: true }]);
2156 expect(
2157 thread?.activities.find((activity) => activity.kind === "user-input.requested")?.payload,
2158 ).toMatchObject({ responseMode: "message", requestId: "codex-async:question-1" });
2159 });
2160
2111 it("does not create assistant segments for whitespace-only buffered text at approval boundaries", async () => {2161 it("does not create assistant segments for whitespace-only buffered text at approval boundaries", async () => {
2112 const harness = await createHarness();2162 const harness = await createHarness();
2113 const startedAt = "2026-03-28T06:28:00.000Z";2163 const startedAt = "2026-03-28T06:28:00.000Z";
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+3-1
...@@ -520,6 +520,7 @@ export function runtimeEventToActivities(...@@ -520,6 +520,7 @@ export function runtimeEventToActivities(
520 payload: {520 payload: {
521 ...(event.requestId ? { requestId: event.requestId } : {}),521 ...(event.requestId ? { requestId: event.requestId } : {}),
522 questions: event.payload.questions,522 questions: event.payload.questions,
523 ...(event.payload.responseMode ? { responseMode: event.payload.responseMode } : {}),
523 },524 },
524 turnId: toTurnId(event.turnId) ?? null,525 turnId: toTurnId(event.turnId) ?? null,
525 ...maybeSequence,526 ...maybeSequence,
...@@ -1739,7 +1740,8 @@ const make = Effect.gen(function* () {...@@ -1739,7 +1740,8 @@ const make = Effect.gen(function* () {
1739 }1740 }
17401741
1741 const pauseForUserTurnId =1742 const pauseForUserTurnId =
1742 event.type === "request.opened" || event.type === "user-input.requested"1743 event.type === "request.opened" ||
1744 (event.type === "user-input.requested" && event.payload.responseMode !== "message")
1743 ? toTurnId(event.turnId)1745 ? toTurnId(event.turnId)
1744 : undefined;1746 : undefined;
1745 if (pauseForUserTurnId) {1747 if (pauseForUserTurnId) {
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+8
...@@ -7,6 +7,7 @@...@@ -7,6 +7,7 @@
7 * @module ProjectionSnapshotQuery7 * @module ProjectionSnapshotQuery
8 */8 */
9import type {9import type {
10 ApprovalRequestId,
10 CheckpointRef,11 CheckpointRef,
11 OrchestrationCheckpointSummary,12 OrchestrationCheckpointSummary,
12 OrchestrationProject,13 OrchestrationProject,
...@@ -16,6 +17,7 @@ import type {...@@ -16,6 +17,7 @@ import type {
16 OrchestrationSearchThreadsResult,17 OrchestrationSearchThreadsResult,
17 OrchestrationShellSnapshot,18 OrchestrationShellSnapshot,
18 OrchestrationThread,19 OrchestrationThread,
20 OrchestrationThreadActivity,
19 OrchestrationThreadDetailSnapshot,21 OrchestrationThreadDetailSnapshot,
20 OrchestrationThreadDetailWindow,22 OrchestrationThreadDetailWindow,
21 OrchestrationThreadShell,23 OrchestrationThreadShell,
...@@ -72,6 +74,12 @@ export interface ProjectionThreadDetailQuery {...@@ -72,6 +74,12 @@ export interface ProjectionThreadDetailQuery {
72 * ProjectionSnapshotQueryShape - Service API for read-model snapshots.74 * ProjectionSnapshotQueryShape - Service API for read-model snapshots.
73 */75 */
74export interface ProjectionSnapshotQueryShape {76export interface ProjectionSnapshotQueryShape {
77 /** Read the latest request or resolution without loading the thread history. */
78 readonly getUserInputActivity: (input: {
79 readonly threadId: ThreadId;
80 readonly requestId: ApprovalRequestId;
81 }) => Effect.Effect<Option.Option<OrchestrationThreadActivity>, ProjectionRepositoryError>;
82
75 /**83 /**
76 * Read the lightweight command snapshot used to bootstrap the in-memory84 * Read the lightweight command snapshot used to bootstrap the in-memory
77 * orchestration engine without hydrating message/activity/checkpoint bodies.85 * orchestration engine without hydrating message/activity/checkpoint bodies.
apps/server/src/orchestration/decider.ts+77-7
...@@ -1,13 +1,19 @@...@@ -1,13 +1,19 @@
1import {1import {
2 EventId,2 EventId,
3 MessageId,
4 UserInputRequestedPayload,
3 type OrchestrationCommand,5 type OrchestrationCommand,
4 type OrchestrationEvent,6 type OrchestrationEvent,
5 type OrchestrationReadModel,7 type OrchestrationReadModel,
6 type OrchestrationThread,8 type OrchestrationThread,
9 type OrchestrationThreadActivity,
7} from "@t3tools/contracts";10} from "@t3tools/contracts";
8import * as DateTime from "effect/DateTime";11import * as DateTime from "effect/DateTime";
9import * as Crypto from "effect/Crypto";12import * as Crypto from "effect/Crypto";
10import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14import * as Schema from "effect/Schema";
15import * as Option from "effect/Option";
16import * as Predicate from "effect/Predicate";
11import type * as PlatformError from "effect/PlatformError";17import type * as PlatformError from "effect/PlatformError";
1218
13import {19import {
...@@ -29,6 +35,7 @@ import { projectEvent } from "./projector.ts";...@@ -29,6 +35,7 @@ import { projectEvent } from "./projector.ts";
29import { threadHasQueuedTurnStart } from "./ThreadSettlementPolicy.ts";35import { threadHasQueuedTurnStart } from "./ThreadSettlementPolicy.ts";
3036
31const nowIso = Effect.map(DateTime.now, DateTime.formatIso);37const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
38const decodeUserInputRequestedPayload = Schema.decodeUnknownOption(UserInputRequestedPayload);
3239
33/**40/**
34 * Blocked-on-you work derived from the thread's retained activities: an41 * Blocked-on-you work derived from the thread's retained activities: an
...@@ -55,12 +62,8 @@ function isStaleRequestFailureDetail(payload: Record<string, unknown> | null): b...@@ -55,12 +62,8 @@ function isStaleRequestFailureDetail(payload: Record<string, unknown> | null): b
55}62}
5663
57// Scans the read model's activities, which the projector caps at the most64// Scans the read model's activities, which the projector caps at the most
58// recent 500. That bound is safe here: an OPEN approval/user-input request65// recent 500 plus pending async questions. Async questions remain actionable
59// blocks its turn, so the thread cannot accumulate hundreds of later66// while the agent works, so they must not expire with the activity window.
60// activities while one is outstanding — a request that has scrolled out of
61// the window is one whose turn kept running, i.e. it was resolved or went
62// stale. (The projection pipeline's pendingApprovalCount reads the same
63// capped stream and stays consistent with this view.)
64function hasOpenBlockingRequest(thread: {67function hasOpenBlockingRequest(thread: {
65 readonly activities: ReadonlyArray<{ readonly kind: string; readonly payload: unknown }>;68 readonly activities: ReadonlyArray<{ readonly kind: string; readonly payload: unknown }>;
66}): boolean {69}): boolean {
...@@ -185,9 +188,11 @@ const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({...@@ -185,9 +188,11 @@ const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({
185export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(function* ({188export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(function* ({
186 command,189 command,
187 readModel,190 readModel,
191 userInputActivity,
188}: {192}: {
189 readonly command: OrchestrationCommand;193 readonly command: OrchestrationCommand;
190 readonly readModel: OrchestrationReadModel;194 readonly readModel: OrchestrationReadModel;
195 readonly userInputActivity?: OrchestrationThreadActivity;
191}): Effect.fn.Return<196}): Effect.fn.Return<
192 DecideOrchestrationCommandResult,197 DecideOrchestrationCommandResult,
193 OrchestrationCommandRejection | PlatformError.PlatformError,198 OrchestrationCommandRejection | PlatformError.PlatformError,
...@@ -1061,11 +1066,76 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -1061,11 +1066,76 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
1061 }1066 }
10621067
1063 case "thread.user-input.respond": {1068 case "thread.user-input.respond": {
1064 yield* requireThread({1069 const thread = yield* requireThread({
1065 readModel,1070 readModel,
1066 command,1071 command,
1067 threadId: command.threadId,1072 threadId: command.threadId,
1068 });1073 });
1074 const request = userInputActivity;
1075 if (
1076 request &&
1077 Predicate.isObject(request.payload) &&
1078 request.payload.responseMode === "message"
1079 ) {
1080 const payload = decodeUserInputRequestedPayload(request.payload);
1081 if (request.kind !== "user-input.requested" || Option.isNone(payload)) {
1082 return yield* new OrchestrationCommandInvariantError({
1083 commandType: command.type,
1084 detail: "This question has already been answered.",
1085 });
1086 }
1087 const replies: string[] = [];
1088 for (const question of payload.value.questions) {
1089 const answer = command.answers[question.id];
1090 if (typeof answer !== "string" || answer.trim().length === 0) {
1091 return yield* new OrchestrationCommandInvariantError({
1092 commandType: command.type,
1093 detail: "Answer each question before sending.",
1094 });
1095 }
1096 replies.push(`${question.question}\n${answer.trim()}`);
1097 }
1098 // Commit the answer and its message together. The normal turn path
1099 // steers a running agent or resumes an idle session.
1100 return yield* decideCommandSequence({
1101 readModel,
1102 commands: [
1103 {
1104 type: "thread.activity.append",
1105 commandId: command.commandId,
1106 threadId: command.threadId,
1107 createdAt: command.createdAt,
1108 activity: {
1109 id: EventId.make(`async-answer:${command.requestId}`),
1110 kind: "user-input.resolved",
1111 summary: "User input submitted",
1112 tone: "info",
1113 turnId: request.turnId,
1114 createdAt: command.createdAt,
1115 payload: {
1116 requestId: command.requestId,
1117 responseMode: "message",
1118 answers: command.answers,
1119 },
1120 },
1121 },
1122 {
1123 type: "thread.turn.start",
1124 commandId: command.commandId,
1125 threadId: command.threadId,
1126 createdAt: command.createdAt,
1127 runtimeMode: thread.runtimeMode,
1128 interactionMode: thread.interactionMode,
1129 message: {
1130 messageId: MessageId.make(`async-answer:${command.requestId}`),
1131 role: "user",
1132 text: replies.join("\n\n"),
1133 attachments: [],
1134 },
1135 },
1136 ],
1137 });
1138 }
1069 return {1139 return {
1070 ...(yield* withEventBase({1140 ...(yield* withEventBase({
1071 aggregateKind: "thread",1141 aggregateKind: "thread",
apps/server/src/orchestration/projector.ts+29-6
...@@ -7,6 +7,7 @@ import {...@@ -7,6 +7,7 @@ import {
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
10import * as Predicate from "effect/Predicate";
1011
11import { toProjectorDecodeError, type OrchestrationProjectorDecodeError } from "./Errors.ts";12import { toProjectorDecodeError, type OrchestrationProjectorDecodeError } from "./Errors.ts";
12import {13import {
...@@ -39,6 +40,28 @@ type ThreadPatch = Partial<Omit<OrchestrationThread, "id" | "projectId">>;...@@ -39,6 +40,28 @@ type ThreadPatch = Partial<Omit<OrchestrationThread, "id" | "projectId">>;
39const MAX_THREAD_MESSAGES = 2_000;40const MAX_THREAD_MESSAGES = 2_000;
40const MAX_THREAD_CHECKPOINTS = 500;41const MAX_THREAD_CHECKPOINTS = 500;
4142
43// Async questions can stay open while the agent produces more activity.
44// Match the database snapshot's pending-question retention.
45function retainThreadActivities(activities: OrchestrationThread["activities"]) {
46 const recentStart = activities.length - 500;
47 if (recentStart <= 0) return activities;
48 const pending = new Map<string, OrchestrationThread["activities"][number]>();
49 for (const activity of activities) {
50 if (!Predicate.isObject(activity.payload)) continue;
51 const requestId = activity.payload.requestId;
52 if (typeof requestId !== "string") continue;
53 if (activity.kind === "user-input.requested" && activity.payload.responseMode === "message") {
54 pending.set(requestId, activity);
55 } else if (activity.kind === "user-input.resolved") {
56 pending.delete(requestId);
57 }
58 }
59 const pendingActivities = new Set(pending.values());
60 return activities.filter(
61 (activity, index) => index >= recentStart || pendingActivities.has(activity),
62 );
63}
64
42function checkpointStatusToLatestTurnState(status: "ready" | "missing" | "error") {65function checkpointStatusToLatestTurnState(status: "ready" | "missing" | "error") {
43 if (status === "error") return "error" as const;66 if (status === "error") return "error" as const;
44 if (status === "missing") return "interrupted" as const;67 if (status === "missing") return "interrupted" as const;
...@@ -804,12 +827,12 @@ export function projectEvent(...@@ -804,12 +827,12 @@ export function projectEvent(
804 return nextBase;827 return nextBase;
805 }828 }
806829
807 const activities = [830 const activities = retainThreadActivities(
808 ...thread.activities.filter((entry) => entry.id !== payload.activity.id),831 [
809 payload.activity,832 ...thread.activities.filter((entry) => entry.id !== payload.activity.id),
810 ]833 payload.activity,
811 .toSorted(compareThreadActivities)834 ].toSorted(compareThreadActivities),
812 .slice(-500);835 );
813836
814 return {837 return {
815 ...nextBase,838 ...nextBase,
apps/server/src/project/ProjectSetupScriptRunner.test.ts+1
...@@ -26,6 +26,7 @@ const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationPro...@@ -26,6 +26,7 @@ const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationPro
2626
27const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) =>27const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) =>
28 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {28 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
29 getUserInputActivity: () => Effect.die("unused"),
29 getCommandReadModel: () => Effect.die("unused"),30 getCommandReadModel: () => Effect.die("unused"),
30 getSnapshot: () => Effect.die("unused"),31 getSnapshot: () => Effect.die("unused"),
31 getShellSnapshot: () => Effect.die("unused"),32 getShellSnapshot: () => Effect.die("unused"),
apps/server/src/provider/Layers/CodexAdapter.test.ts+75
...@@ -1648,6 +1648,81 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -1648,6 +1648,81 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
1648 }),1648 }),
1649 );1649 );
16501650
1651 it.effect("maps async agent questions without ending the turn", () =>
1652 Effect.gen(function* () {
1653 const { adapter, runtime } = yield* startLifecycleRuntime();
1654 const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 2)).pipe(
1655 Effect.forkChild,
1656 );
1657 yield* runtime.emit({
1658 id: asEventId("evt-async-question"),
1659 kind: "notification",
1660 provider: ProviderDriverKind.make("codex"),
1661 threadId: asThreadId("thread-1"),
1662 createdAt: "2026-01-01T00:00:00.000Z",
1663 method: "item/completed",
1664 payload: {
1665 completedAtMs: 0,
1666 threadId: "thread-1",
1667 turnId: "turn-1",
1668 item: {
1669 type: "agentMessage",
1670 id: "async-question-1",
1671 text: "Which package manager?\n- pnpm\n- npm\n\nWhat should it be named?",
1672 phase: "final_answer",
1673 delivery: "async",
1674 questions: [
1675 { title: "Which package manager?", options: ["pnpm", "npm"] },
1676 { title: "What should it be named?" },
1677 ],
1678 },
1679 },
1680 });
1681 yield* runtime.emit({
1682 id: asEventId("evt-async-continued"),
1683 kind: "notification",
1684 provider: ProviderDriverKind.make("codex"),
1685 threadId: asThreadId("thread-1"),
1686 createdAt: "2026-01-01T00:00:01.000Z",
1687 method: "item/agentMessage/delta",
1688 payload: {
1689 threadId: "thread-1",
1690 turnId: "turn-1",
1691 itemId: "message-2",
1692 delta: "I will keep working.",
1693 },
1694 });
1695 const events = Array.from(yield* Fiber.join(eventsFiber));
1696 NodeAssert.equal(events[0]?.type, "user-input.requested");
1697 NodeAssert.equal(events[0]?.requestId, "codex-async:thread-1:async-question-1");
1698 NodeAssert.deepEqual(events[0]?.payload, {
1699 responseMode: "message",
1700 questions: [
1701 {
1702 id: "0",
1703 header: "Question",
1704 question: "Which package manager?",
1705 options: [
1706 { label: "pnpm", description: "" },
1707 { label: "npm", description: "" },
1708 ],
1709 allowCustomAnswer: true,
1710 multiSelect: false,
1711 },
1712 {
1713 id: "1",
1714 header: "Question",
1715 question: "What should it be named?",
1716 options: [],
1717 allowCustomAnswer: true,
1718 multiSelect: false,
1719 },
1720 ],
1721 });
1722 NodeAssert.equal(events[1]?.type, "content.delta");
1723 }),
1724 );
1725
1651 it.effect("unwraps Codex token usage payloads for context window events", () =>1726 it.effect("unwraps Codex token usage payloads for context window events", () =>
1652 Effect.gen(function* () {1727 Effect.gen(function* () {
1653 const { adapter, runtime } = yield* startLifecycleRuntime();1728 const { adapter, runtime } = yield* startLifecycleRuntime();
apps/server/src/provider/Layers/CodexAdapter.ts+22
...@@ -8,6 +8,7 @@...@@ -8,6 +8,7 @@
8 * @module CodexAdapterLive8 * @module CodexAdapterLive
9 */9 */
10import {10import {
11 EventId,
11 type CanonicalItemType,12 type CanonicalItemType,
12 type CanonicalRequestType,13 type CanonicalRequestType,
13 type CodexSettings,14 type CodexSettings,
...@@ -1445,6 +1446,27 @@ function mapToRuntimeEvents(...@@ -1445,6 +1446,27 @@ function mapToRuntimeEvents(
1445 if (!item) {1446 if (!item) {
1446 return [];1447 return [];
1447 }1448 }
1449 if (item.type === "agentMessage" && item.delivery === "async" && item.questions?.length) {
1450 return [
1451 {
1452 ...runtimeEventBase(event, canonicalThreadId),
1453 type: "user-input.requested",
1454 requestId: RuntimeRequestId.make(`codex-async:${canonicalThreadId}:${item.id}`),
1455 eventId: EventId.make(`codex-async:${canonicalThreadId}:${item.id}`),
1456 payload: {
1457 responseMode: "message",
1458 questions: item.questions.map((question, index) => ({
1459 id: String(index),
1460 header: "Question",
1461 question: question.title,
1462 options: (question.options ?? []).map((label) => ({ label, description: "" })),
1463 allowCustomAnswer: true,
1464 multiSelect: false,
1465 })),
1466 },
1467 },
1468 ];
1469 }
1448 const itemType = toCanonicalItemType(item.type);1470 const itemType = toCanonicalItemType(item.type);
1449 if (itemType === "plan") {1471 if (itemType === "plan") {
1450 const detail = itemDetail(itemType, item);1472 const detail = itemDetail(itemType, item);
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+1
...@@ -204,6 +204,7 @@ describe("ProviderSessionReaper", () => {...@@ -204,6 +204,7 @@ describe("ProviderSessionReaper", () => {
204 Layer.provideMerge(Layer.succeed(ProviderService, providerService)),204 Layer.provideMerge(Layer.succeed(ProviderService, providerService)),
205 Layer.provideMerge(205 Layer.provideMerge(
206 Layer.succeed(ProjectionSnapshotQuery, {206 Layer.succeed(ProjectionSnapshotQuery, {
207 getUserInputActivity: () => Effect.die("unused"),
207 getCommandReadModel: () => Effect.die("unused"),208 getCommandReadModel: () => Effect.die("unused"),
208 getSnapshot: () => Effect.die("unused"),209 getSnapshot: () => Effect.die("unused"),
209 getShellSnapshot: () => Effect.die("unused"),210 getShellSnapshot: () => Effect.die("unused"),
apps/server/src/server.test.ts+1
...@@ -926,6 +926,7 @@ const buildAppUnderTest = (options?: {...@@ -926,6 +926,7 @@ const buildAppUnderTest = (options?: {
926 ),926 ),
927 Layer.provide(927 Layer.provide(
928 Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({928 Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({
929 getUserInputActivity: () => Effect.die("unused"),
929 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),930 getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
930 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),931 getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()),
931 getShellSnapshot: () =>932 getShellSnapshot: () =>
apps/server/src/serverRuntimeStartup.reconcile.test.ts+2
...@@ -66,6 +66,7 @@ const makeProviderService = (liveThreadIds: ReadonlyArray<ThreadId> = []) =>...@@ -66,6 +66,7 @@ const makeProviderService = (liveThreadIds: ReadonlyArray<ThreadId> = []) =>
6666
67const queryWithThreads = (threads: ReadonlyArray<ReturnType<typeof makeThread>>) =>67const queryWithThreads = (threads: ReadonlyArray<ReturnType<typeof makeThread>>) =>
68 ({68 ({
69 getUserInputActivity: () => Effect.die("unused"),
69 getCommandReadModel: () => Effect.succeed({ threads } as never),70 getCommandReadModel: () => Effect.succeed({ threads } as never),
70 }) as unknown as ProjectionSnapshotQuery.ProjectionSnapshotQuery["Service"];71 }) as unknown as ProjectionSnapshotQuery.ProjectionSnapshotQuery["Service"];
7172
...@@ -631,6 +632,7 @@ it.effect("does not fail startup when the live provider session inventory cannot...@@ -631,6 +632,7 @@ it.effect("does not fail startup when the live provider session inventory cannot
631 let queried = false;632 let queried = false;
632 return ServerRuntimeStartup.reconcileProviderSessions.pipe(633 return ServerRuntimeStartup.reconcileProviderSessions.pipe(
633 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {634 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
635 getUserInputActivity: () => Effect.die("unused"),
634 getCommandReadModel: () =>636 getCommandReadModel: () =>
635 Effect.sync(() => {637 Effect.sync(() => {
636 queried = true;638 queried = true;
apps/server/src/serverRuntimeStartup.test.ts+4
...@@ -118,6 +118,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa...@@ -118,6 +118,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
118118
119 yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe(119 yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe(
120 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {120 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
121 getUserInputActivity: () => Effect.die("unused"),
121 getCommandReadModel: () => Effect.die("unused"),122 getCommandReadModel: () => Effect.die("unused"),
122 getSnapshot: () => Effect.die("unused"),123 getSnapshot: () => Effect.die("unused"),
123 getShellSnapshot: () => Effect.die("unused"),124 getShellSnapshot: () => Effect.die("unused"),
...@@ -185,6 +186,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa...@@ -185,6 +186,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
185 autoBootstrapProjectFromCwd: true,186 autoBootstrapProjectFromCwd: true,
186 } as never),187 } as never),
187 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {188 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
189 getUserInputActivity: () => Effect.die("unused"),
188 getCommandReadModel: () => Effect.die("unused"),190 getCommandReadModel: () => Effect.die("unused"),
189 getSnapshot: () => Effect.die("unused"),191 getSnapshot: () => Effect.die("unused"),
190 getShellSnapshot: () => Effect.die("unused"),192 getShellSnapshot: () => Effect.die("unused"),
...@@ -250,6 +252,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when...@@ -250,6 +252,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when
250 autoBootstrapProjectFromCwd: true,252 autoBootstrapProjectFromCwd: true,
251 } as never),253 } as never),
252 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {254 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
255 getUserInputActivity: () => Effect.die("unused"),
253 getCommandReadModel: () => Effect.die("unused"),256 getCommandReadModel: () => Effect.die("unused"),
254 getSnapshot: () => Effect.die("unused"),257 getSnapshot: () => Effect.die("unused"),
255 getShellSnapshot: () => Effect.die("unused"),258 getShellSnapshot: () => Effect.die("unused"),
...@@ -312,6 +315,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa...@@ -312,6 +315,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
312 autoBootstrapProjectFromCwd: true,315 autoBootstrapProjectFromCwd: true,
313 } as never),316 } as never),
314 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {317 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
318 getUserInputActivity: () => Effect.die("unused"),
315 getCommandReadModel: () => Effect.die("unused"),319 getCommandReadModel: () => Effect.die("unused"),
316 getSnapshot: () => Effect.die("unused"),320 getSnapshot: () => Effect.die("unused"),
317 getShellSnapshot: () => Effect.die("unused"),321 getShellSnapshot: () => Effect.die("unused"),
apps/web/src/session-logic.test.ts+20
...@@ -250,6 +250,26 @@ describe("derivePendingApprovals", () => {...@@ -250,6 +250,26 @@ describe("derivePendingApprovals", () => {
250});250});
251251
252describe("derivePendingUserInputs", () => {252describe("derivePendingUserInputs", () => {
253 it("keeps free-text questions without suggested answers", () => {
254 const question = {
255 id: "0",
256 header: "Question",
257 question: "What should it be named?",
258 options: [],
259 allowCustomAnswer: true,
260 multiSelect: false,
261 };
262 const activities = [
263 makeActivity({
264 id: "async-question",
265 kind: "user-input.requested",
266 summary: "User input requested",
267 payload: { requestId: "async-1", responseMode: "message", questions: [question] },
268 }),
269 ];
270 expect(derivePendingUserInputs(activities)[0]?.questions).toEqual([question]);
271 });
272
253 it("preserves native choice values and the custom-answer restriction", () => {273 it("preserves native choice values and the custom-answer restriction", () => {
254 const question = {274 const question = {
255 id: "interaction-result",275 id: "interaction-result",
apps/web/src/session-logic.ts+1-1
...@@ -532,7 +532,7 @@ function parseUserInputQuestions(...@@ -532,7 +532,7 @@ function parseUserInputQuestions(
532 };532 };
533 })533 })
534 .filter((option): option is UserInputQuestion["options"][number] => option !== null);534 .filter((option): option is UserInputQuestion["options"][number] => option !== null);
535 if (options.length === 0) {535 if (options.length === 0 && question.allowCustomAnswer === false) {
536 return null;536 return null;
537 }537 }
538 return {538 return {
docs/internals/providers.md+21
...@@ -24,6 +24,27 @@ adapter in a child scope. Adapter implementations live beside them in...@@ -24,6 +24,27 @@ adapter in a child scope. Adapter implementations live beside them in
24[`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's24[`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's
25transport, config, and event shapes are mapped.25transport, config, and event shapes are mapped.
2626
27## Codex async questions
28
29Codex 0.153 exposes `request_user_input_async` through `item/started` and `item/completed`
30notifications. The item has `type: "agentMessage"`, `delivery: "async"`, and a `questions` array.
31Each question has a `title` and an optional `options` array of strings. The tool returns `{"accepted":true}`
32without waiting. This is separate from the `item/tool/requestUserInput` server request.
33See the [Codex tool handler](https://github.com/openai/codex/blob/d979df154cf60e13eafb5453e75b6d84f21c67bf/codex-rs/core/src/tools/handlers/request_user_input_async.rs).
34
35The Codex adapter maps completed question items to `user-input.requested` with
36`responseMode: "message"` and stable request and event IDs. Questions use the existing web,
37desktop, and mobile panels. They stay pending while the turn runs and after it finishes.
38
39The engine reads the request's latest stored activity before deciding a reply. This works after
40startup, when the command snapshot has no activities, and after a resolution leaves the recent
41activity window. The query returns one activity, not the full thread history.
42
43For these requests, the decider saves the resolution and a user message in one transaction.
44The standard turn path delivers the message, including session resume and active-turn input.
45It does not send a JSON-RPC response to Codex. Other providers and blocking Codex questions
46keep their existing response paths.
47
27## Registry and routing48## Registry and routing
2849
29Two registries separate configuration from live processes:50Two registries separate configuration from live processes:
docs/user/providers-codex.md+9
...@@ -34,6 +34,15 @@ In an existing Codex thread, send `/feedback` or `/feedback` followed by a descr...@@ -34,6 +34,15 @@ In an existing Codex thread, send `/feedback` or `/feedback` followed by a descr
34issue. T3 Code uploads the thread and Codex logs to OpenAI and shows a thread ID that you can copy34issue. T3 Code uploads the thread and Codex logs to OpenAI and shows a thread ID that you can copy
35and share with OpenAI employees.35and share with OpenAI employees.
3636
37## Answer questions while Codex works
38
39Codex can ask questions without stopping its work. Choose a suggested answer or enter your own
40in the question panel. Questions without suggested answers accept text.
41
42Your answers are sent as a new message. They reach the current turn while Codex is working, or
43start a new turn if it has finished. Unanswered questions stay available after you reconnect.
44This works in the web, desktop, and mobile apps. Codex must support async questions.
45
37## Sub-agent models46## Sub-agent models
3847
39The web and desktop Agents panel shows each sub-agent's model and reasoning effort when Codex48The web and desktop Agents panel shows each sub-agent's model and reasoning effort when Codex
packages/contracts/src/providerRuntime.ts+3-2
...@@ -491,7 +491,7 @@ export type RequestResolvedPayload = typeof RequestResolvedPayload.Type;...@@ -491,7 +491,7 @@ export type RequestResolvedPayload = typeof RequestResolvedPayload.Type;
491491
492const UserInputQuestionOption = Schema.Struct({492const UserInputQuestionOption = Schema.Struct({
493 label: TrimmedNonEmptyStringSchema,493 label: TrimmedNonEmptyStringSchema,
494 description: TrimmedNonEmptyStringSchema,494 description: Schema.String,
495 value: Schema.optional(Schema.String),495 value: Schema.optional(Schema.String),
496});496});
497export type UserInputQuestionOption = typeof UserInputQuestionOption.Type;497export type UserInputQuestionOption = typeof UserInputQuestionOption.Type;
...@@ -508,8 +508,9 @@ export const UserInputQuestion = Schema.Struct({...@@ -508,8 +508,9 @@ export const UserInputQuestion = Schema.Struct({
508});508});
509export type UserInputQuestion = typeof UserInputQuestion.Type;509export type UserInputQuestion = typeof UserInputQuestion.Type;
510510
511const UserInputRequestedPayload = Schema.Struct({511export const UserInputRequestedPayload = Schema.Struct({
512 questions: Schema.Array(UserInputQuestion),512 questions: Schema.Array(UserInputQuestion),
513 responseMode: Schema.optional(Schema.Literal("message")),
513});514});
514export type UserInputRequestedPayload = typeof UserInputRequestedPayload.Type;515export type UserInputRequestedPayload = typeof UserInputRequestedPayload.Type;
515516
packages/effect-codex-app-server/scripts/generate.ts+55-1
...@@ -330,6 +330,60 @@ function stripNullDefaults(value: Schema.Json): Schema.Json {...@@ -330,6 +330,60 @@ function stripNullDefaults(value: Schema.Json): Schema.Json {
330 ) as Schema.Json;330 ) as Schema.Json;
331}331}
332332
333// Codex 0.153 adds async questions to agent messages. Keep older protocol
334// fields until the next full refresh, including every thread history namespace.
335function addAsyncQuestionFields(value: Schema.Json): Schema.Json {
336 if (Array.isArray(value)) {
337 return value.map(addAsyncQuestionFields);
338 }
339 if (value === null || typeof value !== "object") {
340 return value;
341 }
342 const properties = "properties" in value ? value.properties : undefined;
343 const itemType =
344 properties && typeof properties === "object" && "type" in properties
345 ? properties.type
346 : undefined;
347 if (
348 properties &&
349 typeof properties === "object" &&
350 itemType &&
351 typeof itemType === "object" &&
352 "enum" in itemType &&
353 Array.isArray(itemType.enum) &&
354 itemType.enum.includes("agentMessage")
355 ) {
356 return {
357 ...value,
358 properties: {
359 ...properties,
360 delivery: { anyOf: [{ type: "string", enum: ["async"] }, { type: "null" }] },
361 questions: {
362 anyOf: [
363 {
364 type: "array",
365 items: {
366 type: "object",
367 properties: {
368 title: { type: "string" },
369 options: {
370 anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }],
371 },
372 },
373 required: ["title"],
374 },
375 },
376 { type: "null" },
377 ],
378 },
379 },
380 };
381 }
382 return Object.fromEntries(
383 Object.entries(value).map(([key, child]) => [key, addAsyncQuestionFields(child)]),
384 );
385}
386
333function toPascalCaseMethod(method: string) {387function toPascalCaseMethod(method: string) {
334 return method388 return method
335 .split("/")389 .split("/")
...@@ -648,7 +702,7 @@ const generateFiles = Effect.fn("generateFiles")(function* () {...@@ -648,7 +702,7 @@ const generateFiles = Effect.fn("generateFiles")(function* () {
648 for (const [name, schema] of Object.entries(aggregateSchemas).toSorted(([left], [right]) =>702 for (const [name, schema] of Object.entries(aggregateSchemas).toSorted(([left], [right]) =>
649 left.localeCompare(right),703 left.localeCompare(right),
650 )) {704 )) {
651 generator.addSchema(name, schema as never);705 generator.addSchema(name, addAsyncQuestionFields(schema) as never);
652 }706 }
653707
654 const generatedEntries = new Map<string, string>();708 const generatedEntries = new Map<string, string>();
packages/effect-codex-app-server/src/_generated/schema.gen.ts+272
...@@ -20405,6 +20405,11 @@ export type ServerNotification__ThreadItem =...@@ -20405,6 +20405,11 @@ export type ServerNotification__ThreadItem =
20405 readonly memoryCitation?: ServerNotification__MemoryCitation | null;20405 readonly memoryCitation?: ServerNotification__MemoryCitation | null;
20406 readonly phase?: ServerNotification__MessagePhase | null;20406 readonly phase?: ServerNotification__MessagePhase | null;
20407 readonly text: string;20407 readonly text: string;
20408 readonly delivery?: "async" | null;
20409 readonly questions?: ReadonlyArray<{
20410 readonly title: string;
20411 readonly options?: ReadonlyArray<string> | null;
20412 }> | null;
20408 readonly type: "agentMessage";20413 readonly type: "agentMessage";
20409 }20414 }
20410 | { readonly id: string; readonly text: string; readonly type: "plan" }20415 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -20530,6 +20535,18 @@ export const ServerNotification__ThreadItem = Schema.Union(...@@ -20530,6 +20535,18 @@ export const ServerNotification__ThreadItem = Schema.Union(
20530 ),20535 ),
20531 phase: Schema.optionalKey(Schema.Union([ServerNotification__MessagePhase, Schema.Null])),20536 phase: Schema.optionalKey(Schema.Union([ServerNotification__MessagePhase, Schema.Null])),
20532 text: Schema.String,20537 text: Schema.String,
20538 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
20539 questions: Schema.optionalKey(
20540 Schema.Union([
20541 Schema.Array(
20542 Schema.Struct({
20543 title: Schema.String,
20544 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
20545 }),
20546 ),
20547 Schema.Null,
20548 ]),
20549 ),
20533 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),20550 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
20534 }).annotate({ title: "AgentMessageThreadItem" }),20551 }).annotate({ title: "AgentMessageThreadItem" }),
20535 Schema.Struct({20552 Schema.Struct({
...@@ -21499,6 +21516,11 @@ export type V2ItemCompletedNotification__ThreadItem =...@@ -21499,6 +21516,11 @@ export type V2ItemCompletedNotification__ThreadItem =
21499 readonly memoryCitation?: V2ItemCompletedNotification__MemoryCitation | null;21516 readonly memoryCitation?: V2ItemCompletedNotification__MemoryCitation | null;
21500 readonly phase?: V2ItemCompletedNotification__MessagePhase | null;21517 readonly phase?: V2ItemCompletedNotification__MessagePhase | null;
21501 readonly text: string;21518 readonly text: string;
21519 readonly delivery?: "async" | null;
21520 readonly questions?: ReadonlyArray<{
21521 readonly title: string;
21522 readonly options?: ReadonlyArray<string> | null;
21523 }> | null;
21502 readonly type: "agentMessage";21524 readonly type: "agentMessage";
21503 }21525 }
21504 | { readonly id: string; readonly text: string; readonly type: "plan" }21526 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -21628,6 +21650,18 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union(...@@ -21628,6 +21650,18 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union(
21628 Schema.Union([V2ItemCompletedNotification__MessagePhase, Schema.Null]),21650 Schema.Union([V2ItemCompletedNotification__MessagePhase, Schema.Null]),
21629 ),21651 ),
21630 text: Schema.String,21652 text: Schema.String,
21653 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
21654 questions: Schema.optionalKey(
21655 Schema.Union([
21656 Schema.Array(
21657 Schema.Struct({
21658 title: Schema.String,
21659 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
21660 }),
21661 ),
21662 Schema.Null,
21663 ]),
21664 ),
21631 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),21665 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
21632 }).annotate({ title: "AgentMessageThreadItem" }),21666 }).annotate({ title: "AgentMessageThreadItem" }),
21633 Schema.Struct({21667 Schema.Struct({
...@@ -21965,6 +21999,11 @@ export type V2ItemStartedNotification__ThreadItem =...@@ -21965,6 +21999,11 @@ export type V2ItemStartedNotification__ThreadItem =
21965 readonly memoryCitation?: V2ItemStartedNotification__MemoryCitation | null;21999 readonly memoryCitation?: V2ItemStartedNotification__MemoryCitation | null;
21966 readonly phase?: V2ItemStartedNotification__MessagePhase | null;22000 readonly phase?: V2ItemStartedNotification__MessagePhase | null;
21967 readonly text: string;22001 readonly text: string;
22002 readonly delivery?: "async" | null;
22003 readonly questions?: ReadonlyArray<{
22004 readonly title: string;
22005 readonly options?: ReadonlyArray<string> | null;
22006 }> | null;
21968 readonly type: "agentMessage";22007 readonly type: "agentMessage";
21969 }22008 }
21970 | { readonly id: string; readonly text: string; readonly type: "plan" }22009 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -22092,6 +22131,18 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union(...@@ -22092,6 +22131,18 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union(
22092 Schema.Union([V2ItemStartedNotification__MessagePhase, Schema.Null]),22131 Schema.Union([V2ItemStartedNotification__MessagePhase, Schema.Null]),
22093 ),22132 ),
22094 text: Schema.String,22133 text: Schema.String,
22134 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
22135 questions: Schema.optionalKey(
22136 Schema.Union([
22137 Schema.Array(
22138 Schema.Struct({
22139 title: Schema.String,
22140 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
22141 }),
22142 ),
22143 Schema.Null,
22144 ]),
22145 ),
22095 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),22146 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
22096 }).annotate({ title: "AgentMessageThreadItem" }),22147 }).annotate({ title: "AgentMessageThreadItem" }),
22097 Schema.Struct({22148 Schema.Struct({
...@@ -22601,6 +22652,11 @@ export type V2ReviewStartResponse__ThreadItem =...@@ -22601,6 +22652,11 @@ export type V2ReviewStartResponse__ThreadItem =
22601 readonly memoryCitation?: V2ReviewStartResponse__MemoryCitation | null;22652 readonly memoryCitation?: V2ReviewStartResponse__MemoryCitation | null;
22602 readonly phase?: V2ReviewStartResponse__MessagePhase | null;22653 readonly phase?: V2ReviewStartResponse__MessagePhase | null;
22603 readonly text: string;22654 readonly text: string;
22655 readonly delivery?: "async" | null;
22656 readonly questions?: ReadonlyArray<{
22657 readonly title: string;
22658 readonly options?: ReadonlyArray<string> | null;
22659 }> | null;
22604 readonly type: "agentMessage";22660 readonly type: "agentMessage";
22605 }22661 }
22606 | { readonly id: string; readonly text: string; readonly type: "plan" }22662 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -22726,6 +22782,18 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union(...@@ -22726,6 +22782,18 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union(
22726 ),22782 ),
22727 phase: Schema.optionalKey(Schema.Union([V2ReviewStartResponse__MessagePhase, Schema.Null])),22783 phase: Schema.optionalKey(Schema.Union([V2ReviewStartResponse__MessagePhase, Schema.Null])),
22728 text: Schema.String,22784 text: Schema.String,
22785 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
22786 questions: Schema.optionalKey(
22787 Schema.Union([
22788 Schema.Array(
22789 Schema.Struct({
22790 title: Schema.String,
22791 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
22792 }),
22793 ),
22794 Schema.Null,
22795 ]),
22796 ),
22729 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),22797 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
22730 }).annotate({ title: "AgentMessageThreadItem" }),22798 }).annotate({ title: "AgentMessageThreadItem" }),
22731 Schema.Struct({22799 Schema.Struct({
...@@ -23050,6 +23118,11 @@ export type V2ThreadForkResponse__ThreadItem =...@@ -23050,6 +23118,11 @@ export type V2ThreadForkResponse__ThreadItem =
23050 readonly memoryCitation?: V2ThreadForkResponse__MemoryCitation | null;23118 readonly memoryCitation?: V2ThreadForkResponse__MemoryCitation | null;
23051 readonly phase?: V2ThreadForkResponse__MessagePhase | null;23119 readonly phase?: V2ThreadForkResponse__MessagePhase | null;
23052 readonly text: string;23120 readonly text: string;
23121 readonly delivery?: "async" | null;
23122 readonly questions?: ReadonlyArray<{
23123 readonly title: string;
23124 readonly options?: ReadonlyArray<string> | null;
23125 }> | null;
23053 readonly type: "agentMessage";23126 readonly type: "agentMessage";
23054 }23127 }
23055 | { readonly id: string; readonly text: string; readonly type: "plan" }23128 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -23175,6 +23248,18 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union(...@@ -23175,6 +23248,18 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union(
23175 ),23248 ),
23176 phase: Schema.optionalKey(Schema.Union([V2ThreadForkResponse__MessagePhase, Schema.Null])),23249 phase: Schema.optionalKey(Schema.Union([V2ThreadForkResponse__MessagePhase, Schema.Null])),
23177 text: Schema.String,23250 text: Schema.String,
23251 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
23252 questions: Schema.optionalKey(
23253 Schema.Union([
23254 Schema.Array(
23255 Schema.Struct({
23256 title: Schema.String,
23257 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
23258 }),
23259 ),
23260 Schema.Null,
23261 ]),
23262 ),
23178 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),23263 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
23179 }).annotate({ title: "AgentMessageThreadItem" }),23264 }).annotate({ title: "AgentMessageThreadItem" }),
23180 Schema.Struct({23265 Schema.Struct({
...@@ -23468,6 +23553,11 @@ export type V2ThreadListResponse__ThreadItem =...@@ -23468,6 +23553,11 @@ export type V2ThreadListResponse__ThreadItem =
23468 readonly memoryCitation?: V2ThreadListResponse__MemoryCitation | null;23553 readonly memoryCitation?: V2ThreadListResponse__MemoryCitation | null;
23469 readonly phase?: V2ThreadListResponse__MessagePhase | null;23554 readonly phase?: V2ThreadListResponse__MessagePhase | null;
23470 readonly text: string;23555 readonly text: string;
23556 readonly delivery?: "async" | null;
23557 readonly questions?: ReadonlyArray<{
23558 readonly title: string;
23559 readonly options?: ReadonlyArray<string> | null;
23560 }> | null;
23471 readonly type: "agentMessage";23561 readonly type: "agentMessage";
23472 }23562 }
23473 | { readonly id: string; readonly text: string; readonly type: "plan" }23563 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -23593,6 +23683,18 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union(...@@ -23593,6 +23683,18 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union(
23593 ),23683 ),
23594 phase: Schema.optionalKey(Schema.Union([V2ThreadListResponse__MessagePhase, Schema.Null])),23684 phase: Schema.optionalKey(Schema.Union([V2ThreadListResponse__MessagePhase, Schema.Null])),
23595 text: Schema.String,23685 text: Schema.String,
23686 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
23687 questions: Schema.optionalKey(
23688 Schema.Union([
23689 Schema.Array(
23690 Schema.Struct({
23691 title: Schema.String,
23692 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
23693 }),
23694 ),
23695 Schema.Null,
23696 ]),
23697 ),
23596 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),23698 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
23597 }).annotate({ title: "AgentMessageThreadItem" }),23699 }).annotate({ title: "AgentMessageThreadItem" }),
23598 Schema.Struct({23700 Schema.Struct({
...@@ -23886,6 +23988,11 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem =...@@ -23886,6 +23988,11 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem =
23886 readonly memoryCitation?: V2ThreadMetadataUpdateResponse__MemoryCitation | null;23988 readonly memoryCitation?: V2ThreadMetadataUpdateResponse__MemoryCitation | null;
23887 readonly phase?: V2ThreadMetadataUpdateResponse__MessagePhase | null;23989 readonly phase?: V2ThreadMetadataUpdateResponse__MessagePhase | null;
23888 readonly text: string;23990 readonly text: string;
23991 readonly delivery?: "async" | null;
23992 readonly questions?: ReadonlyArray<{
23993 readonly title: string;
23994 readonly options?: ReadonlyArray<string> | null;
23995 }> | null;
23889 readonly type: "agentMessage";23996 readonly type: "agentMessage";
23890 }23997 }
23891 | { readonly id: string; readonly text: string; readonly type: "plan" }23998 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -24015,6 +24122,18 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union(...@@ -24015,6 +24122,18 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union(
24015 Schema.Union([V2ThreadMetadataUpdateResponse__MessagePhase, Schema.Null]),24122 Schema.Union([V2ThreadMetadataUpdateResponse__MessagePhase, Schema.Null]),
24016 ),24123 ),
24017 text: Schema.String,24124 text: Schema.String,
24125 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
24126 questions: Schema.optionalKey(
24127 Schema.Union([
24128 Schema.Array(
24129 Schema.Struct({
24130 title: Schema.String,
24131 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
24132 }),
24133 ),
24134 Schema.Null,
24135 ]),
24136 ),
24018 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),24137 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
24019 }).annotate({ title: "AgentMessageThreadItem" }),24138 }).annotate({ title: "AgentMessageThreadItem" }),
24020 Schema.Struct({24139 Schema.Struct({
...@@ -24309,6 +24428,11 @@ export type V2ThreadReadResponse__ThreadItem =...@@ -24309,6 +24428,11 @@ export type V2ThreadReadResponse__ThreadItem =
24309 readonly memoryCitation?: V2ThreadReadResponse__MemoryCitation | null;24428 readonly memoryCitation?: V2ThreadReadResponse__MemoryCitation | null;
24310 readonly phase?: V2ThreadReadResponse__MessagePhase | null;24429 readonly phase?: V2ThreadReadResponse__MessagePhase | null;
24311 readonly text: string;24430 readonly text: string;
24431 readonly delivery?: "async" | null;
24432 readonly questions?: ReadonlyArray<{
24433 readonly title: string;
24434 readonly options?: ReadonlyArray<string> | null;
24435 }> | null;
24312 readonly type: "agentMessage";24436 readonly type: "agentMessage";
24313 }24437 }
24314 | { readonly id: string; readonly text: string; readonly type: "plan" }24438 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -24434,6 +24558,18 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union(...@@ -24434,6 +24558,18 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union(
24434 ),24558 ),
24435 phase: Schema.optionalKey(Schema.Union([V2ThreadReadResponse__MessagePhase, Schema.Null])),24559 phase: Schema.optionalKey(Schema.Union([V2ThreadReadResponse__MessagePhase, Schema.Null])),
24436 text: Schema.String,24560 text: Schema.String,
24561 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
24562 questions: Schema.optionalKey(
24563 Schema.Union([
24564 Schema.Array(
24565 Schema.Struct({
24566 title: Schema.String,
24567 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
24568 }),
24569 ),
24570 Schema.Null,
24571 ]),
24572 ),
24437 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),24573 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
24438 }).annotate({ title: "AgentMessageThreadItem" }),24574 }).annotate({ title: "AgentMessageThreadItem" }),
24439 Schema.Struct({24575 Schema.Struct({
...@@ -24735,6 +24871,11 @@ export type V2ThreadResumeResponse__ThreadItem =...@@ -24735,6 +24871,11 @@ export type V2ThreadResumeResponse__ThreadItem =
24735 readonly memoryCitation?: V2ThreadResumeResponse__MemoryCitation | null;24871 readonly memoryCitation?: V2ThreadResumeResponse__MemoryCitation | null;
24736 readonly phase?: V2ThreadResumeResponse__MessagePhase | null;24872 readonly phase?: V2ThreadResumeResponse__MessagePhase | null;
24737 readonly text: string;24873 readonly text: string;
24874 readonly delivery?: "async" | null;
24875 readonly questions?: ReadonlyArray<{
24876 readonly title: string;
24877 readonly options?: ReadonlyArray<string> | null;
24878 }> | null;
24738 readonly type: "agentMessage";24879 readonly type: "agentMessage";
24739 }24880 }
24740 | { readonly id: string; readonly text: string; readonly type: "plan" }24881 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -24860,6 +25001,18 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union(...@@ -24860,6 +25001,18 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union(
24860 ),25001 ),
24861 phase: Schema.optionalKey(Schema.Union([V2ThreadResumeResponse__MessagePhase, Schema.Null])),25002 phase: Schema.optionalKey(Schema.Union([V2ThreadResumeResponse__MessagePhase, Schema.Null])),
24862 text: Schema.String,25003 text: Schema.String,
25004 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
25005 questions: Schema.optionalKey(
25006 Schema.Union([
25007 Schema.Array(
25008 Schema.Struct({
25009 title: Schema.String,
25010 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
25011 }),
25012 ),
25013 Schema.Null,
25014 ]),
25015 ),
24863 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),25016 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
24864 }).annotate({ title: "AgentMessageThreadItem" }),25017 }).annotate({ title: "AgentMessageThreadItem" }),
24865 Schema.Struct({25018 Schema.Struct({
...@@ -25153,6 +25306,11 @@ export type V2ThreadRollbackResponse__ThreadItem =...@@ -25153,6 +25306,11 @@ export type V2ThreadRollbackResponse__ThreadItem =
25153 readonly memoryCitation?: V2ThreadRollbackResponse__MemoryCitation | null;25306 readonly memoryCitation?: V2ThreadRollbackResponse__MemoryCitation | null;
25154 readonly phase?: V2ThreadRollbackResponse__MessagePhase | null;25307 readonly phase?: V2ThreadRollbackResponse__MessagePhase | null;
25155 readonly text: string;25308 readonly text: string;
25309 readonly delivery?: "async" | null;
25310 readonly questions?: ReadonlyArray<{
25311 readonly title: string;
25312 readonly options?: ReadonlyArray<string> | null;
25313 }> | null;
25156 readonly type: "agentMessage";25314 readonly type: "agentMessage";
25157 }25315 }
25158 | { readonly id: string; readonly text: string; readonly type: "plan" }25316 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -25280,6 +25438,18 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union(...@@ -25280,6 +25438,18 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union(
25280 Schema.Union([V2ThreadRollbackResponse__MessagePhase, Schema.Null]),25438 Schema.Union([V2ThreadRollbackResponse__MessagePhase, Schema.Null]),
25281 ),25439 ),
25282 text: Schema.String,25440 text: Schema.String,
25441 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
25442 questions: Schema.optionalKey(
25443 Schema.Union([
25444 Schema.Array(
25445 Schema.Struct({
25446 title: Schema.String,
25447 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
25448 }),
25449 ),
25450 Schema.Null,
25451 ]),
25452 ),
25283 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),25453 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
25284 }).annotate({ title: "AgentMessageThreadItem" }),25454 }).annotate({ title: "AgentMessageThreadItem" }),
25285 Schema.Struct({25455 Schema.Struct({
...@@ -25583,6 +25753,11 @@ export type V2ThreadStartedNotification__ThreadItem =...@@ -25583,6 +25753,11 @@ export type V2ThreadStartedNotification__ThreadItem =
25583 readonly memoryCitation?: V2ThreadStartedNotification__MemoryCitation | null;25753 readonly memoryCitation?: V2ThreadStartedNotification__MemoryCitation | null;
25584 readonly phase?: V2ThreadStartedNotification__MessagePhase | null;25754 readonly phase?: V2ThreadStartedNotification__MessagePhase | null;
25585 readonly text: string;25755 readonly text: string;
25756 readonly delivery?: "async" | null;
25757 readonly questions?: ReadonlyArray<{
25758 readonly title: string;
25759 readonly options?: ReadonlyArray<string> | null;
25760 }> | null;
25586 readonly type: "agentMessage";25761 readonly type: "agentMessage";
25587 }25762 }
25588 | { readonly id: string; readonly text: string; readonly type: "plan" }25763 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -25712,6 +25887,18 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union(...@@ -25712,6 +25887,18 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union(
25712 Schema.Union([V2ThreadStartedNotification__MessagePhase, Schema.Null]),25887 Schema.Union([V2ThreadStartedNotification__MessagePhase, Schema.Null]),
25713 ),25888 ),
25714 text: Schema.String,25889 text: Schema.String,
25890 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
25891 questions: Schema.optionalKey(
25892 Schema.Union([
25893 Schema.Array(
25894 Schema.Struct({
25895 title: Schema.String,
25896 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
25897 }),
25898 ),
25899 Schema.Null,
25900 ]),
25901 ),
25715 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),25902 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
25716 }).annotate({ title: "AgentMessageThreadItem" }),25903 }).annotate({ title: "AgentMessageThreadItem" }),
25717 Schema.Struct({25904 Schema.Struct({
...@@ -26006,6 +26193,11 @@ export type V2ThreadStartResponse__ThreadItem =...@@ -26006,6 +26193,11 @@ export type V2ThreadStartResponse__ThreadItem =
26006 readonly memoryCitation?: V2ThreadStartResponse__MemoryCitation | null;26193 readonly memoryCitation?: V2ThreadStartResponse__MemoryCitation | null;
26007 readonly phase?: V2ThreadStartResponse__MessagePhase | null;26194 readonly phase?: V2ThreadStartResponse__MessagePhase | null;
26008 readonly text: string;26195 readonly text: string;
26196 readonly delivery?: "async" | null;
26197 readonly questions?: ReadonlyArray<{
26198 readonly title: string;
26199 readonly options?: ReadonlyArray<string> | null;
26200 }> | null;
26009 readonly type: "agentMessage";26201 readonly type: "agentMessage";
26010 }26202 }
26011 | { readonly id: string; readonly text: string; readonly type: "plan" }26203 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -26131,6 +26323,18 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union(...@@ -26131,6 +26323,18 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union(
26131 ),26323 ),
26132 phase: Schema.optionalKey(Schema.Union([V2ThreadStartResponse__MessagePhase, Schema.Null])),26324 phase: Schema.optionalKey(Schema.Union([V2ThreadStartResponse__MessagePhase, Schema.Null])),
26133 text: Schema.String,26325 text: Schema.String,
26326 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
26327 questions: Schema.optionalKey(
26328 Schema.Union([
26329 Schema.Array(
26330 Schema.Struct({
26331 title: Schema.String,
26332 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
26333 }),
26334 ),
26335 Schema.Null,
26336 ]),
26337 ),
26134 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),26338 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
26135 }).annotate({ title: "AgentMessageThreadItem" }),26339 }).annotate({ title: "AgentMessageThreadItem" }),
26136 Schema.Struct({26340 Schema.Struct({
...@@ -26424,6 +26628,11 @@ export type V2ThreadUnarchiveResponse__ThreadItem =...@@ -26424,6 +26628,11 @@ export type V2ThreadUnarchiveResponse__ThreadItem =
26424 readonly memoryCitation?: V2ThreadUnarchiveResponse__MemoryCitation | null;26628 readonly memoryCitation?: V2ThreadUnarchiveResponse__MemoryCitation | null;
26425 readonly phase?: V2ThreadUnarchiveResponse__MessagePhase | null;26629 readonly phase?: V2ThreadUnarchiveResponse__MessagePhase | null;
26426 readonly text: string;26630 readonly text: string;
26631 readonly delivery?: "async" | null;
26632 readonly questions?: ReadonlyArray<{
26633 readonly title: string;
26634 readonly options?: ReadonlyArray<string> | null;
26635 }> | null;
26427 readonly type: "agentMessage";26636 readonly type: "agentMessage";
26428 }26637 }
26429 | { readonly id: string; readonly text: string; readonly type: "plan" }26638 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -26551,6 +26760,18 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union(...@@ -26551,6 +26760,18 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union(
26551 Schema.Union([V2ThreadUnarchiveResponse__MessagePhase, Schema.Null]),26760 Schema.Union([V2ThreadUnarchiveResponse__MessagePhase, Schema.Null]),
26552 ),26761 ),
26553 text: Schema.String,26762 text: Schema.String,
26763 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
26764 questions: Schema.optionalKey(
26765 Schema.Union([
26766 Schema.Array(
26767 Schema.Struct({
26768 title: Schema.String,
26769 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
26770 }),
26771 ),
26772 Schema.Null,
26773 ]),
26774 ),
26554 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),26775 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
26555 }).annotate({ title: "AgentMessageThreadItem" }),26776 }).annotate({ title: "AgentMessageThreadItem" }),
26556 Schema.Struct({26777 Schema.Struct({
...@@ -26845,6 +27066,11 @@ export type V2TurnCompletedNotification__ThreadItem =...@@ -26845,6 +27066,11 @@ export type V2TurnCompletedNotification__ThreadItem =
26845 readonly memoryCitation?: V2TurnCompletedNotification__MemoryCitation | null;27066 readonly memoryCitation?: V2TurnCompletedNotification__MemoryCitation | null;
26846 readonly phase?: V2TurnCompletedNotification__MessagePhase | null;27067 readonly phase?: V2TurnCompletedNotification__MessagePhase | null;
26847 readonly text: string;27068 readonly text: string;
27069 readonly delivery?: "async" | null;
27070 readonly questions?: ReadonlyArray<{
27071 readonly title: string;
27072 readonly options?: ReadonlyArray<string> | null;
27073 }> | null;
26848 readonly type: "agentMessage";27074 readonly type: "agentMessage";
26849 }27075 }
26850 | { readonly id: string; readonly text: string; readonly type: "plan" }27076 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -26974,6 +27200,18 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union(...@@ -26974,6 +27200,18 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union(
26974 Schema.Union([V2TurnCompletedNotification__MessagePhase, Schema.Null]),27200 Schema.Union([V2TurnCompletedNotification__MessagePhase, Schema.Null]),
26975 ),27201 ),
26976 text: Schema.String,27202 text: Schema.String,
27203 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
27204 questions: Schema.optionalKey(
27205 Schema.Union([
27206 Schema.Array(
27207 Schema.Struct({
27208 title: Schema.String,
27209 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
27210 }),
27211 ),
27212 Schema.Null,
27213 ]),
27214 ),
26977 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),27215 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
26978 }).annotate({ title: "AgentMessageThreadItem" }),27216 }).annotate({ title: "AgentMessageThreadItem" }),
26979 Schema.Struct({27217 Schema.Struct({
...@@ -27268,6 +27506,11 @@ export type V2TurnStartedNotification__ThreadItem =...@@ -27268,6 +27506,11 @@ export type V2TurnStartedNotification__ThreadItem =
27268 readonly memoryCitation?: V2TurnStartedNotification__MemoryCitation | null;27506 readonly memoryCitation?: V2TurnStartedNotification__MemoryCitation | null;
27269 readonly phase?: V2TurnStartedNotification__MessagePhase | null;27507 readonly phase?: V2TurnStartedNotification__MessagePhase | null;
27270 readonly text: string;27508 readonly text: string;
27509 readonly delivery?: "async" | null;
27510 readonly questions?: ReadonlyArray<{
27511 readonly title: string;
27512 readonly options?: ReadonlyArray<string> | null;
27513 }> | null;
27271 readonly type: "agentMessage";27514 readonly type: "agentMessage";
27272 }27515 }
27273 | { readonly id: string; readonly text: string; readonly type: "plan" }27516 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -27395,6 +27638,18 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union(...@@ -27395,6 +27638,18 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union(
27395 Schema.Union([V2TurnStartedNotification__MessagePhase, Schema.Null]),27638 Schema.Union([V2TurnStartedNotification__MessagePhase, Schema.Null]),
27396 ),27639 ),
27397 text: Schema.String,27640 text: Schema.String,
27641 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
27642 questions: Schema.optionalKey(
27643 Schema.Union([
27644 Schema.Array(
27645 Schema.Struct({
27646 title: Schema.String,
27647 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
27648 }),
27649 ),
27650 Schema.Null,
27651 ]),
27652 ),
27398 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),27653 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
27399 }).annotate({ title: "AgentMessageThreadItem" }),27654 }).annotate({ title: "AgentMessageThreadItem" }),
27400 Schema.Struct({27655 Schema.Struct({
...@@ -27689,6 +27944,11 @@ export type V2TurnStartResponse__ThreadItem =...@@ -27689,6 +27944,11 @@ export type V2TurnStartResponse__ThreadItem =
27689 readonly memoryCitation?: V2TurnStartResponse__MemoryCitation | null;27944 readonly memoryCitation?: V2TurnStartResponse__MemoryCitation | null;
27690 readonly phase?: V2TurnStartResponse__MessagePhase | null;27945 readonly phase?: V2TurnStartResponse__MessagePhase | null;
27691 readonly text: string;27946 readonly text: string;
27947 readonly delivery?: "async" | null;
27948 readonly questions?: ReadonlyArray<{
27949 readonly title: string;
27950 readonly options?: ReadonlyArray<string> | null;
27951 }> | null;
27692 readonly type: "agentMessage";27952 readonly type: "agentMessage";
27693 }27953 }
27694 | { readonly id: string; readonly text: string; readonly type: "plan" }27954 | { readonly id: string; readonly text: string; readonly type: "plan" }
...@@ -27814,6 +28074,18 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union(...@@ -27814,6 +28074,18 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union(
27814 ),28074 ),
27815 phase: Schema.optionalKey(Schema.Union([V2TurnStartResponse__MessagePhase, Schema.Null])),28075 phase: Schema.optionalKey(Schema.Union([V2TurnStartResponse__MessagePhase, Schema.Null])),
27816 text: Schema.String,28076 text: Schema.String,
28077 delivery: Schema.optionalKey(Schema.Union([Schema.Literal("async"), Schema.Null])),
28078 questions: Schema.optionalKey(
28079 Schema.Union([
28080 Schema.Array(
28081 Schema.Struct({
28082 title: Schema.String,
28083 options: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])),
28084 }),
28085 ),
28086 Schema.Null,
28087 ]),
28088 ),
27817 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),28089 type: Schema.Literal("agentMessage").annotate({ title: "AgentMessageThreadItemType" }),
27818 }).annotate({ title: "AgentMessageThreadItem" }),28090 }).annotate({ title: "AgentMessageThreadItem" }),
27819 Schema.Struct({28091 Schema.Struct({
packages/effect-codex-app-server/src/schema.test.ts+23
...@@ -5,6 +5,29 @@ import * as CodexSchema from "./schema.ts";...@@ -5,6 +5,29 @@ import * as CodexSchema from "./schema.ts";
55
6const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse);6const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse);
77
8it("keeps async questions in live notifications and thread history", () => {
9 const item = {
10 type: "agentMessage",
11 id: "question-1",
12 text: "Which package?\n- pnpm\n- npm\n\nWhat should it be named?",
13 phase: "final_answer",
14 delivery: "async",
15 questions: [
16 { title: "Which package manager?", options: ["pnpm", "npm"] },
17 { title: "What should it be named?" },
18 ],
19 } as const;
20 for (const schema of [
21 CodexSchema.ServerNotification__ThreadItem,
22 CodexSchema.V2ItemStartedNotification__ThreadItem,
23 CodexSchema.V2ItemCompletedNotification__ThreadItem,
24 CodexSchema.V2ThreadReadResponse__ThreadItem,
25 CodexSchema.V2ThreadResumeResponse__ThreadItem,
26 ]) {
27 assert.deepEqual(Schema.decodeUnknownSync(schema)(item), item);
28 }
29});
30
8it("accepts Codex 0.150 multi-agent values", () => {31it("accepts Codex 0.150 multi-agent values", () => {
9 const schemas = [32 const schemas = [
10 CodexSchema.ServerNotification__SubAgentActivityKind,33 CodexSchema.ServerNotification__SubAgentActivityKind,