feat(tasks): enforce dynamic execution plans

This commit is contained in:
2026-09-24 16:36:12 +08:00
parent 485ceff7e3
commit e0d37f9da9
13 changed files with 1942 additions and 17 deletions
+649 -2
View File
@@ -2327,6 +2327,40 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/access-requests": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Request Operational Task Access */
post: operations["post_operational_tasks_task_id_access_requests"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/access-requests/{access_request_id}/decision": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Decide Operational Task Access */
post: operations["post_operational_tasks_task_id_access_requests_access_request_id_decision"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/actions": {
parameters: {
query?: never;
@@ -2344,6 +2378,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/actions/{action_id}/confirm-dispatch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Confirm Operational Work Order Dispatch */
post: operations["post_operational_tasks_task_id_actions_action_id_confirm_dispatch"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/actions/{action_id}/decision": {
parameters: {
query?: never;
@@ -2361,6 +2412,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/actions/{action_id}/draft": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
/** Update Operational Work Order Draft */
patch: operations["patch_operational_tasks_task_id_actions_action_id_draft"];
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/actions/{action_id}/execute": {
parameters: {
query?: never;
@@ -2616,6 +2684,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/operational-tasks/{task_id}/withdraw-external-source": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Withdraw Operational Task External Source */
post: operations["post_operational_tasks_task_id_withdraw_external_source"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/patterns": {
parameters: {
query?: never;
@@ -5983,6 +6068,8 @@ export interface components {
condition?: string | null;
/** From */
from: string;
/** Id */
id?: string | null;
/** To */
to: string;
};
@@ -5996,9 +6083,9 @@ export interface components {
reason: string;
/**
* Schema
* @constant
* @enum {string}
*/
schema: "execution-plan@1";
schema: "execution-plan@1" | "execution-plan@2";
/** Title */
title: string;
};
@@ -6010,13 +6097,21 @@ export interface components {
detail: string;
/** Evidence Refs */
evidence_refs?: string[];
/** Gate Ref */
gate_ref?: string | null;
/** Id */
id: string;
/** Kind */
kind?: ("activity" | "decision" | "parallel_split" | "parallel_join" | "gate") | null;
/**
* Owner
* @enum {string}
*/
owner: "agent" | "human";
/** Revisits Node Id */
revisits_node_id?: string | null;
/** Selected Edge Id */
selected_edge_id?: string | null;
/**
* Status
* @enum {string}
@@ -6103,6 +6198,16 @@ export interface components {
source_revision: number;
task: components["schemas"]["OperationalTaskDetail"];
};
/** ExternalSourceWithdrawalCommand */
ExternalSourceWithdrawalCommand: {
/**
* Request Id
* Format: uuid
*/
request_id: string;
/** Source Event Id */
source_event_id: string;
};
/** ExternalWorkOrderSnapshot */
ExternalWorkOrderSnapshot: {
/** Activity */
@@ -6230,6 +6335,10 @@ export interface components {
decided_by?: string | null;
/** Decision Comment */
decision_comment?: string | null;
/** Edited At */
edited_at?: string | null;
/** Edited By */
edited_by?: string | null;
/** Evidence Refs */
evidence_refs: string[];
/** Executed At */
@@ -6552,6 +6661,8 @@ export interface components {
};
/** OperationalTaskDetail */
OperationalTaskDetail: {
/** Access Requests */
access_requests?: components["schemas"]["TaskAccessRequest"][];
/** Actions */
actions: components["schemas"]["HumanApprovalAction"][];
active_run?: components["schemas"]["OperationalTaskRun"] | null;
@@ -6644,6 +6755,12 @@ export interface components {
* @enum {string}
*/
kind: "task_created" | "action_pending" | "report_updated" | "task_completed" | "task_failed" | "run_delivery_degraded" | "access_approval_pending" | "action_approval_pending" | "human_input_required" | "external_coordination_failed";
/**
* Lifecycle Status
* @default active
* @enum {string}
*/
lifecycle_status: "active" | "resolved";
/**
* Notification Id
* Format: uuid
@@ -6655,11 +6772,23 @@ export interface components {
read_by?: string | null;
/** Required Permission */
required_permission?: string | null;
/** Resolution Reason */
resolution_reason?: string | null;
/** Resolved At */
resolved_at?: string | null;
/**
* Status
* @enum {string}
*/
status: "unread" | "read";
/** Subject Id */
subject_id?: string | null;
/**
* Subject Type
* @default task
* @enum {string}
*/
subject_type: "task" | "run" | "access_request" | "action";
/**
* Task Id
* Format: uuid
@@ -6694,6 +6823,11 @@ export interface components {
};
/** OperationalTaskPermissions */
OperationalTaskPermissions: {
/**
* Can Decide Access
* @default false
*/
can_decide_access: boolean;
/** Can Decide Commands */
can_decide_commands: boolean;
/** Can Decide Work Orders */
@@ -7536,6 +7670,82 @@ export interface components {
/** Source Fid */
source_fid: number;
};
/** TaskAccessRequest */
TaskAccessRequest: {
/**
* Access Request Id
* Format: uuid
*/
access_request_id: string;
/** Approved Scopes */
approved_scopes: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[];
/** Comment */
comment?: string | null;
/** Decided At */
decided_at?: string | null;
/** Decided By */
decided_by?: string | null;
/** Node Id */
node_id: string;
/** Reason */
reason: string;
/**
* Requested At
* Format: date-time
*/
requested_at: string;
/** Requested By */
requested_by: string;
/** Scopes */
scopes: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[];
/**
* Status
* @enum {string}
*/
status: "pending" | "approved" | "rejected";
/**
* Task Id
* Format: uuid
*/
task_id: string;
/** Version */
version: number;
};
/** TaskAccessRequestCreate */
TaskAccessRequestCreate: {
/** Node Id */
node_id: string;
/** Reason */
reason: string;
/**
* Request Id
* Format: uuid
*/
request_id: string;
/** Scopes */
scopes: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[];
/** Source Session Id */
source_session_id: string;
};
/** TaskAccessRequestDecision */
TaskAccessRequestDecision: {
/** Approved Scopes */
approved_scopes?: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[];
/** Comment */
comment?: string | null;
/**
* Decision
* @enum {string}
*/
decision: "approved" | "rejected";
/** Expected Version */
expected_version: number;
/**
* Request Id
* Format: uuid
*/
request_id: string;
};
/** TaskAlertCommand */
TaskAlertCommand: {
/** Alert Id */
@@ -7650,6 +7860,8 @@ export interface components {
};
/** TaskSource */
TaskSource: {
/** Deleted At */
deleted_at?: string | null;
/** Event Kind */
event_kind?: string | null;
/** External Id */
@@ -7742,6 +7954,27 @@ export interface components {
/** Title */
title: string;
};
/** WorkOrderConfirmDispatchCommand */
WorkOrderConfirmDispatchCommand: {
/** Expected Version */
expected_version: number;
/**
* Request Id
* Format: uuid
*/
request_id: string;
};
/** WorkOrderDraftUpdateCommand */
WorkOrderDraftUpdateCommand: {
/** Expected Version */
expected_version: number;
payload: components["schemas"]["CreateWorkOrderPayload"];
/**
* Request Id
* Format: uuid
*/
request_id: string;
};
/** WorkbenchAlertSource */
WorkbenchAlertSource: {
/** Run Id */
@@ -19772,6 +20005,171 @@ export interface operations {
};
};
};
post_operational_tasks_task_id_access_requests: {
parameters: {
query?: never;
header: {
"X-Project-Id": string;
};
path: {
task_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TaskAccessRequestCreate"];
};
};
responses: {
/** @description Successful Response */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TaskAccessRequest"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Insufficient permission */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Validation error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Dependency unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
post_operational_tasks_task_id_access_requests_access_request_id_decision: {
parameters: {
query?: never;
header: {
"X-Project-Id": string;
};
path: {
task_id: string;
access_request_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TaskAccessRequestDecision"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TaskAccessRequest"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Insufficient permission */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Validation error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Dependency unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
post_operational_tasks_task_id_actions: {
parameters: {
query?: never;
@@ -19854,6 +20252,90 @@ export interface operations {
};
};
};
post_operational_tasks_task_id_actions_action_id_confirm_dispatch: {
parameters: {
query?: never;
header: {
Authorization?: string | null;
"X-Project-Id": string;
};
path: {
task_id: string;
action_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["WorkOrderConfirmDispatchCommand"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HumanApprovalAction"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Insufficient permission */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Validation error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Dependency unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
post_operational_tasks_task_id_actions_action_id_decision: {
parameters: {
query?: never;
@@ -19937,6 +20419,89 @@ export interface operations {
};
};
};
patch_operational_tasks_task_id_actions_action_id_draft: {
parameters: {
query?: never;
header: {
"X-Project-Id": string;
};
path: {
task_id: string;
action_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["WorkOrderDraftUpdateCommand"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HumanApprovalAction"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Insufficient permission */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Validation error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Dependency unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
post_operational_tasks_task_id_actions_action_id_execute: {
parameters: {
query?: never;
@@ -21171,6 +21736,88 @@ export interface operations {
};
};
};
post_operational_tasks_task_id_withdraw_external_source: {
parameters: {
query?: never;
header: {
"X-Project-Id": string;
};
path: {
task_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ExternalSourceWithdrawalCommand"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["OperationalTaskDetail"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Insufficient permission */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Validation error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Dependency unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
get_patterns: {
parameters: {
query?: {
+26 -1
View File
@@ -151,6 +151,12 @@ export const humanTaskEvidenceCommandSchema = z.object({
export type TaskEvidence = z.infer<typeof taskEvidenceSchema>;
export type TaskReport = z.infer<typeof taskReportSchema>;
export type OperationalTaskDataQuery = z.infer<typeof operationalTaskDataQuerySchema>;
export const expressAccessRequestSchema = z.object({
requestId: z.string().uuid(),
nodeId: z.string().min(1),
scopes: z.array(taskDataSourceSchema).min(1).max(7),
reason: z.string().min(1),
});
export type HumanTaskEvidenceCommand = z.infer<typeof humanTaskEvidenceCommandSchema>;
export type OperationalTaskContext = Record<string, unknown>;
@@ -354,6 +360,18 @@ export class OperationalTaskClient {
return { evidence, snapshot };
}
async requestDataAccess(runtimeSessionId: string, taskId: string, input: {
requestId: string; sourceSessionId: string; nodeId: string; scopes: OperationalTaskDataQuery["source"][]; reason: string;
}) {
return this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/access-requests`, {
method: "POST",
body: JSON.stringify({
request_id: input.requestId, source_session_id: input.sourceSessionId,
node_id: input.nodeId, scopes: input.scopes, reason: input.reason,
}),
});
}
async proposeAction(runtimeSessionId: string, taskId: string, input: ProposedTaskAction) {
const result = await this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/actions`, {
method: "POST",
@@ -475,7 +493,7 @@ const toServerPlan = (input: Record<string, unknown>) => {
const nodes = Array.isArray(input.nodes) ? input.nodes : [];
const edges = Array.isArray(input.edges) ? input.edges : [];
return {
schema: "execution-plan@1",
schema: input.schema === "execution-plan@2" ? "execution-plan@2" : "execution-plan@1",
title: input.title,
reason: input.reason,
nodes: nodes.map((value) => {
@@ -488,6 +506,12 @@ const toServerPlan = (input: Record<string, unknown>) => {
detail: node.detail,
evidence_refs: Array.isArray(node.evidence) ? node.evidence : [],
action_refs: Array.isArray(node.action_refs) ? node.action_refs : [],
...(input.schema === "execution-plan@2" ? {
kind: node.kind,
selected_edge_id: node.selected_edge_id ?? null,
gate_ref: node.gate_ref ?? null,
revisits_node_id: node.revisits_node_id ?? null,
} : {}),
};
}),
edges: edges.map((value) => {
@@ -496,6 +520,7 @@ const toServerPlan = (input: Record<string, unknown>) => {
from: edge.from,
to: edge.to,
...(typeof edge.label === "string" && edge.label.trim() ? { condition: edge.label } : {}),
...(input.schema === "execution-plan@2" ? { id: edge.id } : {}),
};
}),
};
+2 -2
View File
@@ -33,8 +33,8 @@ export const buildAgentTriggerPrompt = (trigger: AgentTriggerContext) => [
event_id: trigger.trigger_id,
})}</external_work_order>`,
"这是合成演练数据,不得表述为生产事实。",
"开始调查前调用 publish_execution_plan 发布本次调查的结构化计划,明确并行依赖和条件分支;执行前、取得结果后、遇到阻塞或修改计划时再次发布完整快照。保留节点 ID、已完成节点及证据,重试用新节点,不要按时间推算进度。",
"根据这张工单的事实动态规划本次调查,不套用预设路线。开始调查前调用 publish_execution_plan 发布 execution-plan@2,明确路线选择、并行分叉与汇合、人工审核点;执行前、取得结果后、遇到阻塞或修改计划时再次发布完整快照。保留节点 ID、已完成节点及证据,重试用新节点,不要按时间推算进度。",
"每个完成节点必须列明实际依据。预设证据标注为合成场景输入,不得声称执行过未调用的数据查询工具。现场采样、清洗、复测、回访等人工工作保持 waiting,写清等待内容,不得因回复结束标记完成。",
"外围样板随事件提供的合成场景输入(不是 Agent 实时查询结果):上游三个模拟水质监测点的数据质量均为 valid,演示指标均在场景阈值内,且相邻片区没有同类投诉;投诉集中在演示小区,二次供水水箱最近一次清洗记录待核验。",
"请按‘上游数据质量正常、上游水质未见异常 → 疑似二次供水环节问题(待现场核验)’形成阶段性判断,并给出处置、复测和回访建议。不得将疑似判断写成已确认事故。",
"依据实际取得的证据判断可能原因,区分合成场景输入、已查询数据和待现场核验事项;给出处置、复测和回访建议。不得将疑似判断写成已确认事故。",
].join("\n");
+1 -1
View File
@@ -105,7 +105,7 @@ export const registerChatSessionRoutes = (
body: JSON.stringify({
session_id: record.sessionId,
model,
message: `执行工况任务${workflow ? `:${workflow}` : ""}。先读取后端任务上下文,再按任务类型更新计划、证据和报告。`,
message: `执行工况任务${workflow ? `:${workflow}` : ""}。先读取后端任务上下文,依据当前工单或人工请求动态规划 execution-plan@2;按实际选择的路线更新计划、证据和报告,数据权限不足时先申请并等待审核。`,
}),
}).then(async (response) => {
const reader = response.body?.getReader();
+52 -3
View File
@@ -2,6 +2,7 @@ import { z } from "zod";
const id = z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
export const executionPlanSchema = z.object({
schema: z.enum(["execution-plan@1", "execution-plan@2"]).default("execution-plan@1"),
title: z.string().trim().min(1).max(120),
reason: z.string().trim().min(1).max(1000),
nodes: z.array(z.object({
@@ -12,18 +13,27 @@ export const executionPlanSchema = z.object({
detail: z.string().max(2000),
evidence: z.array(z.string().min(1).max(1000)).max(12),
action_refs: z.array(z.string().uuid()).max(12).default([]),
kind: z.enum(["activity", "decision", "parallel_split", "parallel_join", "gate"]).optional(),
selected_edge_id: id.nullable().optional(),
gate_ref: z.string().uuid().nullable().optional(),
revisits_node_id: id.nullable().optional(),
})).min(1).max(40),
edges: z.array(z.object({ from: id, to: id, label: z.string().max(120) })).max(80),
edges: z.array(z.object({ id: id.optional(), from: id, to: id, label: z.string().max(120) })).max(80),
}).superRefine((plan, context) => {
const ids = new Set(plan.nodes.map((node) => node.id));
if (ids.size !== plan.nodes.length) context.addIssue({ code: "custom", message: "Duplicate node ID" });
const edges = new Set<string>();
const edgeIds = new Set<string>();
for (const edge of plan.edges) {
const key = `${edge.from}:${edge.to}`;
if (!ids.has(edge.from) || !ids.has(edge.to) || edge.from === edge.to || edges.has(key)) {
context.addIssue({ code: "custom", message: "Invalid or duplicate dependency" });
}
edges.add(key);
if (plan.schema === "execution-plan@2") {
if (!edge.id || edgeIds.has(edge.id)) context.addIssue({ code: "custom", message: "V2 edges require unique IDs" });
if (edge.id) edgeIds.add(edge.id);
}
}
const visiting = new Set<string>();
const visited = new Set<string>();
@@ -37,6 +47,35 @@ export const executionPlanSchema = z.object({
return false;
};
if (plan.nodes.some((node) => visit(node.id))) context.addIssue({ code: "custom", message: "Dependencies must be acyclic; use a new node for a retry" });
if (plan.schema === "execution-plan@2") {
if (plan.nodes.filter((node) => !plan.edges.some((edge) => edge.to === node.id)).length !== 1) {
context.addIssue({ code: "custom", message: "V2 plan requires one start node" });
}
const nodeById = new Map(plan.nodes.map((node) => [node.id, node]));
for (const node of plan.nodes) {
const outgoing = plan.edges.filter((edge) => edge.from === node.id);
const incoming = plan.edges.filter((edge) => edge.to === node.id);
if (!node.kind) context.addIssue({ code: "custom", message: "V2 nodes require kind" });
if (node.kind === "decision") {
if (outgoing.length < 2 || outgoing.some((edge) => !edge.label.trim())) context.addIssue({ code: "custom", message: "Decision routes must be labeled" });
if (node.selected_edge_id && !outgoing.some((edge) => edge.id === node.selected_edge_id)) context.addIssue({ code: "custom", message: "Selected route must leave its decision" });
if (node.status === "completed" && !node.selected_edge_id) context.addIssue({ code: "custom", message: "Completed decision requires a selected route" });
if (node.selected_edge_id) {
for (const edge of outgoing) {
const targetStatus = nodeById.get(edge.to)?.status;
if (edge.id === node.selected_edge_id && ["skipped", "cancelled"].includes(targetStatus ?? "")) context.addIssue({ code: "custom", message: "Selected route cannot be skipped" });
if (edge.id !== node.selected_edge_id && ["running", "completed", "waiting"].includes(targetStatus ?? "")) context.addIssue({ code: "custom", message: "Unchosen route cannot be active" });
}
}
} else if (node.selected_edge_id) context.addIssue({ code: "custom", message: "Only decisions select routes" });
if (node.kind !== "decision" && node.kind !== "parallel_split" && outgoing.length > 1) context.addIssue({ code: "custom", message: "Only decisions or parallel splits may branch" });
if (node.kind === "parallel_split" && outgoing.length < 2) context.addIssue({ code: "custom", message: "Parallel split requires two routes" });
if (node.kind === "parallel_join" && incoming.length < 2) context.addIssue({ code: "custom", message: "Parallel join requires two inputs" });
if (node.kind === "gate" && !node.gate_ref) context.addIssue({ code: "custom", message: "Gate requires a persisted reference" });
if (node.kind !== "gate" && node.gate_ref) context.addIssue({ code: "custom", message: "Only gates reference approvals" });
if (node.revisits_node_id && !ids.has(node.revisits_node_id)) context.addIssue({ code: "custom", message: "Revisited node must exist" });
}
}
for (const node of plan.nodes) {
if (node.status === "running" || node.status === "completed") {
const hasIncompletePredecessor = plan.edges
@@ -50,11 +89,21 @@ export const executionPlanSchema = z.object({
message: "Running or completed nodes require completed or skipped predecessors",
});
}
if (node.status === "completed" && node.evidence.length === 0 && node.action_refs.length === 0) context.addIssue({ code: "custom", message: "Completed nodes require evidence or an approved action receipt" });
if (node.status === "completed" && node.evidence.length === 0 && node.action_refs.length === 0
&& !(plan.schema === "execution-plan@2" && ["parallel_split", "parallel_join", "gate"].includes(node.kind ?? ""))) {
context.addIssue({ code: "custom", message: "Completed nodes require evidence or an approved action receipt" });
}
if ((node.status === "waiting" || node.status === "failed") && !node.detail.trim()) context.addIssue({ code: "custom", message: "Waiting or failed nodes require a reason" });
}
});
// Stored v1 plans remain readable, but every newly published task plan must
// opt into the graph and approval-gate guarantees of v2.
export const newExecutionPlanSchema = executionPlanSchema.refine(
(plan) => plan.schema === "execution-plan@2",
{ message: "New task execution plans must use execution-plan@2", path: ["schema"] },
);
export const executionPlanNeedsTaskPublish = (input: unknown) =>
!(input && typeof input === "object" && (input as Record<string, unknown>).server_published === true);
@@ -84,5 +133,5 @@ export function createExecutionPlanSnapshot(input: unknown, messages: unknown[],
}
}
}
return { ...plan, schema_version: "execution-plan@1", session_id: sessionId, revision: history.length + 1, updated_at: new Date().toISOString() };
return { ...plan, schema_version: plan.schema, session_id: sessionId, revision: history.length + 1, updated_at: new Date().toISOString() };
}
+23
View File
@@ -19,6 +19,7 @@ import { getRuntimeSessionContext } from "./runtime/sessionContext.js";
import {
operationalTaskClient,
operationalTaskDataQuerySchema,
expressAccessRequestSchema,
humanTaskEvidenceCommandSchema,
proposedTaskActionSchema,
taskArtifactBridgeSchema,
@@ -153,6 +154,28 @@ app.post("/internal/operational-task/data-query", async (req, res) => {
}
});
app.post("/internal/operational-task/access-requests", async (req, res) => {
const bridge = readOperationalTaskBridgeContext(req);
if (!bridge || bridge.context.purpose !== "task_execution") {
res.status(403).json({ message: "task execution context required" });
return;
}
const parsed = expressAccessRequestSchema.safeParse(req.body?.request);
if (!parsed.success) {
res.status(400).json({ message: "invalid access request" });
return;
}
try {
res.json(await operationalTaskClient.requestDataAccess(bridge.runtimeSessionId, bridge.taskId, {
...parsed.data, sourceSessionId: bridge.context.clientSessionId,
}));
} catch (error) {
const status = error && typeof error === "object" && "status" in error && typeof error.status === "number"
? error.status : 502;
res.status(status).json({ message: error instanceof Error ? error.message : "access request failed" });
}
});
app.post("/internal/operational-task/actions", async (req, res) => {
const bridge = readOperationalTaskBridgeContext(req);
if (!bridge) {