fix: surface real tool error string in opencode log handler (#736)
opencode's `ToolStateError` carries the failure reason on `state.error`, not `state.output`. our log handler was reading `state.output` and falling back to `(no error message)`, so every tool failure logged a useless line. type the state as a discriminated union (mirrors @opencode-ai/sdk) so the field misread becomes a compile error. operator-facing only: the model already received the real error via opencode's tool-result envelope (verified by running webfetch against a known-404 URL — model reported "Error: Request failed with status code: 404" verbatim). closes #662
This commit is contained in:
committed by
pullfrog[bot]
parent
1f4c3031be
commit
8d6460da1c
+30
-22
@@ -307,6 +307,17 @@ interface OpenCodeStepFinishEvent {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tool-part state, mirroring opencode's `ToolState` (anomalyco/opencode
|
||||||
|
* `session/message-v2.ts`). error parts carry the reason on `error`,
|
||||||
|
* completed parts on `output` — reading the wrong field is what caused
|
||||||
|
* the silent `(no error message)` log in #662.
|
||||||
|
*/
|
||||||
|
type ToolState =
|
||||||
|
| { status: "pending" | "running"; input?: unknown }
|
||||||
|
| { status: "completed"; input?: unknown; output: string }
|
||||||
|
| { status: "error"; input?: unknown; error: string };
|
||||||
|
|
||||||
interface OpenCodeToolUseEvent {
|
interface OpenCodeToolUseEvent {
|
||||||
type: "tool_use";
|
type: "tool_use";
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
@@ -315,7 +326,7 @@ interface OpenCodeToolUseEvent {
|
|||||||
id?: string;
|
id?: string;
|
||||||
callID?: string;
|
callID?: string;
|
||||||
tool?: string;
|
tool?: string;
|
||||||
state?: { status?: string; input?: unknown; output?: string };
|
state?: ToolState;
|
||||||
};
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -324,7 +335,7 @@ interface OpenCodeToolResultEvent {
|
|||||||
type: "tool_result";
|
type: "tool_result";
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
sessionID?: string;
|
sessionID?: string;
|
||||||
part?: { callID?: string; state?: { status?: string; output?: string } };
|
part?: { callID?: string; state?: ToolState };
|
||||||
tool_id?: string;
|
tool_id?: string;
|
||||||
status?: "success" | "error";
|
status?: "success" | "error";
|
||||||
output?: string;
|
output?: string;
|
||||||
@@ -703,11 +714,9 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
|
|||||||
// status="error" through the same `tool_use` event the CLI's run-loop
|
// status="error" through the same `tool_use` event the CLI's run-loop
|
||||||
// (and our injected plugin for subagent parts) emits — without this
|
// (and our injected plugin for subagent parts) emits — without this
|
||||||
// branch the only signal in the user's logs is `» <tool>(...)` with
|
// branch the only signal in the user's logs is `» <tool>(...)` with
|
||||||
// no indication the call failed. error info lives in `state.output`
|
// no indication the call failed.
|
||||||
// (an error string set by the tool layer).
|
|
||||||
if (event.part?.state?.status === "error") {
|
if (event.part?.state?.status === "error") {
|
||||||
const errorMsg = event.part.state.output ?? "(no error message)";
|
log.info(withLabel(label, `» tool call failed: ${event.part.state.error}`));
|
||||||
log.info(withLabel(label, `» tool call failed: ${errorMsg}`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// agent's explicit MCP report_progress takes priority over todo tracking
|
// agent's explicit MCP report_progress takes priority over todo tracking
|
||||||
@@ -723,8 +732,14 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
|
|||||||
},
|
},
|
||||||
tool_result: (event: OpenCodeToolResultEvent) => {
|
tool_result: (event: OpenCodeToolResultEvent) => {
|
||||||
const toolId = event.part?.callID || event.tool_id;
|
const toolId = event.part?.callID || event.tool_id;
|
||||||
const status = event.part?.state?.status || event.status || "unknown";
|
const state = event.part?.state;
|
||||||
const output = event.part?.state?.output || event.output;
|
const status = state?.status ?? event.status ?? "unknown";
|
||||||
|
const payload =
|
||||||
|
state?.status === "completed"
|
||||||
|
? state.output
|
||||||
|
: state?.status === "error"
|
||||||
|
? state.error
|
||||||
|
: event.output;
|
||||||
const label = eventLabel(event);
|
const label = eventLabel(event);
|
||||||
|
|
||||||
timerFor(label).markToolResult();
|
timerFor(label).markToolResult();
|
||||||
@@ -743,12 +758,12 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
|
|||||||
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
||||||
if (toolId && taskDispatchByCallID.has(toolId)) {
|
if (toolId && taskDispatchByCallID.has(toolId)) {
|
||||||
const dispatch = taskDispatchByCallID.get(toolId);
|
const dispatch = taskDispatchByCallID.get(toolId);
|
||||||
if (dispatch) emitSubagentFinished(dispatch, status, output, "exact");
|
if (dispatch) emitSubagentFinished(dispatch, status, payload, "exact");
|
||||||
} else {
|
} else {
|
||||||
const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
|
const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
|
||||||
if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
|
if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
|
||||||
const dispatch = pendingTaskDispatches[0]!;
|
const dispatch = pendingTaskDispatches[0]!;
|
||||||
emitSubagentFinished(dispatch, status, output, "fifo");
|
emitSubagentFinished(dispatch, status, payload, "fifo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,13 +780,8 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
|
|||||||
`» ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
|
`» ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (output) {
|
if (payload) {
|
||||||
log.debug(
|
log.debug(withLabel(label, ` output: ${payload}`));
|
||||||
withLabel(
|
|
||||||
label,
|
|
||||||
` output: ${typeof output === "string" ? output : JSON.stringify(output)}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (toolDuration > 5000) {
|
if (toolDuration > 5000) {
|
||||||
log.info(
|
log.info(
|
||||||
@@ -784,11 +794,9 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
const errorMsg = typeof output === "string" ? output : JSON.stringify(output);
|
log.info(withLabel(label, `» tool call failed: ${payload ?? "(no error message)"}`));
|
||||||
log.info(withLabel(label, `» tool call failed: ${errorMsg}`));
|
} else if (payload) {
|
||||||
} else if (output) {
|
log.debug(withLabel(label, `tool output: ${payload}`));
|
||||||
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
|
||||||
log.debug(withLabel(label, `tool output: ${outputStr}`));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (event: OpenCodeErrorEvent) => {
|
error: (event: OpenCodeErrorEvent) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user