Why: the exec bridge now threads Last*/cache (Agent-Native#39); squad ProcessEvent carries them to hosts (the product's squad runner reads them for usage accounting). Evidence: GOWORK=off go vet green across squad and adapters/native; nested pin at the merged native. Impact: additive event fields. Refs: Agent-Native#39, N3 plan WP6
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
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
|
|
}
|
|
|
|
// 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)
|
|
}
|