Compare commits

...
Author SHA1 Message Date
weichen 4e8d684125 Point the nested agent-native pin at the intent-archive cascade
ci / vet-and-test (pull_request) Successful in 22s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Agent-Native PR #22 landed after Squad #21; squad's nested
native must carry the same head or Dev check-pins fails.

Evidence: 78b6c647 is the #22 merge on agent-native main.

Impact: Nested pin only.
2026-10-07 08:27:39 +00:00
weichen 5403b6e7a9 Merge pull request 'Point the nested intent pin at the eval-v2 head' (#21) from feature/agent-stack-i1 into main
ci / vet-and-test (push) Successful in 26s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-07 08:24:36 +00:00
weichen ebbebdf20b Point the nested intent pins at the eval-v2 head
ci / vet-and-test (pull_request) Successful in 24s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Agent-Intent PR #8 landed the multi-turn eval format; nested
copies must carry it or Dev check-pins fails.

Evidence: intent main head is the #8 merge.

Impact: Nested pins only.
2026-10-07 08:22:54 +00:00
weichen 592a410288 Merge pull request 'Point the nested agent-native pin at the intent cascade head' (#20) from feature/agent-stack-i1 into main
ci / vet-and-test (push) Successful in 23s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-07 07:49:50 +00:00
weichen 7fe41e86a1 Point the nested agent-native pin at the intent cascade head
ci / vet-and-test (pull_request) Successful in 24s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Why: Agent-Native PR #21 landed the nested intent bump; squad's
nested native must carry the same head or Dev check-pins fails.

Evidence: 6ea69293 is the #21 merge on agent-native main; squad
modules run next.

Impact: Nested pin only.
2026-10-07 07:48:40 +00:00
weichen 6b7a991750 Merge pull request 'Enable NFKC folding in the squad intent gate (I1 WP4)' (#19) from feature/agent-stack-i1 into main
ci / vet-and-test (push) Successful in 26s
ci / docker-checkout-hostgateway (push) Successful in 0s
2026-10-07 07:44:14 +00:00
weichen cb398dadc6 Enable NFKC folding in the squad intent gate and bump the intent pin
ci / vet-and-test (pull_request) Successful in 24s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Both intent hosts should fold compatibility-equivalent input
(backend intentgate already does); the nested pin carries the I1
command model the gate depends on.

Evidence: adapters/intent suite ok; all five squad modules green.

Impact: NFKC on in NewFromOverlay; nested agent-intent at 2554b50.
2026-10-07 07:42:52 +00:00
weichen 7ecc04ee61 Merge pull request 'Point the nested agent-native pin at the WP5 head' (#18) from feature/agent-stack-n1 into main
ci / vet-and-test (push) Successful in 22s
ci / docker-checkout-hostgateway (push) Successful in 0s
2026-10-07 06:38:25 +00:00
weichen 3bf100fd9b Point the nested agent-native pin at the WP5 head
ci / vet-and-test (pull_request) Successful in 22s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Dev moved its agent-native pin to c9c2f23 (PRs #19/#20);
check-pins requires the same combination in squad's nested copy.

Evidence: c9c2f23 is the #20 merge on agent-native main; squad
suites run next.

Impact: Nested pin only.
2026-10-07 06:36:39 +00:00
weichen fc9620e05f Merge pull request 'Pass the permission ask port through the native register Config (N1 WP4)' (#17) from feature/agent-stack-n1 into main
ci / vet-and-test (push) Successful in 21s
ci / docker-checkout-hostgateway (push) Successful in 0s
2026-10-07 05:11:42 +00:00
weichen ef14bda11f Pass the permission ask port through the native register Config
ci / vet-and-test (pull_request) Successful in 21s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Squad members must reach the host's HITL verdict when a kernel
hook answers ask - the new native GateOptions.PermissionAsk field
carries it; nil preserves the legacy ask=reject behavior.

Evidence: go vet clean in all five modules; go test ok everywhere.

Impact: Additive Config field plus its buildOptions mapping.
2026-10-07 05:08:42 +00:00
weichen bf5b210bd1 Point the nested agent-native pin at the N1 spec and guardrails increment
Why: Library PR #18 landed AgentSpec, guardrails, subagent events and
the GateOptions.PermissionAsk field; squad's register mapping needs
that field, and check-pins requires the same combination.

Evidence: native main head is the #18 merge; squad suites run next.

Impact: Nested pin only.
2026-10-07 05:07:37 +00:00
weichen a75d359ade Merge pull request 'Point the nested agent-native pin at the merged N1 increment' (#16) from feature/agent-stack-n1 into main
ci / vet-and-test (push) Successful in 21s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-07 04:43:07 +00:00
weichen 1051dc5ae5 Point the nested agent-native pin at the merged N1 increment
ci / vet-and-test (pull_request) Successful in 20s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Dev moved its agent-native pin to c2befaf (PR #17); squad's
nested pin must carry the same combination or check-pins fails.

Evidence: c2befaf is the merge of PR #17 on agent-native main; squad
test suite runs next.

Impact: Nested pin only.
2026-10-07 04:41:43 +00:00
weichen bf5b389d55 Pin agent-native message shaping
ci / vet-and-test (push) Successful in 24s
ci / docker-checkout-hostgateway (push) Successful in 0s
Merge pull request 'Pin agent-native message shaping' (#15) from feature/sillytavern-review into main
2026-10-04 19:44:56 +00:00
weichen 61985d457f Merge remote-tracking branch 'origin/main' into feature/sillytavern-review
ci / vet-and-test (pull_request) Successful in 23s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
2026-10-04 19:42:54 +00:00
weichen 5a57636cc0 Pin agent-native message shaping
ci / vet-and-test (pull_request) Successful in 23s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Dev now builds provider requests through PrepareMessages. Squad's nested copy has to sit on that same commit or the pin check fails.
Evidence: The gitlink moves from e4020d2 to 3a110a7. No squad source changed.
Impact: Squad compiles against the message shaper. Empty MessageShape still leaves transcripts unchanged.
Refs: docs/records/2026-10-04-sillytavern-review.md
2026-10-04 18:28:10 +00:00
weichen 59ca80cee2 Merge pull request 'Pass code orchestration and MCP ask through the squad backend' (#14) from feature/code-orchestration into main
ci / vet-and-test (push) Successful in 22s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-03 18:07:11 +00:00
weichenandCursor dcd523ea05 Pass code orchestration and MCP ask through the squad backend
ci / vet-and-test (pull_request) Successful in 23s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Why: squad members use the same native kernel, so the script switch and the user-input callback have to reach that backend.
Evidence: GOWORK=off go test . in adapters/native.
Impact: the new fields default to off and nil, so existing member runs stay the same. The agent-native pin moves to the orchestration commit.
Refs: feature/code-orchestration
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 18:03:25 +00:00
weichen d9a573e497 Merge pull request 'Pass file checkpoints and spans through the squad native backend' (#13) from feature/durable-rewind into main
ci / vet-and-test (push) Successful in 22s
ci / docker-checkout-hostgateway (push) Successful in 0s
2026-10-03 17:40:15 +00:00
weichenandCursor a3b49e7b1f Pass file checkpoints and spans through the squad native backend
ci / vet-and-test (pull_request) Successful in 21s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: squad members write files and call tools through the same kernel, so rewind and span hooks have to reach that backend.
Evidence: GOWORK=off go test . in adapters/native.
Impact: nil checkpoints and a nil trace func leave member runs unchanged. The agent-native pin moves to the durability commit.
Refs: feature/durable-rewind
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:27:58 +00:00
weichen 55e9b37770 Merge pull request 'Pass tool gates through the squad native backend' (#12) from feature/permission-hooks into main
ci / vet-and-test (push) Successful in 21s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-03 17:06:48 +00:00
weichen beea97e8cf Pin the rebased agent-native hook bus
ci / vet-and-test (pull_request) Successful in 22s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: The hook-bus commit was replayed onto origin/main, so the squad pin has to follow that commit.
Evidence: deps/agent-native is 0a52d9f, which contains origin/main.
Impact: Squad members load the same kernel commit as the top-level pin.
Refs: feature/permission-hooks
2026-10-03 17:02:37 +00:00
weichenandCursor 41dfe7e3ef Pass tool gates through the squad native backend
Why: Squad members used the same kernel as direct chat but dropped stop, reminder, and before-tool hooks.
Evidence: GOWORK=off go test . in adapters/native passed after pinning agent-native to 99d623a.
Impact: Zero GateOptions still builds the previous backend. A set BeforeTool now runs inside the member loop.
Refs: feature/permission-hooks
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:02:28 +00:00
weichen 68b18ec2c5 Merge pull request 'Pin agent-native for context engineering' (#11) from feature/context-engineering into main
ci / vet-and-test (push) Successful in 23s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-03 16:25:49 +00:00
weichenandCursor 4d534508a9 Pin agent-native for context engineering
ci / vet-and-test (pull_request) Successful in 21s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Squad runs on the same native loop, so it should see tool-result clearing, folded reminders, and the compaction retry.
Evidence: deps/agent-native is 6fd1d44.
Impact: No squad behavior change beyond the native loop it already calls.
Refs: feature/context-engineering
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 16:21:18 +00:00
weichen 3abc5944bd Merge pull request 'Pin agent-native to the streaming retry fix' (#10) from feature/llm-robustness into main
ci / vet-and-test (push) Successful in 22s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-03 15:56:15 +00:00
weichenandCursor 9efce79b86 Pin agent-native to the streaming retry fix
ci / vet-and-test (pull_request) Successful in 23s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: Squad members share the same model stream, so the retry and overflow fixes have to be the commit this repo builds.
Evidence: agent-native 9f5751c. No squad behavior change.
Impact: Only the agent-native gitlink moves.
Refs: feature/llm-robustness
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 15:52:36 +00:00
weichen c5c5a24e2c Merge pull request 'Move cross-library squad code into adapter modules' (#9) from feature/agent-stack-d1 into main
ci / vet-and-test (push) Successful in 29s
ci / docker-checkout-hostgateway (push) Successful in 1s
2026-10-03 14:02:33 +00:00
weichen 02c0d6aae4 Pin agent-native to the SSE abort fix
ci / vet-and-test (pull_request) Successful in 25s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Why: the squad checkout has to record the native head that treats a cancelled stream as aborted.
Evidence: go test -count=50 -race ./runtime/llm/ passed on d84a67c.
Impact: gitlink only.
Refs: D1
2026-10-03 13:51:51 +00:00
weichen 5e414ffecc Pin intent and native to the D1 schema heads
ci / vet-and-test (pull_request) Successful in 24s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Why: the squad checkout has to record the same intent and native commits Dev will pin.
Evidence: gitlinks only. Schema and adapter tests already passed on those heads.
Impact: no squad source change.
Refs: D1
2026-10-03 13:43:54 +00:00
weichenandCursor f68e5bde7b Keep the agent-intent import inside the intent adapter
Why: the command has to load guard overlays without naming agent-intent types. Only adapters/intent may import that library.
Evidence: GOWORK=off go test -count=1 in adapters/intent and cmd/agent-squad passed.
Impact: CLI calls NewFromOverlay. Guard domains stay guard.
Refs: D1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 13:43:03 +00:00
weichenandCursor a182d7662b Move cross-library squad code into adapter modules
Why: the squad core has to compile without agent-native or agent-intent. Ports stay in the core; adapters and the CLI own the concrete clients.
Evidence: GOWORK=off go test -count=1 ./squad/ ./store/ ./textextract/ ./schema/ passed, and the intent, sqlite, native, and cmd modules passed their own go test.
Impact: core go.mod keeps error-code-system only. Nested gitlinks stay. Callers register the native backend through agent-squad/adapters/native.
Refs: D1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 13:40:58 +00:00
weichen 20a9eb8078 Close the squad P1 gaps the audit listed for this batch
ci / vet-and-test (push) Successful in 47s
ci / docker-checkout-hostgateway (push) Successful in 0s
Merge via API
2026-10-03 13:22:54 +00:00
57 changed files with 1130 additions and 353 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
agent-squad
/agent-squad
agent-squad-data/
*.test
@@ -6,7 +6,6 @@ import (
"sort"
"strings"
"agent-native/exec"
"agent-squad/squad"
)
@@ -67,7 +66,7 @@ func (g *Gate) EchoRunner() *Runner {
}
// Run 实现 squad.IntentRunner。未知意图/未知 handler fail-closed(不静默降级)。
func (r *Runner) Run(ctx context.Context, step squad.IntentStep) (*exec.Result, error) {
func (r *Runner) Run(ctx context.Context, step squad.IntentStep) (*squad.ExecResult, error) {
if r == nil || r.gate == nil || r.gate.Snapshot() == nil {
return nil, fmt.Errorf("intent runner: gate not configured")
}
@@ -91,12 +90,12 @@ func (r *Runner) Run(ctx context.Context, step squad.IntentStep) (*exec.Result,
return textResult(step, text), nil
}
func textResult(step squad.IntentStep, text string) *exec.Result {
func textResult(step squad.IntentStep, text string) *squad.ExecResult {
patches := make(map[string][]byte, len(step.Targets))
for _, t := range step.Targets {
patches[t] = []byte(text)
}
return &exec.Result{Success: true, Patches: patches, Logs: "intent:" + step.IntentID}
return &squad.ExecResult{Success: true, Patches: patches, Logs: "intent:" + step.IntentID}
}
func echoText(step squad.IntentStep, handlerKey string, _ bool) string {
@@ -31,6 +31,21 @@ type Options struct {
Roots []string
}
// NewFromOverlay 从 JSON overlay 构造闸门,并把 guard 域标成受保护域。
// 命令行用它,这样命令本身不必 import agent-intent。
// NFKC 开启:全角/兼容等价输入与词条同形折叠(与 backend intentgate 一致)。
func NewFromOverlay(ctx context.Context, fsys fs.FS, roots ...string) (*Gate, error) {
return New(ctx, Options{
IntentOptions: intent.Options{
GuardDomains: []string{"guard"},
ProtectedDomains: []string{"guard"},
NFKC: true,
},
FS: fsys,
Roots: roots,
})
}
// New 构造闸门。没有任何意图来源时返回 (nil, nil)——引擎拿到 nil IntentGate,
// 行为与原实现一致(T0 行为解耦)。
func New(ctx context.Context, o Options) (*Gate, error) {
+22
View File
@@ -0,0 +1,22 @@
module agent-squad/adapters/intent
go 1.27
require (
agent-intent v0.0.0
agent-squad v0.0.0
)
require (
error-code-system v0.0.0 // indirect
golang.org/x/text v0.29.0 // indirect
logging-system v0.0.0 // indirect
)
replace agent-intent => ../../deps/agent-intent
replace agent-squad => ../..
replace error-code-system => ../../deps/error-code-system
replace logging-system => ../../deps/logging-system
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
@@ -7,7 +7,6 @@ import (
"strings"
"testing"
"agent-native/exec"
"agent-squad/squad"
)
@@ -16,7 +15,7 @@ type integrationScriptBackend struct{ content string }
func (b *integrationScriptBackend) Name() string { return "script" }
func (b *integrationScriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *integrationScriptBackend) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
patches := map[string][]byte{}
for _, t := range req.Targets {
out := filepath.Join(req.WorkDir, t)
@@ -26,7 +25,7 @@ func (b *integrationScriptBackend) Execute(ctx context.Context, req exec.Request
}
patches[t] = []byte(b.content)
}
return &exec.Result{Success: true, Patches: patches, Logs: "script"}, nil
return &squad.ExecResult{Success: true, Patches: patches, Logs: "script"}, nil
}
const intentReport = "# 竞品分析报告\n\n## 概述\n\n由意图定向的蓝图模板生成的交付物,长度满足最小质检要求。\n\n## 结论\n\n符合预期。\n"
+21
View File
@@ -0,0 +1,21 @@
module agent-squad/adapters/native
go 1.27
require (
agent-native v0.0.0
agent-squad v0.0.0
)
require (
error-code-system v0.0.0 // indirect
logging-system v0.0.0 // indirect
)
replace agent-native => ../../deps/agent-native
replace agent-squad => ../..
replace error-code-system => ../../deps/error-code-system
replace logging-system => ../../deps/logging-system
+203
View File
@@ -0,0 +1,203 @@
// Package native 把 agent-native 接成小队的 Executor 与 ModelClient。
package native
import (
"context"
"fmt"
"strings"
"time"
nativexec "agent-native/adapters/native"
"agent-native/exec"
"agent-native/runtime"
"agent-native/runtime/checkpoint"
"agent-native/runtime/hooks"
"agent-native/runtime/llm"
"agent-native/runtime/mcp"
"agent-native/runtime/subagent"
"agent-native/runtime/tools"
"agent-squad/squad"
)
// Config 是组合根交给 agent-native 后端的装配。零值只做模型执行。
type Config struct {
Client *llm.Client
DefaultModel string
MaxTurns int
Timeout time.Duration
Search tools.SearchProvider
Exec *tools.ExecConfig
Fetch *tools.FetchConfig
MCP *mcp.Manager
SkillsRoots []string
Subagent subagent.Factory
Compact *nativexec.CompactConfig
HostTools []tools.Tool
OnEvent func(ev squad.ProcessEvent)
Stream llm.StreamFunc
MaxToolCalls int
BeforeTool func(call llm.ToolCall) error
AfterTool func(call llm.ToolCall, res llm.Message)
Stop func(last llm.Message, info runtime.StopInfo) runtime.StopDecision
Reminders func(turn int) []string
RepeatGuard bool
Hooks *hooks.Bus
Checkpoints *checkpoint.Log
CheckpointAnchor func() string
Trace func(name string, attrs map[string]string) func()
CodeOrchestration bool
Ask func(ctx context.Context, question string) (string, error)
// PermissionAsk 把内核钩子的 ask 决策转交宿主裁决(nil = 内核保持
// ask 按拒绝处理的旧行为)。经 nativexec.GateOptions 透传。
PermissionAsk func(ctx context.Context, call llm.ToolCall) runtime.AskVerdict
}
// Register 把 "agent-native" 工厂注册进解析器。
func Register(r *squad.BackendResolver, client *llm.Client, defaultModel string, maxTurns int, timeout time.Duration) {
RegisterFull(r, Config{Client: client, DefaultModel: defaultModel, MaxTurns: maxTurns, Timeout: timeout})
}
// RegisterFull 注册带工具装配的 "agent-native" 工厂。
func RegisterFull(r *squad.BackendResolver, cfg Config) {
r.Register("agent-native", func(role squad.RoleSpec) (squad.Executor, error) {
opts, err := buildOptions(cfg, role)
if err != nil {
return nil, err
}
next := &bridge{next: nativexec.New(opts)}
if cfg.OnEvent == nil {
return next, nil
}
return &eventTap{next: next, on: cfg.OnEvent}, nil
})
}
func buildOptions(cfg Config, role squad.RoleSpec) (nativexec.Options, error) {
model := role.Model
if model == "" {
model = cfg.DefaultModel
}
if model == "" {
return nativexec.Options{}, fmt.Errorf("agent-native backend requires a model (role=%s)", role.Role)
}
opts := nativexec.Options{
ModelOptions: nativexec.ModelOptions{
Model: model, MaxTurns: cfg.MaxTurns, Timeout: cfg.Timeout, Stream: cfg.Stream,
},
ToolOptions: nativexec.ToolOptions{
Search: cfg.Search, Fetch: cfg.Fetch, MCP: cfg.MCP,
SkillsRoots: cfg.SkillsRoots, Subagent: cfg.Subagent, HostTools: cfg.HostTools,
Checkpoints: cfg.Checkpoints, CheckpointAnchor: cfg.CheckpointAnchor,
CodeOrchestration: cfg.CodeOrchestration,
},
ObserveOptions: nativexec.ObserveOptions{Trace: cfg.Trace},
SessionOptions: nativexec.SessionOptions{Compact: cfg.Compact, MaxToolCalls: cfg.MaxToolCalls},
GateOptions: nativexec.GateOptions{
BeforeTool: cfg.BeforeTool, AfterTool: cfg.AfterTool, Stop: cfg.Stop,
Reminders: cfg.Reminders, RepeatGuard: cfg.RepeatGuard, Hooks: cfg.Hooks,
Ask: cfg.Ask, PermissionAsk: cfg.PermissionAsk,
},
}
if cfg.Client != nil {
opts.APIBase, opts.APIKey = cfg.Client.BaseURL, cfg.Client.APIKey
}
if cfg.Exec != nil && (role.BashPermission == squad.BashPermissionAllow || role.BashPermission == squad.BashPermissionAsk) {
opts.Exec = cfg.Exec
}
return opts, nil
}
type bridge struct{ next exec.Backend }
func (b *bridge) Name() string { return b.next.Name() }
func (b *bridge) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
res, err := b.next.Execute(ctx, toNative(req))
if res == nil {
return nil, err
}
return fromNative(res), err
}
func toNative(req squad.ExecRequest) exec.Request {
out := exec.Request{
WorkDir: req.WorkDir, Prompt: req.Prompt, Instructions: req.Instructions,
Targets: req.Targets, Timeout: req.Timeout, ToolProfile: req.ToolProfile,
Model: req.Model, ToolAllowlist: req.ToolAllowlist, Skills: req.Skills,
SessionScope: req.SessionScope, ExecutionID: req.ExecutionID, Env: req.Env,
}
if req.Budget != nil {
out.Budget = &exec.Budget{
MaxPromptTokens: req.Budget.MaxPromptTokens, MaxCompletionTokens: req.Budget.MaxCompletionTokens,
MaxToolCalls: req.Budget.MaxToolCalls, Deadline: req.Budget.Deadline,
}
}
if req.OnEvent != nil {
prev := req.OnEvent
out.OnEvent = func(ev exec.ProcessEvent) {
prev(squad.ProcessEvent{
Type: ev.Type, Text: ev.Text, Tool: ev.Tool, Detail: ev.Detail,
PromptTokens: ev.PromptTokens, CompletionTokens: ev.CompletionTokens,
})
}
}
return out
}
func fromNative(res *exec.Result) *squad.ExecResult {
return &squad.ExecResult{
Success: res.Success, Patches: res.Patches, Logs: res.Logs, ErrorReason: res.ErrorReason,
Retryable: res.Retryable, PromptTokens: res.PromptTokens, CompletionTokens: res.CompletionTokens,
}
}
type eventTap struct {
next squad.Executor
on func(squad.ProcessEvent)
}
func (b *eventTap) Name() string { return b.next.Name() }
func (b *eventTap) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
prev := req.OnEvent
req.OnEvent = func(ev squad.ProcessEvent) {
b.on(ev)
if prev != nil {
prev(ev)
}
}
return b.next.Execute(ctx, req)
}
// LLM 把 llm.Client 适配成 ModelClient。
type LLM struct{ Inner *llm.Client }
func (c LLM) Complete(ctx context.Context, turn squad.ModelTurn) (string, error) {
if c.Inner == nil {
return "", fmt.Errorf("llm planner: no model client")
}
msgs := []llm.Message{
{Role: llm.RoleSystem, Content: turn.System},
{Role: llm.RoleUser, Content: turn.User},
}
opts := llm.StreamOptions{MaxTokens: turn.MaxTokens, APIBase: turn.APIBase, APIKey: turn.APIKey}
var sb strings.Builder
for ev := range c.Inner.Stream(ctx, turn.Model, msgs, nil, opts) {
if ev.Kind == llm.KindError {
return "", fmt.Errorf("%s", ev.ErrorMessage)
}
if ev.Kind == llm.KindTextDelta {
sb.WriteString(ev.Text)
}
if ev.Kind == llm.KindMessageEnd {
if ev.Message.IsError {
return "", fmt.Errorf("assistant error: %s", ev.Message.Content)
}
if sb.Len() == 0 {
sb.WriteString(ev.Message.Content)
}
break
}
}
return sb.String(), nil
}
+49
View File
@@ -0,0 +1,49 @@
package native
import (
"testing"
"time"
"agent-native/runtime/llm"
"agent-native/runtime/tools"
"agent-squad/squad"
)
func TestBuildOptionsBashPermissionGating(t *testing.T) {
base := Config{
Client: &llm.Client{BaseURL: "http://x", APIKey: "k"},
DefaultModel: "m1", MaxTurns: 5, Timeout: time.Minute,
Exec: &tools.ExecConfig{Bash: "/bin/bash"},
SkillsRoots: []string{"/skills"},
}
deny, err := buildOptions(base, squad.RoleSpec{Role: "r", BashPermission: squad.BashPermissionDeny})
if err != nil {
t.Fatal(err)
}
if deny.Exec != nil {
t.Fatal("deny role must not get exec tools even when config provides them")
}
ask, _ := buildOptions(base, squad.RoleSpec{Role: "r", BashPermission: squad.BashPermissionAsk})
if ask.Exec == nil {
t.Fatal("ask role gets exec tools")
}
allow, _ := buildOptions(base, squad.RoleSpec{Role: "r", BashPermission: squad.BashPermissionAllow})
if allow.Exec == nil {
t.Fatal("allow role gets exec tools")
}
empty, _ := buildOptions(base, squad.RoleSpec{Role: "r"})
if empty.Exec != nil {
t.Fatal("unset bash_permission defaults to deny")
}
none, _ := buildOptions(Config{Client: base.Client, DefaultModel: "m"}, squad.RoleSpec{Role: "r", BashPermission: squad.BashPermissionAllow})
if none.Exec != nil {
t.Fatal("no Exec config = no exec tools")
}
roleModel, _ := buildOptions(base, squad.RoleSpec{Role: "r", Model: "big"})
if roleModel.Model != "big" {
t.Fatalf("role model must win, got %s", roleModel.Model)
}
if _, err := buildOptions(Config{Client: base.Client}, squad.RoleSpec{Role: "r"}); err == nil {
t.Fatal("missing model must fail-closed")
}
}
+30
View File
@@ -0,0 +1,30 @@
module agent-squad/adapters/sqlite
go 1.27
require (
agent-native v0.0.0
modernc.org/sqlite v1.37.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect
logging-system v0.0.0 // indirect
modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect
)
replace agent-native => ../../deps/agent-native
replace agent-squad => ../..
replace error-code-system => ../../deps/error-code-system
replace logging-system => ../../deps/logging-system
+47
View File
@@ -0,0 +1,47 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+7 -14
View File
@@ -15,12 +15,12 @@ import (
"strings"
"time"
"agent-intent/intent"
"agent-native/runtime/llm"
"agent-squad/platform"
"agent-squad/server"
squadintent "agent-squad/adapters/intent"
nativeadp "agent-squad/adapters/native"
platform "agent-squad/adapters/sqlite"
"agent-squad/cmd/agent-squad/server"
"agent-squad/squad"
"agent-squad/squadintent"
"agent-squad/store"
"logging-system/config"
@@ -122,12 +122,12 @@ func assemble(dir, plannerName, modelOverride, intentRoot string) (*app, error)
fmt.Fprintln(os.Stderr, "提示:llm 规划器需要 AGENT_SQUAD_LLM_BASE_URL/API_KEY/MODEL,回退 rule 规划器")
} else {
client := &llm.Client{BaseURL: baseURL, APIKey: apiKey}
planner = squad.NewLLMPlanner(client, model)
planner = squad.NewLLMPlanner(nativeadp.LLM{Inner: client}, model)
}
}
// 真实智能体执行一律经 agent-native 自研内核(角色 backend=agent-native)。
if baseURL != "" && apiKey != "" && model != "" {
backends.RegisterAgentNative(&llm.Client{BaseURL: baseURL, APIKey: apiKey}, model, 12, 10*time.Minute)
nativeadp.Register(backends, &llm.Client{BaseURL: baseURL, APIKey: apiKey}, model, 12, 10*time.Minute)
}
// 事件双层:FileEventSink 落盘(seq+脱敏,重启可审计)→ FanoutSink(SSE 实时/重放)。
@@ -137,14 +137,7 @@ func assemble(dir, plannerName, modelOverride, intentRoot string) (*app, error)
var catalog *squad.IntentCatalog
var runner squad.IntentRunner
if intentRoot != "" {
g, err := squadintent.New(context.Background(), squadintent.Options{
IntentOptions: intent.Options{
GuardDomains: []string{"guard"},
ProtectedDomains: []string{"guard"},
},
FS: os.DirFS(intentRoot),
Roots: []string{"."},
})
g, err := squadintent.NewFromOverlay(context.Background(), os.DirFS(intentRoot), ".")
if err != nil {
return nil, fmt.Errorf("意图配置加载失败(fail-closed):%w", err)
}
+2 -3
View File
@@ -9,7 +9,6 @@ import (
"path/filepath"
"strings"
"agent-native/exec"
"agent-squad/squad"
)
@@ -21,7 +20,7 @@ type demoBackend struct {
func (b *demoBackend) Name() string { return "demo" }
func (b *demoBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *demoBackend) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
files, ok := b.script[key]
if !ok {
@@ -46,7 +45,7 @@ func (b *demoBackend) Execute(ctx context.Context, req exec.Request) (*exec.Resu
}
patches[t] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches, Logs: "demo backend replay"}, nil
return &squad.ExecResult{Success: true, Patches: patches, Logs: "demo backend replay"}, nil
}
// cmdDemo 离线全链路演示:注册角色 → 规划 → 执行 → HITL 阻塞缺口挂起 →
@@ -10,7 +10,7 @@ import (
"time"
"agent-native/runtime/llm"
"agent-squad/platform"
platform "agent-squad/adapters/sqlite"
)
func cmdPlatform(args []string) int {
@@ -11,7 +11,6 @@ import (
"strings"
"testing"
"agent-native/exec"
"agent-squad/squad"
"agent-squad/store"
)
@@ -21,7 +20,7 @@ type replayBackend struct{ script map[string]map[string]string }
func (b *replayBackend) Name() string { return "replay" }
func (b *replayBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *replayBackend) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
files := b.script[key]
patches := map[string][]byte{}
@@ -37,7 +36,7 @@ func (b *replayBackend) Execute(ctx context.Context, req exec.Request) (*exec.Re
}
patches[t] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches, Logs: "replay"}, nil
return &squad.ExecResult{Success: true, Patches: patches, Logs: "replay"}, nil
}
const deliverable = "# 契约验收交付物\n\n## 概述\n\n本文件由回放后端生成,用于验证 agent-squad 全链路契约:注册、规划、执行、HITL、质检与审计。\n\n## 结论\n\n全链路行为符合契约。\n"
+45
View File
@@ -0,0 +1,45 @@
module agent-squad/cmd/agent-squad
go 1.27
require (
agent-native v0.0.0
agent-squad v0.0.0
agent-squad/adapters/intent v0.0.0
agent-squad/adapters/native v0.0.0
agent-squad/adapters/sqlite v0.0.0
error-code-system v0.0.0
logging-system v0.0.0
)
require (
agent-intent v0.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.29.0 // indirect
modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect
modernc.org/sqlite v1.37.0 // indirect
)
replace agent-intent => ../../deps/agent-intent
replace agent-native => ../../deps/agent-native
replace agent-squad => ../..
replace agent-squad/adapters/intent => ../../adapters/intent
replace agent-squad/adapters/native => ../../adapters/native
replace agent-squad/adapters/sqlite => ../../adapters/sqlite
replace error-code-system => ../../deps/error-code-system
replace logging-system => ../../deps/logging-system
+49
View File
@@ -0,0 +1,49 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+1 -1
View File
@@ -4,7 +4,7 @@ package main
import (
"os"
"agent-squad/cli"
"agent-squad/cmd/agent-squad/cli"
)
func main() {
@@ -15,7 +15,7 @@ import (
"sync"
"time"
"agent-squad/platform"
platform "agent-squad/adapters/sqlite"
"agent-squad/squad"
"agent-squad/store"
@@ -13,7 +13,6 @@ import (
"testing"
"time"
"agent-native/exec"
"agent-squad/squad"
"agent-squad/store"
@@ -25,7 +24,7 @@ type scriptBackend struct{ script map[string]map[string]string }
func (b *scriptBackend) Name() string { return "script" }
func (b *scriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *scriptBackend) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
files := b.script[key]
patches := map[string][]byte{}
@@ -39,7 +38,7 @@ func (b *scriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Re
_ = os.WriteFile(out, []byte(content), 0o644)
patches[t2] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches}, nil
return &squad.ExecResult{Success: true, Patches: patches}, nil
}
const goodReport = "# API 测试交付物\n\n## 概述\n\n脚本后端生成的合格交付物正文,长度超过五十个字符以满足最小质检要求。\n"
+1 -27
View File
@@ -2,32 +2,6 @@ module agent-squad
go 1.27
require (
agent-intent v0.0.0
agent-native v0.0.0
error-code-system v0.0.0
logging-system v0.0.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.29.0 // indirect
modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect
modernc.org/sqlite v1.37.0 // indirect
)
replace agent-intent => ./deps/agent-intent
replace agent-native => ./deps/agent-native
require error-code-system v0.0.0
replace error-code-system => ./deps/error-code-system
replace logging-system => ./deps/logging-system
-25
View File
@@ -1,25 +0,0 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
+1
View File
@@ -0,0 +1 @@
{"key":"demo","title":"示例","steps":[{"id":"s1","role":"writer","title":"起草","parallel":false}]}
+1
View File
@@ -0,0 +1 @@
{"role":"writer","name":"写作者","backend":"stub","tool_profile":"workspace"}
+59
View File
@@ -0,0 +1,59 @@
package schema
import (
"encoding/json"
"os"
"testing"
"agent-squad/squad"
)
func TestSamplesRoundTrip(t *testing.T) {
var role squad.RoleSpec
mustRoundTrip(t, "role.sample.json", &role)
if role.Role != "writer" || role.Name != "写作者" {
t.Fatalf("%+v", role)
}
var bp squad.Blueprint
mustRoundTrip(t, "blueprint.sample.json", &bp)
if bp.Key != "demo" || len(bp.Steps) != 1 {
t.Fatalf("%+v", bp)
}
var plan squad.WorkflowPlan
mustRoundTrip(t, "workflow-plan.sample.json", &plan)
if plan.WorkflowID != "wf-1" || plan.Status != "pending" {
t.Fatalf("%+v", plan)
}
}
func mustRoundTrip(t *testing.T, name string, dest any) {
t.Helper()
raw, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(raw, dest); err != nil {
t.Fatal(err)
}
back, err := json.Marshal(dest)
if err != nil {
t.Fatal(err)
}
again := reflectNew(dest)
if err := json.Unmarshal(back, again); err != nil {
t.Fatal(err)
}
}
func reflectNew(v any) any {
switch v.(type) {
case *squad.RoleSpec:
return &squad.RoleSpec{}
case *squad.Blueprint:
return &squad.Blueprint{}
case *squad.WorkflowPlan:
return &squad.WorkflowPlan{}
default:
panic("unknown sample")
}
}
+1
View File
@@ -0,0 +1 @@
{"workflow_id":"wf-1","title":"示例","status":"pending","current_step_index":0,"steps":[{"id":"s1","role":"writer","title":"起草","parallel":false}]}
+11 -127
View File
@@ -1,24 +1,15 @@
package squad
import (
"context"
"fmt"
"sync"
"time"
"agent-native/adapters/native"
"agent-native/exec"
"agent-native/runtime/llm"
"agent-native/runtime/mcp"
"agent-native/runtime/subagent"
"agent-native/runtime/tools"
"error-code-system/errcode"
)
// BackendFactory 按角色构造后端:角色级 Model/ToolProfile 在工厂闭包层绑定
// (agent-native exec.Request 无 Model 字段,绑定不侵入执行契约)。
type BackendFactory func(role RoleSpec) (exec.Backend, error)
// (agent-native ExecRequest 无 Model 字段,绑定不侵入执行契约)。
type BackendFactory func(role RoleSpec) (Executor, error)
// BackendResolver 角色 → 智能体后端解析器。
// 内置 "stub"(agent-native 离线桩);真实智能体经 RegisterAgentNative 注册
@@ -28,18 +19,18 @@ type BackendFactory func(role RoleSpec) (exec.Backend, error)
type BackendResolver struct {
mu sync.RWMutex
factories map[string]BackendFactory
underlying map[string]exec.Backend // 无工厂直挂的后端实例
wrapped map[string]exec.Backend // 角色(含模型绑定)→ 已包装实例缓存
underlying map[string]Executor // 无工厂直挂的后端实例
wrapped map[string]Executor // 角色(含模型绑定)→ 已包装实例缓存
}
// NewBackendResolver 创建解析器并注册内置后端。
func NewBackendResolver() *BackendResolver {
r := &BackendResolver{
factories: make(map[string]BackendFactory),
underlying: make(map[string]exec.Backend),
wrapped: make(map[string]exec.Backend),
underlying: make(map[string]Executor),
wrapped: make(map[string]Executor),
}
r.RegisterInstance("stub", &exec.StubBackend{})
r.RegisterInstance("stub", &StubExecutor{})
return r
}
@@ -51,119 +42,12 @@ func (r *BackendResolver) Register(name string, f BackendFactory) {
}
// RegisterInstance 直接注册后端实例(忽略角色级绑定)。
func (r *BackendResolver) RegisterInstance(name string, b exec.Backend) {
func (r *BackendResolver) RegisterInstance(name string, b Executor) {
r.mu.Lock()
defer r.mu.Unlock()
r.underlying[name] = b
}
// AgentNativeConfig 声明 agent-native 后端的全量能力装配(组合根提供)。
// 零值 = 仅基础模型执行:搜索/取回/执行/MCP/技能/委派工具一律不注册(fail-closed)。
type AgentNativeConfig struct {
Client *llm.Client
DefaultModel string
MaxTurns int
Timeout time.Duration
// 以下均为 agent-native Options 的直通注入,详见其注释。
Search tools.SearchProvider
Exec *tools.ExecConfig
Fetch *tools.FetchConfig
MCP *mcp.Manager
SkillsRoots []string
Subagent subagent.Factory
Compact *native.CompactConfig
// HostTools 注册进内核工作区。与内置工具同名时该步骤失败,不覆盖。
HostTools []tools.Tool
// OnEvent 收到内核的逐步过程事件(文本、工具、用量)。与 Request.OnEvent 同时生效。
OnEvent func(ev exec.ProcessEvent)
// Stream 测试注入。为空时按 Client 访问模型。
Stream llm.StreamFunc
// MaxToolCalls 单步工具调用上限。0 表示不限制。
MaxToolCalls int
}
// buildAgentNativeOptions 按角色把配置换算为 native.Options。
// BashPermission 三态在此执行:deny = 不装配执行工具(无论组合根是否提供 Exec);
// ask/allow = 装配(ask 的人工门由引擎 permission 挂起点先行把关)。
func buildAgentNativeOptions(cfg AgentNativeConfig, role RoleSpec) (native.Options, error) {
model := role.Model
if model == "" {
model = cfg.DefaultModel
}
if model == "" {
return native.Options{}, fmt.Errorf("agent-native backend requires a model (role=%s)", role.Role)
}
opts := native.Options{
Model: model,
MaxTurns: cfg.MaxTurns,
Timeout: cfg.Timeout,
Search: cfg.Search,
Fetch: cfg.Fetch,
MCP: cfg.MCP,
SkillsRoots: cfg.SkillsRoots,
Subagent: cfg.Subagent,
Compact: cfg.Compact,
HostTools: cfg.HostTools,
Stream: cfg.Stream,
MaxToolCalls: cfg.MaxToolCalls,
}
if cfg.Client != nil {
opts.APIBase, opts.APIKey = cfg.Client.BaseURL, cfg.Client.APIKey
}
// 显式 allow/ask 才装配(注册层默认 deny,但派生/程序构造的 RoleSpec
// 未必经注册——工厂层兜底,空白串不放行)。
if cfg.Exec != nil && (role.BashPermission == BashPermissionAllow || role.BashPermission == BashPermissionAsk) {
opts.Exec = cfg.Exec
}
return opts, nil
}
// processEventBackend 把配置级过程回调接到每次 Execute,同时保留请求自带的 OnEvent。
type processEventBackend struct {
next exec.Backend
on func(ev exec.ProcessEvent)
}
func (b *processEventBackend) Name() string { return b.next.Name() }
func (b *processEventBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
if b.on != nil {
prev := req.OnEvent
req.OnEvent = func(ev exec.ProcessEvent) {
b.on(ev)
if prev != nil {
prev(ev)
}
}
}
return b.next.Execute(ctx, req)
}
// RegisterAgentNative 注册 "agent-native" 后端工厂(基础签名,向后兼容):
// 角色级 Model 覆盖 defaultModel,无附加能力装配。
func (r *BackendResolver) RegisterAgentNative(client *llm.Client, defaultModel string, maxTurns int, timeout time.Duration) {
r.RegisterAgentNativeFull(AgentNativeConfig{
Client: client, DefaultModel: defaultModel, MaxTurns: maxTurns, Timeout: timeout,
})
}
// RegisterAgentNativeFull 注册带全量能力装配的 "agent-native" 工厂:
// 搜索/取回/执行/MCP/技能/委派经配置注入;BashPermission=deny 的角色
// 不装配执行工具。智能体执行全部委托 agent-native,本库不实现 Agent。
func (r *BackendResolver) RegisterAgentNativeFull(cfg AgentNativeConfig) {
r.Register("agent-native", func(role RoleSpec) (exec.Backend, error) {
opts, err := buildAgentNativeOptions(cfg, role)
if err != nil {
return nil, err
}
next := native.New(opts)
if cfg.OnEvent == nil {
return next, nil
}
return &processEventBackend{next: next, on: cfg.OnEvent}, nil
})
}
// ListBackends 返回可用后端名(排序稳定)。
func (r *BackendResolver) ListBackends() []string {
r.mu.RLock()
@@ -191,7 +75,7 @@ func (r *BackendResolver) ListBackends() []string {
// Resolve 解析角色的执行后端:Role.Backend → Options.DefaultBackend → "stub"。
// MaxConcurrentTasks > 1 时以 agent-native 并发限流包装(per-role 缓存,跨步骤共享;
// 创建段持写锁防并行首解析竞态——否则并发步骤会各拿一份限流器)。
func (r *BackendResolver) Resolve(role RoleSpec, defaultBackend string) (exec.Backend, string, error) {
func (r *BackendResolver) Resolve(role RoleSpec, defaultBackend string) (Executor, string, error) {
name := role.Backend
if name == "" {
name = defaultBackend
@@ -219,7 +103,7 @@ func (r *BackendResolver) Resolve(role RoleSpec, defaultBackend string) (exec.Ba
return b, name, nil
}
var b exec.Backend
var b Executor
if hasUnder {
b = under
} else if hasFactory {
@@ -234,7 +118,7 @@ func (r *BackendResolver) Resolve(role RoleSpec, defaultBackend string) (exec.Ba
// MaxConcurrentTasks>=1 即包装限流(1 = 对象级串行;0 = 不限)。
if max := role.MaxConcurrentTasks; max >= 1 {
b = exec.WrapWithConcurrencyLimit(b, max)
b = WrapWithConcurrencyLimit(b, max)
}
r.wrapped[cacheKey] = b
return b, name, nil
+141
View File
@@ -0,0 +1,141 @@
package squad
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
)
func slashClean(p string) string {
return filepath.ToSlash(filepath.Clean(p))
}
// MatchDeliverableRel reports whether a runner rel_path refers to target
// inside workDir. Accepts output.md, ./output.md, absolute paths under workDir,
// and a duplicated step prefix (tasks/01_draft/output.md when workDir already
// ends with tasks/01_draft). Paths that escape workDir never match.
func MatchDeliverableRel(workDir, relPath, target string) bool {
target = slashClean(strings.TrimSpace(target))
if target == "" || target == "." || strings.HasPrefix(target, "../") || strings.Contains(target, "..") {
return false
}
raw := strings.TrimSpace(relPath)
if raw == "" {
return false
}
abs := raw
if !filepath.IsAbs(raw) {
abs = filepath.Join(workDir, raw)
}
abs = filepath.Clean(abs)
rel, err := filepath.Rel(filepath.Clean(workDir), abs)
if err != nil {
return false
}
rel = slashClean(rel)
if rel == ".." || strings.HasPrefix(rel, "../") {
return false
}
if rel == target {
return true
}
if filepath.Base(rel) != filepath.Base(target) {
return false
}
prefix := slashClean(filepath.Dir(rel))
wd := slashClean(workDir)
return prefix != "." && (strings.HasSuffix(wd, "/"+prefix) || strings.HasSuffix(wd, prefix))
}
// CollectTargetPatches maps incoming runner keys onto canonical target names
// and also accepts a non-empty file already at workDir/target. Empty bodies
// do not count — callers must not invent a deliverable.
func CollectTargetPatches(workDir string, targets []string, incoming map[string][]byte) map[string][]byte {
out := make(map[string][]byte, len(targets))
for _, t := range targets {
t = strings.TrimSpace(t)
if t == "" {
continue
}
for k, v := range incoming {
if len(bytes.TrimSpace(v)) == 0 {
continue
}
if MatchDeliverableRel(workDir, k, t) {
out[t] = v
break
}
}
if _, ok := out[t]; ok {
continue
}
p := filepath.Join(workDir, t)
if b, err := os.ReadFile(p); err == nil && len(bytes.TrimSpace(b)) > 0 {
out[t] = b
}
}
return out
}
// MaterializeTargets writes collected non-empty target bodies to workDir/<target>.
// Returns a missing-target error without writing a stub if any required file is absent.
func MaterializeTargets(workDir string, targets []string, incoming map[string][]byte) (map[string][]byte, error) {
collected := CollectTargetPatches(workDir, targets, incoming)
for _, t := range targets {
t = strings.TrimSpace(t)
if t == "" {
continue
}
body, ok := collected[t]
if !ok || len(bytes.TrimSpace(body)) == 0 {
return collected, fmt.Errorf("missing target deliverable %s", t)
}
dest := filepath.Join(workDir, filepath.FromSlash(t))
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return collected, err
}
if err := rejectSymlinkEscape(workDir, dest); err != nil {
return collected, err
}
// Container-written files may be root-owned; make writable before overwrite.
_ = os.Chmod(dest, 0o666)
if err := os.WriteFile(dest, body, 0o644); err != nil {
return collected, err
}
}
return collected, nil
}
func rejectSymlinkEscape(workDir, dest string) error {
root, err := filepath.Abs(workDir)
if err != nil {
return err
}
absDest, err := filepath.Abs(dest)
if err != nil {
return err
}
rel, err := filepath.Rel(root, absDest)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return fmt.Errorf("typed result path rejected")
}
resolvedRoot, err := filepath.EvalSymlinks(root)
if err != nil {
resolvedRoot = root
}
parent := filepath.Dir(absDest)
resolvedParent, err := filepath.EvalSymlinks(parent)
if err != nil {
return fmt.Errorf("typed result path rejected")
}
relParent, err := filepath.Rel(resolvedRoot, resolvedParent)
if err != nil || relParent == ".." || strings.HasPrefix(relParent, ".."+string(os.PathSeparator)) {
return fmt.Errorf("typed result path rejected")
}
if info, err := os.Lstat(absDest); err == nil && info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("typed result path rejected")
}
return nil
}
+2 -4
View File
@@ -8,8 +8,6 @@ import (
"strings"
"testing"
"time"
"agent-native/exec"
)
// scriptBackend 测试用脚本后端:按子目录名回放产出。
@@ -17,7 +15,7 @@ type scriptBackend struct{ script map[string]map[string]string }
func (b *scriptBackend) Name() string { return "script" }
func (b *scriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *scriptBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
files := b.script[key]
patches := map[string][]byte{}
@@ -33,7 +31,7 @@ func (b *scriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Re
}
patches[t] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches}, nil
return &ExecResult{Success: true, Patches: patches}, nil
}
const goodReport = "# 测试交付物\n\n## 概述\n\n这是由脚本后端生成的合格交付物,长度超过五十个字符以满足最小质检要求。\n\n## 内容\n\n正文内容。\n"
+2 -4
View File
@@ -6,8 +6,6 @@ import (
"path/filepath"
"strings"
"testing"
"agent-native/exec"
)
// scriptedEvaluator 第一步后建议:追加一个补漏步骤 + 跳过冗余步骤。
@@ -146,7 +144,7 @@ type mutableScriptBackend struct {
func (b *mutableScriptBackend) Name() string { return "mut" }
func (b *mutableScriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *mutableScriptBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.calls++
if b.mutate != nil && b.calls == b.mutateAt {
b.mutate(b.script)
@@ -164,7 +162,7 @@ func (b *mutableScriptBackend) Execute(ctx context.Context, req exec.Request) (*
_ = os.WriteFile(out, []byte(content), 0o644)
patches[tg] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches}, nil
return &ExecResult{Success: true, Patches: patches}, nil
}
// TestQualityRepairAutoRerun 验证:质检失败 → 自动重跑交付步骤 → 通过。
+1 -3
View File
@@ -3,8 +3,6 @@ package squad
import (
"context"
"fmt"
"agent-native/exec"
)
// IntentRejection 守卫拦截:拒绝该次运行并给出引导("识别即拒绝"语义)。
@@ -96,7 +94,7 @@ type IntentStep struct {
// IntentRunner 系统功能执行器(squadintent 提供实现:查 spec.Handler 并调宿主处理函数)。
type IntentRunner interface {
Run(ctx context.Context, step IntentStep) (*exec.Result, error)
Run(ctx context.Context, step IntentStep) (*ExecResult, error)
}
// DefaultIntentRole 系统功能步的默认角色 key(无后端语义,仅满足编制覆盖)。
+2 -4
View File
@@ -6,8 +6,6 @@ import (
"path/filepath"
"strings"
"testing"
"agent-native/exec"
)
type stubRunner struct {
@@ -16,7 +14,7 @@ type stubRunner struct {
err error
}
func (r *stubRunner) Run(ctx context.Context, step IntentStep) (*exec.Result, error) {
func (r *stubRunner) Run(ctx context.Context, step IntentStep) (*ExecResult, error) {
r.calls = append(r.calls, step)
if r.err != nil {
return nil, r.err
@@ -25,7 +23,7 @@ func (r *stubRunner) Run(ctx context.Context, step IntentStep) (*exec.Result, er
for _, t := range step.Targets {
patches[t] = []byte(r.text)
}
return &exec.Result{Success: true, Patches: patches, Logs: "stub-intent"}, nil
return &ExecResult{Success: true, Patches: patches, Logs: "stub-intent"}, nil
}
func testCatalog() *IntentCatalog {
+6 -8
View File
@@ -8,32 +8,30 @@ import (
"path/filepath"
"strings"
"time"
"agent-native/exec"
)
// leaderExecRequest 构造 leader 轮次的执行请求:与标准步骤同等装配
// (过程事件桥、ToolProfile/Skills/工具白名单、执行标识与预算)——
// 修平此前 leader 轮丢弃 OnEvent 与 ToolProfile 的缺口。
func leaderExecRequest(drv *flowDriver, st *runState, leader RoleSpec, workDir, prompt, instructions string, targets []string, timeout time.Duration) exec.Request {
func leaderExecRequest(drv *flowDriver, st *runState, leader RoleSpec, workDir, prompt, instructions string, targets []string, timeout time.Duration) ExecRequest {
stepID := fmt.Sprintf("leader_turn_%d", st.Leader.Turns)
var budget *exec.Budget
var budget *ExecBudget
if timeout > 0 {
budget = &exec.Budget{Deadline: time.Now().Add(timeout)}
budget = &ExecBudget{Deadline: time.Now().Add(timeout)}
}
return exec.Request{
return ExecRequest{
WorkDir: workDir,
Prompt: prompt,
Instructions: instructions,
Targets: targets,
Timeout: timeout,
ToolProfile: orDefault(leader.ToolProfile, exec.ToolProfileWorkspace),
ToolProfile: orDefault(leader.ToolProfile, ToolProfileWorkspace),
ToolAllowlist: leader.Tools,
Skills: leader.Skills,
SessionScope: st.Meta.ID + "/" + stepID,
ExecutionID: st.Meta.ID + "-" + stepID,
Budget: budget,
OnEvent: func(ev exec.ProcessEvent) {
OnEvent: func(ev ProcessEvent) {
drv.emit(st, Event{Type: EvDetail, StepID: stepID, Role: leader.Role, Text: ev.Text,
Data: marshalJSON(map[string]string{"process_type": ev.Type, "tool": ev.Tool, "detail": ev.Detail})})
},
+10 -12
View File
@@ -10,8 +10,6 @@ import (
"sync"
"testing"
"time"
"agent-native/exec"
)
// leaderScriptBackend leader 模式脚本后端:00_leader 目录按轮次回放
@@ -26,7 +24,7 @@ type leaderScriptBackend struct {
func (b *leaderScriptBackend) Name() string { return "leader-script" }
func (b *leaderScriptBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *leaderScriptBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
if b.prompts != nil {
b.mu.Lock()
@@ -43,7 +41,7 @@ func (b *leaderScriptBackend) Execute(ctx context.Context, req exec.Request) (*e
}
out := filepath.Join(req.WorkDir, "delegation.json")
_ = os.WriteFile(out, []byte(b.decisions[idx]), 0o644)
return &exec.Result{Success: true, Patches: map[string][]byte{"delegation.json": []byte(b.decisions[idx])}}, nil
return &ExecResult{Success: true, Patches: map[string][]byte{"delegation.json": []byte(b.decisions[idx])}}, nil
}
files := b.script[key]
patches := map[string][]byte{}
@@ -57,7 +55,7 @@ func (b *leaderScriptBackend) Execute(ctx context.Context, req exec.Request) (*e
_ = os.WriteFile(out, []byte(content), 0o644)
patches[t] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches,
return &ExecResult{Success: true, Patches: patches,
PromptTokens: 100, CompletionTokens: 50}, nil
}
@@ -67,7 +65,7 @@ func delegateJSON(member, title string) string {
const closeJSON = `{"action":"close","reason":"所有子任务完成,交付物满足目标"}`
func newLeaderEngine(t *testing.T, backend exec.Backend, roles ...RoleSpec) *SquadEngine {
func newLeaderEngine(t *testing.T, backend Executor, roles ...RoleSpec) *SquadEngine {
t.Helper()
dir := t.TempDir()
reg := NewRoleRegistry()
@@ -385,18 +383,18 @@ func TestWakeupGuardMaxFires(t *testing.T) {
type eventingBackend struct {
inner *leaderScriptBackend
mu sync.Mutex
reqs []exec.Request
reqs []ExecRequest
}
func (b *eventingBackend) Name() string { return "leader-script" }
func (b *eventingBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *eventingBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.mu.Lock()
b.reqs = append(b.reqs, req)
b.mu.Unlock()
if req.OnEvent != nil {
req.OnEvent(exec.ProcessEvent{Type: "tool_execution_start", Tool: "grep", Detail: `{"q":"x"}`})
req.OnEvent(exec.ProcessEvent{Type: "tool_execution_end", Tool: "grep", Text: "found"})
req.OnEvent(ProcessEvent{Type: "tool_execution_start", Tool: "grep", Detail: `{"q":"x"}`})
req.OnEvent(ProcessEvent{Type: "tool_execution_end", Tool: "grep", Text: "found"})
}
return b.inner.Execute(ctx, req)
}
@@ -435,7 +433,7 @@ func TestLeaderTurnsEmitDetailEventsAndCarryProfile(t *testing.T) {
t.Fatalf("want completed, got %s err=%s", res.Status, res.Error)
}
// leader 轮的请求带上了 ToolProfile/白名单/技能与标识(修平 leader 请求缺口)。
var leaderReq *exec.Request
var leaderReq *ExecRequest
for i := range eb.reqs {
if strings.HasSuffix(eb.reqs[i].WorkDir, "00_leader") {
leaderReq = &eb.reqs[i]
@@ -444,7 +442,7 @@ func TestLeaderTurnsEmitDetailEventsAndCarryProfile(t *testing.T) {
if leaderReq == nil {
t.Fatal("leader request missing")
}
if leaderReq.ToolProfile != exec.ToolProfileWorkspace || len(leaderReq.ToolAllowlist) != 2 || len(leaderReq.Skills) != 1 {
if leaderReq.ToolProfile != ToolProfileWorkspace || len(leaderReq.ToolAllowlist) != 2 || len(leaderReq.Skills) != 1 {
t.Fatalf("leader request not fully assembled: %+v", leaderReq)
}
if !strings.Contains(leaderReq.ExecutionID, "leader_turn_") {
+11 -26
View File
@@ -5,15 +5,13 @@ import (
"encoding/json"
"fmt"
"strings"
"agent-native/runtime/llm"
)
// LLMPlanner LLM 规划器:用模型理解目标 → 拆解任务 → 选/派角色 → 生成步骤。
// 模型客户端与默认模型在构造时注入(角色级模型在派生角色时由模型自行指定)。
// 输出为严格 JSON;解析失败时重试一次,仍失败则 fail-closed 返回错误。
type LLMPlanner struct {
Client *llm.Client
Client ModelClient
Model string
APIBase string
APIKey string
@@ -25,7 +23,7 @@ type LLMPlanner struct {
}
// NewLLMPlanner 创建 LLM 规划器。
func NewLLMPlanner(client *llm.Client, model string) *LLMPlanner {
func NewLLMPlanner(client ModelClient, model string) *LLMPlanner {
return &LLMPlanner{Client: client, Model: model}
}
@@ -283,33 +281,20 @@ func segmentGoal(text string) string {
// complete 调一次模型并拼接完整文本。
func (p *LLMPlanner) complete(ctx context.Context, system, user string) (string, error) {
msgs := []llm.Message{
{Role: llm.RoleSystem, Content: system},
{Role: llm.RoleUser, Content: user},
if p.Client == nil {
return "", fmt.Errorf("llm planner: no model client")
}
opts := llm.StreamOptions{MaxTokens: p.MaxTokens, APIBase: p.APIBase, APIKey: p.APIKey}
var sb strings.Builder
for ev := range p.Client.Stream(ctx, p.Model, msgs, nil, opts) {
if ev.Kind == llm.KindError {
return "", fmt.Errorf("%s", ev.ErrorMessage)
}
if ev.Kind == llm.KindTextDelta {
sb.WriteString(ev.Text)
}
if ev.Kind == llm.KindMessageEnd {
if ev.Message.IsError {
return "", fmt.Errorf("assistant error: %s", ev.Message.Content)
}
if sb.Len() == 0 {
sb.WriteString(ev.Message.Content)
}
break
}
text, err := p.Client.Complete(ctx, ModelTurn{
Model: p.Model, System: system, User: user,
MaxTokens: p.MaxTokens, APIBase: p.APIBase, APIKey: p.APIKey,
})
if err != nil {
return "", err
}
if ctx.Err() != nil {
return "", ctx.Err()
}
return sb.String(), nil
return text, nil
}
// parseLLMPlan 从模型输出稳健解析 JSON(剥离 think 块/代码围栏/前后杂文)。
+151
View File
@@ -0,0 +1,151 @@
package squad
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// Executor 是小队核心对一次智能体执行的端口。实现放在适配器模块。
type Executor interface {
Name() string
Execute(ctx context.Context, req ExecRequest) (*ExecResult, error)
}
// ModelClient 是规划器对一次模型补全的端口。实现放在适配器模块。
type ModelClient interface {
Complete(ctx context.Context, turn ModelTurn) (string, error)
}
// ModelTurn 是一次规划补全的输入。适配器负责把它变成供应商协议。
type ModelTurn struct {
Model string
System string
User string
MaxTokens int
APIBase string
APIKey string
}
const (
ToolProfileWorkspace = "workspace"
ToolProfileReasoning = "reasoning"
)
// ExecRequest 是一次步骤执行的输入。字段与 native exec.Request 的 JSON 对齐,类型不共享。
type ExecRequest struct {
WorkDir string `json:"work_dir"`
Prompt string `json:"prompt"`
Instructions string `json:"instructions"`
Targets []string `json:"targets"`
Timeout time.Duration `json:"timeout"`
ToolProfile string `json:"tool_profile,omitempty"`
OnEvent func(ProcessEvent) `json:"-"`
Model string `json:"model,omitempty"`
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
Skills []string `json:"skills,omitempty"`
Budget *ExecBudget `json:"budget,omitempty"`
SessionScope string `json:"session_scope,omitempty"`
ExecutionID string `json:"execution_id,omitempty"`
Env map[string]string `json:"env,omitempty"`
}
// ExecBudget 是一次步骤执行的共享预算。
type ExecBudget struct {
MaxPromptTokens int `json:"max_prompt_tokens,omitempty"`
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
MaxToolCalls int `json:"max_tool_calls,omitempty"`
Deadline time.Time `json:"deadline,omitempty"`
}
// ProcessEvent 是过程帧。
type ProcessEvent struct {
Type string
Text string
Tool string
Detail string
PromptTokens int
CompletionTokens int
}
// ExecResult 是一次步骤执行的终态。
type ExecResult struct {
Success bool `json:"success"`
Patches map[string][]byte `json:"patches"`
Logs string `json:"logs"`
ErrorReason string `json:"error_reason"`
Retryable bool `json:"retryable,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
}
// StubExecutor 是离线桩,测试与未接模型时使用。
type StubExecutor struct{}
func (s *StubExecutor) Name() string { return "stub" }
func (s *StubExecutor) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
patches := make(map[string][]byte)
for _, target := range req.Targets {
outPath := filepath.Join(req.WorkDir, target)
_ = os.MkdirAll(filepath.Dir(outPath), 0o755)
var content []byte
if strings.HasSuffix(target, ".json") {
if existing, err := os.ReadFile(outPath); err == nil {
var dummy any
if json.Unmarshal(existing, &dummy) == nil {
content = existing
}
}
if len(content) == 0 {
content = []byte(`{"status":"mock_ok"}`)
}
} else {
content = []byte(fmt.Sprintf("# 模拟生成结果\n\n- 目标文件: %s\n- 提示词摘要: %s\n", target, req.Prompt))
}
_ = os.WriteFile(outPath, content, 0o644)
patches[target] = content
}
return &ExecResult{Success: true, Patches: patches, Logs: "Stub agent execution completed."}, nil
}
// LimitingExecutor 用有界信号量限制底层执行并发。
type LimitingExecutor struct {
inner Executor
sem chan struct{}
}
// WrapWithConcurrencyLimit 包装执行器;max<=0 时默认 8。
func WrapWithConcurrencyLimit(inner Executor, max int) Executor {
if inner == nil {
return nil
}
if max <= 0 {
max = 8
}
return &LimitingExecutor{inner: inner, sem: make(chan struct{}, max)}
}
func (l *LimitingExecutor) Name() string {
if l.inner == nil {
return "limiting"
}
return l.inner.Name()
}
func (l *LimitingExecutor) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
select {
case l.sem <- struct{}{}:
defer func() { <-l.sem }()
case <-ctx.Done():
return &ExecResult{
Success: false,
ErrorReason: fmt.Sprintf("agent concurrency wait canceled: %v", ctx.Err()),
}, ctx.Err()
}
return l.inner.Execute(ctx, req)
}
+2 -4
View File
@@ -9,8 +9,6 @@ import (
"sync"
"testing"
"time"
"agent-native/exec"
)
func approvalScript() map[string]map[string]string {
@@ -114,7 +112,7 @@ type countBackend struct {
func (b *countBackend) Name() string { return "script" }
func (b *countBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *countBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.mu.Lock()
b.n++
b.mu.Unlock()
@@ -188,7 +186,7 @@ type blockBackend struct {
func (b *blockBackend) Name() string { return "script" }
func (b *blockBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *blockBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.once.Do(func() { close(b.started) })
select {
case <-b.release:
+3 -49
View File
@@ -8,22 +8,18 @@ import (
"sync"
"testing"
"time"
"agent-native/exec"
"agent-native/runtime/llm"
"agent-native/runtime/tools"
)
// captureBackend 记录收到的请求(v2 字段贯通断言用)。
type captureBackend struct {
mu sync.Mutex
reqs []exec.Request
reqs []ExecRequest
script map[string]string // 子目录名 → 单一目标内容
}
func (b *captureBackend) Name() string { return "capture" }
func (b *captureBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *captureBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.mu.Lock()
b.reqs = append(b.reqs, req)
b.mu.Unlock()
@@ -39,7 +35,7 @@ func (b *captureBackend) Execute(ctx context.Context, req exec.Request) (*exec.R
_ = os.WriteFile(out, []byte(content), 0o644)
patches[t] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches}, nil
return &ExecResult{Success: true, Patches: patches}, nil
}
func TestStepRequestCarriesRoleSpecV2Fields(t *testing.T) {
@@ -88,48 +84,6 @@ func TestStepRequestCarriesRoleSpecV2Fields(t *testing.T) {
}
}
func TestBuildAgentNativeOptionsBashPermissionGating(t *testing.T) {
base := AgentNativeConfig{
Client: &llm.Client{BaseURL: "http://x", APIKey: "k"},
DefaultModel: "m1", MaxTurns: 5, Timeout: time.Minute,
Exec: &tools.ExecConfig{Bash: "/bin/bash"},
SkillsRoots: []string{"/skills"},
}
deny, err := buildAgentNativeOptions(base, RoleSpec{Role: "r", BashPermission: BashPermissionDeny})
if err != nil {
t.Fatal(err)
}
if deny.Exec != nil {
t.Fatal("deny role must not get exec tools even when config provides them")
}
ask, _ := buildAgentNativeOptions(base, RoleSpec{Role: "r", BashPermission: BashPermissionAsk})
if ask.Exec == nil {
t.Fatal("ask role gets exec tools (the HITL gate runs before the step)")
}
allow, _ := buildAgentNativeOptions(base, RoleSpec{Role: "r", BashPermission: BashPermissionAllow})
if allow.Exec == nil {
t.Fatal("allow role gets exec tools")
}
// 空白名 = 注册时默认 deny,同样不装配。
empty, _ := buildAgentNativeOptions(base, RoleSpec{Role: "r"})
if empty.Exec != nil {
t.Fatal("unset bash_permission defaults to deny at registration; no exec tools")
}
// 无 Exec 配置时任何档位都不装配。
none, _ := buildAgentNativeOptions(AgentNativeConfig{Client: base.Client, DefaultModel: "m"}, RoleSpec{Role: "r", BashPermission: BashPermissionAllow})
if none.Exec != nil {
t.Fatal("no Exec config = no exec tools regardless of permission")
}
// 模型解析:角色级覆盖默认。
roleModel, _ := buildAgentNativeOptions(base, RoleSpec{Role: "r", Model: "big"})
if roleModel.Model != "big" {
t.Fatalf("role model must win, got %s", roleModel.Model)
}
if _, err := buildAgentNativeOptions(AgentNativeConfig{Client: base.Client}, RoleSpec{Role: "r"}); err == nil {
t.Fatal("missing model must fail-closed")
}
}
func TestDeriveInheritsExecutionConfig(t *testing.T) {
reg := NewRoleRegistry()
base := RoleSpec{
+2 -4
View File
@@ -5,8 +5,6 @@ import (
"os"
"path/filepath"
"testing"
"agent-native/exec"
)
type fixedRouter struct{ dec *RouteDecision }
@@ -89,14 +87,14 @@ type captureSkillsBackend struct{ got []string }
func (b *captureSkillsBackend) Name() string { return "capture-skills" }
func (b *captureSkillsBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *captureSkillsBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.got = append([]string{}, req.Skills...)
for _, target := range req.Targets {
path := filepath.Join(req.WorkDir, target)
_ = os.MkdirAll(filepath.Dir(path), 0o755)
_ = os.WriteFile(path, []byte(goodReport), 0o644)
}
return &exec.Result{Success: true, Patches: map[string][]byte{"output.md": []byte(goodReport)}}, nil
return &ExecResult{Success: true, Patches: map[string][]byte{"output.md": []byte(goodReport)}}, nil
}
func TestStepSkillsOverrideRoleSkills(t *testing.T) {
+4 -6
View File
@@ -8,8 +8,6 @@ import (
"sync"
"testing"
"time"
"agent-native/exec"
)
// recordingBackend 记录每次收到的 prompt(按目录),产出按 script。
@@ -22,7 +20,7 @@ type recordingBackend struct {
func (b *recordingBackend) Name() string { return "rec" }
func (b *recordingBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *recordingBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
b.mu.Lock()
b.prompts[key] = append(b.prompts[key], req.Prompt)
@@ -43,7 +41,7 @@ func (b *recordingBackend) Execute(ctx context.Context, req exec.Request) (*exec
if b.tokens {
pt, ct = 120, 80
}
return &exec.Result{Success: true, Patches: patches, PromptTokens: pt, CompletionTokens: ct}, nil
return &ExecResult{Success: true, Patches: patches, PromptTokens: pt, CompletionTokens: ct}, nil
}
func TestSteeringInjectedAtNextStep(t *testing.T) {
@@ -137,7 +135,7 @@ type slowCountBackend struct {
func (b *slowCountBackend) Name() string { return "slow" }
func (b *slowCountBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
func (b *slowCountBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.mu.Lock()
b.cur++
if b.cur > b.peak {
@@ -153,7 +151,7 @@ func (b *slowCountBackend) Execute(ctx context.Context, req exec.Request) (*exec
_ = os.MkdirAll(filepath.Dir(out), 0o755)
_ = os.WriteFile(out, []byte(goodReport), 0o644)
}
return &exec.Result{Success: true, Patches: map[string][]byte{}}, nil
return &ExecResult{Success: true, Patches: map[string][]byte{}}, nil
}
func TestRoleConcurrencyLimit(t *testing.T) {
+200
View File
@@ -0,0 +1,200 @@
package squad
import (
"os"
"path/filepath"
"strings"
"unicode"
)
// MaxTransientStepInvocations is the product-layer bound: the first
// invocation plus one fresh re-invocation. SDK in-process auto_retry is
// separate and must not be confused with this.
const MaxTransientStepInvocations = 2
type FailureClass int
const (
FailureNone FailureClass = iota
FailureTransient
FailurePermanent
)
func (c FailureClass) String() string {
switch c {
case FailureTransient:
return "transient"
case FailurePermanent:
return "permanent"
default:
return "none"
}
}
type TraceSignals struct {
ProviderTransient bool
Settled bool
RetryExhausted bool
HasUsableDeliverable bool // always false: write() args in traces are not deliverables
}
type StepFailureSignals struct {
Err error
Result *ExecResult
WorkDir string
ExtraText string
TargetMissing bool
// TargetExists means this invocation wrote a non-empty file that differs
// from the pre-invoke baseline. Do not retry or overwrite it. The step
// still fails unless the backend already reported a trusted success.
TargetExists bool
}
type FailureAssessment struct {
Class FailureClass
Reason string
}
func ScanTraceText(text string) TraceSignals {
var out TraceSignals
if text == "" {
return out
}
// Line scan only. Never parse write.arguments.content into a body.
for _, line := range strings.Split(text, "\n") {
if line == "" {
continue
}
lower := strings.ToLower(line)
if strings.Contains(lower, `"type":"agent_settled"`) || strings.Contains(lower, `"type": "agent_settled"`) {
out.Settled = true
}
if strings.Contains(lower, `"type":"auto_retry_end"`) && strings.Contains(lower, `"success":false`) {
out.ProviderTransient = true
out.RetryExhausted = true
}
if strings.Contains(lower, `"type":"auto_retry_start"`) {
out.ProviderTransient = true
}
if strings.Contains(lower, `"stopreason":"error"`) {
out.ProviderTransient = true
}
if strings.Contains(lower, "connection error") {
out.ProviderTransient = true
}
if strings.Contains(lower, `"errormessage":"terminated"`) || strings.Contains(lower, `"errormessage": "terminated"`) {
out.ProviderTransient = true
}
}
return out
}
func ReadWorkDirTraces(workDir string) string {
if strings.TrimSpace(workDir) == "" {
return ""
}
p := filepath.Join(workDir, "execution.log")
b, err := os.ReadFile(p)
if err != nil {
return ""
}
const capBytes = 512 << 10
if len(b) > capBytes {
b = b[len(b)-capBytes:]
}
return string(b)
}
func AssessStepFailure(sig StepFailureSignals) FailureAssessment {
reason := combinedFailureText(sig)
lower := strings.ToLower(reason)
trace := ScanTraceText(sig.ExtraText + "\n" + ReadWorkDirTraces(sig.WorkDir))
missing := sig.TargetMissing || strings.Contains(lower, "missing target deliverable")
if sig.TargetExists {
return FailureAssessment{Class: FailurePermanent, Reason: "target-already-present: do not overwrite"}
}
if isPermanentReason(lower) {
return FailureAssessment{Class: FailurePermanent, Reason: "permanent: " + compactReason(reason)}
}
runnerFailed := (sig.Result != nil && sig.Result.Retryable) || isRunnerExplicitFailure(lower)
provider := trace.ProviderTransient || hasProviderTermOrConn(lower)
if runnerFailed {
return FailureAssessment{Class: FailureTransient, Reason: "runner-explicit-failure: " + compactReason(reason)}
}
if provider && missing {
kind := "provider-transient-missing-target"
if trace.Settled {
kind = "settled-failure-missing-target"
}
return FailureAssessment{Class: FailureTransient, Reason: kind + ": " + compactReason(reason)}
}
// Model settled cleanly but never wrote the target (e.g. text-only reply,
// no write tool). One product-layer re-invocation with a corrective prompt
// is worth trying; still bounded by MaxTransientStepInvocations.
if missing && trace.Settled && !provider {
return FailureAssessment{Class: FailureTransient, Reason: "settled-no-deliverable: " + compactReason(reason)}
}
if missing {
return FailureAssessment{Class: FailurePermanent, Reason: "permanent-missing: " + compactReason(reason)}
}
return FailureAssessment{Class: FailurePermanent, Reason: "fail-closed: " + compactReason(reason)}
}
func combinedFailureText(sig StepFailureSignals) string {
var parts []string
if sig.Err != nil {
parts = append(parts, sig.Err.Error())
}
if sig.Result != nil {
if sig.Result.ErrorReason != "" {
parts = append(parts, sig.Result.ErrorReason)
}
if sig.Result.Logs != "" {
parts = append(parts, sig.Result.Logs)
}
}
return strings.Join(parts, " ")
}
func isPermanentReason(lower string) bool {
return strings.Contains(lower, "prompt_empty") ||
strings.Contains(lower, "unauthorized") ||
strings.Contains(lower, "budget_exceeded") ||
strings.Contains(lower, "治理强制终止") ||
strings.Contains(lower, "治理拦截")
}
func hasProviderTermOrConn(lower string) bool {
return strings.Contains(lower, "connection error") ||
strings.Contains(lower, "terminated") ||
strings.Contains(lower, "econnreset") ||
strings.Contains(lower, "socket hang up")
}
func isRunnerExplicitFailure(lower string) bool {
return strings.Contains(lower, "pi remote runner call failed") ||
strings.Contains(lower, "auto_retry_failed") ||
strings.Contains(lower, "auto_retry_end") ||
(hasProviderTermOrConn(lower) && !strings.Contains(lower, "missing target deliverable"))
}
func compactReason(s string) string {
s = strings.Join(strings.FieldsFunc(s, func(r rune) bool {
return r == '\n' || r == '\r'
}), " ")
s = strings.TrimSpace(s)
if s == "" {
return "unknown"
}
// Drop accidental write-argument bodies if a caller stuffed logs here.
if len(s) > 240 {
s = s[:240]
}
return strings.Map(func(r rune) rune {
if unicode.IsControl(r) && r != '\t' {
return -1
}
return r
}, s)
}
+12 -14
View File
@@ -10,8 +10,6 @@ import (
"sync"
"time"
"agent-native/exec"
"error-code-system/errcode"
)
@@ -233,7 +231,7 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
prompt = st.injectSteering(prompt)
instructions := readFileOr(filepath.Join(subDir, "AGENTS.md"), d.opts.Workspaces.ResolveInstructions(role))
var backend exec.Backend
var backend Executor
backendName := ""
if step.IntentID != "" {
backendName = "intent:" + step.IntentID
@@ -253,13 +251,13 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
}
// 步骤级共享预算:重试与首次尝试共用同一截止时刻
//(agent-execution 标准:重试层共享总预算,不随次数翻倍)。
var stepBudget *exec.Budget
var stepBudget *ExecBudget
if d.opts.StepTimeout > 0 {
stepBudget = &exec.Budget{Deadline: time.Now().Add(d.opts.StepTimeout)}
stepBudget = &ExecBudget{Deadline: time.Now().Add(d.opts.StepTimeout)}
}
var result *exec.Result
var result *ExecResult
var err error
for attempt := 1; attempt <= exec.MaxTransientStepInvocations; attempt++ {
for attempt := 1; attempt <= MaxTransientStepInvocations; attempt++ {
reqPrompt := prompt
if attempt > 1 {
prevReason := "交付物缺失"
@@ -270,14 +268,14 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
}
reqPrompt += "\n\n【重试提示】上一次执行未成功(" + prevReason + ")。请务必把结果写入:" + strings.Join(targets, ", ")
}
req := exec.Request{
req := ExecRequest{
WorkDir: subDir,
Prompt: reqPrompt,
Instructions: instructions,
Targets: targets,
Timeout: d.opts.StepTimeout,
ToolProfile: orDefault(role.ToolProfile, exec.ToolProfileWorkspace),
OnEvent: func(ev exec.ProcessEvent) {
ToolProfile: orDefault(role.ToolProfile, ToolProfileWorkspace),
OnEvent: func(ev ProcessEvent) {
d.emit(st, Event{Type: EvDetail, StepID: step.ID, Role: step.Role, Text: ev.Text,
Data: marshalJSON(map[string]string{"process_type": ev.Type, "tool": ev.Tool, "detail": ev.Detail})})
},
@@ -301,7 +299,7 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
Title: step.Title, WorkDir: subDir, Targets: targets,
})
if err == nil && result != nil && len(result.Patches) > 0 {
_, _ = exec.MaterializeTargets(subDir, targets, result.Patches)
_, _ = MaterializeTargets(subDir, targets, result.Patches)
}
} else {
result, err = backend.Execute(ctx, req)
@@ -311,19 +309,19 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
}
// 交付物物化:后端返回 patches 但未写盘时补写。
if err == nil && len(result.Patches) > 0 {
if _, merr := exec.MaterializeTargets(subDir, targets, result.Patches); merr == nil {
if _, merr := MaterializeTargets(subDir, targets, result.Patches); merr == nil {
if allTargetsPresent(subDir, targets) {
result.Success = true
break
}
}
}
assessment := exec.AssessStepFailure(exec.StepFailureSignals{
assessment := AssessStepFailure(StepFailureSignals{
Err: err, Result: result, WorkDir: subDir,
ExtraText: fmt.Sprintf("step=%s backend=%s", step.ID, backendName),
TargetMissing: !allTargetsPresent(subDir, targets),
})
if assessment.Class != exec.FailureTransient || attempt == exec.MaxTransientStepInvocations {
if assessment.Class != FailureTransient || attempt == MaxTransientStepInvocations {
step.Status = "failed"
if err != nil {
step.Error = err.Error()