diff --git a/CHANGELOG.md b/CHANGELOG.md index 101b17f..9e9e263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ release tags and PEP 440 for the Python package. ## Unreleased +- Coordinated Tasks can opt into pinned primary-deliverable acceptance: required fields, + typed JSON equality, and unit-aware rate thresholds with optional result-claim verification. + Missing evidence needs review; deterministic acceptance is separate from execution completion. + Evidence-bound, audited human decisions preserve failed checks; an accepted-result endpoint + gates primary output export. The bilingual Console exposes configuration, results and controls. + Unaccepted opted-in Tasks skip automatic memory capture; completion cards omit result excerpts. - Organizational Memory no longer equates different content in one Memory Type with contradiction. Optional normalized subject keys enable scoped competing-record checks, exposed as UNKNOWN, NO_COMPETING_RECORDS, or REVIEW_REQUIRED rather than a false confirmed conflict. Assessments use diff --git a/alembic/versions/20260930_0058_deliverable_acceptance.py b/alembic/versions/20260930_0058_deliverable_acceptance.py new file mode 100644 index 0000000..1c032a1 --- /dev/null +++ b/alembic/versions/20260930_0058_deliverable_acceptance.py @@ -0,0 +1,58 @@ +"""Add audited deliverable acceptance decisions without changing task execution status. + +Revision ID: 20260930_0058 +Revises: 20260930_0057 +""" + +import sqlalchemy as sa + +from alembic import op + +revision = "20260930_0058" +down_revision = "20260930_0057" +branch_labels = None +depends_on = None + +_OLD_ACTIONS = ( + "ACCEPT_CANDIDATE", + "REJECT_TASK", + "INCREASE_BUDGET_AND_RESUME", + "RECONCILE_MCP_SUCCEEDED", + "RECONCILE_MCP_FAILED", + "BIND_A2A_REMOTE_TASK", + "RECONCILE_A2A_NOT_DELIVERED", + "RECONCILE_RUNTIME_SUCCEEDED", + "RECONCILE_RUNTIME_FAILED", + "RECONCILE_RUNTIME_CANCELED", + "RECONCILE_RUNTIME_TIMED_OUT", +) +_NEW_ACTIONS = ("ACCEPT_DELIVERABLE", "REJECT_DELIVERABLE") + + +def _replace_constraint(actions: tuple[str, ...]) -> None: + op.drop_constraint("ck_task_resolutions_action", "task_resolutions", type_="check") + op.create_check_constraint( + "ck_task_resolutions_action", + "task_resolutions", + "action IN (" + ", ".join(repr(action) for action in actions) + ")", + ) + + +def upgrade() -> None: + _replace_constraint(_OLD_ACTIONS + _NEW_ACTIONS) + + +def downgrade() -> None: + count = ( + op.get_bind() + .execute( + sa.text( + "SELECT count(*) FROM task_resolutions " + "WHERE action IN ('ACCEPT_DELIVERABLE', 'REJECT_DELIVERABLE')" + ) + ) + .scalar_one() + ) + if count: + raise RuntimeError("Cannot downgrade while deliverable acceptance audit records exist") + _replace_constraint(_OLD_ACTIONS) diff --git a/docs/README.md b/docs/README.md index 35b032f..841b2a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ For the exact shipped boundary, use [implementation status](implementation-statu | Try AgentMesh without a key | [Five-minute guide](getting-started.md) | [Customer-feedback task with real-model screenshots](scenarios/customer-feedback-deepseek.md) | | Configure employees, models, or memory | [Model and memory setup](model-and-memory-setup.md) | [SSH-only credential administration](operations/model-connections-ssh.md) | | Notify a Feishu group | [Feishu notifications](integrations/feishu-notifications.md) | [Feature gates](architecture/modules/feature-gates.md) | +| Check a collaborative result before delivery | [Deliverable acceptance](deliverable-acceptance.md) | [Customer-feedback walkthrough](scenarios/customer-feedback-deepseek.md) | | Use a business scenario | [Customer-feedback walkthrough](scenarios/customer-feedback-deepseek.md) | [Market-research scenario](scenarios/market-research.md) | | Deploy or operate AgentMesh | [Administrator best practices](best-practices.md) | [SLO and restore](operations/slo-and-restore.md), [runtime rollback](operations/runtime-direct-cutover-rollback.md) | | Build an extension | [Runtime Extension Protocol](architecture/modules/runtime-extension-protocol.md) | [Extension Starter](https://github.com/0YHR0/AgentMesh-Extension-Starter), [formal module contracts](architecture/modules/formal/README.md) | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 388a75e..66b4b83 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -10,6 +10,7 @@ | 不用 Key 试用 AgentMesh | [5 分钟上手](getting-started.zh-CN.md) | [真实模型客户反馈教程](scenarios/customer-feedback-deepseek.zh-CN.md) | | 配置员工、模型或记忆 | [模型与记忆配置](model-and-memory-setup.zh-CN.md) | [仅通过 SSH 管理凭据](operations/model-connections-ssh.md) | | 通知飞书群 | [飞书通知配置](integrations/feishu-notifications.zh-CN.md) | [Feature Gate](architecture/modules/feature-gates.md) | +| 交付前检查协作结果 | [交付验收](deliverable-acceptance.zh-CN.md) | [客户反馈协作教程](scenarios/customer-feedback-deepseek.zh-CN.md) | | 跑一个业务场景 | [客户反馈协作教程](scenarios/customer-feedback-deepseek.zh-CN.md) | [市场研究场景](scenarios/market-research.zh-CN.md) | | 部署或维护 AgentMesh | [管理员最佳实践](best-practices.zh-CN.md) | [SLO 与恢复](operations/slo-and-restore.md)、[Runtime 回退](operations/runtime-direct-cutover-rollback.md) | | 开发一个扩展 | [Runtime 扩展协议](architecture/modules/runtime-extension-protocol.md) | [Extension Starter](https://github.com/0YHR0/AgentMesh-Extension-Starter)、[正式版模块契约](architecture/modules/formal/README.md) | diff --git a/docs/deliverable-acceptance.md b/docs/deliverable-acceptance.md new file mode 100644 index 0000000..165a6e5 --- /dev/null +++ b/docs/deliverable-acceptance.md @@ -0,0 +1,109 @@ +# Deliverable acceptance + +[English](deliverable-acceptance.md) | [简体中文](deliverable-acceptance.zh-CN.md) + +**An employee finishing work is not the same as the result being acceptable.** +Optional acceptance checks make that distinction visible for collaborative Tasks. +Simple Tasks do not need this configuration and keep their existing behavior. + +## Example: evaluating a pilot launch + +A Researcher collects pilot observations, an Analyst compares them, and an Editor +produces a recommendation. You require a `summary` field and a support-ticket rate +of no more than 12 tickets per 100 buyers. The denominator is **buyers**, not orders: +18 tickets and 100 orders cannot establish this rate if buyer count is missing. + +In the Console: + +1. Create a collaborative Task and bind the three published employees. Set Research + as the Analyst's dependency, and both as the Editor's dependencies. +2. Open the optional delivery restrictions, select the Editor as primary if the plan + has multiple terminal work items, and enable deliverable acceptance. +3. Require the result path `summary`. Optionally enable the rate check: enter the + numerator and denominator values with their exact units, scale `100`, operator + `LTE` (less than or equal), and threshold `12`. Leave an unavailable value blank; + do not substitute an unrelated quantity. +4. If you need a person to approve the result even after checks pass, enable human + review. Run the Task and inspect its independent acceptance panel. + +You can also check an optional structured rate claim in the primary output. For +example, a result `support_rate: {value: 10, unit: "ticket/buyer", scale: 100}` is +checked against the configured source quantities. Checking the input facts alone +does **not** prove that every statement in the generated text is correct. + +## Results and control + +| Acceptance state | Meaning | Accepted-result export | +| --- | --- | --- | +| Not ready | Execution has not produced the pinned primary result | Blocked | +| Passed | All required configured checks pass | Allowed | +| Failed | At least one required check fails | Blocked | +| Needs review | Required evidence is missing, or human review is mandatory | Blocked | +| Human accepted | An authorized person explicitly accepts the exact result/evidence | Allowed; failed/unknown checks remain visible | +| Human rejected | An authorized person rejects the exact result/evidence | Blocked | + +Human acceptance is an **audited override**, not proof that a failed check passed. +It requires the `human_resolution` feature and Task resolution permission. The +decision records the actor, reason, and digests of the policy, output, and evidence. +Refresh before deciding if the output snapshot has changed. Execution stays +`COMPLETED`; it is not confused with the execution-time `WAITING_APPROVAL` barrier. + +The accepted-result download only returns the pinned primary deliverable. Other +attachments are not implicitly validated. Raw results remain available for audit; +they are not a substitute for the accepted-result endpoint. + +## API example + +Include this optional property in a Coordinated Task creation request. Input +quantities are explicit facts supplied by the caller, not an independent data audit. + +```json +{ + "acceptance_policy": { + "require_human_review": false, + "checks": [ + {"key": "summary", "description": "Summary is present", "kind": "OUTPUT_PATH_EXISTS", "path": ["summary"]}, + { + "key": "support-rate", "description": "At most 12 tickets per 100 buyers", + "kind": "RATE_THRESHOLD", + "numerator": {"source": "TASK_INPUT", "path": ["acceptance_facts", "tickets"], "unit": "ticket"}, + "denominator": {"source": "TASK_INPUT", "path": ["acceptance_facts", "buyers"], "unit": "buyer"}, + "scale": 100, "operator": "LTE", "threshold": 12, + "claim": {"source": "DELIVERABLE", "path": ["support_rate"], "unit": "ticket/buyer"} + } + ] + }, + "input": {"acceptance_facts": {"tickets": {"value": 18, "unit": "ticket"}}} +} +``` + +This fragment intentionally omits buyers: the required rate check is `UNKNOWN`, +so the result needs review instead of receiving a fabricated pass. Supply the rest +of the normal Task fields, plan, and published Agent bindings when creating it. + +Supported checks are `OUTPUT_PATH_EXISTS` (present and non-null), +`OUTPUT_PATH_EQUALS` (typed JSON equality), and `RATE_THRESHOLD` (`LTE`/`GTE`). +Quantity sources are `TASK_INPUT` and `DELIVERABLE`; units must match exactly. +Negative numerators, nonpositive denominators, nonfinite numbers, invalid units, +and inconsistent claims cannot pass. Policies are size bounded and pinned at +creation; a plan patch cannot remove or change the primary selection. + +`GET /api/v1/tasks/{id}` includes `deliverable_acceptance`. +`GET /api/v1/tasks/{id}/accepted-deliverable` rejects an unaccepted or unconfigured +result. Human decisions use `POST /api/v1/tasks/{id}/deliverable-acceptance/decision` +with `decision`, `reason`, both expected policy/output digests, and preferably an +`Idempotency-Key`. The server derives the actor from the authenticated principal. + +## Boundaries + +- This is a deterministic contract checker, not a general truth detector, semantic + reviewer, or automatic judge of arbitrary prose. A model's self-reported verdict + does not grant acceptance. +- It does not undo external tool actions already performed during execution. + Irreversible actions still require their own governance and approvals. +- Configured results not yet accepted do not enter automatic completed-Task memory + extraction. Human acceptance does not automatically re-trigger extraction. +- Completion notifications still report execution completion, but omit the result + summary for opted-in Tasks and direct readers to the independent acceptance state. +- Automatic revision loops and a dedicated external publishing connector remain + future work. Existing raw audit APIs are intentionally retained. diff --git a/docs/deliverable-acceptance.zh-CN.md b/docs/deliverable-acceptance.zh-CN.md new file mode 100644 index 0000000..94b8242 --- /dev/null +++ b/docs/deliverable-acceptance.zh-CN.md @@ -0,0 +1,49 @@ +# 交付验收 + +[English](deliverable-acceptance.md) | [简体中文](deliverable-acceptance.zh-CN.md) + +**员工完成执行,不代表结果满足你的要求。** 协作任务可以按需启用交付验收; +简单任务不用配置,原有行为不变。 + +## 场景:是否启动试点 + +研究员收集材料,分析员比较方案,编辑员给出建议。你要求结果包含 `summary`, +并要求「每 100 位买家的客服工单不超过 12 个」。这里的分母是**买家**,不是订单。 +如果材料只有 18 个工单、100 笔订单,没有买家数量,就不能判断这个条件通过。 + +在创建协作任务时,展开可选交付限制,开启交付验收: + +1. 指定主交付物(多个最终工作项时必须选择),填写必填结果路径,例如 `summary`。 +2. 可选开启比例检查:输入分子、分母及单位,倍率 `100`、运算符 `LTE`(小于等于)、 + 阈值 `12`。没有的数据留空,不要用不同口径的数据替代。 +3. 如需最终人工批准,勾选人工审核。运行后分别查看「执行状态」和「验收状态」。 + +可指定主结果中结构化比例声明的路径,如 `support_rate`。对应内容需为 +`{"value":10,"unit":"ticket/buyer","scale":100}`,平台会重算并检查声明是否一致。 +如果只检查输入事实的比例,则**不代表生成文本里的所有结论都已被验证**。 + +## 状态意味着什么 + +- **通过**:所有必需的已配置检查通过,可以下载验收后的主交付物。 +- **未通过**:存在失败的必需检查;不能导出验收结果。 +- **需要审核**:缺少必需数据,或开启了人工审批;不能导出验收结果。 +- **人工接受/拒绝**:具有权限的人说明理由后,对固定结果作出决定。 + 人工接受是有记录的覆盖决定,原来失败或缺失的检查仍保留,并非变成通过。 + +人工决定需要开启 `human_resolution`,并具备任务处置权限。系统保存操作人、理由、 +验收规则和结果摘要及证据摘要;数据改变后旧决定不会自动沿用。任务执行状态仍为 +`COMPLETED`,不会与执行中的审批暂停混淆。导出只覆盖主交付物,不自动覆盖附件。 +原始输出仍可查看,用于排错和审计。 + +## 能力边界 + +支持字段存在、字段严格相等、带单位的比例阈值检查。数据源可以是任务输入或主结果。 +单位必须完全一致;分母不能为零或负数;非有限数、错误单位和错误比例声明不能通过。 +配置在任务创建时固定,不会因重试或改计划偷偷换验收对象。 + +这不是万能的事实判断器,也不会撤销运行中已经调用工具产生的外部动作。输入事实由用户 +提供,平台不保证其来源真实。模型自称「验收通过」没有决定权。 +尚未接受的配置任务不会自动进入完成任务记忆提取;人工接受也不会自动补触发提取。 +飞书完成通知只说明执行完成,配置了验收的任务不自动发送结果摘要。 + +完整请求例子、接口和限制见 [英文参考](deliverable-acceptance.md)。 diff --git a/docs/implementation-status.md b/docs/implementation-status.md index 1fde474..1806c14 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -11,6 +11,7 @@ and non-critical single-team use. It is **not** a production HA or multi-tenant | Area | Current capability | Important limit | | --- | --- | --- | | Tasks and execution | Direct, independently Reviewed, and Coordinated Subtask DAGs; durable Runs/Attempts, retry, pause/resume, cancellation, Handoffs, Plan Patches, and Artifacts | General active-Run replanning and compensation remain future work | +| Deliverable acceptance | Optional pinned Coordinated primary checks, typed/unit-aware rates, audited human decisions, and accepted-result export | Checks cover only configured fields/facts, not arbitrary prose truth or already executed tool actions; see [guide](deliverable-acceptance.md) | | Agents and models | Published immutable Agent Versions, role-bound execution, deterministic keyless demo, encrypted OpenAI/DeepSeek model connections in the authenticated Console | A private DeepSeek Direct and three-employee Coordinated run passed; OpenAI has fixture qualification, not a live-provider acceptance claim | | Collaboration and tools | Governed MCP registry/read tools/idempotent writes; trusted A2A peer registration, delegation, polling, cancellation, and unknown-outcome reconciliation | External providers/peers require configuration; irreversible writes and A2A streaming/push are not supported | | External notifications | Optional Feishu bot cards for Task completion, failure, pending human review, and governed approval requests, backed by durable PostgreSQL delivery jobs | Gate is off by default; app credentials and a test group are required; inbound commands/approvals are not yet supported | @@ -37,8 +38,8 @@ See the [machine-readable report](qualification/a4-2-parity.json) and ## What remains -- Configurable deterministic acceptance for Coordinated business deliverables, distinct from - execution completion ([#187](https://github.com/0YHR0/AgentMesh/issues/187)). +- Automated revision loops, semantic business review, and dedicated post-acceptance publishing + remain future work; [configured deterministic acceptance](deliverable-acceptance.md) is available. - Durable non-LangGraph reattach/restart and production admission ([#136](https://github.com/0YHR0/AgentMesh/issues/136)). - Shared Governed Action SDK beyond MCP/A2A ([#137](https://github.com/0YHR0/AgentMesh/issues/137)). - Repeatable chaos and correctness qualification ([#138](https://github.com/0YHR0/AgentMesh/issues/138)). diff --git a/docs/qualification/a4-2-coordinated-activation.json b/docs/qualification/a4-2-coordinated-activation.json index 9e69094..0e98966 100644 --- a/docs/qualification/a4-2-coordinated-activation.json +++ b/docs/qualification/a4-2-coordinated-activation.json @@ -2,7 +2,7 @@ "schema": "agentmesh.qualification.a4-2-coordinated-activation.v1", "milestone": "A4.2c.2f8", "status": "pending_activation", - "migration_head": "20260930_0057", + "migration_head": "20260930_0058", "downgrade_floor": "20260915_0053", "previous_migration_head": "20260915_0053", "feature_gate": { diff --git a/docs/roadmap.md b/docs/roadmap.md index 3748c6c..7603970 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -9,10 +9,10 @@ only; it is not a claim that a proposal has shipped. ## Next product validation -- Next priority: opt-in acceptance checks for Coordinated deliverables - ([#187](https://github.com/0YHR0/AgentMesh/issues/187)). Reuse the existing Reviewed/checking - primitives where possible; make missing inputs, denominator/unit mismatches and human review - visible without complicating simple tasks. Complete provider output is not business acceptance. +- Validate [opt-in Coordinated deliverable acceptance](deliverable-acceptance.md) on repeatable + real-model workflows. It distinguishes missing inputs, unit mismatches and human review without + complicating simple tasks. Semantic review, automated revision and external post-acceptance + publishing are separate follow-ups, not guaranteed by the current deterministic checker. - Make one real-model customer-feedback workflow easy to repeat from the Console; use the [screenshot walkthrough](scenarios/customer-feedback-deepseek.md) as the current acceptance example. A private DeepSeek run passed; the public demo remains keyless. diff --git a/src/agentmesh/api/console_assets/app.js b/src/agentmesh/api/console_assets/app.js index 8f74ae1..74bae06 100644 --- a/src/agentmesh/api/console_assets/app.js +++ b/src/agentmesh/api/console_assets/app.js @@ -44,7 +44,7 @@ const state = { missionView: "board", missionSelectedId: null, missionPulses: [], missionFilter: storedMissionFilter(), missionReplay: { mode: "live", cursor: -1, playing: false, timer: null }, missionBookmarks: storedMissionBookmarks(), missionCamera: { zoom: 1, autoFit: true, layout: null, panning: null }, - token: sessionStorage.getItem("agentmesh-token") || "" + token: sessionStorage.getItem("agentmesh-token") || "", acceptanceDecision: null }; const $ = (id) => document.getElementById(id); function credentialOriginSafe() { return location.protocol === "https:" || ["localhost", "127.0.0.1", "::1"].includes(location.hostname); } @@ -1515,7 +1515,66 @@ function renderDetail() { $("cancel-button").disabled = terminal.has(task.status); if (state.missionView === "map") renderMissionMap(task); renderDag(task); renderRuns(task); renderPlanning(); renderContextTransfers(); renderActivityTimeline(); renderToolAudit(); renderTaskArtifacts(); - renderTaskResult(task); + renderTaskResult(task); renderDeliverableAcceptance(task); +} + +function renderDeliverableAcceptance(task) { + const acceptance = task.deliverable_acceptance; + const configured = acceptance && acceptance.status !== "NOT_CONFIGURED"; + $("deliverable-acceptance-panel").classList.toggle("hidden", !configured); + if (!configured) return; + $("deliverable-acceptance-status").textContent = t(`Acceptance: ${acceptance.status}`); + $("deliverable-acceptance-summary").textContent = t(acceptance.delivery_allowed + ? "Main output is eligible for delivery. Supporting outputs and attachments are not validated by this gate." + : "Main output delivery is blocked. Execution status is unchanged; review the findings below."); + $("deliverable-acceptance-checks").innerHTML = (acceptance.checks || []).map((check) => `
${escapeHtml(check.description || check.key)}${escapeHtml(t(`Check: ${check.status}`))}

${escapeHtml(check.reason || "")}

${t(check.required ? "Required check" : "Optional check")}
${t("Check evidence")}
${escapeHtml(JSON.stringify(check.evidence || {}, null, 2))}
`).join(""); + const canDecide = task.status === "COMPLETED" && featureEnabled("human_resolution") && Boolean(acceptance.policy_digest && acceptance.deliverable_digest); + for (const id of ["accept-deliverable", "reject-deliverable"]) $(id).classList.toggle("hidden", !canDecide); + $("download-accepted-deliverable").classList.toggle("hidden", acceptance.delivery_allowed !== true); + $("deliverable-acceptance-audit").classList.toggle("hidden", !acceptance.human_decision); + $("deliverable-acceptance-audit-content").textContent = acceptance.human_decision ? JSON.stringify(acceptance.human_decision, null, 2) : ""; +} + +function openDeliverableDecision(decision) { + const task = state.selected; const acceptance = task?.deliverable_acceptance; + if (task?.status !== "COMPLETED" || !featureEnabled("human_resolution") || !acceptance || acceptance.status === "NOT_CONFIGURED" || !acceptance.policy_digest || !acceptance.deliverable_digest || !["ACCEPT", "REJECT"].includes(decision)) return; + state.acceptanceDecision = { taskId: task.id, decision, expected_policy_digest: acceptance.policy_digest, expected_deliverable_digest: acceptance.deliverable_digest, requestId: clientRequestId(), submittedBody: null }; + $("deliverable-decision-form").reset(); $("deliverable-decision-error").textContent = ""; + $("deliverable-decision-title").textContent = t(decision === "ACCEPT" ? "Accept main deliverable" : "Reject main deliverable"); + $("deliverable-decision-submit").classList.toggle("danger", decision === "REJECT"); + $("deliverable-decision-submit").classList.toggle("primary", decision === "ACCEPT"); + $("deliverable-decision-dialog").showModal(); $("deliverable-decision-reason").focus(); +} + +async function submitDeliverableDecision(event) { + event.preventDefault(); const pinned = state.acceptanceDecision; + if (!pinned || !featureEnabled("human_resolution")) return; + const reason = $("deliverable-decision-reason").value.trim(); + if (!reason) { $("deliverable-decision-error").textContent = t("A decision reason is required."); return; } + const body = JSON.stringify({ decision: pinned.decision, reason, expected_policy_digest: pinned.expected_policy_digest, expected_deliverable_digest: pinned.expected_deliverable_digest }); + if (pinned.submittedBody && pinned.submittedBody !== body) pinned.requestId = clientRequestId(); + pinned.submittedBody = body; $("deliverable-decision-submit").disabled = true; $("deliverable-decision-error").textContent = ""; + try { + await api(`/api/v1/tasks/${encodeURIComponent(pinned.taskId)}/deliverable-acceptance/decision`, { method: "POST", headers: { "Idempotency-Key": pinned.requestId }, body }); + $("deliverable-decision-dialog").close(); state.acceptanceDecision = null; + await loadTask(pinned.taskId); toast(t("Deliverable decision recorded.")); + } catch (error) { + $("deliverable-decision-error").textContent = error.status === 401 || error.status === 403 ? t("Deliverable decisions require an authorized reviewer.") : error.message; + if (error.status === 409) await loadTask(pinned.taskId); + } finally { $("deliverable-decision-submit").disabled = false; } +} + +async function downloadAcceptedDeliverable() { + const task = state.selected; + if (!task?.deliverable_acceptance || task.deliverable_acceptance.status === "NOT_CONFIGURED" || task.deliverable_acceptance.delivery_allowed !== true) return; + $("download-accepted-deliverable").disabled = true; + try { + const deliverable = await api(`/api/v1/tasks/${encodeURIComponent(task.id)}/accepted-deliverable`); + const url = URL.createObjectURL(new Blob([JSON.stringify(deliverable, null, 2)], { type: "application/json" })); + const link = document.createElement("a"); link.href = url; link.download = `accepted-main-output-${task.id}.json`; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } catch (error) { toast(error.message, true); await loadTask(task.id); } + finally { $("download-accepted-deliverable").disabled = false; } } function resultSources(task) { @@ -2292,12 +2351,62 @@ function syncExecutionMode() { $("team-fields").disabled = !coordinated; $("concurrency-field").classList.toggle("hidden", !coordinated); $("max-concurrency").disabled = !coordinated; + $("acceptance-options").classList.toggle("hidden", !coordinated); + syncAcceptanceOptions(); $("execution-guidance").textContent = coordinated ? t("Split the goal into deliverables. Each item is pinned to a published employee; prerequisite work must finish first.") : $("execution-mode").value === "REVIEWED" ? t("A deterministic reviewer checks the result and can request bounded revisions. This is a review policy, not a second employee.") : t("Runs with the deployment default, or select a published employee for this task. Reviewed mode uses the deployment default."); } + +function syncAcceptanceOptions() { + const enabled = $("execution-mode").value === "COORDINATED" && $("acceptance-enabled").checked; + $("acceptance-enabled").disabled = $("execution-mode").value !== "COORDINATED"; + $("acceptance-fields").disabled = !enabled; + const rateEnabled = enabled && $("acceptance-rate-enabled").checked; + $("acceptance-rate-fields").disabled = !rateEnabled; + $("acceptance-rate-fields").classList.toggle("hidden", !rateEnabled); +} + +function buildDeliverableAcceptance(options, subtasks, primaryDeliverable) { + if (!options.enabled) return null; + const predecessors = new Set(subtasks.flatMap((item) => item.depends_on || [])); + const finalItems = subtasks.filter((item) => !predecessors.has(item.key)); + if (!primaryDeliverable && finalItems.length !== 1) throw new Error(t("Select a primary deliverable when acceptance has multiple final work items.")); + const paths = [...new Set(String(options.paths || "").split(/[\n,]/).map((path) => path.trim()).filter(Boolean))]; + if (paths.length + (options.rateEnabled ? 1 : 0) > 20) throw new Error(t("Use no more than 20 acceptance checks.")); + if (paths.some((path) => path.split(".").length > 16 || path.split(".").some((part) => !part.trim() || part.length > 128 || ["__proto__", "prototype", "constructor"].includes(part)))) throw new Error(t("Use valid dotted output paths without empty segments.")); + const checks = paths.map((path, index) => ({ key: `required-output-${index + 1}`, description: `${t("Required output path")}: ${path}`, kind: "OUTPUT_PATH_EXISTS", path: path.split("."), required: true })); + const facts = {}; + if (options.rateEnabled) { + const numeratorUnit = String(options.numeratorUnit || "").trim(); const denominatorUnit = String(options.denominatorUnit || "").trim(); + const scale = Number(options.scale); const threshold = Number(options.threshold); + if (!numeratorUnit || !denominatorUnit || numeratorUnit.length > 64 || denominatorUnit.length > 64 || !String(options.scale).trim() || !String(options.threshold).trim() || !Number.isFinite(scale) || scale <= 0 || scale > 10000 || !Number.isFinite(threshold) || threshold < 0 || threshold > 1e18 || !["LTE", "GTE"].includes(options.operator)) throw new Error(t("Set rate units, a positive scale, a comparison, and a numeric threshold.")); + for (const key of ["numerator", "denominator"]) { + const raw = String(options[key] ?? "").trim(); + if (!raw) continue; + const value = Number(raw); + if (!Number.isFinite(value) || value < 0 || value > 1e18 || (value !== 0 && value < 1e-18)) throw new Error(t("Rate facts must be non-negative numbers or left blank for unknown.")); + facts[key] = { value, unit: key === "numerator" ? numeratorUnit : denominatorUnit }; + } + checks.push({ key: "rate-threshold", description: t("Input fact rate threshold"), kind: "RATE_THRESHOLD", required: true, + numerator: { source: "TASK_INPUT", path: ["acceptance_facts", "numerator"], unit: numeratorUnit }, + denominator: { source: "TASK_INPUT", path: ["acceptance_facts", "denominator"], unit: denominatorUnit }, + scale, operator: options.operator, threshold, tolerance: 0.001 }); + const claimPath = String(options.claimPath || "").trim(); + if (claimPath) { + const path = claimPath.split("."); + if (path.length > 16 || path.some((part) => !part.trim() || part.length > 128 || ["__proto__", "prototype", "constructor"].includes(part))) throw new Error(t("Use valid dotted output paths without empty segments.")); + const claimUnit = `${numeratorUnit}/${denominatorUnit}`; + if (claimUnit.length > 64) throw new Error(t("The combined claimed-rate unit must not exceed 64 characters.")); + checks[checks.length - 1].claim = { source: "DELIVERABLE", path, unit: claimUnit }; + checks[checks.length - 1].description = t("Rate threshold and output claim consistency"); + } + } + if (!checks.length) throw new Error(t("Add at least one required output path or rate check.")); + return { policy: { require_human_review: Boolean(options.humanReview), checks }, facts }; +} function openCreate(mode = "DIRECT") { const option = [...$("execution-mode").options].find((item) => item.value === mode); if (option?.disabled) { switchView("setup"); toast("This execution mode needs server setup. See Workspace setup.", true); return; } @@ -2331,6 +2440,16 @@ async function createTask(event) { const primaryDeliverable = mode === "COORDINATED" ? $("primary-deliverable").value : ""; if (primaryDeliverable && !subtasks.some((item) => item.key === primaryDeliverable && !subtasks.some((other) => other.depends_on.includes(item.key)))) { $("form-error").textContent = t("Primary deliverable must be a final work item."); return; } const input = {}; if (materials) input.materials = materials; if (expected) input.expected_output = expected; + let acceptance = null; + try { + acceptance = buildDeliverableAcceptance({ enabled: mode === "COORDINATED" && $("acceptance-enabled").checked, + paths: $("acceptance-paths").value, humanReview: $("acceptance-human-review").checked, rateEnabled: $("acceptance-rate-enabled").checked, + numerator: $("acceptance-numerator").value, numeratorUnit: $("acceptance-numerator-unit").value, + denominator: $("acceptance-denominator").value, denominatorUnit: $("acceptance-denominator-unit").value, + scale: $("acceptance-scale").value, operator: $("acceptance-operator").value, threshold: $("acceptance-threshold").value, + claimPath: $("acceptance-claim-path").value }, subtasks, primaryDeliverable); + } catch (error) { $("form-error").textContent = error.message; return; } + if (acceptance && $("acceptance-rate-enabled").checked) input.acceptance_facts = acceptance.facts; if (successCriteria.length) input.success_criteria = successCriteria; if ($("task-use-memory").checked) { const policy = state.memorySetup?.policy; const companyId = state.memoryCompany?.company?.id; @@ -2338,6 +2457,7 @@ async function createTask(event) { input.company_context = { company_id: companyId, memory_policy_id: policy.id }; } const payload = { objective, input, execution_mode: mode, ...(mode === "REVIEWED" ? { max_revisions: 1 } : {}), ...(mode === "DIRECT" && $("direct-agent").value ? { preferred_agent_id: $("direct-agent").value } : {}), ...(mode === "COORDINATED" ? { subtasks, max_concurrency: Number($("max-concurrency").value), output_policy: primaryDeliverable ? { mode: "selected", primary_subtask_key: primaryDeliverable } : { mode: "auto" }, ...(successCriteria.length ? { goal: { success_criteria: successCriteria } } : {}) } : {}) }; + if (acceptance) payload.acceptance_policy = acceptance.policy; const maxRuns = $("budget-max-runs").value.trim(); const deadline = $("task-deadline").value; if (maxRuns || deadline) { if (!featureEnabled("budget_admission")) { $("form-error").textContent = "Budget admission is disabled. Remove the optional limits or enable the feature on the server."; return; } @@ -2366,6 +2486,7 @@ function renderTaskReview(payload, { mode, objective, expected, successCriteria, const budgetSummary = budgetParts.length ? budgetParts.join(" · ") : t("No extra run or deadline limit"); const deliverableSummary = mode === "COORDINATED" ? (payload.output_policy.primary_subtask_key ? subtasks.find((item) => item.key === payload.output_policy.primary_subtask_key)?.input.role : t("Automatic · final work items")) : t("Task result"); $("task-review-summary").innerHTML = `
${t("Goal")}
${escapeHtml(objective)}
${t("Expected output")}
${escapeHtml(expected || t("Not specified"))}
${t("Success conditions")}
${coordinatedNote}
${t("Execution")}
${escapeHtml(modeLabel)} · ${escapeHtml(assignment)}
${t("Primary deliverable")}
${escapeHtml(deliverableSummary || "")}
${t("Materials")}
${escapeHtml(payload.input.materials ? t("Materials included") : t("No additional materials"))}
${t("Company memory")}
${memorySummary}
${t("Optional limits")}
${escapeHtml(budgetSummary)}
`; + if (payload.acceptance_policy) $("task-review-summary").innerHTML += `
${t("Deliverable acceptance")}
${payload.acceptance_policy.checks.map((check) => escapeHtml(check.description)).join("; ")} · ${t(payload.acceptance_policy.require_human_review ? "Human approval required" : "Automated checks required")}
${t("Acceptance contract and input facts")}
${escapeHtml(JSON.stringify({ policy: payload.acceptance_policy, input_facts: payload.input.acceptance_facts || {} }, null, 2))}
`; } async function submitReviewedTask(runAfterCreate) { @@ -2405,6 +2526,9 @@ $("new-version-button").addEventListener("click", openVersionForm); $("agent-for $("artifact-form").addEventListener("submit", createArtifact); $("new-artifact-version-button").addEventListener("click", openArtifactVersionForm); $("artifact-version-form").addEventListener("submit", createArtifactVersion); $("close-artifact-preview").addEventListener("click", () => $("artifact-preview-panel").classList.add("hidden")); $("approve-approval-button").addEventListener("click", () => openDecision("approve")); $("reject-approval-button").addEventListener("click", () => openDecision("reject")); $("decision-form").addEventListener("submit", submitDecision); $("copy-permit-button").addEventListener("click", copySelectedPermit); $("add-role").addEventListener("click", () => addRole()); $("use-starter-team").addEventListener("click", useStarterTeam); $("create-form").addEventListener("submit", createTask); +$("acceptance-enabled").addEventListener("change", syncAcceptanceOptions); $("acceptance-rate-enabled").addEventListener("change", syncAcceptanceOptions); +$("accept-deliverable").addEventListener("click", () => openDeliverableDecision("ACCEPT")); $("reject-deliverable").addEventListener("click", () => openDeliverableDecision("REJECT")); +$("deliverable-decision-form").addEventListener("submit", submitDeliverableDecision); $("download-accepted-deliverable").addEventListener("click", downloadAcceptedDeliverable); $("company-template-form").addEventListener("submit", installCompanyTemplate); $("company-operations-form").addEventListener("submit", activateCompanyOperations); $("appoint-company-workforce").addEventListener("click", appointCompanyWorkforce); diff --git a/src/agentmesh/api/console_assets/i18n.js b/src/agentmesh/api/console_assets/i18n.js index 2b36e8d..946ff40 100644 --- a/src/agentmesh/api/console_assets/i18n.js +++ b/src/agentmesh/api/console_assets/i18n.js @@ -632,7 +632,67 @@ "Other final work items remain available as supporting deliverables. The execution supervisor remains in the audit record.": "其他终点工作项仍作为附加交付物展示。执行监督记录保留在审计信息中。", "Automatic · final work items": "自动 · 终点工作项", "Task result": "任务结果", - "Primary deliverable must be a final work item.": "主要交付物必须是终点工作项。" + "Primary deliverable must be a final work item.": "主要交付物必须是终点工作项。", + "DELIVERY GATE": "交付门槛", + "Deliverable acceptance": "交付物验收", + "Execution completion and deliverable acceptance are separate. These checks cover only the selected main output, not attached files.": "执行完成与交付物验收是两个独立状态。这些检查仅覆盖所选主要输出,不覆盖附件。", + "Main output is eligible for delivery. Supporting outputs and attachments are not validated by this gate.": "主要输出已允许交付。此门槛未验证辅助输出和附件。", + "Main output delivery is blocked. Execution status is unchanged; review the findings below.": "主要输出交付被阻止。执行状态不变;请查看下方检查结果。", + "Acceptance: NOT_READY": "验收:尚未就绪", + "Acceptance: PASSED": "验收:检查通过", + "Acceptance: FAILED": "验收:检查失败", + "Acceptance: NEEDS_REVIEW": "验收:需要人工审核", + "Acceptance: HUMAN_ACCEPTED": "验收:人工接受", + "Acceptance: HUMAN_REJECTED": "验收:人工拒绝", + "Check: PASS": "通过", + "Check: FAIL": "失败", + "Check: UNKNOWN": "未知", + "Required check": "必需检查", + "Optional check": "可选检查", + "Check evidence": "检查证据", + "Human decision audit": "人工决定审计", + "Accept main deliverable": "接受主要交付物", + "Reject main deliverable": "拒绝主要交付物", + "Download accepted main output": "下载已验收的主要输出", + "Advanced deliverable acceptance": "高级交付物验收", + "Off by default. Verify the main output against explicit checks before delivery. With multiple final work items, select a primary deliverable above.": "默认关闭。交付前按明确条件验证主要输出。如有多个终点工作项,请在上方选择主要交付物。", + "Enable deliverable checks": "启用交付物检查", + "Required output paths": "必需的输出路径", + "One dotted path per line": "每行一个点分隔路径", + "Require human approval after checks": "检查后仍需人工批准", + "Add a rate threshold check": "添加比率阈值检查", + "Rate = numerator / denominator × scale. Enter verified input facts; missing values stay unknown. Without an output claim path, this does not verify a numeric claim in the model output.": "比率 = 分子 / 分母 × 倍率。请填写已核实的输入事实;缺失值保持未知。未指定输出声明路径时,此检查不验证模型输出中的数值声明。", + "Numerator value": "分子数值", + "Numerator unit": "分子单位", + "Denominator value": "分母数值", + "Denominator unit": "分母单位", + "Unknown": "未知", + "Rate scale": "比率倍率", + "Comparison": "比较方式", + "At most (≤)": "不超过(≤)", + "At least (≥)": "不低于(≥)", + "Threshold": "阈值", + "Review main deliverable": "审核主要交付物", + "This is a human override for the pinned policy and main output. Automated findings remain in the audit record; attached files are not included.": "此人工决定针对固定的策略和主要输出。自动检查结果仍保留在审计记录中;附件不在验收范围内。", + "Decision reason": "决定理由", + "Submit decision": "提交决定", + "A decision reason is required.": "必须填写决定理由。", + "Deliverable decision recorded.": "交付物决定已记录。", + "Deliverable decisions require an authorized reviewer.": "交付物决定需要有权限的审核者。", + "Select a primary deliverable when acceptance has multiple final work items.": "验收存在多个终点工作项时,必须选择主要交付物。", + "Use valid dotted output paths without empty segments.": "请使用有效的点分隔输出路径,不能包含空段。", + "Required output path": "必需输出路径", + "Set rate units, a positive scale, a comparison, and a numeric threshold.": "请填写比率单位、正数倍率、比较方式和数值阈值。", + "Rate facts must be non-negative numbers or left blank for unknown.": "比率事实必须是非负数;留空表示未知。", + "Input fact rate threshold": "输入事实比率阈值", + "Add at least one required output path or rate check.": "请至少添加一个必需输出路径或比率检查。", + "Human approval required": "需要人工批准", + "Automated checks required": "需要通过自动检查", + "Acceptance contract and input facts": "验收契约与输入事实", + "Claimed rate output path (optional)": "输出中声明比率的路径(可选)", + 'Leave blank to check input facts only. To compare the output claim, use a dotted path whose value is a typed quantity, for example {"value":12,"unit":"ticket/buyer","scale":100}. Units and scale must match the contract. This checks numeric consistency, not the meaning or truth of the report.': '留空表示仅检查输入事实。要比较输出声明,请填写点分隔路径,其值须为带单位的数量,例如 {"value":12,"unit":"ticket/buyer","scale":100}。单位和倍率必须与契约一致。此项检查数值一致性,不判断报告语义或事实真实性。', + "The combined claimed-rate unit must not exceed 64 characters.": "声明比率的组合单位不能超过 64 个字符。", + "Rate threshold and output claim consistency": "比率阈值与输出声明一致性" }; const bindings = []; diff --git a/src/agentmesh/api/console_assets/index.html b/src/agentmesh/api/console_assets/index.html index 1be1899..d23f23a 100644 --- a/src/agentmesh/api/console_assets/index.html +++ b/src/agentmesh/api/console_assets/index.html @@ -8,8 +8,8 @@ - - + +
@@ -144,6 +144,14 @@

执行结果

运行记录

最新优先
+