Why: F02/F04/F15 - contract requests shared one transcript across session scopes, Writable was dropped, and a torn JSONL tail poisoned the next append; pure-text contract results lost their text. Evidence: adapters/native, runtime/session, runtime/tools and conformance tests pass (verify-u2.json); scope-isolation, write-set and torn-tail regression cases added. Impact: empty scope keeps legacy one-shot semantics; identity-less old transcripts are never auto-adopted by a new scope. Refs: feature/agent-stack-audit-remediation-b1 U2
110 lines
3.7 KiB
Go
110 lines
3.7 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
|
|
}
|
|
scopeID := scopeIdentity(req.SessionScope)
|
|
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,
|
|
SessionScope: scopeID,
|
|
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,
|
|
}
|
|
}
|
|
// CapText / F15: pure-text contract tasks must not force implicit file delivery.
|
|
explicitTargets := len(req.Targets) > 0
|
|
if !explicitTargets && profile == exec.ToolProfileReasoning {
|
|
ctx = withAllowEmptyTargets(ctx)
|
|
}
|
|
ctx = withWritable(ctx, req.Workspace.Writable)
|
|
var finalText string
|
|
ctx = withTextSink(ctx, &finalText)
|
|
|
|
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
|
|
if explicitTargets {
|
|
out.Files = res.Patches
|
|
}
|
|
// CapText: surface the real assistant text for pure-text success.
|
|
out.Text = finalText
|
|
if out.Text == "" && len(res.Patches) == 1 {
|
|
for _, body := range res.Patches {
|
|
out.Text = string(body)
|
|
}
|
|
}
|
|
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
|
|
}
|