Skip to content

Commit 4189899

Browse files
committed
Persist pending runtime mode and session resume cursors
- Track pending runtime mode on threads - Preserve provider session resume cursors in projections - Extend mock ACP agent to exercise permission prompts
1 parent 72f0cac commit 4189899

73 files changed

Lines changed: 4242 additions & 588 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/server/scripts/acp-mock-agent.ts

Lines changed: 144 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,16 @@ const emitInterleavedAssistantToolCalls =
1818
const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
1919
const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1";
2020
const emitElicitation = process.env.T3_ACP_EMIT_ELICITATION === "1";
21+
const permissionRequestCount = Number.parseInt(
22+
process.env.T3_ACP_PERMISSION_REQUEST_COUNT ?? "0",
23+
10,
24+
);
25+
const permissionRequestKind = process.env.T3_ACP_PERMISSION_REQUEST_KIND ?? "execute";
2126
const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
2227
const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
28+
const ignoreCancel = process.env.T3_ACP_IGNORE_CANCEL === "1";
29+
const promptDelayMs = Number.parseInt(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0", 10);
30+
const promptStartedText = process.env.T3_ACP_PROMPT_STARTED_TEXT;
2331
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
2432
const authMethods = (process.env.T3_ACP_AUTH_METHODS ?? "")
2533
.split(",")
@@ -35,6 +43,61 @@ let currentContext = "272k";
3543
let currentFast = false;
3644
const cancelledSessions = new Set<string>();
3745

46+
function mockPermissionRequestPayload(kind: string) {
47+
switch (kind) {
48+
case "edit":
49+
return {
50+
title: "Edit file",
51+
rawInput: {
52+
path: "server/package.json",
53+
},
54+
content: [
55+
{
56+
type: "content" as const,
57+
content: {
58+
type: "text" as const,
59+
text: "Edit server/package.json",
60+
},
61+
},
62+
],
63+
};
64+
default:
65+
return {
66+
title: "`cat server/package.json`",
67+
rawInput: {
68+
command: ["cat", "server/package.json"],
69+
},
70+
content: [
71+
{
72+
type: "content" as const,
73+
content: {
74+
type: "text" as const,
75+
text: "Not in allowlist: cat server/package.json",
76+
},
77+
},
78+
],
79+
};
80+
}
81+
}
82+
83+
function reasoningEffortOptionsForModel(): ReadonlyArray<{
84+
readonly value: string;
85+
readonly name: string;
86+
}> {
87+
return /^gpt-5(?:[.-]|$)/u.test(currentModelId)
88+
? [
89+
{ value: "low", name: "Low" },
90+
{ value: "medium", name: "Medium" },
91+
{ value: "high", name: "High" },
92+
{ value: "xhigh", name: "Extra High" },
93+
]
94+
: [
95+
{ value: "low", name: "Low" },
96+
{ value: "medium", name: "Medium" },
97+
{ value: "high", name: "High" },
98+
];
99+
}
100+
38101
function logExit(reason: string): void {
39102
if (!exitLogPath) {
40103
return;
@@ -91,18 +154,12 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
91154
return [
92155
...baseOptions,
93156
{
94-
id: "reasoning",
157+
id: "reasoning_effort",
95158
name: "Reasoning",
96159
category: "thought_level",
97160
type: "select",
98161
currentValue: currentReasoning,
99-
options: [
100-
{ value: "none", name: "None" },
101-
{ value: "low", name: "Low" },
102-
{ value: "medium", name: "Medium" },
103-
{ value: "high", name: "High" },
104-
{ value: "extra-high", name: "Extra High" },
105-
],
162+
options: reasoningEffortOptionsForModel(),
106163
},
107164
{
108165
id: "context",
@@ -146,16 +203,12 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
146203
return [
147204
...baseOptions,
148205
{
149-
id: "reasoning",
206+
id: "reasoning_effort",
150207
name: "Reasoning",
151208
category: "thought_level",
152209
type: "select",
153210
currentValue: currentReasoning,
154-
options: [
155-
{ value: "low", name: "Low" },
156-
{ value: "medium", name: "Medium" },
157-
{ value: "high", name: "High" },
158-
],
211+
options: reasoningEffortOptionsForModel(),
159212
},
160213
{
161214
id: "thinking",
@@ -171,6 +224,18 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
171224
}
172225

173226
return [
227+
{
228+
id: "mode",
229+
name: "Mode",
230+
category: "mode",
231+
type: "select" as const,
232+
currentValue: currentModeId,
233+
options: availableModes.map((mode) => ({
234+
value: mode.id,
235+
name: mode.name,
236+
...(mode.description ? { description: mode.description } : {}),
237+
})),
238+
},
174239
{
175240
id: "model",
176241
name: "Model",
@@ -180,10 +245,19 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
180245
options: [
181246
{ value: "default", name: "Auto" },
182247
{ value: "composer-2", name: "Composer 2" },
183-
{ value: "composer-2[fast=true]", name: "Composer 2 Fast" },
184-
{ value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" },
248+
{ value: "gpt-5.3-codex", name: "Codex 5.3" },
249+
{ value: "gpt-5.4", name: "GPT-5.4" },
250+
{ value: "claude-opus-4-6", name: "Opus 4.6" },
185251
],
186252
},
253+
{
254+
id: "reasoning_effort",
255+
name: "Reasoning Effort",
256+
category: "thought_level",
257+
type: "select" as const,
258+
currentValue: currentReasoning,
259+
options: reasoningEffortOptionsForModel(),
260+
},
187261
];
188262
}
189263

@@ -283,7 +357,10 @@ const program = Effect.gen(function* () {
283357
if (request.configId === "model" && typeof request.value === "string") {
284358
currentModelId = request.value;
285359
}
286-
if (request.configId === "reasoning" && typeof request.value === "string") {
360+
if (
361+
(request.configId === "reasoning" || request.configId === "reasoning_effort") &&
362+
typeof request.value === "string"
363+
) {
287364
currentReasoning = request.value;
288365
}
289366
if (request.configId === "context" && typeof request.value === "string") {
@@ -314,6 +391,9 @@ const program = Effect.gen(function* () {
314391

315392
yield* agent.handleCancel(({ sessionId }) =>
316393
Effect.sync(() => {
394+
if (ignoreCancel) {
395+
return;
396+
}
317397
cancelledSessions.add(String(sessionId ?? "mock-session-1"));
318398
}),
319399
);
@@ -322,6 +402,20 @@ const program = Effect.gen(function* () {
322402
Effect.gen(function* () {
323403
const requestedSessionId = String(request.sessionId ?? sessionId);
324404

405+
if (promptStartedText) {
406+
yield* agent.client.sessionUpdate({
407+
sessionId: requestedSessionId,
408+
update: {
409+
sessionUpdate: "agent_message_chunk",
410+
content: { type: "text", text: promptStartedText },
411+
},
412+
});
413+
}
414+
415+
if (Number.isFinite(promptDelayMs) && promptDelayMs > 0) {
416+
yield* Effect.sleep(`${promptDelayMs} millis`);
417+
}
418+
325419
if (emitInterleavedAssistantToolCalls) {
326420
const toolCallId = "tool-call-1";
327421

@@ -453,6 +547,39 @@ const program = Effect.gen(function* () {
453547
return { stopReason: cancelled ? "cancelled" : "end_turn" };
454548
}
455549

550+
if (Number.isFinite(permissionRequestCount) && permissionRequestCount > 0) {
551+
for (let index = 0; index < permissionRequestCount; index += 1) {
552+
const toolCallId = `permission-tool-call-${index + 1}`;
553+
const payload = mockPermissionRequestPayload(permissionRequestKind);
554+
yield* agent.client.requestPermission({
555+
sessionId: requestedSessionId,
556+
toolCall: {
557+
toolCallId,
558+
title: payload.title,
559+
kind: permissionRequestKind as AcpSchema.ToolKind,
560+
status: "pending",
561+
content: payload.content,
562+
rawInput: payload.rawInput,
563+
},
564+
options: [
565+
{ optionId: "allow-once", name: "Allow once", kind: "allow_once" },
566+
{ optionId: "allow-always", name: "Allow always", kind: "allow_always" },
567+
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
568+
],
569+
});
570+
}
571+
572+
yield* agent.client.sessionUpdate({
573+
sessionId: requestedSessionId,
574+
update: {
575+
sessionUpdate: "agent_message_chunk",
576+
content: { type: "text", text: promptResponseText ?? "hello from mock" },
577+
},
578+
});
579+
580+
return { stopReason: "end_turn" };
581+
}
582+
456583
if (emitGenericToolPlaceholders) {
457584
const toolCallId = "tool-call-generic-1";
458585

apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ describe("OrchestrationEngine", () => {
124124
},
125125
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
126126
runtimeMode: "full-access" as const,
127+
pendingRuntimeMode: null,
127128
branch: null,
128129
worktreePath: null,
129130
latestTurn: null,

apps/server/src/orchestration/Layers/ProjectionPipeline.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
568568
title: event.payload.title,
569569
modelSelection: event.payload.modelSelection,
570570
runtimeMode: event.payload.runtimeMode,
571+
pendingRuntimeMode: event.payload.pendingRuntimeMode,
571572
interactionMode: event.payload.interactionMode,
572573
branch: event.payload.branch,
573574
worktreePath: event.payload.worktreePath,
@@ -645,6 +646,22 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
645646
yield* projectionThreadRepository.upsert({
646647
...existingRow.value,
647648
runtimeMode: event.payload.runtimeMode,
649+
pendingRuntimeMode: null,
650+
updatedAt: event.payload.updatedAt,
651+
});
652+
return;
653+
}
654+
655+
case "thread.pending-runtime-mode-set": {
656+
const existingRow = yield* projectionThreadRepository.getById({
657+
threadId: event.payload.threadId,
658+
});
659+
if (Option.isNone(existingRow)) {
660+
return;
661+
}
662+
yield* projectionThreadRepository.upsert({
663+
...existingRow.value,
664+
pendingRuntimeMode: event.payload.runtimeMode,
648665
updatedAt: event.payload.updatedAt,
649666
});
650667
return;
@@ -947,6 +964,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
947964
providerName: event.payload.session.providerName,
948965
runtimeMode: event.payload.session.runtimeMode,
949966
activeTurnId: event.payload.session.activeTurnId,
967+
resumeCursor: event.payload.session.resumeCursor ?? null,
950968
lastError: event.payload.session.lastError,
951969
updatedAt: event.payload.session.updatedAt,
952970
});

0 commit comments

Comments
 (0)