mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
complete_step now only says a step is done: it names the step and what is now true, and the host advances the task list as before. It no longer asks for commands, files or criteria to cite and no longer checks them against the session's receipts. conclude_blocked existed to let the readiness gate through and goes with it; the prompts that pointed at it now ask for a sentence in the reply. Plan steps lose their acceptance and verification fields: the submit_plan schema, the planner and executor prompts, the rendering and the re-approval rule all stop mentioning them, and plans saved with them still load. The executor's no-action nudge now keys on a plan naming files only.
80 lines
2.3 KiB
Go
80 lines
2.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"path/filepath"
|
|
|
|
"reasonix/internal/runtime/plancontract"
|
|
"reasonix/internal/safety/evidence"
|
|
)
|
|
|
|
// SetPlanContract records the approved plan this turn executes, or clears it
|
|
// when the turn runs without one. The coordinator sets it before every executor
|
|
// run so a turn never inherits the previous turn's plan.
|
|
func (a *Agent) SetPlanContract(plan *plancontract.Plan) {
|
|
if a == nil {
|
|
return
|
|
}
|
|
a.sess.todoMu.Lock()
|
|
defer a.sess.todoMu.Unlock()
|
|
if plan == nil {
|
|
a.planContract = nil
|
|
return
|
|
}
|
|
copied := *plan
|
|
a.planContract = &copied
|
|
}
|
|
|
|
// PlanContract is the plan the current turn runs under, or nil for none.
|
|
func (a *Agent) PlanContract() *plancontract.Plan {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
a.sess.todoMu.Lock()
|
|
defer a.sess.todoMu.Unlock()
|
|
if a.planContract == nil {
|
|
return nil
|
|
}
|
|
copied := *a.planContract
|
|
return &copied
|
|
}
|
|
|
|
// withContractState attaches what a tool call needs to check a claim against the
|
|
// approved work: the canonical task list, and the criterion ids a proof may
|
|
// cite. They travel together because both answer "does this claim name
|
|
// something real".
|
|
func (a *Agent) withContractState(ctx context.Context) context.Context {
|
|
return evidence.WithTodoState(ctx, a.CanonicalTodoState())
|
|
}
|
|
|
|
// mutationEscapesPlan reports whether a pending write touches a path the
|
|
// approved plan never named. A plan that named no touchpoints says nothing
|
|
// about scope, so nothing escapes it: silence is not a claim that everything is
|
|
// out of bounds. Directory containment counts — a plan naming a file implies
|
|
// its directory is in play, which is how a test file beside it stays in scope.
|
|
func (a *Agent) mutationEscapesPlan(toolName string, args json.RawMessage) bool {
|
|
plan := a.PlanContract()
|
|
if plan == nil {
|
|
return false
|
|
}
|
|
allowed := map[string]bool{}
|
|
for _, step := range plan.Steps {
|
|
for _, p := range append(append([]string{}, step.VerifiedFiles...), step.CandidateFiles...) {
|
|
clean := filepath.Clean(p)
|
|
allowed[clean] = true
|
|
allowed[filepath.Dir(clean)] = true
|
|
}
|
|
}
|
|
if len(allowed) == 0 {
|
|
return false
|
|
}
|
|
for _, p := range evidence.ReceiptFromToolCall(toolName, args, false, evidence.ToolFacts{ReadOnly: true}).Paths {
|
|
clean := filepath.Clean(p)
|
|
if !allowed[clean] && !allowed[filepath.Dir(clean)] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|