mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-03 16:05:26 +08:00
Support nested fenced blocks in stream fence tracking
This commit is contained in:
@@ -8,6 +8,7 @@ const {
|
||||
parseToolCallsPayload,
|
||||
parseMarkupToolCalls,
|
||||
parseTextKVToolCalls,
|
||||
stripFencedCodeBlocks,
|
||||
} = require('./parse_payload');
|
||||
|
||||
const TOOL_NAME_LOOSE_PATTERN = /[^a-z0-9]+/g;
|
||||
@@ -44,6 +45,9 @@ function parseToolCallsDetailed(text, toolNames) {
|
||||
return result;
|
||||
}
|
||||
result.sawToolCallSyntax = looksLikeToolCallSyntax(normalized);
|
||||
if (shouldSkipToolCallParsingForCodeFenceExample(normalized)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const candidates = buildToolCallCandidates(normalized);
|
||||
let parsed = [];
|
||||
@@ -89,6 +93,9 @@ function parseStandaloneToolCallsDetailed(text, toolNames) {
|
||||
return result;
|
||||
}
|
||||
result.sawToolCallSyntax = looksLikeToolCallSyntax(trimmed);
|
||||
if (shouldSkipToolCallParsingForCodeFenceExample(trimmed)) {
|
||||
return result;
|
||||
}
|
||||
const candidates = buildToolCallCandidates(trimmed);
|
||||
let parsed = [];
|
||||
for (const c of candidates) {
|
||||
@@ -230,6 +237,24 @@ function looksLikeToolCallSyntax(text) {
|
||||
|| lower.includes('function.name:');
|
||||
}
|
||||
|
||||
function shouldSkipToolCallParsingForCodeFenceExample(text) {
|
||||
if (!looksLikeToolCallSyntax(text) || looksLikeMarkupToolSyntax(text)) {
|
||||
return false;
|
||||
}
|
||||
const stripped = stripFencedCodeBlocks(text);
|
||||
return !looksLikeToolCallSyntax(stripped);
|
||||
}
|
||||
|
||||
function looksLikeMarkupToolSyntax(text) {
|
||||
const raw = toStringSafe(text);
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
return /<(?:(?:[a-z0-9_:-]+:)?(?:tool_call|function_call|invoke)\b)/i.test(raw)
|
||||
|| /<(?:[a-z0-9_:-]+:)?function_calls\b/i.test(raw)
|
||||
|| /<(?:[a-z0-9_:-]+:)?tool_use\b/i.test(raw);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractToolNames,
|
||||
parseToolCalls,
|
||||
|
||||
@@ -114,6 +114,9 @@ function parseToolCallsPayload(payload) {
|
||||
return [];
|
||||
}
|
||||
if (decoded.tool_calls) {
|
||||
if (isLikelyChatMessageEnvelope(decoded)) {
|
||||
return [];
|
||||
}
|
||||
return parseToolCallList(decoded.tool_calls);
|
||||
}
|
||||
|
||||
@@ -121,6 +124,21 @@ function parseToolCallsPayload(payload) {
|
||||
return one ? [one] : [];
|
||||
}
|
||||
|
||||
function isLikelyChatMessageEnvelope(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(value, 'tool_calls')) {
|
||||
return false;
|
||||
}
|
||||
const role = toStringSafe(value.role).trim().toLowerCase();
|
||||
if (role === 'assistant' || role === 'tool' || role === 'user' || role === 'system') {
|
||||
return true;
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(value, 'tool_call_id')
|
||||
|| Object.prototype.hasOwnProperty.call(value, 'content');
|
||||
}
|
||||
|
||||
function parseMarkupToolCalls(text) {
|
||||
const raw = toStringSafe(text).trim();
|
||||
if (!raw) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
'use strict';
|
||||
const { resetIncrementalToolState, noteText, insideCodeFence } = require('./state');
|
||||
const {
|
||||
resetIncrementalToolState,
|
||||
noteText,
|
||||
insideCodeFenceWithState,
|
||||
} = require('./state');
|
||||
const { parseStandaloneToolCallsDetailed } = require('./parse');
|
||||
const { extractJSONObjectFrom } = require('./jsonscan');
|
||||
|
||||
@@ -53,7 +57,7 @@ function processToolSieveChunk(state, chunk, toolNames) {
|
||||
if (!pending) {
|
||||
break;
|
||||
}
|
||||
const start = findToolSegmentStart(pending);
|
||||
const start = findToolSegmentStart(state, pending);
|
||||
if (start >= 0) {
|
||||
const prefix = pending.slice(0, start);
|
||||
if (prefix) {
|
||||
@@ -143,7 +147,7 @@ function findSuspiciousPrefixStart(s) {
|
||||
return start;
|
||||
}
|
||||
|
||||
function findToolSegmentStart(s) {
|
||||
function findToolSegmentStart(state, s) {
|
||||
if (!s) {
|
||||
return -1;
|
||||
}
|
||||
@@ -168,7 +172,7 @@ function findToolSegmentStart(s) {
|
||||
const keyIdx = bestKeyIdx;
|
||||
const start = s.slice(0, keyIdx).lastIndexOf('{');
|
||||
const candidateStart = start >= 0 ? start : keyIdx;
|
||||
if (!insideCodeFence(s.slice(0, candidateStart))) {
|
||||
if (!insideCodeFenceWithState(state, s.slice(0, candidateStart))) {
|
||||
return candidateStart;
|
||||
}
|
||||
offset = keyIdx + matchedKeyword.length;
|
||||
@@ -211,7 +215,7 @@ function consumeToolCapture(state, toolNames) {
|
||||
}
|
||||
const prefixPart = captured.slice(0, actualStart);
|
||||
const suffixPart = captured.slice(obj.end);
|
||||
if (insideCodeFence((state.recentTextTail || '') + prefixPart)) {
|
||||
if (insideCodeFenceWithState(state, prefixPart)) {
|
||||
return {
|
||||
ready: true,
|
||||
prefix: captured,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const TOOL_SIEVE_CONTEXT_TAIL_LIMIT = 256;
|
||||
const TOOL_SIEVE_CONTEXT_TAIL_LIMIT = 4096;
|
||||
|
||||
function createToolSieveState() {
|
||||
return {
|
||||
@@ -8,6 +8,9 @@ function createToolSieveState() {
|
||||
capture: '',
|
||||
capturing: false,
|
||||
recentTextTail: '',
|
||||
codeFenceStack: [],
|
||||
codeFencePendingTicks: 0,
|
||||
codeFenceLineStart: true,
|
||||
pendingToolRaw: '',
|
||||
pendingToolCalls: [],
|
||||
disableDeltas: false,
|
||||
@@ -34,6 +37,7 @@ function noteText(state, text) {
|
||||
if (!state || !hasMeaningfulText(text)) {
|
||||
return;
|
||||
}
|
||||
updateCodeFenceState(state, text);
|
||||
state.recentTextTail = appendTail(state.recentTextTail, text, TOOL_SIEVE_CONTEXT_TAIL_LIMIT);
|
||||
}
|
||||
|
||||
@@ -63,6 +67,91 @@ function insideCodeFence(text) {
|
||||
return ticks % 2 === 1;
|
||||
}
|
||||
|
||||
function insideCodeFenceWithState(state, text) {
|
||||
if (!state) {
|
||||
return insideCodeFence(text);
|
||||
}
|
||||
const simulated = simulateCodeFenceState(
|
||||
Array.isArray(state.codeFenceStack) ? state.codeFenceStack : [],
|
||||
Number.isInteger(state.codeFencePendingTicks) ? state.codeFencePendingTicks : 0,
|
||||
state.codeFenceLineStart !== false,
|
||||
text,
|
||||
);
|
||||
return simulated.stack.length > 0;
|
||||
}
|
||||
|
||||
function updateCodeFenceState(state, text) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const next = simulateCodeFenceState(
|
||||
Array.isArray(state.codeFenceStack) ? state.codeFenceStack : [],
|
||||
Number.isInteger(state.codeFencePendingTicks) ? state.codeFencePendingTicks : 0,
|
||||
state.codeFenceLineStart !== false,
|
||||
text,
|
||||
);
|
||||
state.codeFenceStack = next.stack;
|
||||
state.codeFencePendingTicks = next.pendingTicks;
|
||||
state.codeFenceLineStart = next.lineStart;
|
||||
}
|
||||
|
||||
function simulateCodeFenceState(stack, pendingTicks, lineStart, text) {
|
||||
const chunk = typeof text === 'string' ? text : '';
|
||||
const nextStack = Array.isArray(stack) ? [...stack] : [];
|
||||
let ticks = Number.isInteger(pendingTicks) ? pendingTicks : 0;
|
||||
let atLineStart = lineStart !== false;
|
||||
|
||||
const flushTicks = () => {
|
||||
if (ticks > 0) {
|
||||
if (atLineStart && ticks >= 3) {
|
||||
applyFenceMarker(nextStack, ticks);
|
||||
}
|
||||
atLineStart = false;
|
||||
ticks = 0;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < chunk.length; i += 1) {
|
||||
const ch = chunk[i];
|
||||
if (ch === '`') {
|
||||
ticks += 1;
|
||||
continue;
|
||||
}
|
||||
flushTicks();
|
||||
if (ch === '\n' || ch === '\r') {
|
||||
atLineStart = true;
|
||||
continue;
|
||||
}
|
||||
if ((ch === ' ' || ch === '\t') && atLineStart) {
|
||||
continue;
|
||||
}
|
||||
atLineStart = false;
|
||||
}
|
||||
// keep ticks for cross-chunk continuation.
|
||||
return {
|
||||
stack: nextStack,
|
||||
pendingTicks: ticks,
|
||||
lineStart: atLineStart,
|
||||
};
|
||||
}
|
||||
|
||||
function applyFenceMarker(stack, ticks) {
|
||||
if (!Array.isArray(stack)) {
|
||||
return;
|
||||
}
|
||||
if (stack.length === 0) {
|
||||
stack.push(ticks);
|
||||
return;
|
||||
}
|
||||
const top = stack[stack.length - 1];
|
||||
if (ticks >= top) {
|
||||
stack.pop();
|
||||
return;
|
||||
}
|
||||
// nested/open inner fence using longer marker for robustness.
|
||||
stack.push(ticks);
|
||||
}
|
||||
|
||||
function hasMeaningfulText(text) {
|
||||
return toStringSafe(text) !== '';
|
||||
}
|
||||
@@ -88,6 +177,8 @@ module.exports = {
|
||||
appendTail,
|
||||
looksLikeToolExampleContext,
|
||||
insideCodeFence,
|
||||
insideCodeFenceWithState,
|
||||
updateCodeFenceState,
|
||||
hasMeaningfulText,
|
||||
toStringSafe,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
var toolCallPattern = regexp.MustCompile(`\{\s*["']tool_calls["']\s*:\s*\[(.*?)\]\s*\}`)
|
||||
var fencedJSONPattern = regexp.MustCompile("(?s)```(?:json)?\\s*(.*?)\\s*```")
|
||||
var fencedCodeBlockPattern = regexp.MustCompile("(?s)```[\\s\\S]*?```")
|
||||
var markupToolSyntaxPattern = regexp.MustCompile(`(?i)<(?:(?:[a-z0-9_:-]+:)?(?:tool_call|function_call|invoke)\b|(?:[a-z0-9_:-]+:)?function_calls\b|(?:[a-z0-9_:-]+:)?tool_use\b)`)
|
||||
|
||||
func buildToolCallCandidates(text string) []string {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
@@ -173,3 +175,22 @@ func looksLikeToolExampleContext(text string) bool {
|
||||
}
|
||||
return strings.Contains(t, "```")
|
||||
}
|
||||
|
||||
func shouldSkipToolCallParsingForCodeFenceExample(text string) bool {
|
||||
if !looksLikeToolCallSyntax(text) || looksLikeMarkupToolSyntax(text) {
|
||||
return false
|
||||
}
|
||||
stripped := strings.TrimSpace(stripFencedCodeBlocks(text))
|
||||
return !looksLikeToolCallSyntax(stripped)
|
||||
}
|
||||
|
||||
func looksLikeMarkupToolSyntax(text string) bool {
|
||||
return markupToolSyntaxPattern.MatchString(text)
|
||||
}
|
||||
|
||||
func stripFencedCodeBlocks(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
return fencedCodeBlockPattern.ReplaceAllString(text, " ")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ func ParseToolCallsDetailed(text string, availableToolNames []string) ToolCallPa
|
||||
return result
|
||||
}
|
||||
result.SawToolCallSyntax = looksLikeToolCallSyntax(text)
|
||||
if shouldSkipToolCallParsingForCodeFenceExample(text) {
|
||||
return result
|
||||
}
|
||||
|
||||
candidates := buildToolCallCandidates(text)
|
||||
var parsed []ParsedToolCall
|
||||
@@ -74,6 +77,9 @@ func ParseStandaloneToolCallsDetailed(text string, availableToolNames []string)
|
||||
return result
|
||||
}
|
||||
result.SawToolCallSyntax = looksLikeToolCallSyntax(trimmed)
|
||||
if shouldSkipToolCallParsingForCodeFenceExample(trimmed) {
|
||||
return result
|
||||
}
|
||||
candidates := buildToolCallCandidates(trimmed)
|
||||
var parsed []ParsedToolCall
|
||||
for _, candidate := range candidates {
|
||||
@@ -183,6 +189,9 @@ func parseToolCallsPayload(payload string) []ParsedToolCall {
|
||||
switch v := decoded.(type) {
|
||||
case map[string]any:
|
||||
if tc, ok := v["tool_calls"]; ok {
|
||||
if isLikelyChatMessageEnvelope(v) {
|
||||
return nil
|
||||
}
|
||||
return parseToolCallList(tc)
|
||||
}
|
||||
if parsed, ok := parseToolCallItem(v); ok {
|
||||
@@ -194,6 +203,28 @@ func parseToolCallsPayload(payload string) []ParsedToolCall {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLikelyChatMessageEnvelope(v map[string]any) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
if _, ok := v["tool_calls"]; !ok {
|
||||
return false
|
||||
}
|
||||
if role, ok := v["role"].(string); ok {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "assistant", "tool", "user", "system":
|
||||
return true
|
||||
}
|
||||
}
|
||||
if _, ok := v["tool_call_id"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := v["content"]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func looksLikeToolCallSyntax(text string) bool {
|
||||
lower := strings.ToLower(text)
|
||||
return strings.Contains(lower, "tool_calls") ||
|
||||
|
||||
@@ -19,11 +19,11 @@ func TestParseToolCalls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsFromFencedJSON(t *testing.T) {
|
||||
func TestParseToolCallsIgnoresFencedJSON(t *testing.T) {
|
||||
text := "I will call tools now\n```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"news\"}}]}\n```"
|
||||
calls := ParseToolCalls(text, []string{"search"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected fenced tool_call payload to be parsed, got %#v", calls)
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected fenced tool_call payload to be ignored, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +112,17 @@ func TestParseStandaloneToolCallsSupportsMixedProsePayload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStandaloneToolCallsParsesFencedCodeBlock(t *testing.T) {
|
||||
func TestParseStandaloneToolCallsIgnoresFencedCodeBlock(t *testing.T) {
|
||||
fenced := "```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"go\"}}]}\n```"
|
||||
if calls := ParseStandaloneToolCalls(fenced, []string{"search"}); len(calls) != 1 {
|
||||
t.Fatalf("expected fenced tool_call payload to be parsed, got %#v", calls)
|
||||
if calls := ParseStandaloneToolCalls(fenced, []string{"search"}); len(calls) != 0 {
|
||||
t.Fatalf("expected fenced tool_call payload to be ignored, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStandaloneToolCallsIgnoresChatTranscriptEnvelope(t *testing.T) {
|
||||
transcript := `[{"role":"user","content":"请展示完整会话"},{"role":"assistant","content":null,"tool_calls":[{"function":{"name":"search","arguments":"{\"q\":\"go\"}"}}]}]`
|
||||
if calls := ParseStandaloneToolCalls(transcript, []string{"search"}); len(calls) != 0 {
|
||||
t.Fatalf("expected transcript envelope not to trigger tool call parse, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -409,8 +409,8 @@ func TestParseToolCallsWithFunctionWrapper(t *testing.T) {
|
||||
func TestParseStandaloneToolCallsFencedCodeBlock(t *testing.T) {
|
||||
fenced := "Here's an example:\n```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"go\"}}]}\n```\nDon't execute this."
|
||||
calls := ParseStandaloneToolCalls(fenced, []string{"search"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected fenced code block to be parsed, got %d calls", len(calls))
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected fenced code block to be ignored, got %d calls", len(calls))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"calls": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"input": {
|
||||
"path": "README.MD"
|
||||
}
|
||||
}
|
||||
],
|
||||
"calls": [],
|
||||
"sawToolCallSyntax": true,
|
||||
"rejectedByPolicy": false,
|
||||
"rejectedToolNames": []
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"calls": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"input": {
|
||||
"path": "README.MD"
|
||||
}
|
||||
}
|
||||
],
|
||||
"calls": [],
|
||||
"sawToolCallSyntax": true,
|
||||
"rejectedByPolicy": false,
|
||||
"rejectedToolNames": []
|
||||
|
||||
@@ -84,7 +84,7 @@ test('parseToolCalls rejects all names when toolNames is empty (Go strict parity
|
||||
assert.deepEqual(detailed.rejectedToolNames, ['not_in_schema']);
|
||||
});
|
||||
|
||||
test('parseToolCalls supports fenced json and function.arguments string payload', () => {
|
||||
test('parseToolCalls ignores tool_call payloads that exist only inside fenced code blocks', () => {
|
||||
const text = [
|
||||
'I will call a tool now.',
|
||||
'```json',
|
||||
@@ -92,9 +92,7 @@ test('parseToolCalls supports fenced json and function.arguments string payload'
|
||||
'```',
|
||||
].join('\n');
|
||||
const calls = parseToolCalls(text, ['read_file']);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].name, 'read_file');
|
||||
assert.equal(calls[0].input.path, 'README.md');
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test('parseToolCalls parses text-kv fallback payload', () => {
|
||||
@@ -134,10 +132,23 @@ test('parseStandaloneToolCalls parses mixed prose payload', () => {
|
||||
assert.equal(standaloneCalls.length, 1);
|
||||
});
|
||||
|
||||
test('parseStandaloneToolCalls parses fenced code block tool_call payload', () => {
|
||||
test('parseStandaloneToolCalls ignores fenced code block tool_call payload', () => {
|
||||
const fenced = ['```json', '{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}', '```'].join('\n');
|
||||
const calls = parseStandaloneToolCalls(fenced, ['read_file']);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test('parseStandaloneToolCalls ignores chat transcript message envelope with tool_calls', () => {
|
||||
const transcript = JSON.stringify([
|
||||
{ role: 'user', content: '请展示完整会话' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [{ function: { name: 'read_file', arguments: '{"path":"README.MD"}' } }],
|
||||
},
|
||||
]);
|
||||
const calls = parseStandaloneToolCalls(transcript, ['read_file']);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
|
||||
@@ -348,6 +359,59 @@ test('sieve preserves closed fence before standalone tool payload', () => {
|
||||
assert.equal(leakedText.toLowerCase().includes('tool_calls'), false);
|
||||
});
|
||||
|
||||
test('sieve does not trigger tool calls for long fenced examples beyond legacy tail window', () => {
|
||||
const longPadding = 'x'.repeat(700);
|
||||
const events = runSieve(
|
||||
[
|
||||
`前置说明\n\`\`\`json\n${longPadding}\n`,
|
||||
'{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}\n',
|
||||
'```',
|
||||
'\n后置说明',
|
||||
],
|
||||
['read_file'],
|
||||
);
|
||||
const hasTool = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
|
||||
const leakedText = collectText(events);
|
||||
assert.equal(hasTool, false);
|
||||
assert.equal(leakedText.includes('后置说明'), true);
|
||||
assert.equal(leakedText.toLowerCase().includes('tool_calls'), true);
|
||||
});
|
||||
|
||||
test('sieve keeps fence state when triple-backticks are split across chunks', () => {
|
||||
const events = runSieve(
|
||||
[
|
||||
'示例开始\n``',
|
||||
'`json\n{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}\n',
|
||||
'```',
|
||||
'\n示例结束',
|
||||
],
|
||||
['read_file'],
|
||||
);
|
||||
const hasTool = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
|
||||
const leakedText = collectText(events);
|
||||
assert.equal(hasTool, false);
|
||||
assert.equal(leakedText.includes('示例结束'), true);
|
||||
assert.equal(leakedText.toLowerCase().includes('tool_calls'), true);
|
||||
});
|
||||
|
||||
test('sieve ignores tool-like payload inside nested fences and resumes detection after close', () => {
|
||||
const events = runSieve(
|
||||
[
|
||||
'外层示例开始\n````markdown\n',
|
||||
'```json\n{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}\n```\n',
|
||||
'````\n',
|
||||
'{"tool_calls":[{"name":"read_file","input":{"path":"README2.MD"}}]}',
|
||||
],
|
||||
['read_file'],
|
||||
);
|
||||
const calls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
|
||||
const leakedText = collectText(events);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].input.path, 'README2.MD');
|
||||
assert.equal(leakedText.includes('README.MD'), true);
|
||||
assert.equal(leakedText.includes('README2.MD'), false);
|
||||
});
|
||||
|
||||
test('formatOpenAIStreamToolCalls reuses ids with the same idStore', () => {
|
||||
const idStore = new Map();
|
||||
const calls = [{ name: 'read_file', input: { path: 'README.MD' } }];
|
||||
|
||||
Reference in New Issue
Block a user