AgentToolkit.call() never checks response.ok:
private async call(method: string, path: string, body?: Record<string, unknown>): Promise<unknown> {
const response = await fetch(`${this.baseUrl}${path}`, { ... });
return response.json(); // <-- no status check
}
Every one of the 10 tools routes through this. A 402, 403, 429, or 500 is parsed as JSON and returned to the agent as if it were a successful result.
Failure scenario
An agent calls wave_create_stream with an API key whose rate_limit_tier is null (a real, currently-open condition — wave-av/wave-gateway#622). The gateway returns 402 with a billing error body. call() returns that body. The tool resolves successfully. The agent proceeds as though it has a live stream and continues issuing calls against a stream that was never created.
The same shape hides a 429: instead of backing off, the agent treats the rate-limit body as data and keeps hammering — the exact behaviour .wave-rules/require-rate-limit-awareness.md exists to prevent.
It also fails less gracefully than it looks: if the error body is not JSON (an HTML 502 from an edge, say), response.json() throws a raw SyntaxError with no status, no URL, and no body — actively worse for debugging than the error it is masking.
Fix
#58 added WaveToolError and assertOk() in src/tools/shared.ts for exactly this, and the three new toolkits use them: throw on non-2xx, carrying status and the response body verbatim, with .isRateLimited flagging 429. AgentToolkit should adopt the same path:
const response = await fetch(...);
await assertOk(toolName, response);
return response.json();
This is a breaking change and should be treated as one, which is why #58 deliberately left it alone rather than sneaking it in. Callers who today receive an error body as a resolved value will start seeing a thrown WaveToolError. That is the correct behaviour, but it warrants a minor-version bump and a CHANGELOG note.
Sequencing: do #60 first. Changing error semantics across all 10 tools with no running test suite is how a regression ships quietly.
Pre-existing; surfaced while building #58 for wave-av/wave-context#72.
AgentToolkit.call()never checksresponse.ok:Every one of the 10 tools routes through this. A
402,403,429, or500is parsed as JSON and returned to the agent as if it were a successful result.Failure scenario
An agent calls
wave_create_streamwith an API key whoserate_limit_tieris null (a real, currently-open condition — wave-av/wave-gateway#622). The gateway returns402with a billing error body.call()returns that body. The tool resolves successfully. The agent proceeds as though it has a live stream and continues issuing calls against a stream that was never created.The same shape hides a
429: instead of backing off, the agent treats the rate-limit body as data and keeps hammering — the exact behaviour.wave-rules/require-rate-limit-awareness.mdexists to prevent.It also fails less gracefully than it looks: if the error body is not JSON (an HTML 502 from an edge, say),
response.json()throws a rawSyntaxErrorwith no status, no URL, and no body — actively worse for debugging than the error it is masking.Fix
#58 added
WaveToolErrorandassertOk()insrc/tools/shared.tsfor exactly this, and the three new toolkits use them: throw on non-2xx, carryingstatusand the responsebodyverbatim, with.isRateLimitedflagging 429.AgentToolkitshould adopt the same path:This is a breaking change and should be treated as one, which is why #58 deliberately left it alone rather than sneaking it in. Callers who today receive an error body as a resolved value will start seeing a thrown
WaveToolError. That is the correct behaviour, but it warrants a minor-version bump and a CHANGELOG note.Sequencing: do #60 first. Changing error semantics across all 10 tools with no running test suite is how a regression ships quietly.
Pre-existing; surfaced while building #58 for wave-av/wave-context#72.