Skip to content
Merged
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
131 changes: 130 additions & 1 deletion assistant/src/daemon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
setBroadcastToAllClients,
} from "../acp/index.js";
import { enrichMessageWithSourcePaths } from "../agent/attachments.js";
import type { AgentEvent } from "../agent/loop.js";
import {
createAssistantMessage,
createUserMessage,
Expand Down Expand Up @@ -1673,6 +1674,125 @@ function extractConversationId(msg: ServerMessage): string | undefined {
return undefined;
}

/**
* Translate a raw {@link AgentEvent} from the agent loop into the
* corresponding {@link ServerMessage} wire frame. The normal user-turn
* path does this via the full state-aware handler in
* `conversation-agent-loop-handlers.ts`; the wake path has no tool
* accounting, title generation, or activity-state tracking to worry
* about, so we only need the subset that produces client-visible
* frames. Events that have no client-visible wire shape (usage, error,
* preview/input-json deltas, etc.) are dropped — they produce no UI.
*
* Keeping this translator co-located with the wake adapter preserves
* the runtime/daemon layering: `runtime/agent-wake.ts` never imports
* `message-protocol.ts` or wire shapes, and the daemon owns all
* translation from agent-loop semantics to client frames.
*/
function translateAgentEventToServerMessage(
event: AgentEvent,
conversationId: string,
): ServerMessage | null {
switch (event.type) {
case "text_delta":
return {
type: "assistant_text_delta",
text: event.text,
conversationId,
};
case "thinking_delta":
return {
type: "assistant_thinking_delta",
thinking: event.thinking,
conversationId,
Comment on lines +1703 to +1707
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect streamThinking setting in wake event translation

The new wake translator always emits assistant_thinking_delta for thinking_delta events, but the canonical conversation path only forwards thinking deltas when streamThinking is enabled. In deployments with thinking.streamThinking=false, wake-originated turns will still expose thinking output to clients, which diverges from configured behavior. This translation should be gated (or suppressed) the same way as the normal handler.

Useful? React with 👍 / 👎.

};
case "tool_use":
return {
type: "tool_use_start",
toolName: event.name,
input: event.input,
conversationId,
toolUseId: event.id,
};
case "tool_use_preview_start":
return {
type: "tool_use_preview_start",
toolUseId: event.toolUseId,
toolName: event.toolName,
conversationId,
};
case "tool_output_chunk":
return {
type: "tool_output_chunk",
chunk: event.chunk,
conversationId,
toolUseId: event.toolUseId,
};
case "tool_result": {
const imageBlocks = event.contentBlocks?.filter(
(b): b is Extract<typeof b, { type: "image" }> => b.type === "image",
);
const imageDataList = imageBlocks?.length
? imageBlocks.map((b) => b.source.data)
: undefined;
return {
type: "tool_result",
toolName: "",
result: event.content,
isError: event.isError,
diff: event.diff,
status: event.status,
conversationId,
imageData: imageDataList?.[0],
imageDataList,
toolUseId: event.toolUseId,
};
}
case "server_tool_start":
return {
type: "tool_use_start",
toolName: event.name,
input: event.input,
conversationId,
toolUseId: event.toolUseId,
};
case "server_tool_complete": {
let resultText = "";
if (Array.isArray(event.content) && event.content.length > 0) {
resultText = (event.content as unknown[])
.filter(
(r): r is { type: string; title: string; url: string } =>
typeof r === "object" &&
r != null &&
(r as { type?: string }).type === "web_search_result",
)
.map((r) => `${r.title}\n${r.url}`)
.join("\n\n");
}
return {
type: "tool_result",
toolName: "web_search",
result: resultText,
isError: event.isError,
conversationId,
toolUseId: event.toolUseId,
};
}
case "message_complete":
return {
type: "message_complete",
conversationId,
};
// No wire frame for these — usage/error/input_json_delta are either
// server-internal (accounting/classification) or app-only debug
// streams the client doesn't surface for wake-originated turns.
case "input_json_delta":
case "usage":
case "error":
return null;
}
}

/**
* Adapt a live {@link Conversation} to the narrow {@link WakeTarget}
* surface expected by `wakeAgentForOpportunity()`. Kept here so the
Expand All @@ -1687,7 +1807,16 @@ function conversationToWakeTarget(conversation: Conversation): WakeTarget {
pushMessage: (msg) => {
conversation.messages.push(msg);
},
emitToClient: (msg) => conversation.sendToClient(msg),
emitAgentEvent: (event) => {
const frame = translateAgentEventToServerMessage(
event,
conversation.conversationId,
);
if (frame) conversation.sendToClient(frame);
},
isProcessing: () => conversation.isProcessing(),
markProcessing: (on) => {
conversation.processing = on;
},
};
}
Loading
Loading