Documentation
¶
Overview ¶
Package staging manages the decision-package staging table that sits between an Agent's raw decision output and production commit. A single Record represents one Agent decision (product decision #1: one decision = one Record, not per tool call). A Record traverses a 7-state machine:
pending_tech -> rejected_tech | pending_ml pending_ml -> rejected_ml | approved approved -> executed | failed
Transitions split into two control regimes (product decision Y):
- Internal, pulled by staging.Engine: draft staging calls the tech-tier validator.Validator, then the biz-tier Validator, to drive the first two rows. Engine owns the transition atomically.
- External, pushed by the platform layer: once a Record reaches approved, the caller that actually writes to production (WMS API, SQL commit, HTTP POST, ...) calls MarkExecuted or MarkFailed on the Store to close the loop. core has no authority over the production-write path, so it cannot drive the final arc itself.
Both tiers reuse validator.Validator (product decision X). A separate TechValidator / BizValidator interface would duplicate the same contract ("review a staged diff, emit a Verdict") and block reuse of reflector.EvaluatorAsValidator for customers who want to plug an Evaluator as either tier. Verdict.Score + Severity (Warn / Block) carry enough signal to express both "SQL ran but plan is poor" (Warn) and "ML scored 0.3" (Score=0.3 + Warn).
Customers access staging through a pull-only API (product decision #5): core never pushes into customer databases. Query the Store on demand; build whatever integration (cron job, dashboard) fits.
staging 管理决策包级的 staging 表, 位于 Agent 原始决策产出与生产 commit 之间. 一条 Record 代表一次 Agent 决策 (产品决策 #1: 一次 决策 = 一条 Record, 不是每个工具调用一条). Record 走 7 状态机:
pending_tech -> rejected_tech | pending_ml pending_ml -> rejected_ml | approved approved -> executed | failed
transition 分两种控制模式 (产品决策 Y):
- 内部, staging.Engine 主动拉动: draft staging 先调技术层 validator.Validator, 再调业务层 Validator, 推动前两行. Engine 以原子 transition 拥有该步.
- 外部, 平台层主动推入: Record 到达 approved 后, 真正写生产 (WMS API / SQL commit / HTTP POST 等) 的调用方再调 Store 的 MarkExecuted 或 MarkFailed 闭环. core 对生产写路径无权限, 无法自行驱动最后一段 arc.
两层都复用 validator.Validator (产品决策 X). 另造 TechValidator / BizValidator 只是复制同一契约 ("审 staged diff 产 Verdict"), 并且阻断客户把 Evaluator 经 reflector.EvaluatorAsValidator 适配进任一层. Verdict.Score + Severity (Warn / Block) 的组合足以 表达 "SQL 过了但 plan 差" (Warn) 与 "ML 打分 0.3" (Score=0.3 + Warn).
客户接入 staging 走 pull-only API (产品决策 #5): core 不向客户库 推送, 客户按需查 Store, 自己搭接集成 (cron / dashboard) 即可.
Index ¶
- Variables
- func IsLegal(from, to State) bool
- type AllowAlwaysGuard
- type DependencyGuard
- type Engine
- func (e *Engine) MarkExecuted(ctx context.Context, id string, proof any) (Record, error)
- func (e *Engine) MarkFailed(ctx context.Context, id string, reason string) (Record, error)
- func (e *Engine) Stage(ctx context.Context, sessionID string, diff validator.DiffInput) (Record, error)
- func (e *Engine) ValidateBiz(ctx context.Context, id string) (Record, error)
- func (e *Engine) ValidateTech(ctx context.Context, id string) (Record, error)
- type InMemoryStore
- func (s *InMemoryStore) Get(_ context.Context, id string) (Record, error)
- func (s *InMemoryStore) List(_ context.Context, q Query) ([]Record, error)
- func (s *InMemoryStore) ListBySession(ctx context.Context, sessionID string) ([]Record, error)
- func (s *InMemoryStore) ListStuck(_ context.Context, state State, olderThan time.Duration) ([]Record, error)
- func (s *InMemoryStore) MarkExecuted(_ context.Context, id string, proof any) (Record, error)
- func (s *InMemoryStore) MarkFailed(_ context.Context, id string, reason string) (Record, error)
- func (s *InMemoryStore) SetUpdatedAtForTest(id string, t time.Time)
- func (s *InMemoryStore) Stage(_ context.Context, r Record) (Record, error)
- func (s *InMemoryStore) UpdateBizVerdict(_ context.Context, id string, v validator.Verdict) (Record, error)
- func (s *InMemoryStore) UpdateTechVerdict(_ context.Context, id string, v validator.Verdict) (Record, error)
- type Query
- type Record
- type State
- type Store
- type TenantDenyGuard
Constants ¶
This section is empty.
Variables ¶
var ( // ErrIllegalTransition indicates a caller attempted to transition // a Record from a source state to an unreachable destination // (per the static transitions matrix) or to a state outside the // declared 7-state enum. // // ErrIllegalTransition 表示调用方试图将 Record 从源状态转到静态 // transitions 矩阵不可达的目的, 或转到 7 状态枚举之外的值. ErrIllegalTransition = errors.New("staging: illegal state transition") // ErrRecordNotFound is returned by Get / Update / Mark operations // when the id does not exist in the Store. // // ErrRecordNotFound 在 Get / Update / Mark 操作的 id 不存在时返回. ErrRecordNotFound = errors.New("staging: record not found") // ErrAlreadyFinal indicates an attempt to transition out of a // terminal state (rejected_tech / rejected_ml / executed / // failed). Distinct from ErrIllegalTransition so callers can // treat "already done" differently from "wire error". // // ErrAlreadyFinal 表示试图从终态 (rejected_tech / rejected_ml / // executed / failed) 转出. 与 ErrIllegalTransition 区分, 让调用方 // 可以把 "已完成" 和 "接线错" 分开处理. ErrAlreadyFinal = errors.New("staging: record already in final state") // ErrDependencyDenied is returned when DependencyGuard refuses // a transition that would otherwise be legal by the static // matrix. Engine callers retry (dependency may clear) or // surface to a human. // // ErrDependencyDenied 在 DependencyGuard 拒绝一个静态矩阵本来 // 放行的 transition 时返回. Engine 调用方可重试 (依赖可能清除) // 或上报人工. ErrDependencyDenied = errors.New("staging: dependency guard denied transition") )
Sentinel errors for state-machine and store failures. Store and Engine implementations wrap these with %w so callers classify with errors.Is.
状态机与存储层的哨兵错误. Store 与 Engine 实现以 %w 包装, 调用方用 errors.Is 分类.
Functions ¶
func IsLegal ¶
IsLegal reports whether from -> to is a legal transition per the static matrix. Callers MUST call this before persisting any state change; illegal transitions indicate a programming error or a corrupted Record.
IsLegal 不咨询 DependencyGuard -- guard 是在 IsLegal 返回 true 之后 的正交第二道闸.
IsLegal 判定 from -> to 是否合法 (仅静态矩阵). 调用方持久化任何 状态变更前必须先调此函数; 非法 transition 代表编程错误或 Record 已损坏.
IsLegal 不咨询 DependencyGuard -- guard 是 IsLegal 返回 true 之后的 正交第二道闸.
Types ¶
type AllowAlwaysGuard ¶
type AllowAlwaysGuard struct{}
AllowAlwaysGuard is the no-op DependencyGuard: it approves every transition. Intended for tests, dev harnesses, and single-tenant deployments where cross-decision dependencies are not modelled.
Production deployments that care about session-level ordering MUST supply their own DependencyGuard. staging.NewEngine panics on a nil guard -- the zero-value fail-fast mirrors validator.Validator's nil-panic contract (commit 1b0a860) and forces callers to explicitly opt in to "no dependency constraints" by passing AllowAlwaysGuard{} rather than silently accepting a nil field.
AllowAlwaysGuard 是空操作 DependencyGuard: 每个 transition 都放行. 用于测试 / dev 环境 / 不建模跨决策依赖的单租户部署.
关心 session 级顺序的生产部署必须自供 DependencyGuard. staging.NewEngine 对 nil guard 直接 panic -- 零值 fail-fast 对齐 validator.Validator 的 nil-panic 契约 (commit 1b0a860), 逼调用方显式传 AllowAlwaysGuard{} 表达 "无依赖约束" 的意图, 而非静默接受 nil 字段.
func (AllowAlwaysGuard) AllowTransition ¶
AllowTransition always returns (true, nil).
AllowTransition 始终返回 (true, nil).
type DependencyGuard ¶
type DependencyGuard interface {
// AllowTransition reports whether r may move to to.
//
// AllowTransition 判定 r 能否转移到 to.
AllowTransition(ctx context.Context, r Record, to State) (allow bool, err error)
}
DependencyGuard gates each transition on an orthogonal dependency condition (product decision #3). IsLegal covers the static state matrix; DependencyGuard covers caller-defined ordering rules that sit outside the matrix -- e.g. "this Record must wait until its upstream Record is executed", "this Record's decision references an external approval ticket that has not cleared yet".
core intentionally does not prescribe a dependency shape. Callers encode whatever semantics their domain needs inside their AllowTransition implementation, typically reading hints from Record.Metadata. This mirrors the plug-in model used by validator.Validator and reflector's adapters.
Error distinguishes guard failure from a deliberate deny:
- (true, nil) -> transition may proceed.
- (false, nil) -> guard consciously denies; engine treats it as a signal to retry later or surface to a human.
- (_, non-nil) -> guard could not decide (backend failure); engine MUST fail closed (treat as deny) so a broken guard does not silently wave everything through.
AllowTransition is invoked AFTER the Validator produces its Verdict and BEFORE the Store persists the transition, so guards can base decisions on the Verdict as well as the Record.
DependencyGuard 给每个 transition 加一个正交依赖条件闸 (产品决策 #3). IsLegal 覆盖静态状态矩阵; DependencyGuard 覆盖 调用方定义的矩阵外的顺序规则 -- 例如 "此 Record 必须等上游 Record executed", "此 Record 的决策引用了一个尚未清的外部审批单".
core 刻意不规定依赖形态. 调用方在自己的 AllowTransition 实现里编 入业务所需语义, 通常读取 Record.Metadata 的 hint. 与 validator.Validator 和 reflector adapter 所用的 plug-in 模型一致.
error 区分 guard 失败与主动拒:
- (true, nil) -> transition 可进行.
- (false, nil) -> guard 主动拒; engine 视为 "稍后重试或 上报人工" 的信号.
- (_, non-nil) -> guard 无法决策 (后端故障); engine 必须 fail-closed (按拒处理), 避免坏 guard 悄悄放行.
AllowTransition 在 Validator 产出 Verdict 之后 / Store 持久化之前 被调用, guard 因此既能看到 Verdict 也能看到 Record.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine wires a Store, two validator.Validator implementations (tech + biz tiers), and a DependencyGuard into the mixed-control state machine (product decision Y). It owns the two internal arcs (pending_tech -> rejected_tech | pending_ml and pending_ml -> rejected_ml | approved) by actively calling the Validators. The terminal approved -> executed | failed arc is driven externally: the platform layer calls MarkExecuted or MarkFailed on the Store (or on Engine, which delegates) once the WMS / production write resolves.
Engine is stateless -- all persistence lives in Store. Safe for concurrent use; the Store and Validators underneath carry the concurrency burden.
Engine 将 Store / 两个 validator.Validator (技术 + 业务层) / DependencyGuard 组装为混合控制状态机 (产品决策 Y). 它以主动调 Validator 的方式拥有内部两段 arc (pending_tech -> rejected_tech | pending_ml 与 pending_ml -> rejected_ml | approved). 终端 approved -> executed | failed arc 由外部驱动: 平台层在 WMS / 生产 写动作落地后调 Store 的 MarkExecuted 或 MarkFailed (或 Engine 同名 方法, Engine 仅转发).
Engine 无状态 -- 持久化全在 Store. 并发安全由底层 Store 与 Validator 承担.
func NewEngine ¶
func NewEngine(store Store, tech, biz validator.Validator, guard DependencyGuard) *Engine
NewEngine constructs an Engine. All four arguments are required; passing nil for any of them panics (consistent with validator.NewValidatedTool and reflector adapters: construction- time fail-fast surfaces wiring errors at startup instead of at the first request).
NewEngine 构造 Engine. 四个参数均必填; 任一为 nil 直接 panic (对齐 validator.NewValidatedTool 与 reflector adapter 的构造期 fail-fast 模式, 把接线错误在启动时暴露, 而非推到首次请求).
func (*Engine) MarkExecuted ¶
MarkExecuted exposes the external-push arc: the platform layer calls this after a successful production commit, attaching proof (WMS receipt / idempotency key / transaction id) for audit. Thin delegation to Store.MarkExecuted.
MarkExecuted 暴露外部推入 arc: 平台层在生产 commit 成功后调用, 附 proof (WMS 回单 / 幂等 key / 事务 id) 用于审计. 薄转发 Store.MarkExecuted.
func (*Engine) MarkFailed ¶
MarkFailed exposes the external-push arc for failed production commit, with reason for the audit trail. Thin delegation.
MarkFailed 暴露生产 commit 失败的外部推入 arc, reason 用于审计. 薄转发.
func (*Engine) Stage ¶
func (e *Engine) Stage(ctx context.Context, sessionID string, diff validator.DiffInput) (Record, error)
Stage delegates to Store.Stage with a freshly constructed pending_tech Record carrying the given Diff under sessionID.
Stage 向 Store.Stage 转发, 用给定 Diff 和 sessionID 构造 pending_tech Record.
func (*Engine) ValidateBiz ¶
ValidateBiz runs the biz-tier Validator against the Record's Diff and drives pending_ml -> rejected_ml | approved with the same fail-closed + guard semantics as ValidateTech.
ValidateBiz 用业务层 Validator 审 Record.Diff, 推 pending_ml -> rejected_ml | approved, fail-closed 与 guard 语义同 ValidateTech.
func (*Engine) ValidateTech ¶
ValidateTech runs the tech-tier Validator against the Record's Diff and drives pending_tech -> rejected_tech | pending_ml.
Failure semantics:
- validator.Validator returns a non-nil error. The Engine treats this fail-closed: it synthesizes a Verdict {Approved=false, Severity=Block, Reason=wrapped error, ValidatorName=tech.Name()} and routes the Record to rejected_tech. A broken Validator must not silently pass.
- DependencyGuard returns (false, _) or (_, non-nil error) on the approved-path (i.e. the Validator said Approved=true). Engine returns ErrDependencyDenied without persisting any state change; caller retries later or surfaces to humans.
The rejected-path (Validator said Approved=false) does NOT consult the guard -- denying a rejection is not a meaningful operation, and consulting the guard would let a misconfigured guard block the rejection audit trail.
ValidateTech 用技术层 Validator 审 Record.Diff, 推 pending_tech -> rejected_tech | pending_ml.
失败语义:
- validator.Validator 返回非 nil error. Engine 按 fail-closed 处理: 合成 Verdict {Approved=false, Severity=Block, Reason=包装后的错误, ValidatorName=tech.Name()} 并把 Record 路由到 rejected_tech. 坏掉的 Validator 不能静默放行.
- DependencyGuard 在放行路径 (Validator 说 Approved=true) 返回 (false, _) 或 (_, 非 nil error). Engine 返回 ErrDependencyDenied 不持久化状态变更; 调用方可稍后重试或上报人工.
拒绝路径 (Validator 说 Approved=false) 不咨询 guard -- 对拒绝 再拒绝无意义, 且咨询 guard 会让一个配错的 guard 阻断拒绝审计链.
type InMemoryStore ¶
type InMemoryStore struct {
// contains filtered or unexported fields
}
InMemoryStore is the reference Store implementation backed by a map + sync.Mutex. Intended strictly for tests, dev harnesses, and single-process demos. Production deployments MUST use a SQL-backed Store from the platform layer; InMemoryStore offers no durability, no cross-process coordination, and no crash recovery.
ID strategy: monotonically incrementing uint64 formatted as a decimal string. Stable within a single process lifetime, not stable across restarts (another reason this is not for prod).
InMemoryStore 是 map + sync.Mutex 的参考 Store 实现. 严格限定为 测试 / dev 环境 / 单进程 demo. 生产部署必须用平台层的 SQL-backed Store; InMemoryStore 无持久化, 无跨进程协调, 无崩溃恢复.
ID 策略: 单调递增 uint64 以十进制字符串表示. 单进程生命周期内稳定, 跨重启不稳定 (这也是不用于生产的原因).
func NewInMemoryStore ¶
func NewInMemoryStore() *InMemoryStore
NewInMemoryStore constructs an empty InMemoryStore ready for use.
NewInMemoryStore 构造空的 InMemoryStore 可直接使用.
func (*InMemoryStore) List ¶
List implements Store. Returns records matching q ordered by CreatedAt ascending. Zero-value fields do not constrain.
func (*InMemoryStore) ListBySession ¶
ListBySession implements Store.
func (*InMemoryStore) ListStuck ¶
func (s *InMemoryStore) ListStuck(_ context.Context, state State, olderThan time.Duration) ([]Record, error)
ListStuck implements Store. Returns records in `state` whose UpdatedAt is older than time.Now() - olderThan.
func (*InMemoryStore) MarkExecuted ¶
MarkExecuted implements Store. Idempotent on already-executed source: returns stored Record preserving the ORIGINAL proof (first-write-wins). Returns ErrAlreadyFinal on failed source; ErrIllegalTransition on other non-approved source.
func (*InMemoryStore) MarkFailed ¶
MarkFailed implements Store. Idempotent on already-failed source: returns stored Record preserving the ORIGINAL reason. Returns ErrAlreadyFinal on executed source.
func (*InMemoryStore) SetUpdatedAtForTest ¶
func (s *InMemoryStore) SetUpdatedAtForTest(id string, t time.Time)
SetUpdatedAtForTest overrides Record.UpdatedAt on the record with the given id. TEST-ONLY: production code MUST NOT call this. It exists solely so ListStuck tests can simulate time passage without real sleep. No-op if id is not found.
SetUpdatedAtForTest 改写指定 id 的 Record.UpdatedAt. 仅供测试: 生产代码禁止调用. 目的是让 ListStuck 测试不真实 sleep 即可模拟 时间流逝. id 未找到时 no-op.
func (*InMemoryStore) Stage ¶
Stage implements Store. Rejects empty SessionID and any State other than pending_tech with ErrIllegalTransition.
func (*InMemoryStore) UpdateBizVerdict ¶
func (s *InMemoryStore) UpdateBizVerdict(_ context.Context, id string, v validator.Verdict) (Record, error)
UpdateBizVerdict implements Store.
func (*InMemoryStore) UpdateTechVerdict ¶
func (s *InMemoryStore) UpdateTechVerdict(_ context.Context, id string, v validator.Verdict) (Record, error)
UpdateTechVerdict implements Store.
type Query ¶
type Query struct {
// SessionID filters to Records of a single Agent session. Empty
// string does not constrain.
//
// SessionID 过滤单个 Agent session 的 Record. 空字符串不约束.
SessionID string
// States filters to Records currently in any of the listed
// states. Empty slice does not constrain.
//
// States 过滤当前处于列表中任一状态的 Record. 空 slice 不约束.
States []State
// Since filters to Records whose UpdatedAt is at or after the
// given time. Zero time does not constrain.
//
// Since 过滤 UpdatedAt 在给定时间或之后的 Record. 零值不约束.
Since time.Time
// Limit caps the number of Records returned. 0 means no limit.
// Store implementations SHOULD honor a sane default upper bound
// even when Limit is 0 to protect against pathological queries.
//
// Limit 限制返回 Record 数. 0 表示无限制. Store 实现即使 Limit=0
// 也应设一个合理上限以防病态查询.
Limit int
}
Query is the pull-only filter set for Store.List / ListStuck. All fields are optional and AND-ed together; zero-value fields do not constrain.
Product decision #5: customers pull from core via this API, core does not push into customer databases. Build whatever integration (cron job, dashboard, event sink) fits on top of List.
Query 是 Store.List / ListStuck 的 pull-only 过滤集. 所有字段可选 且以 AND 组合, 零值不约束.
产品决策 #5: 客户从 core 主动拉取, core 不向客户库推送. 在 List 之上自行搭接集成 (cron / dashboard / 事件 sink).
type Record ¶
type Record struct {
// ID is the globally unique record identifier assigned at Stage
// time by the Store implementation (UUID for SQL, monotonic int
// for InMemoryStore). Callers MUST treat it as opaque.
//
// ID 是 Stage 时由 Store 实现分配的全局唯一标识 (SQL 用 UUID,
// InMemoryStore 用单调递增 int). 调用方视为不透明值.
ID string
// SessionID groups Records emitted within one Agent session.
// Required: Stage rejects empty SessionID. Platform layers use
// it to reconstruct per-session audit trails and to implement
// session-level batch operations (bulk cancel, session rollback).
//
// SessionID 把同一 Agent session 产出的 Record 归簇. 必填:
// Stage 拒绝空 SessionID. 平台层据此还原 session 级审计链, 并实现
// session 级批量操作 (批量取消 / session 回滚).
SessionID string
// State is the current lifecycle state. Transitions are validated
// against the static matrix (IsLegal) + DependencyGuard before
// being persisted by a Store.
//
// State 是当前生命周期状态. transition 先过静态矩阵 (IsLegal) +
// DependencyGuard, 然后才由 Store 持久化.
State State
// Diff is the decision payload that both tech and biz Validators
// review. SourceTool dispatches decode; Raw carries the
// serialized decision (opaque to staging); Metadata carries
// free-form context. The whole decision package sits in a
// single Diff -- product decision I (bundle-level atomicity).
//
// Diff 是两层 Validator 都审的决策载荷. SourceTool 分发 decode;
// Raw 承载序列化决策 (对 staging 不透明); Metadata 承载自由上下文.
// 整个决策包装在一个 Diff 里 -- 产品决策 I (决策包级原子).
Diff validator.DiffInput
// TechVerdict is populated by Store.UpdateTechVerdict when the
// tech tier approves or blocks. Zero value (empty ValidatorName)
// means the tech tier has not yet reviewed this Record. Consumers
// querying the Store for audit purposes read Severity / Reason
// off this field to explain the rejected_tech outcome or the
// tech-passed advisory (Warn) on an approved Record.
//
// TechVerdict 在技术层放行或阻断时由 Store.UpdateTechVerdict 填充.
// 零值 (ValidatorName 为空) 表示技术层尚未审. 审计场景的消费方
// 从 Severity / Reason 读取以解释 rejected_tech 结果, 或放行
// Record 上技术层留下的 advisory (Warn) 信息.
TechVerdict validator.Verdict
// BizVerdict is populated by Store.UpdateBizVerdict when the
// biz (ML / rules / LLM) tier approves or blocks. Zero value
// means the biz tier has not yet reviewed.
//
// BizVerdict 在业务层 (ML / 规则 / LLM) 放行或阻断时由
// Store.UpdateBizVerdict 填充. 零值表示业务层尚未审.
BizVerdict validator.Verdict
// ExecutionError is populated by Store.MarkFailed with the
// caller-supplied reason when a previously approved Record
// fails at production commit time. Empty string on any
// non-failed Record.
//
// ExecutionError 由 Store.MarkFailed 在放行后的 Record 生产
// commit 失败时填入调用方提供的 reason. 非 failed 状态 Record
// 为空字符串.
ExecutionError string
// ExecutionProof is populated by Store.MarkExecuted with the
// caller-supplied opaque evidence that a previously approved
// Record really landed in production. Typical shapes (decided
// by the caller, not core): WMS return receipt, idempotency
// key, transaction id. staging does not inspect it; it is
// stored verbatim for audit chain integrity.
//
// ExecutionProof 由 Store.MarkExecuted 在放行后的 Record 真正
// 落地生产时填入调用方提供的 opaque 证据. 典型形态 (调用方决定,
// 非 core): WMS 回单 / 幂等 key / 事务 id. staging 不解读,
// 原样存储供审计链完整性.
ExecutionProof any
// CreatedAt records when the Record was first staged. Set by
// Store.Stage and never changes afterwards.
//
// CreatedAt 记录 Record 首次被 stage 的时刻. 由 Store.Stage 写入,
// 此后不再变化.
CreatedAt time.Time
// UpdatedAt records the most recent transition time. Doubles as
// the "entered current state at" timestamp, because every
// transition updates it. Store.ListStuck filters on UpdatedAt
// to surface Records that have been in a state too long (a
// common watchdog pattern for the approved -> executed external
// arc).
//
// UpdatedAt 记录最近一次 transition 时刻. 也兼作 "进入当前状态的
// 时间" 戳 -- 每次 transition 都更新. Store.ListStuck 基于
// UpdatedAt 过滤找出在某状态滞留过久的 Record (approved ->
// executed 外部 arc 的常见 watchdog 模式).
UpdatedAt time.Time
// Metadata mirrors validator.DiffInput.Metadata at Record level.
// Intended for caller-defined context: tenant id, priority tier,
// correlation id for tracing, the "upstream record ids" field a
// DependencyGuard implementation consults, etc. staging reads
// none of it; it is forwarded verbatim through Store.
//
// Metadata 对齐 validator.DiffInput.Metadata 的 Record 级位.
// 用于调用方定义的上下文: tenant id / 优先级 / 追踪 correlation
// id / DependencyGuard 实现读取的 "上游 record id" 字段等等.
// staging 不读取其中任何字段, Store 原样转发.
Metadata map[string]any
}
Record is one decision-package staged for review. One Record per Agent decision (product decision #1): whether the decision touches one table or several, the bundle is one Record and is approved / rejected / executed as a unit. Sub-operation granularity (per-table retry / per-row rollback) is intentionally not modelled at this level -- core stays schema-agnostic, and callers who need finer granularity split an Agent decision into multiple Stage() calls.
Record 是一条送审的决策包. 每次 Agent 决策一条 (产品决策 #1): 无论该决策触及一张或多张表, 打包作为一个 Record, 以一个单位放行 / 拒绝 / 执行. 子操作粒度 (单表重试 / 单行回滚) 刻意不在本层建模 -- core 保持与 schema 无关, 需要更细粒度的调用方把一次 Agent 决策 拆成多次 Stage() 调用即可.
func NewDraft ¶
NewDraft constructs a pending_tech Record ready for Stage. The ID is left empty for the Store to populate at persistence time; CreatedAt / UpdatedAt are set to time.Now().UTC() so the two timestamps match on a freshly-staged Record.
NewDraft 构造 pending_tech 状态的 Record, 准备 Stage. ID 留空由 Store 持久化时填充; CreatedAt / UpdatedAt 设为 time.Now().UTC(), 新 stage 的 Record 两时间戳相同.
type State ¶
type State string
State enumerates the 7 lifecycle states a Record occupies. String values are stable -- SQL implementations store them as TEXT and downstream tooling (dashboards, CLI) matches on them verbatim.
State 枚举 Record 生命周期的 7 个状态. 字符串值稳定 -- SQL 实现 以 TEXT 落表, 下游工具 (dashboard / CLI) 按字面匹配.
const ( StatePendingTech State = "pending_tech" StateRejectedTech State = "rejected_tech" StatePendingML State = "pending_ml" StateRejectedML State = "rejected_ml" StateApproved State = "approved" StateExecuted State = "executed" StateFailed State = "failed" )
State values. Four of them are terminal (see IsFinal); the other three are in-flight.
State 取值. 其中 4 个为终态 (见 IsFinal), 其余 3 个在途.
type Store ¶
type Store interface {
// Stage persists a pending_tech Record and returns it with its
// ID populated. The passed-in Record's State MUST be
// pending_tech; any other state returns ErrIllegalTransition.
// SessionID MUST be non-empty -- empty SessionID is a
// programming error surfaced immediately.
//
// Stage 持久化 pending_tech Record 并回填 ID. 入参 Record 的
// State 必须为 pending_tech, 其他状态返回 ErrIllegalTransition.
// SessionID 必须非空 -- 空 SessionID 属编程错误立即暴露.
Stage(ctx context.Context, r Record) (Record, error)
// Get returns the Record with the given ID, or ErrRecordNotFound.
//
// Get 按 ID 返回 Record, 未找到返回 ErrRecordNotFound.
Get(ctx context.Context, id string) (Record, error)
// List returns Records matching q, ordered by CreatedAt ascending.
// Empty Query returns everything (subject to the Store's default
// upper bound -- see Query.Limit godoc).
//
// List 返回匹配 q 的 Record, 按 CreatedAt 升序. 空 Query 返回全部
// (受 Store 默认上限约束 -- 见 Query.Limit godoc).
List(ctx context.Context, q Query) ([]Record, error)
// ListBySession is a convenience over List with Query{SessionID:
// sessionID}. Returns Records of the given Agent session ordered
// by CreatedAt. Distinct method because per-session lookups are
// hot-path for audit UIs and deserve a named API.
//
// ListBySession 是 List 带 Query{SessionID: sessionID} 的便捷包装.
// 返回指定 Agent session 的 Record, 按 CreatedAt 升序. 单独
// 列出因为 per-session 查询是审计 UI 热路径, 值得专名.
ListBySession(ctx context.Context, sessionID string) ([]Record, error)
// ListStuck returns Records that have been in state for at
// least olderThan duration, measured from Record.UpdatedAt
// against time.Now(). Intended for platform-layer watchdogs:
// approved Records that do not transition to executed/failed
// within a business-defined SLA are the canonical use case
// (decision Y's external-push arc has no internal timeout).
//
// ListStuck 返回在某状态滞留至少 olderThan 的 Record, 从
// Record.UpdatedAt 对 time.Now() 测量. 用于平台层 watchdog:
// approved Record 未在业务 SLA 内推进到 executed/failed 是典型
// 场景 (决策 Y 的外部推 arc 无内置超时).
ListStuck(ctx context.Context, state State, olderThan time.Duration) ([]Record, error)
// UpdateTechVerdict transitions pending_tech -> rejected_tech or
// pending_ml based on v.Approved and stamps v into
// Record.TechVerdict. Returns ErrIllegalTransition if the
// Record is not in pending_tech. UpdatedAt is refreshed.
//
// UpdateTechVerdict 按 v.Approved 把 pending_tech 推至
// rejected_tech 或 pending_ml, 同时把 v 写入 Record.TechVerdict.
// 当前状态非 pending_tech 返回 ErrIllegalTransition. UpdatedAt
// 刷新.
UpdateTechVerdict(ctx context.Context, id string, v validator.Verdict) (Record, error)
// UpdateBizVerdict transitions pending_ml -> rejected_ml or
// approved based on v.Approved and stamps v into
// Record.BizVerdict. Returns ErrIllegalTransition if the
// Record is not in pending_ml. UpdatedAt is refreshed.
//
// UpdateBizVerdict 按 v.Approved 把 pending_ml 推至 rejected_ml
// 或 approved, 同时把 v 写入 Record.BizVerdict. 当前状态非
// pending_ml 返回 ErrIllegalTransition. UpdatedAt 刷新.
UpdateBizVerdict(ctx context.Context, id string, v validator.Verdict) (Record, error)
// MarkExecuted transitions approved -> executed and stamps
// proof into Record.ExecutionProof. Idempotent: second call on
// executed Record returns the stored Record with the ORIGINAL
// proof preserved and nil error (proof is written once at
// first transition; subsequent calls do not overwrite).
//
// Other source states return ErrIllegalTransition (pending_*,
// rejected_*) or ErrAlreadyFinal (failed).
//
// MarkExecuted 把 approved 推至 executed 并把 proof 写入
// Record.ExecutionProof. 幂等: 对 executed Record 的二次调用返回
// 存储记录 (保留首次 transition 时的 proof) 与 nil error --
// proof 仅首次 transition 写入, 后续调用不覆盖.
//
// 其他源状态返回 ErrIllegalTransition (pending_* / rejected_*)
// 或 ErrAlreadyFinal (failed).
MarkExecuted(ctx context.Context, id string, proof any) (Record, error)
// MarkFailed transitions approved -> failed and stamps reason
// into Record.ExecutionError. Idempotent on already-failed
// source (original reason preserved).
//
// Other source states return ErrIllegalTransition or
// ErrAlreadyFinal (executed) per the same taxonomy as
// MarkExecuted.
//
// MarkFailed 把 approved 推至 failed 并把 reason 写入
// Record.ExecutionError. 对已 failed 源幂等 (保留原 reason).
//
// 其他源状态按 MarkExecuted 相同分类返回 ErrIllegalTransition
// 或 ErrAlreadyFinal (executed).
MarkFailed(ctx context.Context, id string, reason string) (Record, error)
}
Store persists Records and enforces the 7-state machine at the transaction boundary. SQL implementations MUST wrap every state transition in a single transaction that SELECTs the current State, verifies it matches the expected source, and UPDATEs -- this is the same optimistic-lock pattern SQLCASTool uses for generic staging writes (see core/pkg/tools/builtin/sql_cas.go). Returning ErrIllegalTransition on state mismatch MUST rollback the transaction; partial writes are a contract violation.
Concurrency: all methods MUST be safe for concurrent use from multiple goroutines. The InMemoryStore reference implementation uses sync.Mutex; SQL implementations rely on row-level locking plus the SELECT-then-UPDATE state check.
Idempotency: MarkExecuted on an already-executed Record and MarkFailed on an already-failed Record both return the stored Record with nil error (idempotent). Any other "second call on terminal state" returns ErrAlreadyFinal so callers distinguish "I did this before" from "the state machine disagrees".
Store 持久化 Record 并在事务边界强制 7 状态机. SQL 实现必须在单个 事务内 SELECT 当前 State 校验与期望源一致再 UPDATE -- 与 SQLCASTool 对通用 staging 写入的乐观锁模式同宗 (见 core/pkg/tools/builtin/sql_cas.go). 源状态不匹配时返回 ErrIllegalTransition 并必须 rollback; 半写是契约违反.
并发: 所有方法必须多 goroutine 并发安全. InMemoryStore 参考实现 用 sync.Mutex; SQL 实现靠行锁 + SELECT-then-UPDATE 状态校验.
幂等: 已 executed 的 Record 再调 MarkExecuted / 已 failed 的再调 MarkFailed 都返回存储记录 + nil error (幂等). 其他 "对终态的第二次 调用" 返回 ErrAlreadyFinal, 让调用方区分 "我之前做过" 与 "状态机 不同意".
type TenantDenyGuard ¶
type TenantDenyGuard struct {
// BlockedTenants maps tenant id to bool (true = blocked).
// A nil or empty map denies nothing.
//
// BlockedTenants 把 tenant id 映射到 bool (true = 被禁).
// nil 或空 map 不拒任何 tenant.
BlockedTenants map[string]bool
}
TenantDenyGuard denies every transition whose Record.Metadata carries a tenant id listed in BlockedTenants. This is the canonical "metadata-driven guard" shape: the guard reads a well-known key out of Record.Metadata (here, "tenant") and decides based on a deny-list. Production deployments usually extend or wrap this with their own dependency logic; it serves as both a working example and a minimal building block.
Metadata key: "tenant" (string). Missing / wrong-type value is treated as "not on the deny-list" -- the guard only blocks when it positively identifies a blocked tenant.
TenantDenyGuard 拒绝所有 Record.Metadata 中 tenant id 在 BlockedTenants 里的 transition. 这是典型的 "metadata-driven guard" 形态: 从 Record.Metadata 读一个约定 key (这里是 "tenant") 按 deny-list 决定. 生产部署通常以此为起点扩展或包装自己的依赖 逻辑; 既是可用示例, 也是最小构件.
Metadata key: "tenant" (string). 缺失或类型不对视为 "不在 deny-list" -- guard 仅在明确识别到被禁 tenant 时才拒.
func (TenantDenyGuard) AllowTransition ¶
AllowTransition denies the transition when the Record's tenant metadata is in BlockedTenants; allows otherwise.
AllowTransition 在 Record 的 tenant metadata 位于 BlockedTenants 时拒绝 transition; 其他情况放行.