Files
Agent-Squad/squad/port.go
T
weichen 676a1ede8c Carry tool-call identity and outcome through squad events
Why: F14 squad half - the event port dropped CallID/Status/ErrorReason,
so downstream hosts paired by tool name and showed failures as success.
Evidence: bridge and process-event tests pass (verify-u11b.json);
taps/wrappers preserve the fields; concurrent callbacks copy per-event.
Impact: new omitempty fields only; legacy events byte-identical.
Refs: feature/agent-stack-audit-remediation-b2 U11b
2026-10-22 03:25:28 +00:00

210 lines
7.6 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 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
// 以下在 Type=usage 时填写(N3 WP6:上一请求与缓存命中拆分)。
LastPromptTokens int
LastCompletionTokens int
CacheReadTokens int
CacheWriteTokens int
// CallID 是本次工具调用的身份(与 native exec.ProcessEvent.CallID 同语义):
// tool_call_delta / tool_execution_start / tool_execution_end 携带同一 ID,
// 消费方按 ID 配对而不是按到达顺序——并行工具调用会交错到达。
// 空串 = 上游未填充(legacy 版本),不是「一次无名调用」。
CallID string `json:"call_id,omitempty"`
// Status 是 tool_execution_end 的真实结局,取值见 ToolStatus*;
// 空串 = legacy/未知,不据此判定失败(与既有行为一致)。
Status string `json:"status,omitempty"`
// ErrorReason 是 Status=error 时的真实失败原因原文(与 native 同语义);
// 空串 = 无失败或上游未填充。
ErrorReason string `json:"error_reason,omitempty"`
}
// ProcessEvent.Status 的取值域(与 native 侧一致;空串 = legacy/未知)。
const (
ToolStatusOK = "ok"
ToolStatusError = "error"
ToolStatusDenied = "denied"
ToolStatusCanceled = "canceled"
)
// ExecResult 是一次步骤执行的终态。
//
// 语义契约(F05/U3b):Success=true 才是经过验证的成功;仅在交付物齐全且
// ExecutionID 复用前提下成立。仅凭已物化的文件存在不能把失败翻成成功。
// 后端可以选择性填写以下可选字段;缺省 = 旧的读取行为。
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"`
// Canceled 为 true 时表示执行是被请求/中断终止的,不是业务失败。
// F05/KTD4:取消结果不应被 retry 改判为可重试;这是用户/系统显式意图。
Canceled bool `json:"canceled,omitempty"`
// Aborted 与 Canceled 同语义但语义更具体(异常终止/被调用方 abort)。
Aborted bool `json:"aborted,omitempty"`
// SideEffects 报告执行是否可能产生过外部动作。空值 = "possible"(旧行为)。
// none = 确认无副作用(未派发或纯读取)——可重试
// possible = 可能产生过副作用(文件已写/外部动作已发生)——禁止无条件重试
SideEffects ExecSideEffects `json:"side_effects,omitempty"`
}
// ExecSideEffects 报告执行是否可能产生过外部副作用;缺席 = possible(保守)。
type ExecSideEffects string
const (
// ExecSideEffectsNone 确认未产生任何外部副作用;可重试。
ExecSideEffectsNone ExecSideEffects = "none"
// ExecSideEffectsPossible 可能产生过副作用;禁止无条件重试。
ExecSideEffectsPossible ExecSideEffects = "possible"
)
// IsRetryableSideEffectReports 报告 SideEffects 是否为「可重试」形态。
// 默认 possible 视为不可重试(保守):调用方必须显式声明 none 才放开重试。
func (r *ExecResult) IsRetryableSideEffectReports() bool {
return r != nil && r.SideEffects == ExecSideEffectsNone
}
// IsCanceled 报告结果是否被取消/中止(不是业务失败)。
func (r *ExecResult) IsCanceled() bool {
return r != nil && (r.Canceled || r.Aborted)
}
// 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)
}