Why: N1 WP5/WP6 library side - the gate decision logic, artifact checks, verdict parsing and progress-reminder throttling move from the product into a gates package behind ports, and the native backend gains a contract form so conformance has a real implementation to drive. Evidence: go vet clean; go test ./... 27 packages ok, including the conformance suite (succeeded/descriptor/error-sideeffects) against the real backend. Impact: new runtime/gates package (ClaimGate, CheckArtifacts, ParseVerdict, ProgressReminder, shared caps); adapters/native.Backend gains Contract() returning an exec.ContractBackend wrapper - the legacy facade keeps its signature so squad consumption is untouched. Product texts (fix prompts, verifier prompts) stay injected by hosts. Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP5/WP6
90 lines
3.1 KiB
Go
90 lines
3.1 KiB
Go
package native
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"agent-native/exec"
|
|
)
|
|
|
|
// contractBackend 把 Backend 适配为 exec.ContractBackend。
|
|
// 独立类型的原因:旧门面 Execute(ctx, exec.Request) 与契约的
|
|
// Execute(ctx, exec.ContractRequest) 签名冲突,同名方法不能并存;
|
|
// 包装让现有 squad 消费(旧门面)与契约消费(conformance/Core)互不影响。
|
|
type contractBackend struct{ b *Backend }
|
|
|
|
// Contract 返回该后端的契约形态(N1 WP6;旧门面保持不变)。
|
|
func (b *Backend) Contract() exec.ContractBackend {
|
|
return contractBackend{b: b}
|
|
}
|
|
|
|
func (c contractBackend) Descriptor() exec.BackendDescriptor {
|
|
return exec.BackendDescriptor{
|
|
Name: c.b.Name(),
|
|
Capabilities: []exec.Capability{exec.CapText, exec.CapStream, exec.CapTools},
|
|
}
|
|
}
|
|
|
|
func (c contractBackend) Execute(ctx context.Context, req exec.ContractRequest) (*exec.ContractResult, error) {
|
|
var onEvent func(ev exec.ProcessEvent)
|
|
if req.OnEvent != nil {
|
|
next := req.OnEvent
|
|
onEvent = func(ev exec.ProcessEvent) {
|
|
next(exec.Event{Kind: exec.EventKind(ev.Type), Text: ev.Text, Tool: ev.Tool, Detail: ev.Detail})
|
|
}
|
|
}
|
|
// 契约语义:空 ToolPolicy.Profile 等同 reasoning(无工具纯文本任务)。
|
|
profile := strings.TrimSpace(req.ToolPolicy.Profile)
|
|
if profile == "" {
|
|
profile = exec.ToolProfileReasoning
|
|
}
|
|
old := exec.Request{
|
|
Prompt: req.Prompt,
|
|
Instructions: req.Instructions,
|
|
ExecutionID: req.ExecutionID,
|
|
Targets: append([]string(nil), req.Targets...),
|
|
ToolProfile: profile,
|
|
ToolAllowlist: append([]string(nil), req.ToolPolicy.Allowlist...),
|
|
WorkDir: req.Workspace.Root,
|
|
OnEvent: onEvent,
|
|
}
|
|
if b := req.ContractBudget; b.MaxPromptTokens > 0 || b.MaxCompletionTokens > 0 || !b.Deadline.IsZero() {
|
|
old.Budget = &exec.Budget{
|
|
MaxPromptTokens: b.MaxPromptTokens,
|
|
MaxCompletionTokens: b.MaxCompletionTokens,
|
|
Deadline: b.Deadline,
|
|
}
|
|
}
|
|
res, err := c.b.Execute(ctx, old)
|
|
if err != nil {
|
|
// 传输/实现级错误:按契约映射为 Unknown,不经 Go error 返回业务终态。
|
|
return &exec.ContractResult{
|
|
Outcome: exec.OutcomeUnknown,
|
|
Err: &exec.Error{Code: exec.ErrProviderUnavailable, Reason: err.Error(), SideEffects: exec.SideEffectPossible},
|
|
}, nil
|
|
}
|
|
out := &exec.ContractResult{
|
|
Logs: res.Logs,
|
|
Usage: exec.Usage{
|
|
PromptTokens: res.PromptTokens,
|
|
CompletionTokens: res.CompletionTokens,
|
|
},
|
|
}
|
|
switch {
|
|
case res.Success:
|
|
out.Outcome = exec.OutcomeSucceeded
|
|
out.Files = res.Patches
|
|
case errors.Is(ctx.Err(), context.Canceled):
|
|
out.Outcome = exec.OutcomeCanceled
|
|
out.Err = &exec.Error{Code: exec.ErrCanceled, Reason: res.ErrorReason, SideEffects: exec.SideEffectPossible}
|
|
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
|
out.Outcome = exec.OutcomeFailed
|
|
out.Err = &exec.Error{Code: exec.ErrDeadlineExceeded, Reason: res.ErrorReason, SideEffects: exec.SideEffectPossible}
|
|
default:
|
|
out.Outcome = exec.OutcomeFailed
|
|
out.Err = &exec.Error{Code: exec.ErrProviderRejected, Reason: res.ErrorReason, Retryable: res.Retryable, SideEffects: exec.SideEffectPossible}
|
|
}
|
|
return out, nil
|
|
}
|