精简动作审查输入并支持审批来源定位 (#125)

* fix: 精简审查背景并区分人工消息与调度输入

* fix: 模型审查仅提交当前调用与选定背景

* feat: 审批来源定位原会话并移除 Worker 审查背景
This commit is contained in:
RuoJi6 authored and GitHub committed 2026-09-15 23:51:37 +08:00
1 parent 0d6fa880b8
commit 04d25eb133
30 files changed
+1143 -465

No files matched your search

-18
View File
@@ -1,28 +1,10 @@
package db
import (
"context"
"encoding/json"
"fmt"
"time"
)
// OperationReviewContext takes one database snapshot of the task's original
// description/goal and registered constraints. Discovered task_scope assets are
// deliberately excluded: discovery does not grant authorization.
func (s *ExplorationStore) OperationReviewContext(ctx context.Context) (description, goal string, constraints []Constraint, err error) {
var raw []byte
err = s.db.QueryRowContext(ctx, `SELECT COALESCE(e.description,''), e.goal,
COALESCE((SELECT jsonb_agg(jsonb_build_object(
'id', c.id, 'kind', c.kind, 'text', c.text, 'origin', COALESCE(c.origin,''), 'created_at', c.created_at
) ORDER BY c.id) FROM task_constraints c WHERE c.exploration_id=e.id), '[]'::jsonb)
FROM explorations e WHERE e.id=$1`, s.expID).Scan(&description, &goal, &raw)
if err == nil {
err = json.Unmarshal(raw, &constraints)
}
return
}
// Constraint is one operator-authored operation constraint for a task: kind=allow
// (permitted operations) or kind=deny (forbidden operations), free-text. Stored in
// task_constraints, keyed by exploration_id (cascades with the exploration).
+120
View File
@@ -0,0 +1,120 @@
package db
import (
"errors"
"fmt"
"strconv"
)
var ErrInterceptTaskDeleted = errors.New("任务已被删除或归档")
var ErrInterceptSessionDeleted = errors.New("对应会话或执行记录已被删除或不存在")
var ErrInterceptExecutionUnavailable = errors.New("未找到可唯一关联的原始工具调用;记录可能已删除,或旧审批没有保存关联 ID")
// InterceptExecution is a navigation target read from original activity rows.
// It is not model context and never falls back to matching command text.
type InterceptExecution struct {
ConversationID *int64 `json:"conversation_id,omitempty"`
TaskID *string `json:"task_id,omitempty"`
Session string `json:"session"`
Seq int64 `json:"seq"`
Items []Activity `json:"-"`
}
func (d *DB) GetInterceptExecution(id int64) (*InterceptExecution, error) {
approval, err := d.GetInterceptDetail(id)
if err != nil || approval == nil {
return nil, err
}
audit := approval.Audit
if audit == nil || audit.Correlation != "exact" || audit.ToolUseID == "" {
return nil, ErrInterceptExecutionUnavailable
}
var query string
var scope any
if approval.ConversationID != nil {
scope = *approval.ConversationID
query = `SELECT id, NULL::bigint, COALESCE(worker,''), kind, COALESCE(tool,''), tool_use_id, is_error, COALESCE(summary,''), created_at, NULL::integer FROM conversation_activities WHERE conversation_id=$1`
} else if approval.TaskID != nil {
taskID, parseErr := strconv.ParseInt(*approval.TaskID, 10, 64)
if parseErr != nil || taskID <= 0 {
return nil, ErrInterceptExecutionUnavailable
}
var exists bool
if err := d.QueryRow(`SELECT EXISTS(SELECT 1 FROM tasks WHERE id=$1 AND archived_at IS NULL AND deleted_at IS NULL)`, taskID).Scan(&exists); err != nil {
return nil, err
}
if !exists {
return nil, ErrInterceptTaskDeleted
}
scope = taskID
query = `SELECT id, node_id, COALESCE(worker,''), kind, COALESCE(tool,''), tool_use_id, is_error, COALESCE(summary,''), created_at, main_seg FROM activity WHERE exploration_id=(SELECT exploration_id FROM tasks WHERE id=$1 AND archived_at IS NULL AND deleted_at IS NULL)`
} else {
return nil, ErrInterceptExecutionUnavailable
}
// Three matches suffice to detect duplicate IDs without loading a transcript.
rows, err := d.Query(query+` AND tool_use_id=$2 AND kind IN ('tool_use','tool_result') ORDER BY id LIMIT 3`, scope, audit.ToolUseID)
if err != nil {
return nil, err
}
defer rows.Close()
out := &InterceptExecution{ConversationID: approval.ConversationID, TaskID: approval.TaskID}
for rows.Next() {
var a Activity
if err := rows.Scan(&a.ID, &a.NodeID, &a.Worker, &a.Kind, &a.Tool, &a.ToolUseID, &a.IsError, &a.Summary, &a.CreatedAt, &a.MainSeg); err != nil {
return nil, err
}
out.Items = append(out.Items, a)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(out.Items) == 0 {
return nil, ErrInterceptSessionDeleted
}
if len(out.Items) > 2 {
return nil, ErrInterceptExecutionUnavailable
}
call := out.Items[0]
if call.Kind != "tool_use" || call.Tool != approval.ToolName {
return nil, ErrInterceptExecutionUnavailable
}
if len(out.Items) == 2 {
result := out.Items[1]
if result.Kind != "tool_result" || result.Worker != call.Worker || (result.Tool != "" && result.Tool != call.Tool) || !sameOptionalInt64(call.NodeID, result.NodeID) || !sameMainSegment(call.MainSeg, result.MainSeg) {
return nil, ErrInterceptExecutionUnavailable
}
}
out.Seq = call.ID
if approval.ConversationID == nil {
switch {
case call.Worker == "mainagent":
seg := 0
if call.MainSeg != nil {
seg = *call.MainSeg
}
out.Session = fmt.Sprintf("main:%d", seg)
case call.Worker == "planner":
out.Session = "plan"
case call.NodeID != nil:
out.Session = fmt.Sprintf("intent:%d", *call.NodeID)
default:
return nil, ErrInterceptExecutionUnavailable
}
}
return out, nil
}
func sameOptionalInt64(a, b *int64) bool {
return (a == nil && b == nil) || (a != nil && b != nil && *a == *b)
}
func sameMainSegment(a, b *int) bool {
av, bv := 0, 0
if a != nil {
av = *a
}
if b != nil {
bv = *b
}
return av == bv
}
+150
View File
@@ -0,0 +1,150 @@
package db
import (
"errors"
"fmt"
"testing"
)
func TestInterceptExecutionNavigation(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.Close() })
approval := func(conv int64, task, id string, exact bool) int64 {
t.Helper()
audit := &InterceptAudit{ToolUseID: id, Correlation: "exact"}
if !exact {
audit.Correlation = "ambiguous"
}
n, err := d.CreateInterceptPending(0, conv, task, "display-name-is-not-a-session-id", "Bash", []byte(`{"command":"pwd"}`), "review", audit)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _, _ = d.Exec(`DELETE FROM intercept_pending WHERE id=$1`, n) })
return n
}
conv := func() int64 {
t.Helper()
c, err := d.CreateConversation("test", "navigation", nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _, _ = d.Exec(`DELETE FROM conversations WHERE id=$1`, c.ID) })
return c.ID
}
addConv := func(c int64, kind, id string) int64 {
t.Helper()
seq, err := d.AppendConvActivity(c, Activity{Worker: "test", Kind: kind, Tool: "Bash", ToolUseID: id, Summary: "pwd"})
if err != nil {
t.Fatal(err)
}
return seq
}
t.Run("conversation scope and old paginated call", func(t *testing.T) {
c1, c2 := conv(), conv()
seq := addConv(c1, "tool_use", "same-id")
addConv(c1, "tool_result", "same-id")
addConv(c2, "tool_use", "same-id")
addConv(c2, "tool_result", "same-id")
for range 210 {
addConv(c1, "text", "")
}
id := approval(c1, "", "same-id", true)
got, err := d.GetInterceptExecution(id)
if err != nil || got.Seq != seq || len(got.Items) != 2 || *got.ConversationID != c1 {
t.Fatalf("wrong conversation target: %+v %v", got, err)
}
addConv(c1, "tool_use", "same-id")
if _, err := d.GetInterceptExecution(id); !errors.Is(err, ErrInterceptExecutionUnavailable) {
t.Fatal("duplicate call ID selected an arbitrary execution")
}
})
t.Run("task sessions and result pairing", func(t *testing.T) {
task, err := d.CreateTask("navigation", "fixture", nil, 0, 600)
if err != nil {
t.Fatal(err)
}
exp := task.ExplorationID
t.Cleanup(func() {
_, _ = d.Exec(`DELETE FROM tasks WHERE id=$1`, task.ID)
_, _ = d.Exec(`DELETE FROM explorations WHERE id=$1`, exp)
})
es := d.Exploration(exp)
intent, err := es.AddIntent(map[string]any{"summary": "do not parse agent label"}, 1, nil, "planner")
if err != nil {
t.Fatal(err)
}
seg := 3
for _, tc := range []struct {
worker, key string
node *int64
segment *int
}{{"work#7", fmt.Sprintf("intent:%d", intent), &intent, nil}, {"planner", "plan", nil, nil}, {"mainagent", "main:3", nil, &seg}, {"mainagent", "main:0", nil, nil}} {
callID := "call-" + tc.key
a := Activity{Worker: tc.worker, NodeID: tc.node, MainSeg: tc.segment, Kind: "tool_use", Tool: "Bash", ToolUseID: callID, Summary: "pwd"}
seq, err := es.AppendActivity(a)
if err != nil {
t.Fatal(err)
}
a.Kind = "tool_result"
if _, err := es.AppendActivity(a); err != nil {
t.Fatal(err)
}
got, err := d.GetInterceptExecution(approval(0, fmt.Sprint(task.ID), callID, true))
if err != nil || got.Seq != seq || got.Session != tc.key || len(got.Items) != 2 {
t.Fatalf("wrong task session: %+v %v", got, err)
}
}
deletedCall := "deleted-session-call"
deletedSeq, err := es.AppendActivity(Activity{Worker: "work#9", NodeID: &intent, Kind: "tool_use", Tool: "Bash", ToolUseID: deletedCall})
if err != nil {
t.Fatal(err)
}
deletedApproval := approval(0, fmt.Sprint(task.ID), deletedCall, true)
if _, err = d.Exec(`DELETE FROM activity WHERE id=$1`, deletedSeq); err != nil {
t.Fatal(err)
}
if _, err = d.GetInterceptExecution(deletedApproval); !errors.Is(err, ErrInterceptSessionDeleted) {
t.Fatalf("deleted session: %v", err)
}
if _, err = d.Exec(`UPDATE tasks SET archived_at=NOW() WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
if _, err = d.GetInterceptExecution(deletedApproval); !errors.Is(err, ErrInterceptTaskDeleted) {
t.Fatalf("archived task: %v", err)
}
if _, err = d.Exec(`UPDATE tasks SET archived_at=NULL WHERE id=$1`, task.ID); err != nil {
t.Fatal(err)
}
a := Activity{Worker: "work#7", NodeID: &intent, Kind: "tool_use", Tool: "Bash", ToolUseID: "unpaired", Summary: "pwd"}
if _, err := es.AppendActivity(a); err != nil {
t.Fatal(err)
}
id := approval(0, fmt.Sprint(task.ID), "unpaired", true)
got, err := d.GetInterceptExecution(id)
if err != nil || len(got.Items) != 1 {
t.Fatal("pending call is not navigable")
}
a.Kind = "tool_result"
a.Worker = "different-worker"
if _, err := es.AppendActivity(a); err != nil {
t.Fatal(err)
}
if _, err := d.GetInterceptExecution(id); !errors.Is(err, ErrInterceptExecutionUnavailable) {
t.Fatal("cross-worker result was paired")
}
})
if _, err := d.GetInterceptExecution(approval(conv(), "", "missing", true)); !errors.Is(err, ErrInterceptSessionDeleted) {
t.Fatalf("missing call: %v", err)
}
for _, id := range []int64{approval(conv(), "", "", true), approval(conv(), "", "duplicate", false)} {
if _, err := d.GetInterceptExecution(id); !errors.Is(err, ErrInterceptExecutionUnavailable) {
t.Fatalf("invented execution for %d", id)
}
}
if got, err := d.GetInterceptExecution(-1); err != nil || got != nil {
t.Fatal("missing approval")
}
}
+4
View File
@@ -316,6 +316,8 @@ ALTER TABLE activity ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{
ALTER TABLE activity ADD COLUMN IF NOT EXISTS main_seg INTEGER;
CREATE INDEX IF NOT EXISTS idx_act_node ON activity(exploration_id, node_id, id);
CREATE INDEX IF NOT EXISTS idx_act_since ON activity(exploration_id, id);
CREATE INDEX IF NOT EXISTS idx_act_tool_call ON activity(exploration_id, tool_use_id, id)
WHERE kind IN ('tool_use', 'tool_result');
-- Main/Plan history pages filter by worker (both carry NULL node_id, so idx_act_node
-- can't distinguish them); this covers reverse pagination of those sessions.
CREATE INDEX IF NOT EXISTS idx_act_worker ON activity(exploration_id, worker, id);
@@ -970,6 +972,8 @@ CREATE TABLE IF NOT EXISTS conversation_activities (
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_conv_act ON conversation_activities(conversation_id, id);
CREATE INDEX IF NOT EXISTS idx_conv_act_tool_call ON conversation_activities(conversation_id, tool_use_id, id)
WHERE kind IN ('tool_use', 'tool_result');
-- =====================================================================
-- J. Agent 触发器