Compare commits

..

18 Commits

Author SHA1 Message Date
CJACK.
d50d39e2e5 Merge pull request #169 from CJackHwang/dev
Merge pull request #168 from CJackHwang/codex/fix-vercel-deployment-issue-with-api-calls

fix(js): avoid false tool-call capture on plain tool_calls prose
2026-03-30 16:08:39 +08:00
CJACK.
01393837be Merge pull request #168 from CJackHwang/codex/fix-vercel-deployment-issue-with-api-calls
fix(js): avoid false tool-call capture on plain tool_calls prose
2026-03-30 16:01:51 +08:00
CJACK.
1fe1240240 fix(js): prevent XML wrapper attribute tool_calls scan loop 2026-03-30 15:59:34 +08:00
CJACK.
c07736fbea chore: set shared tool-sieve context tail window to 2048 2026-03-30 15:41:38 +08:00
CJACK.
775bf3b578 refactor(js): align tool-sieve segment start and tail window with go 2026-03-30 15:41:26 +08:00
CJACK.
ab3943ebeb test(js): cover numbered planning prose around tool calls 2026-03-30 15:39:09 +08:00
CJACK.
6efba7b2e4 fix(js): avoid false tool-call capture on plain tool_calls prose 2026-03-30 12:51:33 +08:00
CJACK.
765d0231cd Merge pull request #166 from CJackHwang/dev
chore: relocate sha3 WASM asset to internal directory and update build configurations
2026-03-30 12:23:46 +08:00
CJACK.
aebf3e9119 Merge pull request #167 from CJackHwang/codex/remove-dangling-agent-xml-tags
Fix dangling agent XML cleanup and XML-escape tool-call prompt serialization
2026-03-30 12:22:58 +08:00
CJACK.
535d9298a7 Scope dangling result-tag cleanup to leaked wrapper fragments 2026-03-30 12:22:04 +08:00
CJACK.
b790545d82 Fix dangling agent XML cleanup and escape tool call prompt XML 2026-03-30 11:23:16 +08:00
CJACK.
034c00f10e Merge pull request #163 from CJackHwang/dev
docs: update API documentation, deployment guides, and README with new admin endpoints, compatibility notes, and build instructions
2026-03-29 19:50:40 +08:00
CJACK.
390f7580e5 Merge pull request #156 from CJackHwang/dev
Merge pull request #153 from CJackHwang/codex/investigate-tool-execution-bugs-in-output-7ocr8f

Relax tool-name allow-listing and improve tool-call detection/parsing across adapters and sieve
2026-03-22 21:40:03 +08:00
CJACK.
586d31e556 Merge pull request #151 from CJackHwang/dev
Merge pull request #149 from CJackHwang/codex/fix-tool-miscall-during-complex-json-test

Ignore tool_call payloads inside fenced code blocks and chat envelopes; stream-aware code-fence tracking
2026-03-22 16:51:17 +08:00
CJACK.
c4a73e871a Merge pull request #148 from CJackHwang/dev
Merge pull request #147 from CJackHwang/codex/fix-tool-call-history-retrieval

Preserve tool call/result roundtrip and raw payloads across Claude, Gemini and OpenAI adapters
2026-03-22 13:43:26 +08:00
CJACK.
25b3292497 Merge pull request #146 from CJackHwang/dev
Merge pull request #145 from CJackHwang/codex/determine-which-pr-fixes-json-leak-issue

Merge pull request #144 from CJackHwang/codex/refactor-codebase-to-remove-redundancy

Refactor tool-sieve and response streaming, remove unused helpers and UI wrappers
2026-03-22 11:05:54 +08:00
CJACK.
11f66db87d Merge pull request #142 from CJackHwang/dev
Merge pull request #141 from CJackHwang/codex/investigate-json-leakage-in-vercel-deployment-rh84s1

Fix raw tool-call JSON leaks when feature_match mode is off
2026-03-22 08:55:29 +08:00
CJACK.
7131b06e26 Merge pull request #138 from CJackHwang/dev
Merge pull request #135 from CJackHwang/codex/add-global-token-refresh-logic

Sanitize leaked tool-history markers, simplify normalization, and add managed token refresh
2026-03-22 01:27:27 +08:00
8 changed files with 135 additions and 5 deletions

View File

