fix(agents): guard interactive-summary against non-object outputs

Without an object check, a malformed wire payload (string/number/null
inside an unknown) would throw at the `'skipped' in resume` line. Bail
on non-object outputs and return undefined so the caller falls back to
just the tool name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Robin Braumann 2026-04-26 20:42:04 +02:00
parent 79fc8614f0
commit 9c2fbc7218
2 changed files with 17 additions and 1 deletions

View File

@ -15,6 +15,15 @@ describe('summariseInteractiveOutput', () => {
expect(summariseInteractiveOutput(ASK_QUESTION_TOOL_NAME, undefined)).toBeUndefined();
});
it.each([null, 'oops', 42, true, ['x']])(
'returns undefined for non-object output (%p)',
(value) => {
expect(summariseInteractiveOutput(ASK_CREDENTIAL_TOOL_NAME, value)).toBeUndefined();
expect(summariseInteractiveOutput(ASK_LLM_TOOL_NAME, value)).toBeUndefined();
expect(summariseInteractiveOutput(ASK_QUESTION_TOOL_NAME, value)).toBeUndefined();
},
);
it('joins ask_question option labels when input + output present', () => {
const input = {
question: 'Where to post?',

View File

@ -17,12 +17,19 @@ import {
* Returns `undefined` for non-interactive tools or when the output isn't
* shaped as expected callers fall back to rendering just the tool name.
*/
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function summariseInteractiveOutput(
toolName: string,
output: unknown,
input?: unknown,
): string | undefined {
if (output === undefined || output === null) return undefined;
// Output comes off the wire as `unknown`; treat anything non-object-shaped
// as malformed and bail. This prevents `in` / property access from
// throwing when a malformed payload sneaks through.
if (!isPlainObject(output)) return undefined;
if (toolName === ASK_QUESTION_TOOL_NAME) {
const resume = output as AskQuestionResume;