fix(sse): handle space-separated arg name/value in Composer tool calls (port from 9router#1811) (#7116)

parseInnerCall only split arg segments on a newline between the arg name
and its value. Cursor's live Composer/Auto output has been observed using a
single space instead, so those segments were treated as one long
(space-containing) arg name with an empty value, silently no-opping
Write/tool calls for Composer/Auto models.

Fall back to splitting on the first whitespace boundary when no newline is
present in the segment.

Reported-by: way-art (https://github.com/decolua/9router/issues/1811)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-16 14:13:43 -03:00
committed by GitHub
parent a62141210b
commit 0130a4bbb2
3 changed files with 43 additions and 5 deletions

View File

@@ -0,0 +1 @@
- **fix(sse):** Cursor Composer/Auto tool calls that separate the arg name and value with a space instead of a newline (e.g. `path /Users/.../test`) no longer produce empty-valued, malformed argument keys, fixing silent no-op Write/tool calls. (thanks @way-art)

View File

@@ -126,15 +126,28 @@ function parseInnerCall(body: string): { name: string; arguments: string } | nul
const args: Record<string, unknown> = {};
for (const seg of segments) {
if (!seg) continue;
// Each segment is `arg_name\nvalue\n...`. The arg name is the first
// line; everything after the first newline is the value (verbatim,
// including additional newlines).
// Each segment is normally `arg_name\nvalue\n...`: the arg name is the
// first line, everything after the first newline is the value
// (verbatim, including additional newlines). Some live Composer/Auto
// captures instead separate the arg name and value with a single space
// on the same line (no newline at all in the segment) — fall back to
// splitting on the first whitespace boundary in that case so the value
// isn't swallowed into an empty-valued, space-containing "arg name".
const idxNl = seg.indexOf("\n");
let argName: string;
let argValue: string;
if (idxNl < 0) {
argName = seg.trim();
argValue = "";
const idxSp = seg.search(/\s/);
if (idxSp < 0) {
argName = seg.trim();
argValue = "";
} else {
argName = seg.slice(0, idxSp).trim();
// Unlike the newline-delimited form, a space-delimited value has no
// multi-line content to preserve — trim the trailing whitespace left
// over from the boundary with the next `<tool▁sep>` marker.
argValue = seg.slice(idxSp + 1).trim();
}
} else {
argName = seg.slice(0, idxNl).trim();
argValue = seg.slice(idxNl + 1);

View File

@@ -214,3 +214,27 @@ test("feedStreamingChunk: noop after done state", () => {
assert.equal(out.safeDelta, "");
assert.equal(out.ready, false);
});
// ─── Regression: space-separated arg name/value (9router#1811) ───────────────
// Cursor's real Composer/Auto output has been observed using a single space
// (instead of a newline) between the arg name and its value inside a
// <tool▁sep> segment, e.g. "<tool▁sep>path /Users/.../test". The parser
// must still extract {path: "/Users/.../test"} rather than treating the whole
// segment as the (empty-valued) arg name.
test("parseComposerToolCalls: parses args separated by a space instead of a newline (Cursor Composer live capture)", () => {
const text =
"<tool▁calls▁begin><tool▁call▁begin> Write " +
"<tool▁sep>path /Users/kabawagang/Desktop/Code/iOS_Review/test " +
"<tool▁sep>contents 22\n\n<tool▁call▁end><tool▁calls▁end>";
const result = parseComposerToolCalls(text);
assert.equal(result.toolCalls.length, 1);
const tc = result.toolCalls[0];
assert.equal(tc.function.name, "Write");
const args = JSON.parse(tc.function.arguments);
assert.deepEqual(args, {
path: "/Users/kabawagang/Desktop/Code/iOS_Review/test",
contents: 22,
});
});