@@ -23,6 +23,9 @@ var leakedAgentXMLBlockPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?is)<new_task\b[^>]*>(.*?)</new_task>`),
}
var leakedAgentWrapperTagPattern = regexp.MustCompile(`(?is)</?(?:attempt_completion|ask_followup_question|new_task)\b[^>]*>`)
var leakedAgentWrapperPlusResultOpenPattern = regexp.MustCompile(`(?is)<(?:attempt_completion|ask_followup_question|new_task)\b[^>]*>\s*<result>`)
var leakedAgentResultPlusWrapperClosePattern = regexp.MustCompile(`(?is)</result>\s*</(?:attempt_completion|ask_followup_question|new_task)\b[^>]*>`)
var leakedAgentResultTagPattern = regexp.MustCompile(`(?is)</?result>`)
func sanitizeLeakedOutput(text string) string {
@@ -50,5 +53,18 @@ func sanitizeLeakedAgentXMLBlocks(text string) string {
return leakedAgentResultTagPattern.ReplaceAllString(submatches[1], "")
})
}
// Fallback for truncated output streams: strip any dangling wrapper tags
// that were not part of a complete block replacement. If we detect leaked
// wrapper tags, strip only adjacent <result> tags to avoid exposing agent
// markup without altering unrelated user-visible <result> examples.
if leakedAgentWrapperTagPattern.MatchString(out) {
out = leakedAgentWrapperPlusResultOpenPattern.ReplaceAllStringFunc(out, func(match string) string {
return leakedAgentResultTagPattern.ReplaceAllString(match, "")
})
out = leakedAgentResultPlusWrapperClosePattern.ReplaceAllStringFunc(out, func(match string) string {
return leakedAgentResultTagPattern.ReplaceAllString(match, "")
})
out = leakedAgentWrapperTagPattern.ReplaceAllString(out, "")
}
return out
}

View File

@@ -41,3 +41,28 @@ func TestSanitizeLeakedOutputPreservesStandaloneResultTags(t *testing.T) {
t.Fatalf("unexpected sanitize result for standalone result tag: %q", got)
}
}
func TestSanitizeLeakedOutputRemovesDanglingAgentXMLOpeningTags(t *testing.T) {
raw := "Done.<attempt_completion><result>Some final answer"
got := sanitizeLeakedOutput(raw)
if got != "Done.Some final answer" {
t.Fatalf("unexpected sanitize result for dangling opening tags: %q", got)
}
}
func TestSanitizeLeakedOutputRemovesDanglingAgentXMLClosingTags(t *testing.T) {
raw := "Done.Some final answer</result></attempt_completion>"
got := sanitizeLeakedOutput(raw)
if got != "Done.Some final answer" {
t.Fatalf("unexpected sanitize result for dangling closing tags: %q", got)
}
}
func TestSanitizeLeakedOutputPreservesUnrelatedResultTagsWhenWrapperLeaks(t *testing.T) {
raw := "Done.<attempt_completion><result>Some final answer\nExample XML: <result>value</result>"
got := sanitizeLeakedOutput(raw)
want := "Done.Some final answer\nExample XML: <result>value</result>"
if got != want {
t.Fatalf("unexpected sanitize result for mixed leaked wrapper + xml example: %q", got)
}
}

View File

@@ -34,7 +34,8 @@ type toolCallDelta struct {
Arguments string
}
const toolSieveContextTailLimit = 256
// Keep in sync with JS TOOL_SIEVE_CONTEXT_TAIL_LIMIT.
const toolSieveContextTailLimit = 2048
func (s *toolStreamSieveState) resetIncrementalToolState() {
s.disableDeltas = false

View File

@@ -102,7 +102,10 @@ function extractToolCallObjects(text) {
const obj = extractJSONObjectFrom(raw, start);
if (obj.ok) {
out.push(raw.slice(start, obj.end).trim());
offset = obj.end;
// Ensure forward progress even when the matched keyword is outside
// the extracted JSON object (e.g. closing XML wrapper tags containing
// "tool_calls" after an earlier JSON arguments object).
offset = Math.max(obj.end, idx + matched.length);
idx = -1;
break;
}

View File

@@ -1,6 +1,7 @@
'use strict';
const TOOL_SIEVE_CONTEXT_TAIL_LIMIT = 4096;
// Keep in sync with Go toolSieveContextTailLimit.
const TOOL_SIEVE_CONTEXT_TAIL_LIMIT = 2048;
function createToolSieveState() {
return {

View File

@@ -5,6 +5,12 @@ import (
"strings"
)
var promptXMLTextEscaper = strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
)
// FormatToolCallsForPrompt renders a tool_calls slice into the canonical
// prompt-visible history block used across adapters.
func FormatToolCallsForPrompt(raw any) string {
@@ -82,8 +88,8 @@ func formatToolCallForPrompt(call map[string]any) string {
}
return " <tool_call>\n" +
" <tool_name>" + name + "</tool_name>\n" +
" <parameters>" + StringifyToolCallArguments(argsRaw) + "</parameters>\n" +
" <tool_name>" + escapeXMLText(name) + "</tool_name>\n" +
" <parameters>" + escapeXMLText(StringifyToolCallArguments(argsRaw)) + "</parameters>\n" +
" </tool_call>"
}
@@ -122,3 +128,10 @@ func asString(v any) string {
}
return ""
}
func escapeXMLText(v string) string {
if v == "" {
return ""
}
return promptXMLTextEscaper.Replace(v)
}

View File

@@ -26,3 +26,16 @@ func TestFormatToolCallsForPromptXML(t *testing.T) {
t.Fatalf("unexpected formatted tool call XML: %q", got)
}
}
func TestFormatToolCallsForPromptEscapesXMLEntities(t *testing.T) {
got := FormatToolCallsForPrompt([]any{
map[string]any{
"name": "search<&>",
"arguments": `{"q":"a < b && c > d"}`,
},
})
want := "<tool_calls>\n <tool_call>\n <tool_name>search&lt;&amp;&gt;</tool_name>\n <parameters>{\"q\":\"a &lt; b &amp;&amp; c &gt; d\"}</parameters>\n </tool_call>\n</tool_calls>"
if got != want {
t.Fatalf("unexpected escaped tool call XML: %q", got)
}
}

View File

@@ -227,6 +227,24 @@ test('sieve flushes incomplete captured XML tool blocks without leaking raw tags
assert.equal(leakedText.includes('<tool_call'), false);
});
test('sieve captures XML wrapper tags with attributes without leaking wrapper text', () => {
const events = runSieve(
[
'前置正文H。',
'<tool_calls id="x"><tool_call><tool_name>read_file</tool_name><parameters>{"path":"README.MD"}</parameters></tool_call></tool_calls>',
'后置正文I。',
],
['read_file'],
);
const leakedText = collectText(events);
const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
assert.equal(hasToolCall, true);
assert.equal(leakedText.includes('前置正文H。'), true);
assert.equal(leakedText.includes('后置正文I。'), true);
assert.equal(leakedText.includes('<tool_calls id=\"x\">'), false);
assert.equal(leakedText.includes('</tool_calls>'), false);
});
test('sieve still intercepts large tool json payloads over previous capture limit', () => {
const large = 'a'.repeat(9000);
const payload = `{"tool_calls":[{"name":"read_file","input":{"path":"${large}"}}]}`;
@@ -252,6 +270,46 @@ test('sieve keeps plain text intact in tool mode when no tool call appears', ()
assert.equal(leakedText, '你好,这是普通文本回复。请继续。');
});
test('sieve keeps plain "tool_calls" prose as text when no valid payload follows', () => {
const events = runSieve(
['前置。', '这里提到 tool_calls 只是解释,不是调用。', '后置。'],
['read_file'],
);
const leakedText = collectText(events);
const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
assert.equal(hasToolCall, false);
assert.equal(leakedText.includes('tool_calls'), true);
assert.equal(leakedText, '前置。这里提到 tool_calls 只是解释,不是调用。后置。');
});
test('sieve keeps numbered planning prose before a real tool payload (mobile-chat style)', () => {
const events = runSieve(
[
'好的,我会依次测试每个工具,先把所有工具都调用一遍,然后汇总结果给你看。\n\n1. 获取当前时间\n',
'{"tool_calls":[{"name":"get_current_time","input":{}}]}',
],
['get_current_time'],
);
const leakedText = collectText(events);
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
assert.equal(finalCalls.length, 1);
assert.equal(finalCalls[0].name, 'get_current_time');
assert.equal(leakedText.includes('先把所有工具都调用一遍'), true);
assert.equal(leakedText.includes('1. 获取当前时间'), true);
assert.equal(leakedText.toLowerCase().includes('tool_calls'), false);
});
test('sieve keeps numbered planning prose when no tool payload follows', () => {
const events = runSieve(
['好的,我会依次测试每个工具。\n\n1. 获取当前时间'],
['get_current_time'],
);
const leakedText = collectText(events);
const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0);
assert.equal(hasToolCall, false);
assert.equal(leakedText, '好的,我会依次测试每个工具。\n\n1. 获取当前时间');
});
test('sieve emits unknown tool payload (no args) as executable tool call', () => {
const events = runSieve(
['{"tool_calls":[{"name":"not_in_schema"}]}', '后置正文G。'],