Skip to content

fix(sdk): name failing tools in limits-guard; host-provided image resizer - #50

Merged
matej21 merged 2 commits into
mainfrom
fix/limits-tool-name-and-image-resizer
Oct 15, 2026
Merged

matej21 merged 2 commits into
mainfrom
fix/limits-tool-name-and-image-resizer

Conversation

@matej21

@matej21 matej21 commented Oct 15, 2026

Copy link
Copy Markdown
Member

Two independent fixes in @roj-ai/sdk.

1. limits-guard counted every tool failure under unknown

A host saw its coding agent retry a failing tool 13 times, although maxConsecutiveToolFailures: 5 and maxRepeatedToolCalls: 4 were configured.

Cause: plugin reducers run after the core reducer (createApplyEvent composes baseReducer first). By the time limits-guard handles tool_completed / tool_failed, the core reducer has cleared executingToolCall and removed the call from pendingToolCalls. resolveToolName therefore always returned unknown, so:

  • failures of different tools shared one counter,
  • any successful tool call reset that counter,
  • the status warning to the model ("Tool X has failed N times") never named the tool.

Reproduced against the reducer: fail, success, fail, fail gave {"unknown":{"count":2}}.

Fix: read the name from the pendingToolResults entry with the same tool call id. The core reducer has just added that entry, with the name it derived from the same sources (executingToolCall, then pendingToolCalls). The limits counters now use the same name as the tool result that the model sees.

The repeated identical call limit (maxRepeatedToolCalls) was not affected. It counts on tool_started, which carries toolName. A regression test now covers it.

2. Host-provided image resizer

bootstrap() always built a VipsImageResizer for the LLM provider's image processor and for the upload image classifier. A host without OS processes cannot run vipsheader / vipsthumbnail, for example a host that runs the agent inside a Cloudflare Durable Object. On such a host, large screenshots are never shrunk and do not reach the model.

New public API:

// @roj-ai/sdk/platform (also exported as types from @roj-ai/sdk)
interface Platform {
	// ...
	/** Absent on hosts with a process table, which keep resizing with vips. */
	imageResizer?: ImageResizer
}

interface ImageResizer {
	resize(filePath: string, mimeType: string, options?: ImageResizeOptions): Promise<ImageResizeResult>
}
interface ImageResizeOptions { maxFileSizeBytes?: number; maxDimension?: number; signal?: AbortSignal }
interface ImageResizeResult { path: string; mimeType: string; tempFile?: string }
  • The resizer goes on Platform, like the other optional host capabilities (shell, git, createServiceExecutor, sessionLog, llmCallLog).
  • The existing ImageResizer interface already covers what both call sites need, so this PR documents and exports it instead of adding a new interface.
  • Paths are on the host's FileSystem: callers read the result and unlink tempFile through platform.fs.
  • bootstrap() resolves one resizer, platform.imageResizer ?? new VipsImageResizer(...), and passes it to both call sites. The two vips instances it replaces had identical configuration, and the classifier still passes its own 1024px limit on each call.

Tests

  • plugins/limits-guard/tool-failures.test.ts (reducer): a failure is counted under the tool's name. A failing tool that alternates with successes of another tool still reaches the limit. A success of the same tool resets the count. Two of these tests fail on main.
  • limits-guard.integration.test.ts: a failing tool that alternates with a successful one gets a status warning that names it (Tool "flaky_tool" has failed 2 times), then the agent is paused with Tool 'flaky_tool' failed 3 consecutive times. On main, the agent runs until maxTurns. A second test checks that the repeated identical call limit pauses the agent first (4 repeats with maxConsecutiveToolFailures: 5).
  • bootstrap-image-resizer.test.ts: a host resizer is called by the Anthropic provider's image processor and by the image classifier, with no process calls. Without a host resizer, both paths run vipsheader. The first test fails on main.
  • Local: bun run lint, bun run ts:build, tsc -p packages/sdk/tsconfig.test.json, and bun run test (1828 pass, 0 fail).

🤖 Generated with Claude Code

matej21 and others added 2 commits October 15, 2026 17:29
The limits-guard reducer runs after the core reducer, which has already
cleared the executing tool call and dropped the call from pendingToolCalls.
Every tool_completed/tool_failed therefore resolved to the tool name
"unknown": failures of different tools shared one counter, any successful
tool reset it, and the warning to the model never named the tool. In a
host that retried a failing tool many times, maxConsecutiveToolFailures
never fired.

Take the name from the pendingToolResults entry that the core reducer
has just recorded for the same tool call id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bootstrap() always built a VipsImageResizer, for both the LLM provider's
image processor and the upload image classifier. A host without OS
processes, such as one that runs the agent inside a Cloudflare Durable
Object, cannot run vipsheader/vipsthumbnail, so large screenshots never
shrink and do not reach the model.

Platform gets an optional imageResizer, like the other host capabilities
(shell, git, sessionLog, llmCallLog). Both call sites use it; a host that
omits it keeps the vips resizer. The existing ImageResizer interface is
what both call sites need, so it is documented and exported from the
package root and @roj-ai/sdk/platform instead of adding a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matej21
matej21 marked this pull request as ready for review October 15, 2026 15:36
@matej21
matej21 merged commit 3c03c82 into main Oct 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant