mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 03:21:58 +00:00
77fc0efa10
为什么:口径升版是被迫处置的事件(02 做法 7、拍板 L6),已交付的答案以前没人追;目录事实被驳回、表结构变了也没有地方标。
- ledger/uses.py(新):反查旧版本被谁用过——比对工件里等价已证明的问数回答(含轮次、对话主人)、W 的正式运行(定时任务、
应用各取最近 4 期)、固定了它的工作流版本发起的正式运行、固定这一版的应用、导出审计(导出人也是主人);预告、召回、
版本用量共用这一份
- ledger/recall.py(新):计划按(旧版本、新版本、input_key)去重成组,从已有召回运行重算(幂等、不建任务表);每组一次
召回运行(origin_kind=recall,探索运行),派生图经 derive.recall_graph(新旧两版取数和口径卡带前缀、召回卡里算两版之差);
不能重算照实记原因(数据部分不能复用、新版没这张卡、入口参数不齐、图里有人工节点或写入);每次发布最多 recall_limit 组,
超出的手动接着算;收尾时按渲染的旧值新值判数变没变:变了开 recall 待办(开登录按主人各一条、关登录派给任何人)、推
ledger.recall,没变在原答案上记 ledger.recalled;待办「按新版重新出具」走 issue.check_target 同一套判法、「知道了」
- 目录事实被驳回或修改(审阅、人工编辑、保存提案、批量审阅之后):冻结目录里看得到这一项的探索运行记 ledger.basis_changed,
按新目录多出错误级 SQL 检查问题的才开待办;表结构变动消费 source_changes(游标、处理时改了目录的当场交过去),标依据的
表结构已变,受影响的口径试算一次,算不出来才通知发布人
- 发布钩子在 commit_publish 提交之后排队(自动的几样和调度循环同一个开关),GET /api/workflows/{id}/recall-preview 给发布
弹窗预告;GET/POST /api/ledger/recall/{wf}/{v}[/continue] 给版本历史看批次、接着算;版本用量加问数回答、导出、召回
- ledger.recall 打开订阅(全局、个人渠道),同一渠道只推一次,消息不放数;新事件 ledger.recalled、ledger.basis_changed
登记为封存后追加的事件,可验证报告离线核对页认得;E4 每次发布召回了几组、数变了的比例
- runner 收尾时召回运行在后台收尾,启动时接着做没发完的批次
147 lines
9.3 KiB
Python
147 lines
9.3 KiB
Python
"""流程:待办、派给谁、生效记录、通知(应用平台 A4、A5,docs/design/app-platform-and-look.md §2.4、§2.5、§3.7、§3.8)。
|
||
|
||
一次中断对应一条 Approval(给引擎)和一条 Task(给人)。Task 下挂若干条 TaskAssignment:派给具体的人或组,
|
||
按 policy(any / all / quorum)汇总后才恢复运行。核实待办(kind=reconcile)没有 Approval,approval_id 为空。
|
||
|
||
ActionRecord 是写外部系统的权威记录:先写记录再发送;节点每次执行都先按 action_key 查,已生效的不再发,
|
||
结果未知的永不自动重发。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from sqlalchemy import ForeignKey, Index, Integer, String, Text
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.db.base import Base, TimestampMixin, UTCDateTime, new_id, utcnow
|
||
|
||
#: 待办的种类。人工节点三种 + 现有的几种中断 + 动作确认、核实、等外部系统 + 个人应用提交审核(review,
|
||
#: docs/design/normal-user-apps.md N7,app/process/tasks.REVIEW_KIND)+ 口径召回(recall,docs/design/next-round/02-口径账本.md
|
||
#: 做法 7:口径升版后答案的数变了、答案依据的目录或表结构变了;挂在召回运行或答案的运行上,没有 Approval,app/ledger/recall)
|
||
TASK_KINDS = ("approve", "input", "edit", "tool", "code", "dual_check", "action_confirm", "reconcile", "signal",
|
||
"review", "recall")
|
||
#: pending 等人;resolving 已凑齐、正在恢复运行(条件 UPDATE 抢到的那个请求负责);resolved / cancelled / expired 终态
|
||
TASK_STATUSES = ("pending", "resolving", "resolved", "cancelled", "expired")
|
||
#: 动作状态。simulated:探索运行里端点没有 test 环境时的演练,不发送
|
||
ACTION_STATES = ("pending", "sent", "confirmed", "failed", "unknown", "simulated")
|
||
|
||
|
||
class Task(Base, TimestampMixin):
|
||
__tablename__ = "tasks"
|
||
__table_args__ = (Index("ix_tasks_status_due", "status", "due_at"),)
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
approval_id: Mapped[str | None] = mapped_column(String(32), default=None, unique=True)
|
||
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id", ondelete="CASCADE"), index=True)
|
||
node_id: Mapped[str | None] = mapped_column(String(64), default=None)
|
||
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||
app_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
kind: Mapped[str] = mapped_column(String(20), default="approve")
|
||
title: Mapped[str] = mapped_column(String(300), default="")
|
||
# agentlab.form/1;没有表单的(批准 / 驳回)为空
|
||
form: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
# {"kind": "any"} | {"kind": "all"} | {"kind": "quorum", "m": 2}
|
||
policy: Mapped[dict[str, Any]] = mapped_column(default=lambda: {"kind": "any"})
|
||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||
due_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
remind_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
# {"action": "remind" | "escalate" | "reject", "every"?: ISO 时长, "to_slot"?: 岗位}。没有「自动批准」
|
||
on_timeout: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
escalation_level: Mapped[int] = mapped_column(Integer, default=0)
|
||
# 等外部系统(kind=signal)时,入站 signal 按它找到这条待办
|
||
correlation_key: Mapped[str | None] = mapped_column(String(200), default=None, index=True)
|
||
# 判定结果:{"decision": ..., "values": ..., "signoffs": [...]}
|
||
outcome: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
resolved_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||
resolved_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
|
||
|
||
class TaskAssignment(Base):
|
||
__tablename__ = "task_assignments"
|
||
__table_args__ = (Index("ix_assign_principal", "principal_kind", "principal", "status"),)
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), index=True)
|
||
principal_kind: Mapped[str] = mapped_column(String(20)) # user | group | anyone | hook
|
||
principal: Mapped[str] = mapped_column(String(100), default="")
|
||
# 从哪来:slot:<岗位> | initiator | expression | escalation | reassign
|
||
source: Mapped[str] = mapped_column(String(40), default="")
|
||
status: Mapped[str] = mapped_column(String(20), default="pending") # pending | responded | withdrawn
|
||
response: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
responded_by: Mapped[str | None] = mapped_column(String(100), default=None)
|
||
responded_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
# 回复人是不是认证过的身份(off 模式下是自报署名,记 False)
|
||
verified: Mapped[bool] = mapped_column(default=False)
|
||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|
||
|
||
|
||
class ActionRecord(Base, TimestampMixin):
|
||
__tablename__ = "action_records"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
# sha256(run_id, node_id, exec);同一次执行第 n 次发送的幂等键是 f"{action_key}:{attempt}"
|
||
action_key: Mapped[str] = mapped_column(String(64), index=True)
|
||
attempt: Mapped[int] = mapped_column(Integer, default=1)
|
||
idem_key: Mapped[str] = mapped_column(String(80), unique=True)
|
||
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id", ondelete="CASCADE"), index=True)
|
||
node_id: Mapped[str] = mapped_column(String(64))
|
||
exec_no: Mapped[int] = mapped_column(Integer, default=1)
|
||
app_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||
workflow_id: Mapped[str | None] = mapped_column(String(32), default=None, index=True)
|
||
workflow_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
graph_hash: Mapped[str | None] = mapped_column(String(64), default=None)
|
||
capability: Mapped[str] = mapped_column(String(200))
|
||
capability_version: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
endpoint_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
environment: Mapped[str] = mapped_column(String(20), default="prod") # prod | test | simulated
|
||
payload_hash: Mapped[str] = mapped_column(String(64))
|
||
# 遮罩后的预览(确认卡上给人看的那一份)
|
||
preview: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
state: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||
confirm_task_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
request_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
response_status: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
# 回执:外部单号等,原文落工件,这里存 {artifact, summary}
|
||
receipt: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
error: Mapped[str | None] = mapped_column(Text, default=None)
|
||
lookup_attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||
next_lookup_at: Mapped[datetime | None] = mapped_column(default=None)
|
||
# 核实结论:{"by": ..., "how": "lookup" | "manual", "outcome": "confirmed" | "not_applied" | "resend" | "abandon", "external_id"?}
|
||
resolution: Mapped[dict[str, Any] | None] = mapped_column(default=None)
|
||
|
||
|
||
class NotifyChannel(Base, TimestampMixin):
|
||
"""通知渠道。格式、签名、发送同定时任务的 Webhook(四种:generic / feishu / wecom / dingtalk)。"""
|
||
|
||
__tablename__ = "notify_channels"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
name: Mapped[str] = mapped_column(String(100))
|
||
scope: Mapped[str] = mapped_column(String(20), default="global") # global | app | user
|
||
owner_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
format: Mapped[str] = mapped_column(String(20), default="generic")
|
||
url: Mapped[str] = mapped_column(Text) # 密文
|
||
secret: Mapped[str | None] = mapped_column(Text, default=None) # 密文
|
||
events: Mapped[list[Any]] = mapped_column(default=list)
|
||
enabled: Mapped[bool] = mapped_column(default=True)
|
||
|
||
|
||
class NotifyDelivery(Base):
|
||
"""通用投递记录。定时任务的投递仍写 webhook_deliveries(schedule_id 非空,SQLite 改不了可空性)。"""
|
||
|
||
__tablename__ = "notify_deliveries"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||
channel_id: Mapped[str] = mapped_column(ForeignKey("notify_channels.id", ondelete="CASCADE"), index=True)
|
||
subject_kind: Mapped[str] = mapped_column(String(20)) # task | run | action | channel(渠道的测试消息)
|
||
subject_id: Mapped[str] = mapped_column(String(32))
|
||
event: Mapped[str] = mapped_column(String(40))
|
||
run_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
task_id: Mapped[str | None] = mapped_column(String(32), default=None)
|
||
status: Mapped[str] = mapped_column(String(20))
|
||
http_status: Mapped[int | None] = mapped_column(Integer, default=None)
|
||
attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||
error: Mapped[str | None] = mapped_column(Text, default=None)
|
||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|