Files
Agent-Native/adapters/native/contract.go
weichen afbb567301 Bind session scope identity, enforce write sets, repair torn JSONL
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
2026-10-21 16:03:04 +00:00

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
}