吸收数字员工入口路由干预回写

This commit is contained in:
baiyanyun
2026-06-11 18:31:41 +08:00
parent d292c34579
commit 4c862ef4b7
16 changed files with 1151 additions and 191 deletions

View File

@@ -30,6 +30,9 @@ import type {
CreatePlanRequest,
RouteDecisionTimelineResponse,
RouteDecisionPolicyPullResult,
RouteInterventionDecisionRequest,
RouteInterventionDecisionResponse,
RouteInterventionTimelineResponse,
IngressRouteRequest,
IngressRouteResponse,
GovernanceCommandRequest,
@@ -496,6 +499,20 @@ export function getRouteDecisionTimeline(): Promise<RouteDecisionTimelineRespons
return apiFetch<RouteDecisionTimelineResponse>('/api/route-decisions');
}
export function getRouteInterventions(): Promise<RouteInterventionTimelineResponse> {
return apiFetch<RouteInterventionTimelineResponse>('/api/route-decisions/interventions');
}
export function decideRouteIntervention(
routeDecisionId: string,
input: RouteInterventionDecisionRequest,
): Promise<RouteInterventionDecisionResponse> {
return apiFetch<RouteInterventionDecisionResponse>(`/api/route-decisions/${encodeURIComponent(routeDecisionId)}/intervention`, {
method: 'POST',
body: JSON.stringify(input),
});
}
export function routeIngress(
body: IngressRouteRequest,
): Promise<IngressRouteResponse> {

View File

@@ -42,6 +42,10 @@ import type {
RouteDecisionPolicyPullResult,
RouteDecisionRecord,
RouteDecisionTimelineResponse,
RouteInterventionDecisionRequest,
RouteInterventionDecisionResponse,
RouteInterventionRecord,
RouteInterventionTimelineResponse,
SkillSpec,
UserNotification,
} from '@/types/api';
@@ -78,7 +82,9 @@ declare global {
dispatchPlanReadySteps?: (planId: string) => Promise<ReadyStepDispatchResult>;
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
listRouteInterventions?: (options?: { limit?: number }) => Promise<RouteInterventionRecord[]>;
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
recordRouteInterventionResolution?: (input: QimingclawRouteInterventionResolutionInput) => Promise<RouteInterventionRecord>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
accessArtifact?: (artifactId: string, action: 'preview' | 'download' | 'open_location', options?: { actor?: string | null; source?: string | null }) => Promise<QimingclawArtifactAccessResponse>;
@@ -377,6 +383,7 @@ interface QimingclawRouteDecisionInput {
}
interface QimingclawRouteDecisionPolicyInput {
routeDecisionId?: string | null;
routeKind?: string | null;
intentKind?: string | null;
reasonCodes?: string[] | null;
@@ -408,6 +415,30 @@ interface QimingclawRouteDecisionPolicyResult {
error?: string | null;
}
interface QimingclawRouteInterventionResolutionInput {
routeDecisionId: string;
approvalId?: string | null;
decision: string;
createdCommandId?: string | null;
commandAccepted?: boolean | null;
commandError?: string | null;
blockedReason?: string | null;
policyResult?: string | null;
policySource?: string | null;
matchedIntent?: string | null;
routeKind?: string | null;
intentKind?: string | null;
reasonCodes?: string[] | null;
candidateSkills?: string[] | null;
contextRefs?: Record<string, unknown> | null;
entrypoint?: string | null;
actor?: string | null;
correlationId?: string | null;
message?: string | null;
payload?: Record<string, unknown> | null;
occurredAt?: string | null;
}
interface QimingclawRiskPolicyInput {
actionKind?: string | null;
actionLabel?: string | null;
@@ -903,6 +934,17 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'GET' && pathname === '/api/route-decisions') {
return await readRouteDecisions(Number(url.searchParams.get('limit')) || 80) as T;
}
if (method === 'GET' && pathname === '/api/route-decisions/interventions') {
return await readRouteInterventions(Number(url.searchParams.get('limit')) || 80) as T;
}
const routeInterventionMatch = pathname.match(/^\/api\/route-decisions\/([^/]+)\/intervention$/);
if (method === 'POST' && routeInterventionMatch) {
return await handleRouteInterventionDecision(
decodeURIComponent(routeInterventionMatch[1] || ''),
parseJsonBody<RouteInterventionDecisionRequest>(options.body),
await readQimingclawSnapshot(),
) as T;
}
if (method === 'POST' && pathname === '/api/ingress/route') {
return await handleIngressRoute(parseJsonBody<IngressRouteRequest>(options.body), await readQimingclawSnapshot()) as T;
}
@@ -1251,6 +1293,9 @@ async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: num
}
try {
const result = await bridge.pullApprovalUpdates();
if (result?.ok !== false) {
await resolveApprovedRouteInterventions(await readQimingclawSnapshot());
}
return {
ok: result?.ok !== false,
applied: Number(result?.applied ?? 0),
@@ -1263,6 +1308,24 @@ async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: num
}
}
async function resolveApprovedRouteInterventions(snapshot: QimingclawSnapshot | null): Promise<void> {
const timeline = await readRouteInterventions(120);
const approved = timeline.interventions.filter((item) => (
item.intervention_status === 'approved'
&& item.approval_id
&& !item.resolved_at
)).slice(0, 5);
for (const intervention of approved) {
await handleRouteInterventionDecision(intervention.route_decision_id, {
approval_id: intervention.approval_id,
decision: 'approved',
actor: 'management-api',
}, snapshot).catch((error) => {
console.warn('[qimingclawAdapter] auto route intervention resolution failed:', error);
});
}
}
async function evaluateBridgeRiskPolicy(input: QimingclawRiskPolicyInput): Promise<QimingclawRiskPolicyResult> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.evaluateRiskPolicy) {
@@ -3228,12 +3291,19 @@ function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): Digital
const rows = routeDecisionRows(snapshot);
const latest = rows[0] ?? null;
const policy = routeDecisionPolicy(snapshot?.uiState ?? readState());
const resolutionEvents = runtimeEvents(snapshot).filter((event) => event.kind === 'route_decision_intervention_resolved');
const resolvedRouteIds = new Set(resolutionEvents.map((event) => stringValue(asRecord(event.payload)?.route_decision_id)).filter(Boolean));
const latestResolution = resolutionEvents.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt))[0] ?? null;
const latestResolutionPayload = asRecord(latestResolution?.payload) ?? null;
return {
total: rows.length,
mapped_count: rows.filter((row) => stringValue(row.created_command_id)).length,
attention_count: rows.filter((row) => ['warn', 'error'].includes(stringValue(row.attention_level))).length,
blocked_count: rows.filter((row) => stringValue(row.policy_result) === 'blocked').length,
approval_required_count: rows.filter((row) => stringValue(row.policy_result) === 'approval_required').length,
pending_intervention_count: rows.filter((row) => stringValue(row.policy_result) !== 'accepted' && !resolvedRouteIds.has(stringValue(row.route_decision_id))).length,
restored_count: resolutionEvents.filter((event) => asRecord(event.payload)?.command_accepted === true).length,
latest_intervention_status: stringValue(latestResolutionPayload?.status ?? latestResolutionPayload?.decision) || null,
policy_source: policy.source ?? null,
policy_last_pulled_at: policy.last_pulled_at ?? null,
policy_last_pull_error: policy.last_pull_error ?? null,
@@ -5846,6 +5916,224 @@ async function readRouteDecisions(limit = 80): Promise<RouteDecisionTimelineResp
}
}
async function readRouteInterventions(limit = 80): Promise<RouteInterventionTimelineResponse> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.listRouteInterventions) {
return { has_route_intervention_state: false, interventions: [] };
}
try {
const interventions = await bridge.listRouteInterventions({ limit });
return { has_route_intervention_state: true, interventions: interventions ?? [] };
} catch (error) {
console.warn('[qimingclawAdapter] list route interventions failed:', error);
return { has_route_intervention_state: false, interventions: [] };
}
}
async function handleRouteInterventionDecision(
routeDecisionId: string,
body: RouteInterventionDecisionRequest,
snapshot: QimingclawSnapshot | null,
): Promise<RouteInterventionDecisionResponse> {
const decisionText = stringValue(body.decision) || 'reject';
const approvalId = stringValue(body.approval_id) || routeInterventionApprovalId(routeDecisionId, snapshot);
const existing = await readRouteInterventions(160);
const existingRecord = existing.interventions.find((item) => item.route_decision_id === routeDecisionId) ?? null;
const detail = routeDecisionDetail(routeDecisionId, snapshot);
const routeDecision = detail?.decision ?? routeInterventionToRouteDecision(existingRecord, routeDecisionId);
if (!routeDecision) {
const intervention = await recordRouteInterventionResolution({
routeDecisionId,
approvalId,
decision: decisionText,
commandAccepted: false,
commandError: 'route_intervention_missing_context',
actor: body.actor ?? 'digital_employee_operator',
occurredAt: new Date().toISOString(),
});
return { accepted: false, route_decision_id: routeDecisionId, intervention, error: 'invalid_context', message: '入口路由上下文缺失,无法恢复执行。' };
}
await recordLocalRouteApprovalDecision(approvalId, decisionText, body.actor ?? 'digital_employee_operator', snapshot);
let command: GovernanceCommandResponse | null = null;
let commandError: string | null = null;
if (isApprovedInterventionDecision(decisionText)) {
const routeCommand = buildGovernanceCommandForRouteDecision(routeDecision, detail?.body ?? routeDecisionToIngressBody(routeDecision), snapshot);
if (routeCommand.command) {
command = await handleGovernanceCommand(routeCommand.command, snapshot);
if (!command.accepted) commandError = command.error || 'route_intervention_command_rejected';
} else {
commandError = routeCommand.reasonCode || 'route_intervention_missing_context';
}
}
const intervention = await recordRouteInterventionResolution({
routeDecisionId,
approvalId,
decision: decisionText,
createdCommandId: command?.command_id ?? null,
commandAccepted: command?.accepted === true,
commandError,
blockedReason: routeDecision.blocked_reason ?? null,
policyResult: routeDecision.policy_result ?? null,
policySource: routeDecision.policy_source ?? null,
matchedIntent: routeDecision.matched_intent ?? null,
routeKind: routeDecision.route_kind,
intentKind: routeDecision.intent_kind,
reasonCodes: routeDecision.reason_codes,
candidateSkills: routeDecision.candidate_skills,
contextRefs: routeDecision.context_refs,
entrypoint: routeDecision.entrypoint ?? null,
actor: body.actor ?? 'digital_employee_operator',
correlationId: routeDecision.correlation_id,
payload: { comment: body.comment ?? null },
occurredAt: new Date().toISOString(),
});
return {
accepted: intervention.intervention_status !== 'failed',
route_decision_id: routeDecisionId,
intervention,
command,
error: commandError || undefined,
message: intervention.resolution_duplicate
? '入口路由干预已处理。'
: isApprovedInterventionDecision(decisionText)
? command?.command_id ? '入口路由干预已通过并恢复执行。' : '入口路由干预已通过。'
: '入口路由干预已拒绝。',
};
}
async function recordRouteInterventionResolution(input: QimingclawRouteInterventionResolutionInput): Promise<RouteInterventionRecord> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.recordRouteInterventionResolution) {
return {
route_decision_id: input.routeDecisionId,
approval_id: input.approvalId ?? null,
route_kind: input.routeKind ?? 'ChatOnly',
intent_kind: input.intentKind ?? 'chat',
policy_result: input.policyResult ?? null,
intervention_status: input.commandError ? 'failed' : isApprovedInterventionDecision(input.decision) ? 'approved' : 'rejected',
decision: input.decision,
created_command_id: input.createdCommandId ?? null,
blocked_reason: input.blockedReason ?? null,
matched_intent: input.matchedIntent ?? null,
policy_source: input.policySource ?? null,
reason_codes: input.reasonCodes ?? [],
candidate_skills: input.candidateSkills ?? [],
context_refs: input.contextRefs ?? {},
entrypoint: input.entrypoint ?? null,
actor: input.actor ?? null,
correlation_id: input.correlationId ?? null,
approval_status: null,
recorded_at: input.occurredAt ?? new Date().toISOString(),
resolved_at: input.occurredAt ?? new Date().toISOString(),
resolution_duplicate: false,
};
}
return bridge.recordRouteInterventionResolution(input);
}
function routeDecisionDetail(routeDecisionId: string, snapshot: QimingclawSnapshot | null): { decision: RouteDecisionRecord; body: IngressRouteRequest } | null {
const event = runtimeEvents(snapshot).find((item) => {
if (item.kind !== 'route_decision') return false;
const payload = asRecord(item.payload) ?? {};
const decision = routeDecisionFromRuntimePayload(payload, item);
return decision.id === routeDecisionId;
});
if (!event) return null;
const payload = asRecord(event.payload) ?? {};
const decision = routeDecisionFromRuntimePayload(payload, event);
const envelope = asRecord(payload.envelope) ?? {};
return {
decision,
body: {
entrypoint: decision.entrypoint as IngressRouteRequest['entrypoint'] ?? 'chat',
actor: decision.actor ?? undefined,
text: stringValue(envelope.text) || undefined,
payload: asRecord(envelope.payload) ?? {},
attachments: arrayValue(envelope.attachments).filter((item): item is string => typeof item === 'string'),
timestamp: decision.recorded_at,
correlation_id: decision.correlation_id,
idempotency_key: `route-intervention:${decision.id}`,
context_refs: decision.context_refs,
},
};
}
function routeInterventionToRouteDecision(record: RouteInterventionRecord | null, routeDecisionId: string): RouteDecisionRecord | null {
if (!record) return null;
return {
id: routeDecisionId,
route_kind: record.route_kind,
intent_kind: record.intent_kind,
reason_codes: record.reason_codes ?? [],
candidate_skills: record.candidate_skills ?? [],
context_refs: record.context_refs ?? {},
created_command_id: record.created_command_id,
correlation_id: record.correlation_id || routeDecisionId,
creates_plan: record.route_kind === 'PlanControl' || record.route_kind === 'PlanExecution',
creates_task_run: record.route_kind === 'DirectRun' || record.route_kind === 'PlanExecution',
approval_mode: record.approval_id ? 'manual' : null,
policy_result: record.policy_result,
policy_source: record.policy_source,
blocked_reason: record.blocked_reason,
approval_id: record.approval_id,
matched_intent: record.matched_intent,
recorded_at: record.recorded_at,
entrypoint: record.entrypoint,
actor: record.actor,
};
}
function routeDecisionToIngressBody(decision: RouteDecisionRecord): IngressRouteRequest {
return {
entrypoint: decision.entrypoint as IngressRouteRequest['entrypoint'] ?? 'chat',
actor: decision.actor ?? undefined,
payload: {},
attachments: [],
timestamp: decision.recorded_at,
correlation_id: decision.correlation_id,
idempotency_key: `route-intervention:${decision.id}`,
context_refs: decision.context_refs,
};
}
function routeInterventionApprovalId(routeDecisionId: string, snapshot: QimingclawSnapshot | null): string | null {
const detail = routeDecisionDetail(routeDecisionId, snapshot);
if (detail?.decision.approval_id) return detail.decision.approval_id;
const row = routeDecisionRows(snapshot).find((item) => stringValue(item.route_decision_id) === routeDecisionId);
return stringValue(row?.approval_id) || null;
}
async function recordLocalRouteApprovalDecision(
approvalId: string | null,
decision: string,
actor: string,
snapshot: QimingclawSnapshot | null,
): Promise<void> {
if (!approvalId) return;
const date = todayInputDate();
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
state.decisionResultsByDate = state.decisionResultsByDate ?? {};
state.decisionResultsByDate[date] = {
...(state.decisionResultsByDate[date] ?? {}),
[approvalId]: decision,
};
await saveState(state, 'decide_approval', date, { decision_id: approvalId, decision, actor });
await window.QimingClawBridge?.digital?.submitApprovalDecision?.({
approval_id: approvalId,
decision,
date,
actor,
source: 'sgrobot-route-intervention',
}).catch(() => null);
}
function isApprovedInterventionDecision(decision: string): boolean {
return ['approve', 'approved', 'accept', 'accepted'].includes(decision.toLowerCase());
}
async function handleIngressRoute(
body: IngressRouteRequest,
snapshot: QimingclawSnapshot | null,
@@ -5881,6 +6169,7 @@ async function handleIngressRoute(
actor: body.actor ?? null,
};
const routePolicy = await evaluateBridgeRouteDecisionPolicy({
routeDecisionId: decision.id,
routeKind: decision.route_kind,
intentKind: decision.intent_kind,
reasonCodes: decision.reason_codes,

View File

@@ -8,6 +8,7 @@ import {
dispatchPlanReadySteps,
getDigitalEmployeeWorkday,
getPlanStepGraph,
getRouteInterventions,
getSchedulerJobs,
governanceCommand,
postDigitalEmployeeWorkdayAction,
@@ -16,6 +17,7 @@ import {
pullRiskApprovalPolicy,
pullRouteDecisionPolicy,
recordApprovalAction,
decideRouteIntervention,
restartManagedService,
} from '@/lib/api';
import { isTauri } from '@/lib/tauri';
@@ -33,6 +35,7 @@ import type {
GovernanceCommandKind,
PlanStepBusinessRow,
PlanStepDependencyDetail,
RouteInterventionRecord,
} from '@/types/api';
import type { DigitalNavigationTarget } from './navigation';
@@ -990,6 +993,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
const [blockedPlanSteps, setBlockedPlanSteps] = useState<PlanStepBusinessRow[]>([]);
const [planStepGraphRows, setPlanStepGraphRows] = useState<PlanStepBusinessRow[]>([]);
const [blockedPlanStepError, setBlockedPlanStepError] = useState<string | null>(null);
const [routeInterventions, setRouteInterventions] = useState<RouteInterventionRecord[]>([]);
const [routeInterventionError, setRouteInterventionError] = useState<string | null>(null);
const [blockedInputTarget, setBlockedInputTarget] = useState<BlockedPlanStepInputTarget | null>(null);
const [blockedInputDraft, setBlockedInputDraft] = useState('');
const [profileForm, setProfileForm] = useState({
@@ -1076,7 +1081,19 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败');
});
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise, planStepGraphPromise]);
const routeInterventionPromise = getRouteInterventions()
.then((response) => {
if (!mountedRef.current) return;
setRouteInterventions(response.interventions.slice(0, 6));
setRouteInterventionError(null);
})
.catch((error: unknown) => {
if (!mountedRef.current) return;
setRouteInterventions([]);
setRouteInterventionError(error instanceof Error ? error.message : '路由干预加载失败');
});
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise, planStepGraphPromise, routeInterventionPromise]);
if (mountedRef.current) {
setProjectionLoading(false);
}
@@ -1458,6 +1475,30 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
}
}, [fetchProjection]);
const resolveRouteIntervention = useCallback(async (intervention: RouteInterventionRecord, decision: 'approved' | 'rejected') => {
const actionKey = `route-intervention:${intervention.route_decision_id}:${decision}`;
setActionBusy(actionKey);
setActionError(null);
setActionNotice(null);
try {
const result = await decideRouteIntervention(intervention.route_decision_id, {
approval_id: intervention.approval_id,
decision,
actor: 'digital_employee_operator',
});
if (!mountedRef.current) return;
await fetchProjection();
if (!mountedRef.current) return;
setActionNotice(result.message || (decision === 'approved' ? '入口路由干预已通过。' : '入口路由干预已拒绝。'));
} catch (error) {
if (mountedRef.current) {
setActionError(error instanceof Error ? error.message : '处理入口路由干预失败');
}
} finally {
if (mountedRef.current) setActionBusy(null);
}
}, [fetchProjection]);
const copySyncFailureDiagnostic = useCallback(async (failure: ManagementSyncFailure) => {
const diagnostic = buildSyncFailureDiagnostic(failure, managementSyncStatus);
try {
@@ -2177,6 +2218,45 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
{actionError || actionNotice || planDataError || workdayProjection?.demo_reason}
</div>
)}
{(routeInterventions.length > 0 || routeInterventionError) && (
<div className="de-management-sync-failures" aria-label="入口路由干预" style={{ marginBottom: 10 }}>
{routeInterventionError && <p className="de-empty-text">{routeInterventionError}</p>}
{routeInterventions.slice(0, 4).map((intervention) => {
const status = intervention.intervention_status;
const pending = !intervention.resolved_at && ['pending', 'blocked', 'approved'].includes(status);
const approveKey = `route-intervention:${intervention.route_decision_id}:approved`;
const rejectKey = `route-intervention:${intervention.route_decision_id}:rejected`;
return (
<div key={intervention.route_decision_id} className="de-management-sync-failure" title={intervention.blocked_reason || intervention.route_decision_id}>
<span>{intervention.entrypoint || 'route'}</span>
<strong>{intervention.route_kind} / {intervention.intent_kind}</strong>
<em>{status}</em>
<small>{intervention.blocked_reason || intervention.matched_intent || intervention.approval_id || intervention.route_decision_id}</small>
{pending && (
<div className="de-task-agent-actions">
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={actionBusy === approveKey}
onClick={() => { void resolveRouteIntervention(intervention, 'approved'); }}
>
{status === 'approved' ? '恢复' : '通过'}
</button>
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={actionBusy === rejectKey}
onClick={() => { void resolveRouteIntervention(intervention, 'rejected'); }}
>
</button>
</div>
)}
</div>
);
})}
</div>
)}
{managedServiceIssues.length > 0 && (
<div className="de-management-sync-failures" aria-label="服务状态处理" style={{ marginBottom: 10 }}>
{managedServiceIssues.map((service) => {
@@ -2360,9 +2440,12 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span className={routeSummary.attention_count > 0 ? 'is-warning' : ''}><em></em><strong>{routeSummary.attention_count}</strong></span>
{(routeSummary.blocked_count ?? 0) > 0 && <span className="is-warning"><em></em><strong>{routeSummary.blocked_count}</strong></span>}
{(routeSummary.approval_required_count ?? 0) > 0 && <span className="is-warning"><em></em><strong>{routeSummary.approval_required_count}</strong></span>}
{(routeSummary.pending_intervention_count ?? 0) > 0 && <span className="is-warning"><em></em><strong>{routeSummary.pending_intervention_count}</strong></span>}
{(routeSummary.restored_count ?? 0) > 0 && <span><em></em><strong>{routeSummary.restored_count}</strong></span>}
<small>
{latestRoute ? `${latestRoute.route_kind} / ${latestRoute.intent_kind} · ${latestRoute.status_label}` : '暂无路由决策'}
{routeSummary.policy_source ? ` · 策略${routeSummary.policy_source === 'remote' ? '远端' : '本地'}` : ''}
{routeSummary.latest_intervention_status ? ` · 干预${routeSummary.latest_intervention_status}` : ''}
{routeSummary.policy_last_pull_error ? ` · 拉取失败` : ''}
</small>
</div>

View File

@@ -409,6 +409,52 @@ export interface RouteDecisionTimelineResponse {
decisions: RouteDecisionRecord[];
}
export interface RouteInterventionRecord {
route_decision_id: string;
approval_id: string | null;
route_kind: string;
intent_kind: string;
policy_result: string | null;
intervention_status: 'pending' | 'blocked' | 'approved' | 'rejected' | 'executed' | 'failed' | 'resolved' | string;
decision: string | null;
created_command_id: string | null;
blocked_reason: string | null;
matched_intent: string | null;
policy_source: string | null;
reason_codes: string[];
candidate_skills: string[];
context_refs: Record<string, unknown>;
entrypoint: string | null;
actor: string | null;
correlation_id: string | null;
approval_status: string | null;
recorded_at: string;
resolved_at: string | null;
resolution_event_id?: string | null;
resolution_duplicate?: boolean;
}
export interface RouteInterventionTimelineResponse {
has_route_intervention_state: boolean;
interventions: RouteInterventionRecord[];
}
export interface RouteInterventionDecisionRequest {
approval_id?: string | null;
decision: 'approve' | 'approved' | 'reject' | 'rejected' | string;
actor?: string | null;
comment?: string | null;
}
export interface RouteInterventionDecisionResponse {
accepted: boolean;
route_decision_id: string;
intervention: RouteInterventionRecord;
command?: unknown;
message?: string;
error?: GovernanceCommandErrorCode | string;
}
export interface RouteDecisionPolicy {
enabled: boolean;
auto_create_governance_commands: boolean;
@@ -989,6 +1035,9 @@ export interface DigitalEmployeeRouteSummary {
attention_count: number;
blocked_count?: number;
approval_required_count?: number;
pending_intervention_count?: number;
restored_count?: number;
latest_intervention_status?: string | null;
policy_source?: string | null;
policy_last_pulled_at?: string | null;
policy_last_pull_error?: string | null;