@botcord/daemon 0.2.46 → 0.2.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -601,10 +601,24 @@ function normalizeAssistantText(text) {
|
|
|
601
601
|
if (!finalMatch && selected.trimStart().toLowerCase().startsWith("<think")) {
|
|
602
602
|
return "";
|
|
603
603
|
}
|
|
604
|
-
return selected
|
|
604
|
+
return stripLeadingBoundaryResidue(selected
|
|
605
605
|
.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, "")
|
|
606
606
|
.replace(/<\/?final>/gi, "")
|
|
607
|
-
.trim();
|
|
607
|
+
.trim());
|
|
608
|
+
}
|
|
609
|
+
function stripLeadingBoundaryResidue(text) {
|
|
610
|
+
if (!text.startsWith("<"))
|
|
611
|
+
return text;
|
|
612
|
+
// Keep real HTML/XML-ish tags and common comparison operators. A lone
|
|
613
|
+
// leading "<" before normal prose can be left behind when ACP streams a
|
|
614
|
+
// structural marker boundary separately from the final assistant text.
|
|
615
|
+
if (startsWithRealAngleSyntax(text))
|
|
616
|
+
return text;
|
|
617
|
+
return text.slice(1).trimStart();
|
|
618
|
+
}
|
|
619
|
+
function startsWithRealAngleSyntax(text) {
|
|
620
|
+
return (/^<\/?[A-Za-z][A-Za-z0-9:-]*(?:\s|>|\/>)/.test(text) ||
|
|
621
|
+
/^<(?:\s|=|<)/.test(text));
|
|
608
622
|
}
|
|
609
623
|
function createAssistantTextFilter() {
|
|
610
624
|
let pending = "";
|
|
@@ -709,7 +723,19 @@ function createAssistantTextFilter() {
|
|
|
709
723
|
if (!flush && knownPrefixes.some((prefix) => prefix.startsWith(lower))) {
|
|
710
724
|
return out;
|
|
711
725
|
}
|
|
712
|
-
|
|
726
|
+
if (!flush && pending === "<") {
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
if (!startsWithRealAngleSyntax(pending)) {
|
|
730
|
+
pending = pending.slice(1).trimStart();
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (seenFinal) {
|
|
734
|
+
out += "<";
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
fallback += "<";
|
|
738
|
+
}
|
|
713
739
|
pending = pending.slice(1);
|
|
714
740
|
}
|
|
715
741
|
if (flush && !seenFinal && fallback) {
|
package/package.json
CHANGED
|
@@ -265,6 +265,178 @@ describe("OpenclawAcpAdapter.run", () => {
|
|
|
265
265
|
);
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
it("strips a lone leading ACP boundary marker from final prompt text", async () => {
|
|
269
|
+
const child = new FakeChild();
|
|
270
|
+
const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
|
|
271
|
+
const gateway: ResolvedOpenclawGateway = {
|
|
272
|
+
name: "local",
|
|
273
|
+
url: "ws://127.0.0.1:1",
|
|
274
|
+
openclawAgent: "main",
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
child.stdin.on("data", (chunk: Buffer) => {
|
|
278
|
+
for (const line of chunk.toString("utf8").split("\n").filter(Boolean)) {
|
|
279
|
+
const frame = JSON.parse(line);
|
|
280
|
+
if (frame.method === "initialize") {
|
|
281
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { protocolVersion: 1 } }) + "\n");
|
|
282
|
+
} else if (frame.method === "session/new") {
|
|
283
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { sessionId: "sid-boundary" } }) + "\n");
|
|
284
|
+
} else if (frame.method === "session/prompt") {
|
|
285
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { text: "<你好!终于可以正常交流了。" } }) + "\n");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const res = await adapter.run({
|
|
291
|
+
text: "hi",
|
|
292
|
+
sessionId: null,
|
|
293
|
+
cwd: "/tmp",
|
|
294
|
+
accountId: "ag_alice",
|
|
295
|
+
signal: new AbortController().signal,
|
|
296
|
+
trustLevel: "owner",
|
|
297
|
+
gateway,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(res.text).toBe("你好!终于可以正常交流了。");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("strips a lone leading ACP boundary marker from streamed fallback text", async () => {
|
|
304
|
+
const child = new FakeChild();
|
|
305
|
+
const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
|
|
306
|
+
const gateway: ResolvedOpenclawGateway = {
|
|
307
|
+
name: "local",
|
|
308
|
+
url: "ws://127.0.0.1:1",
|
|
309
|
+
openclawAgent: "main",
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
child.stdin.on("data", (chunk: Buffer) => {
|
|
313
|
+
for (const line of chunk.toString("utf8").split("\n").filter(Boolean)) {
|
|
314
|
+
const frame = JSON.parse(line);
|
|
315
|
+
if (frame.method === "initialize") {
|
|
316
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { protocolVersion: 1 } }) + "\n");
|
|
317
|
+
} else if (frame.method === "session/new") {
|
|
318
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { sessionId: "sid-stream-boundary" } }) + "\n");
|
|
319
|
+
} else if (frame.method === "session/prompt") {
|
|
320
|
+
for (const text of ["<", "好!终于可以正常交流了。"]) {
|
|
321
|
+
child.stdout.write(
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
jsonrpc: "2.0",
|
|
324
|
+
method: "session/update",
|
|
325
|
+
params: {
|
|
326
|
+
sessionId: "sid-stream-boundary",
|
|
327
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } },
|
|
328
|
+
},
|
|
329
|
+
}) + "\n",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { stopReason: "end_turn" } }) + "\n");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const blocks: any[] = [];
|
|
338
|
+
const res = await adapter.run({
|
|
339
|
+
text: "hi",
|
|
340
|
+
sessionId: null,
|
|
341
|
+
cwd: "/tmp",
|
|
342
|
+
accountId: "ag_alice",
|
|
343
|
+
signal: new AbortController().signal,
|
|
344
|
+
trustLevel: "owner",
|
|
345
|
+
gateway,
|
|
346
|
+
onBlock: (b) => blocks.push(b),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(res.text).toBe("好!终于可以正常交流了。");
|
|
350
|
+
const assistantChunks = blocks
|
|
351
|
+
.filter((b) => b.kind === "assistant_text")
|
|
352
|
+
.map((b) => b.raw.params.update.content[0].text);
|
|
353
|
+
expect(assistantChunks).toEqual(["好!终于可以正常交流了。"]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("preserves real leading angle syntax in streamed fallback text", async () => {
|
|
357
|
+
const child = new FakeChild();
|
|
358
|
+
const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
|
|
359
|
+
const gateway: ResolvedOpenclawGateway = {
|
|
360
|
+
name: "local",
|
|
361
|
+
url: "ws://127.0.0.1:1",
|
|
362
|
+
openclawAgent: "main",
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
child.stdin.on("data", (chunk: Buffer) => {
|
|
366
|
+
for (const line of chunk.toString("utf8").split("\n").filter(Boolean)) {
|
|
367
|
+
const frame = JSON.parse(line);
|
|
368
|
+
if (frame.method === "initialize") {
|
|
369
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { protocolVersion: 1 } }) + "\n");
|
|
370
|
+
} else if (frame.method === "session/new") {
|
|
371
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { sessionId: "sid-angle" } }) + "\n");
|
|
372
|
+
} else if (frame.method === "session/prompt") {
|
|
373
|
+
for (const text of ["<", "b>bold</b> and ", "<", " 5"]) {
|
|
374
|
+
child.stdout.write(
|
|
375
|
+
JSON.stringify({
|
|
376
|
+
jsonrpc: "2.0",
|
|
377
|
+
method: "session/update",
|
|
378
|
+
params: {
|
|
379
|
+
sessionId: "sid-angle",
|
|
380
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } },
|
|
381
|
+
},
|
|
382
|
+
}) + "\n",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { stopReason: "end_turn" } }) + "\n");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const res = await adapter.run({
|
|
391
|
+
text: "hi",
|
|
392
|
+
sessionId: null,
|
|
393
|
+
cwd: "/tmp",
|
|
394
|
+
accountId: "ag_alice",
|
|
395
|
+
signal: new AbortController().signal,
|
|
396
|
+
trustLevel: "owner",
|
|
397
|
+
gateway,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(res.text).toBe("<b>bold</b> and < 5");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("forwards slash-command user text to session/prompt unchanged", async () => {
|
|
404
|
+
const child = new FakeChild();
|
|
405
|
+
const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
|
|
406
|
+
const gateway: ResolvedOpenclawGateway = {
|
|
407
|
+
name: "local",
|
|
408
|
+
url: "ws://127.0.0.1:1",
|
|
409
|
+
openclawAgent: "main",
|
|
410
|
+
};
|
|
411
|
+
let promptPayload: any = null;
|
|
412
|
+
|
|
413
|
+
child.stdin.on("data", (chunk: Buffer) => {
|
|
414
|
+
for (const line of chunk.toString("utf8").split("\n").filter(Boolean)) {
|
|
415
|
+
const frame = JSON.parse(line);
|
|
416
|
+
if (frame.method === "initialize") {
|
|
417
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { protocolVersion: 1 } }) + "\n");
|
|
418
|
+
} else if (frame.method === "session/new") {
|
|
419
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { sessionId: "sid-slash" } }) + "\n");
|
|
420
|
+
} else if (frame.method === "session/prompt") {
|
|
421
|
+
promptPayload = frame.params.prompt;
|
|
422
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { text: "ok" } }) + "\n");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await adapter.run({
|
|
428
|
+
text: "/start",
|
|
429
|
+
sessionId: null,
|
|
430
|
+
cwd: "/tmp",
|
|
431
|
+
accountId: "ag_alice",
|
|
432
|
+
signal: new AbortController().signal,
|
|
433
|
+
trustLevel: "owner",
|
|
434
|
+
gateway,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(promptPayload).toEqual([{ type: "text", text: "/start" }]);
|
|
438
|
+
});
|
|
439
|
+
|
|
268
440
|
it("respawns the pooled child when gateway.url or gateway.token changes under the same name", async () => {
|
|
269
441
|
function newChild(): FakeChild {
|
|
270
442
|
const c = new FakeChild();
|
|
@@ -709,10 +709,26 @@ function normalizeAssistantText(text: string | undefined): string {
|
|
|
709
709
|
if (!finalMatch && selected.trimStart().toLowerCase().startsWith("<think")) {
|
|
710
710
|
return "";
|
|
711
711
|
}
|
|
712
|
-
return selected
|
|
712
|
+
return stripLeadingBoundaryResidue(selected
|
|
713
713
|
.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, "")
|
|
714
714
|
.replace(/<\/?final>/gi, "")
|
|
715
|
-
.trim();
|
|
715
|
+
.trim());
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function stripLeadingBoundaryResidue(text: string): string {
|
|
719
|
+
if (!text.startsWith("<")) return text;
|
|
720
|
+
// Keep real HTML/XML-ish tags and common comparison operators. A lone
|
|
721
|
+
// leading "<" before normal prose can be left behind when ACP streams a
|
|
722
|
+
// structural marker boundary separately from the final assistant text.
|
|
723
|
+
if (startsWithRealAngleSyntax(text)) return text;
|
|
724
|
+
return text.slice(1).trimStart();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function startsWithRealAngleSyntax(text: string): boolean {
|
|
728
|
+
return (
|
|
729
|
+
/^<\/?[A-Za-z][A-Za-z0-9:-]*(?:\s|>|\/>)/.test(text) ||
|
|
730
|
+
/^<(?:\s|=|<)/.test(text)
|
|
731
|
+
);
|
|
716
732
|
}
|
|
717
733
|
|
|
718
734
|
function createAssistantTextFilter(): {
|
|
@@ -820,7 +836,18 @@ function createAssistantTextFilter(): {
|
|
|
820
836
|
return out;
|
|
821
837
|
}
|
|
822
838
|
|
|
823
|
-
|
|
839
|
+
if (!flush && pending === "<") {
|
|
840
|
+
return out;
|
|
841
|
+
}
|
|
842
|
+
if (!startsWithRealAngleSyntax(pending)) {
|
|
843
|
+
pending = pending.slice(1).trimStart();
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (seenFinal) {
|
|
847
|
+
out += "<";
|
|
848
|
+
} else {
|
|
849
|
+
fallback += "<";
|
|
850
|
+
}
|
|
824
851
|
pending = pending.slice(1);
|
|
825
852
|
}
|
|
826
853
|
if (flush && !seenFinal && fallback) {
|