feat: fix post tool use feedback injection (#800)

This commit is contained in:
jnjpng
2026-02-03 14:42:34 -08:00
committed by GitHub
parent 6947e8d837
commit 103a630833
2 changed files with 100 additions and 56 deletions

View File

@@ -274,6 +274,34 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
);
expect(parsed.agent_id).toBe("agent-456");
});
test("collects stderr feedback on exit 2", async () => {
createHooksConfig({
PostToolUse: [
{
matcher: "*",
hooks: [
{
type: "command",
command: "echo 'PostToolUse feedback' >&2 && exit 2",
},
],
},
],
});
const result = await runPostToolUseHooks(
"Bash",
{ command: "ls" },
{ status: "success", output: "file.txt" },
undefined,
tempDir,
);
// Stderr collected as feedback on exit 2
expect(result.feedback).toHaveLength(1);
expect(result.feedback[0]).toContain("PostToolUse feedback");
});
});
// ============================================================================

View File

@@ -958,25 +958,30 @@ export async function executeTool(
stderr ? stderr.join("\n") : undefined,
);
// Run PostToolUse hooks (async, non-blocking)
// Run PostToolUse hooks - exit 2 injects stderr into agent context
// Note: preceding_reasoning/assistant_message not available here - tracked in accumulator for server tools
runPostToolUseHooks(
internalName,
args as Record<string, unknown>,
{
status: toolStatus,
output: getDisplayableToolReturn(flattenedResponse),
},
options?.toolCallId,
undefined, // workingDirectory
undefined, // agentId
undefined, // precedingReasoning - not available in tool manager context
undefined, // precedingAssistantMessage - not available in tool manager context
).catch(() => {
let postToolUseFeedback: string[] = [];
try {
const postHookResult = await runPostToolUseHooks(
internalName,
args as Record<string, unknown>,
{
status: toolStatus,
output: getDisplayableToolReturn(flattenedResponse),
},
options?.toolCallId,
undefined, // workingDirectory
undefined, // agentId
undefined, // precedingReasoning - not available in tool manager context
undefined, // precedingAssistantMessage - not available in tool manager context
);
postToolUseFeedback = postHookResult.feedback;
} catch {
// Silently ignore hook errors - don't affect tool execution
});
}
// Run PostToolUseFailure hooks when tool returns error status (async, feeds stderr back to agent)
// Run PostToolUseFailure hooks when tool returns error status
let postToolUseFailureFeedback: string[] = [];
if (toolStatus === "error") {
const errorOutput =
typeof flattenedResponse === "string"
@@ -994,33 +999,36 @@ export async function executeTool(
undefined, // precedingReasoning - not available in tool manager context
undefined, // precedingAssistantMessage - not available in tool manager context
);
// Feed stderr (feedback) back to the agent
if (failureHookResult.feedback.length > 0) {
const feedbackMessage = `\n\n[PostToolUseFailure hook feedback]:\n${failureHookResult.feedback.join("\n")}`;
let finalToolReturn: ToolReturnContent;
if (typeof flattenedResponse === "string") {
finalToolReturn = flattenedResponse + feedbackMessage;
} else if (Array.isArray(flattenedResponse)) {
// Append feedback as a new text content block
finalToolReturn = [
...flattenedResponse,
{ type: "text" as const, text: feedbackMessage },
];
} else {
finalToolReturn = flattenedResponse;
}
return {
toolReturn: finalToolReturn,
status: toolStatus,
...(stdout && { stdout }),
...(stderr && { stderr }),
};
}
postToolUseFailureFeedback = failureHookResult.feedback;
} catch {
// Silently ignore hook execution errors
}
}
// Combine feedback from both hook types and inject into tool return
const allFeedback = [...postToolUseFeedback, ...postToolUseFailureFeedback];
if (allFeedback.length > 0) {
const feedbackMessage = `\n\n[Hook feedback]:\n${allFeedback.join("\n")}`;
let finalToolReturn: ToolReturnContent;
if (typeof flattenedResponse === "string") {
finalToolReturn = flattenedResponse + feedbackMessage;
} else if (Array.isArray(flattenedResponse)) {
// Append feedback as a new text content block
finalToolReturn = [
...flattenedResponse,
{ type: "text" as const, text: feedbackMessage },
];
} else {
finalToolReturn = flattenedResponse;
}
return {
toolReturn: finalToolReturn,
status: toolStatus,
...(stdout && { stdout }),
...(stderr && { stderr }),
};
}
// Return the full response (truncation happens in UI layer only)
return {
toolReturn: flattenedResponse,
@@ -1057,22 +1065,26 @@ export async function executeTool(
errorMessage,
);
// Run PostToolUse hooks for error case (async, non-blocking)
runPostToolUseHooks(
internalName,
args as Record<string, unknown>,
{ status: "error", output: errorMessage },
options?.toolCallId,
undefined, // workingDirectory
undefined, // agentId
undefined, // precedingReasoning - not available in tool manager context
undefined, // precedingAssistantMessage - not available in tool manager context
).catch(() => {
// Run PostToolUse hooks for error case - exit 2 injects stderr
let postToolUseFeedback: string[] = [];
try {
const postHookResult = await runPostToolUseHooks(
internalName,
args as Record<string, unknown>,
{ status: "error", output: errorMessage },
options?.toolCallId,
undefined, // workingDirectory
undefined, // agentId
undefined, // precedingReasoning - not available in tool manager context
undefined, // precedingAssistantMessage - not available in tool manager context
);
postToolUseFeedback = postHookResult.feedback;
} catch {
// Silently ignore hook errors
});
}
// Run PostToolUseFailure hooks (async, non-blocking, feeds stderr back to agent)
let finalErrorMessage = errorMessage;
// Run PostToolUseFailure hooks - exit 2 injects stderr
let postToolUseFailureFeedback: string[] = [];
try {
const failureHookResult = await runPostToolUseFailureHooks(
internalName,
@@ -1085,14 +1097,18 @@ export async function executeTool(
undefined, // precedingReasoning - not available in tool manager context
undefined, // precedingAssistantMessage - not available in tool manager context
);
// Feed stderr (feedback) back to the agent
if (failureHookResult.feedback.length > 0) {
finalErrorMessage = `${errorMessage}\n\n[PostToolUseFailure hook feedback]:\n${failureHookResult.feedback.join("\n")}`;
}
postToolUseFailureFeedback = failureHookResult.feedback;
} catch {
// Silently ignore hook execution errors
}
// Combine feedback from both hook types
const allFeedback = [...postToolUseFeedback, ...postToolUseFailureFeedback];
const finalErrorMessage =
allFeedback.length > 0
? `${errorMessage}\n\n[Hook feedback]:\n${allFeedback.join("\n")}`
: errorMessage;
// Don't console.error here - it pollutes the TUI
// The error message is already returned in toolReturn
return {