镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit 061f6e9

Browse files
only21milalanjdsclaude
committed
fix(server): imported ACP thinking shows as reasoning, not replies
Loading an ACP session replays agent_thought_chunk updates into the adapter's snapshot as assistant text, so the native history import wrote the agent's thinking as chat replies. The snapshot now lists which of its messages came from thought chunks, and the importer writes those as run-less reasoning items, the item type live ACP turns use. Co-authored-by: Alan Justino da Silva <155623+alanjds@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent de09c91 commit 061f6e9

5 files changed

Lines changed: 72 additions & 62 deletions

File tree

‎apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2620,12 +2620,20 @@ describe("AcpAdapterV2", () => {
26202620
content: { type: "text", text: "after plan" },
26212621
},
26222622
});
2623+
yield* sessionUpdateHandler!({
2624+
sessionId: "mock-session-1",
2625+
update: {
2626+
sessionUpdate: "agent_thought_chunk",
2627+
content: { type: "text", text: "weighing it" },
2628+
},
2629+
});
26232630

26242631
const snapshot = yield* runtime.readThreadSnapshot({ providerThread });
26252632
assert.deepEqual(
26262633
snapshot.messages.map((message) => message.text),
2627-
["before plan", "after plan"],
2634+
["before plan", "after plan", "weighing it"],
26282635
);
2636+
assert.deepEqual(snapshot.reasoningMessageIds, [snapshot.messages[2]!.id]);
26292637
}).pipe(Effect.provide(layerTest), Effect.scoped),
26302638
);
26312639

