Files
Agent-System-Backend/internal/runtime/llm_direct_e1_test.go
T
weichen 4df81ef1da
ci / vet-and-test (pull_request) Successful in 43s
Enable parallel tool execution, fix delegate truncation, sandbox deps layer
Why: Real runs were fully serial: the direct path never set the kernel's
parallel tool mode, delegate_task handed the parent only the child's
160-char summary (the full text was dropped), MiniMax streamed its
reasoning inside content <think> blocks, and the EXT-16 sandbox hid
~/.local so python-docx/openpyxl/pptx could not even import. All four
were measured defects (F1/F2/F3/F5) in the delegation/routing proposal.

Why: Serial-only mutual exclusion is not enough for order-sensitive
tools - the card-stream test failed with out-of-order appends - so the
ordering moved into the kernel (SerialTools partition, Agent-Native
#44); process events pair by CallID (#42) and tool results write back
through the injected call context (#43).

Evidence: go test ./... green, including new regression anchors:
TestDelegateToolReturnsFullText, TestToolEventsPairByCallID,
TestToolExecModeAndSerialSet, TestOpenLLMClientReasoningSplitAuto,
TestSandboxArgvDepsLayer, TestExecEnvJoinsPythonPath.

Impact: Direct runs now execute independent same-turn calls
(searches, reads, delegate_task, review_findings) concurrently by
default (runtime.parallelTools=false restores sequential); card flow,
plans, workflows, exec primitives and file edits keep model-declared
order. delegate_task returns the capped full child text. MiniMax-*
models get reasoning_split=true so thinking streams via
reasoning_content. --exec-deps-dir/AGENT_SYSTEM_EXEC_DEPS_DIR mounts a
read-only dependency layer into the sandbox and merges PYTHONPATH.
Bumps deps/agent-native to c348441.

Refs: proposal 2026-10-10-agent-delegation-routing-sandbox.md (E1,
F1-F5); Agent-Native #42/#43/#44; Dev task feature/agent-e1-parallel-fixes.
2026-10-10 09:02:10 +00:00

104 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtime
import (
"strings"
"testing"
nruntime "agent-native/runtime"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
// ---- F1:delegate_task 回传子代理全文,不再只有 160 字摘要 ----
func TestDelegateToolReturnsFullText(t *testing.T) {
full := strings.Repeat("子代理结论细节。", 60) // 480 字,远超 160
f := &fakeRunCtx{spawnAgentFn: func(agentID, name, task, hint string) (core.AgentResultPayload, error) {
return core.AgentResultPayload{Status: core.RunCompleted, Text: full, Summary: "完成(160字内摘要)"}, nil
}}
st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}}
res := st.runTool("delegate_task", `{"name":"调研","task":"查证X"}`)
if res.IsError {
t.Fatalf("delegate failed: %s", res.Content)
}
if !strings.Contains(res.Content, "子代理结论细节。子代理结论细节。子代理结论细节。") {
t.Fatalf("delegate result must carry the full child text, got: %.120s…", res.Content)
}
if strings.Contains(res.Content, "160字内摘要") {
t.Fatalf("summary must be fallback only, got: %.120s…", res.Content)
}
}
// ---- F5:并行执行下工具事件按 CallID 配对 ----
func TestToolEventsPairByCallID(t *testing.T) {
f := &fakeRunCtx{}
st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}}
st.queued = []agentllm.ToolCall{
{ID: "c1", Name: "knowledge_search", Arguments: `{}`},
{ID: "c2", Name: "web_search", Arguments: `{}`},
}
// 乱序进出:c1 先开始,c2 先结束(并行下的真实时序)。
st.onEvent(nruntime.Event{Type: "tool_execution_start", Tool: "knowledge_search", CallID: "c1"})
st.onEvent(nruntime.Event{Type: "tool_execution_start", Tool: "web_search", CallID: "c2"})
st.onEvent(nruntime.Event{Type: "tool_execution_end", Tool: "web_search", CallID: "c2"})
st.onEvent(nruntime.Event{Type: "tool_execution_end", Tool: "knowledge_search", CallID: "c1"})
if len(st.inflight) != 0 {
t.Fatalf("inflight must be empty after both ends: %v", st.inflight)
}
if len(st.finalParts) != 4 {
t.Fatalf("finalParts = %d (want 4: call+result ×2)", len(st.finalParts))
}
// 结束顺序决定部件顺序:先 c2 后 c1。
if got := st.finalParts[0].ToolCall.ID; got != "c2" {
t.Fatalf("first tool part = %s (want c2)", got)
}
if got := st.finalParts[1].ToolResult.CallID; got != "c2" {
t.Fatalf("first result part = %s (want c2)", got)
}
if got := st.finalParts[2].ToolCall.ID; got != "c1" {
t.Fatalf("second tool part = %s (want c1)", got)
}
if got := st.finalParts[3].ToolResult.CallID; got != "c1" {
t.Fatalf("second result part = %s (want c1)", got)
}
}
// ---- F5:执行模式与串行集合 ----
func TestToolExecModeAndSerialSet(t *testing.T) {
if got := toolExecMode(core.EffectiveSettings{}); got != nruntime.ExecParallel {
t.Fatalf("default must be parallel, got %q", got)
}
off := core.EffectiveSettings{"runtime": map[string]any{"parallelTools": false}}
if got := toolExecMode(off); got != "" {
t.Fatalf("parallelTools=false must fall back to sequential, got %q", got)
}
// 顺序敏感工具必须串行;可并行的读检索与委派不得误入。
for _, name := range []string{"update_plan", "update_todos", "submit_card", "card_begin", "card_append", "card_commit", "run_workflow", "squad_orchestrate", "run_shell", "run_python", "register_artifact"} {
if !serialTools[name] {
t.Fatalf("serialTools missing %s", name)
}
}
for _, name := range []string{"delegate_task", "review_findings", "knowledge_search", "web_search", "fs_read_file", "write_artifact"} {
if serialTools[name] {
t.Fatalf("serialTools must not contain %s", name)
}
}
}
// ---- F2:MiniMax 前缀自动置位 reasoning_split ----
func TestOpenLLMClientReasoningSplitAuto(t *testing.T) {
if c := openLLMClient("http://x", "k", "MiniMax-M2.7-highspeed"); !c.ReasoningSplit {
t.Fatal("MiniMax model must set ReasoningSplit")
}
if c := openLLMClient("http://x", "k", "MiniMax-M3"); !c.ReasoningSplit {
t.Fatal("MiniMax M3 must set ReasoningSplit")
}
if c := openLLMClient("http://x", "k", "claude-sonnet-5"); c.ReasoningSplit {
t.Fatal("non-MiniMax model must not set ReasoningSplit")
}
}