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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2620,12 +2620,21 @@ describe("AcpAdapterV2", () => {
content: { type: "text", text: "after plan" },
},
});
yield* sessionUpdateHandler!({
sessionId: "mock-session-1",
update: {
sessionUpdate: "agent_thought_chunk",
content: { type: "text", text: "weighing it" },
},
});

const snapshot = yield* runtime.readThreadSnapshot({ providerThread });
assert.deepEqual(
snapshot.messages.map((message) => message.text),
["before plan", "after plan"],
["before plan", "after plan", "weighing it"],
);
// Thoughts replay as assistant text; the snapshot says which ones they are.
assert.deepEqual(snapshot.reasoningMessageIds, [snapshot.messages[2]!.id]);
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

Expand Down
14 changes: 13 additions & 1 deletion apps/server/src/project/AgentSessionImporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,8 +489,10 @@ it.effect(
providerThread,
providerTurns: [],
runtimeRequests: [],
reasoningMessageIds: [replayed("assistant", "Check the build log").id],
messages: [
replayed("user", "Fix the build"),
replayed("assistant", "Check the build log"),
replayed("assistant", "Fixed it"),
replayed("assistant", " "),
],
Expand Down Expand Up @@ -524,7 +526,7 @@ it.effect(
).toBeUndefined();

failRead = false;
expect(yield* importHistory).toBe(2);
expect(yield* importHistory).toBe(3);
expect(resolved).toEqual([
CODEX_MODEL_SELECTION.instanceId,
CODEX_MODEL_SELECTION.instanceId,
Expand All @@ -551,6 +553,16 @@ it.effect(
["user_message", null, "Fix the build"],
["assistant_message", null, "Fixed it"],
]);
// Replayed thinking is a reasoning item, not an assistant reply.
const activity = yield* projections.getTimelinePage(threadId, {
view: "activity",
limit: 10,
});
expect(
activity.items.flatMap(({ item }) =>
item.type === "reasoning" ? [[item.runId, item.text]] : [],
),
).toEqual([[null, "Check the build log"]]);
}).pipe(Effect.scoped, Effect.provide(persistence)),
);

Expand Down
44 changes: 41 additions & 3 deletions apps/server/src/project/AgentSessionImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,17 @@ function dateTime(value: string): DateTime.Utc {
return DateTime.makeUnsafe(value);
}

/** A native-history entry; `reasoning` is the agent's thinking, shown apart from its replies. */
interface NativeHistoryMessage {
readonly role: AgentSessionScanner.AgentSessionThreadMessage["role"] | "reasoning";
readonly text: string;
readonly createdAt: string;
}

function messageEvents(input: {
readonly threadId: ThreadId;
readonly index: number;
readonly message: AgentSessionScanner.AgentSessionThreadMessage;
readonly message: NativeHistoryMessage;
}): ReadonlyArray<OrchestrationV2DomainEvent> {
const ordinal = input.index + 1;
const suffix = String(input.index).padStart(6, "0");
Expand All @@ -119,6 +126,36 @@ function messageEvents(input: {
`${IMPORT_EVENT_PREFIX}:turn-item:${input.threadId}:${suffix}`,
);
const at = dateTime(input.message.createdAt);
if (input.message.role === "reasoning") {
const reasoning: OrchestrationV2TurnItem = {
id: turnItemId,
threadId: input.threadId,
runId: null,
nodeId: null,
providerThreadId: null,
providerTurnId: null,
nativeItemRef: null,
parentItemId: null,
ordinal,
status: "completed",
title: null,
startedAt: at,
completedAt: at,
updatedAt: at,
type: "reasoning",
text: input.message.text,
streaming: false,
};
return [
{
id: EventId.make(`${IMPORT_EVENT_PREFIX}:turn-item:${input.threadId}:${suffix}`),
type: "turn-item.updated",
threadId: input.threadId,
occurredAt: at,
payload: reasoning,
},
];
}
const message: OrchestrationV2ConversationMessage = {
createdBy: input.message.role === "user" ? "user" : "agent",
creationSource: "server",
Expand Down Expand Up @@ -494,7 +531,7 @@ const make = Effect.gen(function* () {
);
// Thread commands write whole thread rows, so hold their lock from the
// re-read through the write to keep both this marker and their changes.
const commit = (messages: ReadonlyArray<AgentSessionScanner.AgentSessionThreadMessage>) =>
const commit = (messages: ReadonlyArray<NativeHistoryMessage>) =>
threadCommands.withLock(
input.threadId,
Effect.gen(function* () {
Expand Down Expand Up @@ -554,11 +591,12 @@ const make = Effect.gen(function* () {
Effect.timeout(NATIVE_HISTORY_READ_TIMEOUT),
Effect.scoped,
);
const reasoningIds = new Set(snapshot.reasoningMessageIds ?? []);
const messages = snapshot.messages.flatMap((message) =>
(message.role === "user" || message.role === "assistant") && message.text.trim() !== ""
? [
{
role: message.role,
role: reasoningIds.has(message.id) ? ("reasoning" as const) : message.role,
text: message.text,
createdAt: DateTime.formatIso(message.createdAt),
},
Expand Down
13 changes: 13 additions & 0 deletions packages/provider-acp/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,8 @@ type PendingRuntimeRequest = {
interface SnapshotMessageState {
readonly order: Array<string>;
readonly messages: Map<string, OrchestrationV2ConversationMessage>;
/** Keys of `messages` loaded from thought chunks; they are stored as assistant text. */
readonly reasoningKeys?: ReadonlySet<string>;
loadingRole: "user" | "assistant" | "thought" | null;
loadingMessageId: string | null;
loadingIndex: number;
Expand Down Expand Up @@ -4548,6 +4550,11 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
return {
order: current.order.includes(key) ? current.order : [...current.order, key],
messages,
...(role === "thought"
? { reasoningKeys: new Set([...(current.reasoningKeys ?? []), key]) }
: current.reasoningKeys === undefined
? {}
: { reasoningKeys: current.reasoningKeys }),
loadingRole: role,
loadingMessageId: sourceMessageId ?? (startsNew ? null : current.loadingMessageId),
loadingIndex,
Expand Down Expand Up @@ -9279,6 +9286,12 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
const message = state.messages.get(key);
return message === undefined ? [] : [message];
}),
reasoningMessageIds: state.order.flatMap((key) => {
const message = state.messages.get(key);
return message !== undefined && state.reasoningKeys?.has(key) === true
? [message.id]
: [];
}),
runtimeRequests: [],
providerPayload: { protocol: ACP_PROTOCOL, sessionId },
};
Expand Down
2 changes: 2 additions & 0 deletions packages/provider-core/src/server/ProviderAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ export interface ProviderAdapterV2ThreadSnapshot {
readonly providerThread: OrchestrationV2ProviderThread;
readonly providerTurns: ReadonlyArray<OrchestrationV2ProviderTurn>;
readonly messages: ReadonlyArray<OrchestrationV2ConversationMessage>;
/** Ids in `messages` that hold the agent's reasoning, not its reply. */
readonly reasoningMessageIds?: ReadonlyArray<OrchestrationV2ConversationMessage["id"]>;
readonly runtimeRequests: ReadonlyArray<OrchestrationV2RuntimeRequest>;
readonly providerPayload?: unknown;
}
Expand Down
Loading