‎apps/server/src/project/AgentSessionImporter.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,10 @@ it.effect(
489489
providerThread,
490490
providerTurns: [],
491491
runtimeRequests: [],
492+
reasoningMessageIds: [MessageId.make("replayed:Check the build log")],
492493
messages: [
493494
replayed("user", "Fix the build"),
495+
replayed("assistant", "Check the build log"),
494496
replayed("assistant", "Fixed it"),
495497
replayed("assistant", " "),
496498
],
@@ -524,7 +526,7 @@ it.effect(
524526
).toBeUndefined();
525527

526528
failRead = false;
527-
expect(yield* importHistory).toBe(2);
529+
expect(yield* importHistory).toBe(3);
528530
expect(resolved).toEqual([
529531
CODEX_MODEL_SELECTION.instanceId,
530532
CODEX_MODEL_SELECTION.instanceId,
@@ -540,7 +542,7 @@ it.effect(
540542
);
541543
expect(yield* importHistory).toBe(0);
542544
expect(opened).toHaveLength(2);
543-
const page = yield* projections.getTimelinePage(threadId, { view: "messages", limit: 10 });
545+
const page = yield* projections.getTimelinePage(threadId, { view: "activity", limit: 10 });
544546
expect(
545547
page.items.map(({ item }) => [
546548
item.type,
@@ -549,6 +551,7 @@ it.effect(
549551
]),
550552
).toEqual([
551553
["user_message", null, "Fix the build"],
554+
["reasoning", null, "Check the build log"],
552555
["assistant_message", null, "Fixed it"],
553556
]);
554557
}).pipe(Effect.scoped, Effect.provide(persistence)),

‎apps/server/src/project/AgentSessionImporter.ts‎

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,14 @@ function dateTime(value: string): DateTime.Utc {
107107
return DateTime.makeUnsafe(value);
108108
}
109109

110+
type HistoryMessage = Omit<AgentSessionScanner.AgentSessionThreadMessage, "role"> & {
111+
readonly role: AgentSessionScanner.AgentSessionThreadMessage["role"] | "reasoning";
112+
};
113+
110114
function messageEvents(input: {
111115
readonly threadId: ThreadId;
112116
readonly index: number;
113-
readonly message: AgentSessionScanner.AgentSessionThreadMessage;
117+
readonly message: HistoryMessage;
114118
}): ReadonlyArray<OrchestrationV2DomainEvent> {
115119
const ordinal = input.index + 1;
116120
const suffix = String(input.index).padStart(6, "0");
@@ -119,20 +123,6 @@ function messageEvents(input: {
119123
`${IMPORT_EVENT_PREFIX}:turn-item:${input.threadId}:${suffix}`,
120124
);
121125
const at = dateTime(input.message.createdAt);
122-
const message: OrchestrationV2ConversationMessage = {
123-
createdBy: input.message.role === "user" ? "user" : "agent",
124-
creationSource: "server",
125-
id: messageId,
126-
threadId: input.threadId,
127-
runId: null,
128-
nodeId: null,
129-
role: input.message.role,
130-
text: input.message.text,
131-
attachments: [],
132-
streaming: false,
133-
createdAt: at,
134-
updatedAt: at,
135-
};
136126
const common = {
137127
id: turnItemId,
138128
threadId: input.threadId,
@@ -149,6 +139,32 @@ function messageEvents(input: {
149139
completedAt: at,
150140
updatedAt: at,
151141
};
142+
const turnItemEvent = (payload: OrchestrationV2TurnItem): OrchestrationV2DomainEvent => ({
143+
id: EventId.make(`${IMPORT_EVENT_PREFIX}:turn-item:${input.threadId}:${suffix}`),
144+
type: "turn-item.updated",
145+
threadId: input.threadId,
146+
occurredAt: at,
147+
payload,
148+
});
149+
if (input.message.role === "reasoning") {
150+
return [
151+
turnItemEvent({ ...common, type: "reasoning", text: input.message.text, streaming: false }),
152+
];
153+
}
154+
const message: OrchestrationV2ConversationMessage = {
155+
createdBy: input.message.role === "user" ? "user" : "agent",
156+
creationSource: "server",
157+
id: messageId,
158+
threadId: input.threadId,
159+
runId: null,
160+
nodeId: null,
161+
role: input.message.role,
162+
text: input.message.text,
163+
attachments: [],
164+
streaming: false,
165+
createdAt: at,
166+
updatedAt: at,
167+
};
152168
const turnItem: OrchestrationV2TurnItem =
153169
input.message.role === "user"
154170
? {
@@ -176,13 +192,7 @@ function messageEvents(input: {
176192
occurredAt: at,
177193
payload: message,
178194
},
179-
{
180-
id: EventId.make(`${IMPORT_EVENT_PREFIX}:turn-item:${input.threadId}:${suffix}`),
181-
type: "turn-item.updated",
182-
threadId: input.threadId,
183-
occurredAt: at,
184-
payload: turnItem,
185-
},
195+
turnItemEvent(turnItem),
186196
];
187197
}
188198

@@ -494,7 +504,7 @@ const make = Effect.gen(function* () {
494504
);
495505
// Thread commands write whole thread rows, so hold their lock from the
496506
// re-read through the write to keep both this marker and their changes.
497-
const commit = (messages: ReadonlyArray<AgentSessionScanner.AgentSessionThreadMessage>) =>
507+
const commit = (messages: ReadonlyArray<HistoryMessage>) =>
498508
threadCommands.withLock(
499509
input.threadId,
500510
Effect.gen(function* () {
@@ -554,11 +564,12 @@ const make = Effect.gen(function* () {
554564
Effect.timeout(NATIVE_HISTORY_READ_TIMEOUT),
555565
Effect.scoped,
556566
);
567+
const reasoningIds = new Set(snapshot.reasoningMessageIds);
557568
const messages = snapshot.messages.flatMap((message) =>
558569
(message.role === "user" || message.role === "assistant") && message.text.trim() !== ""
559570
? [
560571
{
561-
role: message.role,
572+
role: reasoningIds.has(message.id) ? ("reasoning" as const) : message.role,
562573
text: message.text,
563574
createdAt: DateTime.formatIso(message.createdAt),
564575
},

‎packages/provider-acp/src/server/adapter.ts‎

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,11 +1641,22 @@ type PendingRuntimeRequest = {
16411641
interface SnapshotMessageState {
16421642
readonly order: Array<string>;
16431643
readonly messages: Map<string, OrchestrationV2ConversationMessage>;
1644+
/** Messages loaded from thought chunks; `messages` stores them as assistant text. */
1645+
readonly reasoningIds: ReadonlySet<OrchestrationV2ConversationMessage["id"]>;
16441646
loadingRole: "user" | "assistant" | "thought" | null;
16451647
loadingMessageId: string | null;
16461648
loadingIndex: number;
16471649
}
16481650

1651+
const emptySnapshotState = (): SnapshotMessageState => ({
1652+
order: [],
1653+
messages: new Map(),
1654+
reasoningIds: new Set(),
1655+
loadingRole: null,
1656+
loadingMessageId: null,
1657+
loadingIndex: 0,
1658+
});
1659+
16491660
/** How long a Stop waits for the agent to confirm before the user is offered Force stop. */
16501661
const ACP_STOP_CONFIRM_TIMEOUT_MS = 10_000;
16511662
const TOOL_UPDATE_PERSIST_EVERY = 10;
@@ -1962,13 +1973,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
19621973
options.testHooks?.onNativeResponseLifecycle ?? (() => Effect.void);
19631974
const nextElicitationOrdinal = yield* Ref.make(0);
19641975
const providerTurns = yield* Ref.make(new Map<string, OrchestrationV2ProviderTurn>());
1965-
const snapshot = yield* Ref.make<SnapshotMessageState>({
1966-
order: [],
1967-
messages: new Map(),
1968-
loadingRole: null,
1969-
loadingMessageId: null,
1970-
loadingIndex: 0,
1971-
});
1976+
const snapshot = yield* Ref.make(emptySnapshotState());
19721977

19731978
const awaitRuntimeTeardown = Effect.fnUntraced(function* () {
19741979
const state = yield* Ref.get(runtimeTeardownState);
@@ -4548,6 +4553,10 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
45484553
return {
45494554
order: current.order.includes(key) ? current.order : [...current.order, key],
45504555
messages,
4556+
reasoningIds:
4557+
role === "thought"
4558+
? new Set(current.reasoningIds).add(messageId)
4559+
: current.reasoningIds,
45514560
loadingRole: role,
45524561
loadingMessageId: sourceMessageId ?? (startsNew ? null : current.loadingMessageId),
45534562
loadingIndex,
@@ -7105,13 +7114,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
71057114
yield* Ref.set(promptInstructionStates, new Map());
71067115
yield* Ref.set(compactionMarkers, new Map());
71077116
yield* Ref.set(providerTurns, new Map());
7108-
yield* Ref.set(snapshot, {
7109-
order: [],
7110-
messages: new Map(),
7111-
loadingRole: null,
7112-
loadingMessageId: null,
7113-
loadingIndex: 0,
7114-
});
7117+
yield* Ref.set(snapshot, emptySnapshotState());
71157118
yield* continuationPermit.withPermit(
71167119
Effect.gen(function* () {
71177120
yield* Ref.update(continuationGeneration, (value) => value + 1);
@@ -8049,13 +8052,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
80498052
yield* Ref.set(activeSessionSetup, null);
80508053
yield* Ref.set(activeSelection, null);
80518054
yield* Ref.set(activeInteractionMode, null);
8052-
yield* Ref.set(snapshot, {
8053-
order: [],
8054-
messages: new Map(),
8055-
loadingRole: null,
8056-
loadingMessageId: null,
8057-
loadingIndex: 0,
8058-
});
8055+
yield* Ref.set(snapshot, emptySnapshotState());
80598056
return true;
80608057
});
80618058

@@ -8737,13 +8734,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
87378734
threadInput.providerThread.appThreadId,
87388735
).pipe(Effect.tapError(() => restorePreviousItemIdentity));
87398736
if ((yield* Ref.get(activeSessionId)) !== sessionId || restartAfterInterrupt) {
8740-
yield* Ref.set(snapshot, {
8741-
order: [],
8742-
messages: new Map(),
8743-
loadingRole: null,
8744-
loadingMessageId: null,
8745-
loadingIndex: 0,
8746-
});
8737+
yield* Ref.set(snapshot, emptySnapshotState());
87478738
const activated = yield* activateSession(
87488739
sessionId,
87498740
threadInput.providerThread.appThreadId,
@@ -9241,13 +9232,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
92419232
detail: "ACP driver does not support session/load snapshots",
92429233
});
92439234
}
9244-
yield* Ref.set(snapshot, {
9245-
order: [],
9246-
messages: new Map(),
9247-
loadingRole: null,
9248-
loadingMessageId: null,
9249-
loadingIndex: 0,
9250-
});
9235+
yield* Ref.set(snapshot, emptySnapshotState());
92519236
const mcpContext = yield* readMcpContext(
92529237
snapshotInput.providerThread.appThreadId,
92539238
);
@@ -9279,6 +9264,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
92799264
const message = state.messages.get(key);
92809265
return message === undefined ? [] : [message];
92819266
}),
9267+
reasoningMessageIds: [...state.reasoningIds],
92829268
runtimeRequests: [],
92839269
providerPayload: { protocol: ACP_PROTOCOL, sessionId },
92849270
};

‎packages/provider-core/src/server/ProviderAdapter.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,8 @@ export interface ProviderAdapterV2ThreadSnapshot {
517517
readonly providerThread: OrchestrationV2ProviderThread;
518518
readonly providerTurns: ReadonlyArray<OrchestrationV2ProviderTurn>;
519519
readonly messages: ReadonlyArray<OrchestrationV2ConversationMessage>;
520+
/** Ids in `messages` that hold the agent's reasoning, not its reply. */
521+
readonly reasoningMessageIds?: ReadonlyArray<OrchestrationV2ConversationMessage["id"]>;
520522
readonly runtimeRequests: ReadonlyArray<OrchestrationV2RuntimeRequest>;
521523
readonly providerPayload?: unknown;
522524
}

0 commit comments

Comments
 (0)