
















@@ -21,21 +21,191 @@ function resolveContextToolNames(context: Parameters<StreamFn>[1]): Set<string>
2121return new Set(names);
2222}
232324-function couldStillBePlainTextToolCall(text: string): boolean {
24+function couldStillBePlainTextToolCall(text: string, toolNames: Set<string>): boolean {
2525if (text.length > 256_000) {
2626return false;
2727}
2828const trimmed = text.trimStart();
2929return (
3030trimmed.length === 0 ||
31-trimmed.startsWith("[") ||
32-trimmed.startsWith("<|channel|>") ||
33-trimmed.startsWith("commentary") ||
34-trimmed.startsWith("analysis") ||
35-trimmed.startsWith("final")
31+couldStillBeBracketedToolCall(trimmed, toolNames) ||
32+couldStillBeHarmonyToolCall(trimmed, toolNames)
3633);
3734}
383536+function matchesLiteralPrefix(text: string, literal: string): boolean {
37+return literal.startsWith(text) || text.startsWith(literal);
38+}
39+40+function skipHorizontalWhitespace(text: string, start: number): number {
41+let cursor = start;
42+while (text[cursor] === " " || text[cursor] === "\t") {
43+cursor += 1;
44+}
45+return cursor;
46+}
47+48+function isToolNameChar(char: string | undefined): boolean {
49+return Boolean(char && /[A-Za-z0-9_-]/.test(char));
50+}
51+52+function hasToolNamePrefix(toolNames: Set<string>, prefix: string): boolean {
53+for (const toolName of toolNames) {
54+if (toolName.startsWith(prefix)) {
55+return true;
56+}
57+}
58+return false;
59+}
60+61+function couldStillBeJsonPayload(text: string, start: number): boolean {
62+let cursor = start;
63+while (cursor < text.length && /\s/.test(text[cursor] ?? "")) {
64+cursor += 1;
65+}
66+return cursor >= text.length || text[cursor] === "{";
67+}
68+69+function couldStillBeBracketedToolCall(text: string, toolNames: Set<string>): boolean {
70+if (!text.startsWith("[")) {
71+return false;
72+}
73+74+const toolPrefix = "[tool:";
75+if (matchesLiteralPrefix(text, toolPrefix)) {
76+if (text.length <= toolPrefix.length) {
77+return true;
78+}
79+let cursor = toolPrefix.length;
80+while (isToolNameChar(text[cursor])) {
81+cursor += 1;
82+}
83+const name = text.slice(toolPrefix.length, cursor);
84+if (!name || !hasToolNamePrefix(toolNames, name)) {
85+return false;
86+}
87+if (cursor >= text.length) {
88+return true;
89+}
90+if (text[cursor] !== "]") {
91+return false;
92+}
93+return couldStillBeJsonPayload(text, cursor + 1);
94+}
95+96+let cursor = 1;
97+while (isToolNameChar(text[cursor])) {
98+cursor += 1;
99+}
100+const name = text.slice(1, cursor);
101+if (!name || !hasToolNamePrefix(toolNames, name)) {
102+return false;
103+}
104+if (cursor >= text.length) {
105+return true;
106+}
107+if (text[cursor] !== "]") {
108+return false;
109+}
110+111+cursor = skipHorizontalWhitespace(text, cursor + 1);
112+if (cursor >= text.length) {
113+return true;
114+}
115+if (text[cursor] === "\r") {
116+if (cursor + 1 >= text.length) {
117+return true;
118+}
119+return couldStillBeJsonPayload(text, text[cursor + 1] === "\n" ? cursor + 2 : cursor + 1);
120+}
121+if (text[cursor] !== "\n") {
122+return false;
123+}
124+return couldStillBeJsonPayload(text, cursor + 1);
125+}
126+127+function couldStillBeHarmonyToolCall(text: string, toolNames: Set<string>): boolean {
128+const channelMarker = "<|channel|>";
129+let cursor = 0;
130+if (matchesLiteralPrefix(text, channelMarker)) {
131+if (text.length <= channelMarker.length) {
132+return true;
133+}
134+cursor = channelMarker.length;
135+}
136+137+const rest = text.slice(cursor);
138+const channel = ["commentary", "analysis", "final"].find((candidate) =>
139+matchesLiteralPrefix(rest, candidate),
140+);
141+if (!channel) {
142+return false;
143+}
144+if (rest.length <= channel.length) {
145+return true;
146+}
147+148+cursor += channel.length;
149+cursor = skipHorizontalWhitespace(text, cursor);
150+if (cursor >= text.length) {
151+return true;
152+}
153+154+const toMarker = "to=";
155+const toRest = text.slice(cursor);
156+if (!matchesLiteralPrefix(toRest, toMarker)) {
157+return false;
158+}
159+if (toRest.length <= toMarker.length) {
160+return true;
161+}
162+163+cursor += toMarker.length;
164+const nameStart = cursor;
165+while (isToolNameChar(text[cursor])) {
166+cursor += 1;
167+}
168+const name = text.slice(nameStart, cursor);
169+if (!name || !hasToolNamePrefix(toolNames, name)) {
170+return false;
171+}
172+if (cursor >= text.length) {
173+return true;
174+}
175+176+cursor = skipHorizontalWhitespace(text, cursor);
177+if (cursor >= text.length) {
178+return true;
179+}
180+if (!toolNames.has(name)) {
181+return false;
182+}
183+184+const codeMarker = "code";
185+const codeRest = text.slice(cursor);
186+if (!matchesLiteralPrefix(codeRest, codeMarker)) {
187+return false;
188+}
189+if (codeRest.length <= codeMarker.length) {
190+return true;
191+}
192+193+cursor += codeMarker.length;
194+while (cursor < text.length && /\s/.test(text[cursor] ?? "")) {
195+cursor += 1;
196+}
197+if (cursor >= text.length) {
198+return true;
199+}
200+201+const messageMarker = "<|message|>";
202+const messageRest = text.slice(cursor);
203+if (matchesLiteralPrefix(messageRest, messageMarker)) {
204+return true;
205+}
206+return text[cursor] === "{";
207+}
208+39209function createSyntheticToolCallId(): string {
40210return `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`;
41211}
@@ -179,7 +349,7 @@ function wrapPlainTextToolCallStream(
179349} else if (typeof record?.content === "string" && !bufferedText) {
180350bufferedText = record.content;
181351}
182-if (!couldStillBePlainTextToolCall(bufferedText)) {
352+if (!couldStillBePlainTextToolCall(bufferedText, toolNames)) {
183353flushBufferedTextEvents();
184354}
185355continue;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。