feat: add orchestrator controls to memindadm
This commit is contained in:
@@ -100,6 +100,8 @@ export function createAdminApp(services) {
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
orchestratorConfigService,
|
||||
orchestratorObservabilityService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
@@ -426,6 +428,71 @@ export function createAdminApp(services) {
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.get('/orchestrator/config', requireAdmin, async (_req, res) => {
|
||||
if (!orchestratorConfigService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorConfigService.getAdminConfig());
|
||||
});
|
||||
|
||||
const updateOrchestratorConfig = async (req, res) => {
|
||||
if (!orchestratorConfigService?.updateAdminConfig) {
|
||||
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorConfigService.updateAdminConfig(
|
||||
req.body?.config ?? req.body ?? {},
|
||||
{ updatedBy: req.currentUser.id },
|
||||
));
|
||||
};
|
||||
|
||||
adminApi.put('/orchestrator/config', requireAdmin, updateOrchestratorConfig);
|
||||
adminApi.patch('/orchestrator/config', requireAdmin, updateOrchestratorConfig);
|
||||
|
||||
adminApi.get('/orchestrator/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!orchestratorConfigService?.getRuntimeState) {
|
||||
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorConfigService.getRuntimeState({ probe: true }));
|
||||
});
|
||||
|
||||
adminApi.get('/orchestrator/shadow-runs', requireAdmin, async (req, res) => {
|
||||
if (!orchestratorObservabilityService?.listShadowRuns) {
|
||||
return res.status(503).json({ message: 'Orchestrator 观测服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorObservabilityService.listShadowRuns({
|
||||
hours: req.query.hours,
|
||||
limit: req.query.limit,
|
||||
status: req.query.status,
|
||||
}));
|
||||
});
|
||||
|
||||
adminApi.get('/orchestrator/execution-plans', requireAdmin, async (req, res) => {
|
||||
if (!orchestratorObservabilityService?.listExecutionPlans) {
|
||||
return res.status(503).json({ message: 'Orchestrator Dry-run 观测服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorObservabilityService.listExecutionPlans({
|
||||
hours: req.query.hours,
|
||||
limit: req.query.limit,
|
||||
selection: req.query.selection,
|
||||
}));
|
||||
});
|
||||
|
||||
adminApi.get('/orchestrator/canary-readiness', requireAdmin, async (_req, res) => {
|
||||
if (!orchestratorObservabilityService?.getCanaryReadiness) {
|
||||
return res.status(503).json({ message: 'Orchestrator 观测服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorObservabilityService.getCanaryReadiness());
|
||||
});
|
||||
|
||||
adminApi.get('/orchestrator/shadow-runs/:runId', requireAdmin, async (req, res) => {
|
||||
if (!orchestratorObservabilityService?.getShadowRun) {
|
||||
return res.status(503).json({ message: 'Orchestrator 观测服务未启用' });
|
||||
}
|
||||
const result = await orchestratorObservabilityService.getShadowRun(req.params.runId);
|
||||
if (!result) return res.status(404).json({ message: 'Shadow run 不存在' });
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => {
|
||||
const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,12 @@ export async function bootstrapAdminServices() {
|
||||
} = await importMemind('mindspace-config.mjs');
|
||||
const { createMemoryV2AdminConfigService } = await importMemind('memory-v2-admin-config.mjs');
|
||||
const { createMindSearchConfigService } = await importMemind('mindsearch-config.mjs');
|
||||
const { createOrchestratorAdminConfigService } = await importMemind(
|
||||
'services/orchestrator/admin-config.mjs',
|
||||
);
|
||||
const { createOrchestratorObservabilityService } = await importMemind(
|
||||
'services/orchestrator/observability.mjs',
|
||||
);
|
||||
const { createPersonalMemoryCandidateStore } = await importMemind('memory-v2-personal-store.mjs');
|
||||
const { createSkillRuntimeAdminConfigService } = await importMemind('skill-runtime-admin-config.mjs');
|
||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||
@@ -99,6 +105,14 @@ export async function bootstrapAdminServices() {
|
||||
});
|
||||
const mindSearchConfigService = createMindSearchConfigService(pool, { env: process.env });
|
||||
await mindSearchConfigService.ensureSchema();
|
||||
const orchestratorConfigService = createOrchestratorAdminConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
await orchestratorConfigService.ensureSchema();
|
||||
const orchestratorObservabilityService = createOrchestratorObservabilityService({
|
||||
pool,
|
||||
configService: orchestratorConfigService,
|
||||
});
|
||||
const personalMemoryCandidateStore = createPersonalMemoryCandidateStore(pool);
|
||||
const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, {
|
||||
env: process.env,
|
||||
@@ -165,6 +179,8 @@ export async function bootstrapAdminServices() {
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
orchestratorConfigService,
|
||||
orchestratorObservabilityService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
|
||||
@@ -106,6 +106,8 @@ ready
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
orchestratorConfigService,
|
||||
orchestratorObservabilityService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
@@ -132,6 +134,8 @@ ready
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
orchestratorConfigService,
|
||||
orchestratorObservabilityService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { AssetGatewayPage } from './admin/pages/AssetGatewayPage';
|
||||
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
|
||||
import { AnalyticsConfigPage } from './admin/pages/AnalyticsConfigPage';
|
||||
import { SkillRuntimePage } from './admin/pages/SkillRuntimePage';
|
||||
import { OrchestratorPage } from './admin/pages/OrchestratorPage';
|
||||
import { defaultHomePath } from './lib/routes';
|
||||
import { OpsLayout } from './ops/components/OpsLayout';
|
||||
import { AnalyticsPage } from './ops/pages/AnalyticsPage';
|
||||
@@ -127,6 +128,7 @@ function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void }
|
||||
<Route path="memory-v2" element={<MemoryV2Page />} />
|
||||
<Route path="mindsearch" element={<MindSearchPage />} />
|
||||
<Route path="skill-runtime" element={<SkillRuntimePage />} />
|
||||
<Route path="orchestrator" element={<OrchestratorPage />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
@@ -175,6 +177,7 @@ function loginRedirectPath(pathname: string, role: string | undefined) {
|
||||
|| pathname.startsWith('/mindspace')
|
||||
|| pathname.startsWith('/memory-v2')
|
||||
|| pathname.startsWith('/skill-runtime')
|
||||
|| pathname.startsWith('/orchestrator')
|
||||
|| pathname.startsWith('/providers')
|
||||
|| pathname.startsWith('/wechat')
|
||||
|| pathname.startsWith('/asset-gateway')
|
||||
|
||||
@@ -32,6 +32,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
{ to: '/memory-v2', label: 'Memory V2' },
|
||||
{ to: '/mindsearch', label: 'MindSearch' },
|
||||
{ to: '/skill-runtime', label: 'Skill Runtime' },
|
||||
{ to: '/orchestrator', label: '任务编排' },
|
||||
{ to: '/system-tests', label: '系统测试验证' },
|
||||
{ to: '/capabilities', label: '能力' },
|
||||
{ to: '/skills', label: '技能' },
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
getOrchestratorCanaryReadiness,
|
||||
getOrchestratorConfig,
|
||||
getOrchestratorExecutionPlans,
|
||||
getOrchestratorRuntime,
|
||||
getOrchestratorShadowRun,
|
||||
getOrchestratorShadowRuns,
|
||||
updateOrchestratorConfig,
|
||||
} from '../../api/client';
|
||||
import type {
|
||||
OrchestratorCanaryReadiness,
|
||||
OrchestratorConfig,
|
||||
OrchestratorConfigState,
|
||||
OrchestratorExecutionPlanList,
|
||||
OrchestratorServiceHealth,
|
||||
OrchestratorShadowRunDetail,
|
||||
OrchestratorShadowRunList,
|
||||
} from '../../types/orchestrator';
|
||||
|
||||
const runtimeReasons: Record<string, string> = {
|
||||
mode_off: '当前为关闭模式,所有任务继续由 Native Agent Run 执行。',
|
||||
service_url_missing: '尚未配置 Orchestrator 服务地址,LangGraph 不会接管任务。',
|
||||
kill_switch: '环境级紧急熔断已开启,强制回退 Native。',
|
||||
execution_gate_disabled: '执行交接闸门被硬锁定,当前仅生成 Dry-run 决策。',
|
||||
environment_execution_gate_disabled: 'memindadm 已请求交接,但环境闸门仍关闭。',
|
||||
};
|
||||
|
||||
const readinessLabels: Record<string, string> = {
|
||||
shadow_mode: '保持 Shadow',
|
||||
service_healthy: '服务健康',
|
||||
durable_checkpoint: '持久化 checkpoint',
|
||||
durable_executor_job_store: '持久化 Executor Job Store',
|
||||
observe_only: '仅观察不执行',
|
||||
sample_volume: '有效样本量',
|
||||
shadow_success_rate: 'Shadow 成功率',
|
||||
shadow_skip_rate: 'Shadow 跳过率',
|
||||
latency_coverage: '延迟数据覆盖率',
|
||||
latency_p95: '延迟 P95',
|
||||
native_settled_rate: 'Native 终态覆盖率',
|
||||
session_coverage: '真实会话覆盖',
|
||||
sample_freshness: '最近样本新鲜度',
|
||||
complete_window: '统计窗口完整',
|
||||
};
|
||||
|
||||
function listToText(values: string[]) {
|
||||
return values.join('\n');
|
||||
}
|
||||
|
||||
function textToList(value: string) {
|
||||
return [...new Set(value.split(/\r?\n|,/).map((item) => item.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
function formatTime(value?: number | null) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—';
|
||||
}
|
||||
|
||||
function formatPercent(value?: number | null) {
|
||||
return value == null ? '—' : `${(value * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function formatLatency(value?: number | null) {
|
||||
return value == null ? '—' : `${value} ms`;
|
||||
}
|
||||
|
||||
function shortId(value: string) {
|
||||
return value.length > 16 ? `${value.slice(0, 8)}…${value.slice(-6)}` : value;
|
||||
}
|
||||
|
||||
function readinessValue(check: OrchestratorCanaryReadiness['checks'][number]) {
|
||||
if (check.actual == null) return '无数据';
|
||||
if (['shadow_success_rate', 'latency_coverage', 'native_settled_rate'].includes(check.id)) {
|
||||
return formatPercent(Number(check.actual));
|
||||
}
|
||||
if (check.id === 'latency_p95') return formatLatency(Number(check.actual));
|
||||
if (check.id === 'sample_freshness') return `${Number(check.actual).toFixed(1)} 小时`;
|
||||
if (typeof check.actual === 'boolean') return check.actual ? '是' : '否';
|
||||
return String(check.actual);
|
||||
}
|
||||
|
||||
export function OrchestratorPage() {
|
||||
const [state, setState] = useState<OrchestratorConfigState | null>(null);
|
||||
const [draft, setDraft] = useState<OrchestratorConfig | null>(null);
|
||||
const [workflowAllowlist, setWorkflowAllowlist] = useState('');
|
||||
const [userAllowlist, setUserAllowlist] = useState('');
|
||||
const [shadow, setShadow] = useState<OrchestratorShadowRunList | null>(null);
|
||||
const [plans, setPlans] = useState<OrchestratorExecutionPlanList | null>(null);
|
||||
const [readiness, setReadiness] = useState<OrchestratorCanaryReadiness | null>(null);
|
||||
const [health, setHealth] = useState<OrchestratorServiceHealth | null>(null);
|
||||
const [detail, setDetail] = useState<OrchestratorShadowRunDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [configState, shadowState, planState, readinessState] = await Promise.all([
|
||||
getOrchestratorConfig(),
|
||||
getOrchestratorShadowRuns({ hours: 24, limit: 100 }),
|
||||
getOrchestratorExecutionPlans({ hours: 24, limit: 100 }),
|
||||
getOrchestratorCanaryReadiness(),
|
||||
]);
|
||||
setState(configState);
|
||||
setDraft(configState.config);
|
||||
setWorkflowAllowlist(listToText(configState.config.workflowAllowlist));
|
||||
setUserAllowlist(listToText(configState.config.userAllowlist));
|
||||
setShadow(shadowState);
|
||||
setPlans(planState);
|
||||
setReadiness(readinessState);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Orchestrator 数据加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, []);
|
||||
|
||||
const normalizedDraft = useMemo(() => draft ? {
|
||||
...draft,
|
||||
workflowAllowlist: textToList(workflowAllowlist),
|
||||
userAllowlist: textToList(userAllowlist),
|
||||
} : null, [draft, workflowAllowlist, userAllowlist]);
|
||||
|
||||
const dirty = Boolean(
|
||||
state && normalizedDraft && JSON.stringify(state.config) !== JSON.stringify(normalizedDraft),
|
||||
);
|
||||
|
||||
const save = async () => {
|
||||
if (!normalizedDraft) return;
|
||||
if (['canary', 'active'].includes(normalizedDraft.mode) && !normalizedDraft.serviceUrl) {
|
||||
setError('Canary / Active 模式必须先配置 Orchestrator URL。');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
normalizedDraft.executionEnabled
|
||||
&& !window.confirm('确认请求执行交接?环境闸门关闭时仍只会 Dry-run。')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
try {
|
||||
const result = await updateOrchestratorConfig(normalizedDraft);
|
||||
setState(result);
|
||||
setDraft(result.config);
|
||||
setWorkflowAllowlist(listToText(result.config.workflowAllowlist));
|
||||
setUserAllowlist(listToText(result.config.userAllowlist));
|
||||
setMessage(`Orchestrator 配置 v${result.configVersion} 已保存。`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async () => {
|
||||
setChecking(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await getOrchestratorRuntime();
|
||||
setHealth(result.serviceHealth);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '服务检查失败');
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openDetail = async (runId: string) => {
|
||||
setError(null);
|
||||
try {
|
||||
setDetail(await getOrchestratorShadowRun(runId));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Run 详情加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="admin-page"><p>正在加载 Workflow Orchestrator…</p></div>;
|
||||
if (!state || !draft) {
|
||||
return <div className="admin-page"><p className="banner banner-error">{error ?? '配置不可用'}</p></div>;
|
||||
}
|
||||
|
||||
const runtimeMessage = state.runtime.reason
|
||||
? (runtimeReasons[state.runtime.reason] ?? state.runtime.reason)
|
||||
: state.runtime.shadowsLangGraph
|
||||
? 'Shadow 已生效:Native 继续执行,LangGraph 只观察。'
|
||||
: state.runtime.executesLangGraph
|
||||
? '执行交接已开启。'
|
||||
: '配置有效。';
|
||||
|
||||
return (
|
||||
<div className="admin-page orchestrator-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>Workflow Orchestrator</h2>
|
||||
<p className="muted">
|
||||
LangGraph 以可插拔服务接入;Off 与 Shadow 不改变现有 Native Agent Run 执行路径。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="orchestrator-summary">
|
||||
<div><span>配置版本</span><strong>v{state.configVersion}</strong></div>
|
||||
<div><span>运行状态</span><strong>{state.runtime.effective ? '有效' : '安全回退'}</strong></div>
|
||||
<div><span>配置来源</span><strong>{state.source}</strong></div>
|
||||
<div><span>执行交接</span><strong>{state.runtime.executionHandoff.enabled ? '已开启' : 'Dry-run 锁定'}</strong></div>
|
||||
<div><span>更新时间</span><strong>{formatTime(state.updatedAt)}</strong></div>
|
||||
</div>
|
||||
<p className={`orchestrator-state ${state.runtime.reason === 'kill_switch' ? 'danger' : ''}`}>
|
||||
{runtimeMessage}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="orchestrator-section-head">
|
||||
<div>
|
||||
<h2>Executor Gateway</h2>
|
||||
<p className="muted">执行器默认关闭,最终状态以 Orchestrator 健康检查为准。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="orchestrator-executors">
|
||||
{state.executors.map((executor) => (
|
||||
<article key={executor.id}>
|
||||
<div><strong>{executor.label}</strong><span>{executor.status}</span></div>
|
||||
<p>{executor.capabilities.join(' · ') || '无能力声明'}</p>
|
||||
<small>dispatch {executor.dispatchImplemented ? 'implemented' : 'disabled'}</small>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="orchestrator-section-head">
|
||||
<div>
|
||||
<h2>Shadow 运行观测</h2>
|
||||
<p className="muted">最近 24 小时 Native 与 LangGraph 观察结果,不改变执行权。</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-btn" onClick={() => void load()}>刷新</button>
|
||||
</div>
|
||||
|
||||
{readiness && (
|
||||
<div className={`orchestrator-readiness ${readiness.ready ? 'ready' : ''}`}>
|
||||
<div>
|
||||
<strong>Canary 准入评估</strong>
|
||||
<b>{readiness.ready ? '达到门槛,等待人工审批' : '继续保持 Shadow'}</b>
|
||||
</div>
|
||||
<p>
|
||||
有效样本 {readiness.samples.eligibleObservations} / {readiness.thresholds.minObservations}
|
||||
{' · '}排除 smoke {readiness.samples.excludedSynthetic}
|
||||
{' · '}真实会话 {readiness.samples.distinctSessions} / {readiness.thresholds.minDistinctSessions}
|
||||
</p>
|
||||
<div className="orchestrator-checks">
|
||||
{readiness.checks.map((check) => (
|
||||
<span key={check.id} className={check.passed ? 'passed' : 'blocked'}>
|
||||
{check.passed ? '通过' : '阻塞'} · {readinessLabels[check.id] ?? check.id}:{readinessValue(check)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="orchestrator-metrics">
|
||||
<div><span>观察请求</span><strong>{shadow?.metrics.observations ?? '—'}</strong></div>
|
||||
<div><span>成功率</span><strong>{formatPercent(shadow?.metrics.successRate)}</strong></div>
|
||||
<div><span>失败数</span><strong>{shadow?.metrics.failures ?? '—'}</strong></div>
|
||||
<div><span>延迟 P95</span><strong>{formatLatency(shadow?.metrics.latencyP95Ms)}</strong></div>
|
||||
<div><span>Native 成功 / 失败</span><strong>{shadow ? `${shadow.metrics.nativeSucceeded} / ${shadow.metrics.nativeFailed}` : '—'}</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead><tr><th>时间</th><th>Run</th><th>Shadow</th><th>Native</th><th>延迟</th><th>任务 / Adapter</th></tr></thead>
|
||||
<tbody>
|
||||
{(shadow?.runs ?? []).map((run) => (
|
||||
<tr key={run.eventId}>
|
||||
<td>{formatTime(run.observedAt)}</td>
|
||||
<td><button type="button" className="ghost-btn" onClick={() => void openDetail(run.runId)}>{shortId(run.runId)}</button></td>
|
||||
<td>{run.shadowStatus}</td>
|
||||
<td>{run.nativeStatus}</td>
|
||||
<td>{formatLatency(run.latencyMs)}</td>
|
||||
<td>{run.taskType ?? '—'}{run.synthetic ? '(smoke)' : ''}<small>{run.executorAdapter ?? '—'}</small></td>
|
||||
</tr>
|
||||
))}
|
||||
{!shadow?.runs.length && <tr><td colSpan={6}>当前没有 Shadow 结果。</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{detail && (
|
||||
<div className="orchestrator-detail">
|
||||
<div className="orchestrator-section-head">
|
||||
<div><strong>Run 详情:{detail.native.runId}</strong><p className="muted">Native {detail.native.status} · LangGraph {detail.remote.available ? 'available' : 'unavailable'}</p></div>
|
||||
<button type="button" className="ghost-btn" onClick={() => setDetail(null)}>关闭</button>
|
||||
</div>
|
||||
<div>
|
||||
<pre>{JSON.stringify(detail.remote.state, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(detail.remote.events, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="orchestrator-section-head">
|
||||
<div><h2>执行路由 Dry-run</h2><p className="muted">候选路由仅观测,实际执行引擎必须保持 Native。</p></div>
|
||||
</div>
|
||||
{(plans?.metrics.handoffAllowed ?? 0) > 0 && (
|
||||
<p className="banner banner-error">检测到 handoffAllowed=true,请立即检查执行闸门。</p>
|
||||
)}
|
||||
<div className="orchestrator-metrics">
|
||||
<div><span>路由决策</span><strong>{plans?.metrics.decisions ?? '—'}</strong></div>
|
||||
<div><span>候选命中率</span><strong>{formatPercent(plans?.metrics.candidateSelectionRate)}</strong></div>
|
||||
<div><span>LangGraph / Native</span><strong>{plans ? `${plans.metrics.candidateSelections} / ${plans.metrics.nativeSelections}` : '—'}</strong></div>
|
||||
<div><span>Native 终态覆盖</span><strong>{formatPercent(plans?.metrics.nativeSettledRate)}</strong></div>
|
||||
<div><span>真实会话</span><strong>{plans?.metrics.distinctSessions ?? '—'}</strong></div>
|
||||
</div>
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead><tr><th>时间</th><th>Run</th><th>模式</th><th>候选 → 实际</th><th>原因</th><th>Native</th></tr></thead>
|
||||
<tbody>
|
||||
{(plans?.plans ?? []).map((plan) => (
|
||||
<tr key={plan.eventId}>
|
||||
<td>{formatTime(plan.plannedAt)}</td>
|
||||
<td className="mono">{shortId(plan.runId)}</td>
|
||||
<td>{plan.mode ?? '—'}</td>
|
||||
<td>{plan.candidateEngine} → {plan.effectiveEngine}</td>
|
||||
<td>{plan.candidateReason ?? plan.reason ?? '—'}</td>
|
||||
<td>{plan.nativeStatus}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!plans?.plans.length && <tr><td colSpan={6}>当前没有 Dry-run 路由决策。</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<h2>路由与服务配置</h2>
|
||||
<div className="orchestrator-form-grid">
|
||||
<label><span>运行模式</span><select value={draft.mode} onChange={(event) => setDraft({ ...draft, mode: event.target.value as OrchestratorConfig['mode'] })}>
|
||||
<option value="off">Off — Native only</option>
|
||||
<option value="shadow">Shadow — Native 执行,LangGraph 观察</option>
|
||||
<option value="canary">Canary — 白名单/百分比灰度</option>
|
||||
<option value="active">Active — 工作流白名单接管</option>
|
||||
</select></label>
|
||||
<label><span>主执行引擎</span><select value={draft.primaryEngine} onChange={(event) => setDraft({ ...draft, primaryEngine: event.target.value })}>
|
||||
{state.engines.map((engine) => <option key={engine.id} value={engine.id}>{engine.label}{engine.configured ? '' : '(未配置)'}</option>)}
|
||||
</select></label>
|
||||
<label><span>灰度百分比</span><input type="number" min={0} max={100} value={draft.rolloutPercent} onChange={(event) => setDraft({ ...draft, rolloutPercent: Number(event.target.value) })} /></label>
|
||||
<label><span>请求超时(毫秒)</span><input type="number" min={500} max={60000} value={draft.requestTimeoutMs} onChange={(event) => setDraft({ ...draft, requestTimeoutMs: Number(event.target.value) })} /></label>
|
||||
<label className="wide"><span>Orchestrator URL</span><input value={draft.serviceUrl} onChange={(event) => setDraft({ ...draft, serviceUrl: event.target.value })} placeholder="http://127.0.0.1:8093" /></label>
|
||||
</div>
|
||||
<div className="orchestrator-health">
|
||||
<span>{health ? `${health.ok ? '健康' : '不可用'}(${health.status},${health.latencyMs} ms)` : '尚未检查服务连接'}</span>
|
||||
<button type="button" className="ghost-btn" onClick={() => void checkHealth()} disabled={checking}>{checking ? '检查中…' : '测试连接'}</button>
|
||||
</div>
|
||||
<div className="orchestrator-toggles">
|
||||
<label><input type="checkbox" checked={draft.requireHealthy} onChange={(event) => setDraft({ ...draft, requireHealthy: event.target.checked })} />接管前要求服务健康</label>
|
||||
<label><input type="checkbox" checked={draft.fallbackToNative} onChange={(event) => setDraft({ ...draft, fallbackToNative: event.target.checked })} />不可用时回退 Native</label>
|
||||
<label className="handoff"><input type="checkbox" checked={draft.executionEnabled} onChange={(event) => setDraft({ ...draft, executionEnabled: event.target.checked })} />请求启用执行交接(仍受环境闸门与 Kill Switch 约束)</label>
|
||||
</div>
|
||||
<div className="orchestrator-allowlists">
|
||||
<label><span>工作流白名单</span><textarea rows={6} value={workflowAllowlist} onChange={(event) => setWorkflowAllowlist(event.target.value)} /></label>
|
||||
<label><span>用户白名单</span><textarea rows={6} value={userAllowlist} onChange={(event) => setUserAllowlist(event.target.value)} /></label>
|
||||
</div>
|
||||
<div className="admin-actions orchestrator-actions">
|
||||
<button type="button" className="ghost-btn" onClick={() => void load()} disabled={saving}>放弃修改</button>
|
||||
<button type="button" className="send-btn" onClick={() => void save()} disabled={saving || !dirty}>{saving ? '保存中…' : '保存配置'}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,6 +51,15 @@ import type {
|
||||
MindSearchConfig,
|
||||
MindSearchServiceTestResult,
|
||||
} from '../types';
|
||||
import type {
|
||||
OrchestratorCanaryReadiness,
|
||||
OrchestratorConfig,
|
||||
OrchestratorConfigState,
|
||||
OrchestratorExecutionPlanList,
|
||||
OrchestratorRuntimeState,
|
||||
OrchestratorShadowRunDetail,
|
||||
OrchestratorShadowRunList,
|
||||
} from '../types/orchestrator';
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
@@ -679,6 +688,63 @@ export async function testMindSearchService(serviceId: string): Promise<MindSear
|
||||
return portalFetch(`/admin-api/mindsearch/services/${encodeURIComponent(serviceId)}/test`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// ── Workflow Orchestrator ─────────────────────────────
|
||||
|
||||
export async function getOrchestratorConfig() {
|
||||
return portalFetch<OrchestratorConfigState>('/admin-api/orchestrator/config');
|
||||
}
|
||||
|
||||
export async function updateOrchestratorConfig(config: OrchestratorConfig) {
|
||||
return portalFetch<OrchestratorConfigState>('/admin-api/orchestrator/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOrchestratorRuntime() {
|
||||
return portalFetch<OrchestratorRuntimeState>('/admin-api/orchestrator/runtime');
|
||||
}
|
||||
|
||||
export async function getOrchestratorShadowRuns(params: {
|
||||
hours?: number;
|
||||
limit?: number;
|
||||
status?: 'all' | 'succeeded' | 'failed';
|
||||
} = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.hours) query.set('hours', String(params.hours));
|
||||
if (params.limit) query.set('limit', String(params.limit));
|
||||
if (params.status) query.set('status', params.status);
|
||||
return portalFetch<OrchestratorShadowRunList>(
|
||||
`/admin-api/orchestrator/shadow-runs?${query}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getOrchestratorExecutionPlans(params: {
|
||||
hours?: number;
|
||||
limit?: number;
|
||||
selection?: 'all' | 'candidate' | 'native';
|
||||
} = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.hours) query.set('hours', String(params.hours));
|
||||
if (params.limit) query.set('limit', String(params.limit));
|
||||
if (params.selection) query.set('selection', params.selection);
|
||||
return portalFetch<OrchestratorExecutionPlanList>(
|
||||
`/admin-api/orchestrator/execution-plans?${query}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getOrchestratorCanaryReadiness() {
|
||||
return portalFetch<OrchestratorCanaryReadiness>(
|
||||
'/admin-api/orchestrator/canary-readiness',
|
||||
);
|
||||
}
|
||||
|
||||
export async function getOrchestratorShadowRun(runId: string) {
|
||||
return portalFetch<OrchestratorShadowRunDetail>(
|
||||
`/admin-api/orchestrator/shadow-runs/${encodeURIComponent(runId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Policies ──────────────────────────────────────────
|
||||
|
||||
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
|
||||
|
||||
+206
@@ -446,6 +446,212 @@ body,
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ── Workflow Orchestrator ─────────────────────────── */
|
||||
|
||||
.orchestrator-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.orchestrator-page .admin-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.orchestrator-page h2 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.orchestrator-summary,
|
||||
.orchestrator-executors,
|
||||
.orchestrator-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.orchestrator-summary > div,
|
||||
.orchestrator-metrics > div,
|
||||
.orchestrator-executors article {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
|
||||
.orchestrator-summary span,
|
||||
.orchestrator-metrics span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-state,
|
||||
.orchestrator-readiness {
|
||||
margin: 14px 0 0;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(224, 175, 91, 0.35);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(224, 175, 91, 0.08);
|
||||
}
|
||||
|
||||
.orchestrator-state.danger {
|
||||
border-color: rgba(224, 86, 86, 0.4);
|
||||
background: rgba(224, 86, 86, 0.08);
|
||||
}
|
||||
|
||||
.orchestrator-section-head,
|
||||
.orchestrator-executors article > div,
|
||||
.orchestrator-readiness > div,
|
||||
.orchestrator-health {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-executors article p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-executors article span {
|
||||
color: var(--color-warning);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-readiness.ready {
|
||||
border-color: rgba(70, 176, 120, 0.4);
|
||||
background: rgba(70, 176, 120, 0.08);
|
||||
}
|
||||
|
||||
.orchestrator-readiness p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.orchestrator-checks {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
align-items: start !important;
|
||||
justify-content: stretch !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
.orchestrator-checks span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-checks .passed {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.orchestrator-checks .blocked {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.orchestrator-metrics {
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.orchestrator-table small,
|
||||
.admin-table td small {
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.orchestrator-detail {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.orchestrator-detail > div:last-child {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-detail pre {
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-elevated);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.orchestrator-form-grid,
|
||||
.orchestrator-allowlists {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.orchestrator-form-grid label,
|
||||
.orchestrator-allowlists label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orchestrator-form-grid .wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.orchestrator-health {
|
||||
margin: 14px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.orchestrator-toggles {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.orchestrator-toggles label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.orchestrator-toggles input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.orchestrator-toggles .handoff {
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(224, 175, 91, 0.35);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(224, 175, 91, 0.08);
|
||||
}
|
||||
|
||||
.orchestrator-actions {
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.orchestrator-form-grid,
|
||||
.orchestrator-allowlists {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.orchestrator-section-head,
|
||||
.orchestrator-health {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.providers-page {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
export type OrchestratorMode = 'off' | 'shadow' | 'canary' | 'active';
|
||||
|
||||
export type OrchestratorConfig = {
|
||||
mode: OrchestratorMode;
|
||||
primaryEngine: string;
|
||||
fallbackEngine: string;
|
||||
serviceUrl: string;
|
||||
requestTimeoutMs: number;
|
||||
rolloutPercent: number;
|
||||
userAllowlist: string[];
|
||||
workflowAllowlist: string[];
|
||||
fallbackToNative: boolean;
|
||||
requireHealthy: boolean;
|
||||
executionEnabled: boolean;
|
||||
};
|
||||
|
||||
export type OrchestratorRuntime = {
|
||||
killSwitch: boolean;
|
||||
configured: boolean;
|
||||
effective: boolean;
|
||||
reason: string | null;
|
||||
executesLangGraph: boolean;
|
||||
plansLangGraph: boolean;
|
||||
shadowsLangGraph: boolean;
|
||||
executionHandoff: {
|
||||
implemented: boolean;
|
||||
requested: boolean;
|
||||
enabled: boolean;
|
||||
reason: string | null;
|
||||
environmentGate: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type OrchestratorConfigState = {
|
||||
config: OrchestratorConfig;
|
||||
configVersion: number;
|
||||
updatedBy: string | null;
|
||||
updatedAt: number | null;
|
||||
source: string;
|
||||
runtime: OrchestratorRuntime;
|
||||
engines: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
kind: string;
|
||||
configured: boolean;
|
||||
capabilities: string[];
|
||||
}>;
|
||||
executors: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
kind: string;
|
||||
enabled: boolean;
|
||||
dispatchImplemented: boolean;
|
||||
status: string;
|
||||
capabilities: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export type OrchestratorServiceHealth = {
|
||||
checkedAt: number;
|
||||
ok: boolean;
|
||||
status: string;
|
||||
latencyMs: number;
|
||||
httpStatus: number | null;
|
||||
details: {
|
||||
service: string | null;
|
||||
checkpoint: { kind?: string; durable?: boolean } | null;
|
||||
execution: string | null;
|
||||
executorGateway: {
|
||||
dispatchImplemented: boolean;
|
||||
executionEnabled: boolean;
|
||||
store: { kind: string | null; durable: boolean } | null;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type OrchestratorRuntimeState = OrchestratorConfigState & {
|
||||
serviceHealth: OrchestratorServiceHealth;
|
||||
};
|
||||
|
||||
export type OrchestratorShadowRun = {
|
||||
eventId: string;
|
||||
runId: string;
|
||||
requestId: string;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
nativeStatus: string;
|
||||
nativeAttempts: number;
|
||||
shadowStatus: 'succeeded' | 'failed';
|
||||
engine: string;
|
||||
configVersion: number | null;
|
||||
phase: string | null;
|
||||
taskType: string | null;
|
||||
synthetic: boolean;
|
||||
executorAdapter: string | null;
|
||||
latencyMs: number | null;
|
||||
error: { code: string; message: string } | null;
|
||||
observedAt: number;
|
||||
nativeCompletedAt: number | null;
|
||||
};
|
||||
|
||||
export type OrchestratorShadowRunList = {
|
||||
generatedAt: number;
|
||||
window: { hours: number; from: number };
|
||||
metrics: {
|
||||
observations: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
successRate: number | null;
|
||||
failureRate: number | null;
|
||||
latencyP50Ms: number | null;
|
||||
latencyP95Ms: number | null;
|
||||
nativeSucceeded: number;
|
||||
nativeFailed: number;
|
||||
lastObservedAt: number | null;
|
||||
sampled: boolean;
|
||||
};
|
||||
runs: OrchestratorShadowRun[];
|
||||
};
|
||||
|
||||
export type OrchestratorExecutionPlanList = {
|
||||
generatedAt: number;
|
||||
window: { hours: number; from: number };
|
||||
metrics: {
|
||||
decisions: number;
|
||||
candidateSelections: number;
|
||||
candidateSelectionRate: number | null;
|
||||
nativeSelections: number;
|
||||
nativeSucceeded: number;
|
||||
nativeFailed: number;
|
||||
nativeSettledRate: number | null;
|
||||
handoffAllowed: number;
|
||||
distinctSessions: number;
|
||||
lastPlannedAt: number | null;
|
||||
candidateReasons: Array<{ value: string; count: number }>;
|
||||
taskTypes: Array<{ value: string; count: number }>;
|
||||
sampled: boolean;
|
||||
};
|
||||
plans: Array<{
|
||||
eventId: string;
|
||||
runId: string;
|
||||
requestId: string;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
nativeStatus: string;
|
||||
nativeAttempts: number;
|
||||
mode: string | null;
|
||||
candidateEngine: string;
|
||||
effectiveEngine: string;
|
||||
fallbackEngine: string;
|
||||
reason: string | null;
|
||||
candidateReason: string | null;
|
||||
configVersion: number | null;
|
||||
bucket: number | null;
|
||||
taskType: string | null;
|
||||
dryRun: boolean;
|
||||
handoffAllowed: boolean;
|
||||
plannedAt: number;
|
||||
nativeCompletedAt: number | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type OrchestratorCanaryReadinessCheck = {
|
||||
id: string;
|
||||
passed: boolean;
|
||||
actual: string | number | boolean | null;
|
||||
target: string | number | boolean | null;
|
||||
};
|
||||
|
||||
export type OrchestratorCanaryReadiness = {
|
||||
generatedAt: number;
|
||||
ready: boolean;
|
||||
recommendation: 'keep_shadow' | 'manual_canary_review';
|
||||
window: { hours: number; from: number };
|
||||
thresholds: {
|
||||
hours: number;
|
||||
minObservations: number;
|
||||
minSuccessRate: number;
|
||||
maxP95LatencyMs: number;
|
||||
minLatencyCoverageRate: number;
|
||||
minNativeSettledRate: number;
|
||||
minDistinctSessions: number;
|
||||
maxHoursSinceLastObservation: number;
|
||||
};
|
||||
samples: {
|
||||
totalObservations: number;
|
||||
eligibleObservations: number;
|
||||
excludedSynthetic: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
successRate: number | null;
|
||||
latencyCoverageRate: number | null;
|
||||
latencyP95Ms: number | null;
|
||||
nativeSettledRate: number | null;
|
||||
distinctSessions: number;
|
||||
lastObservedAt: number | null;
|
||||
hoursSinceLastObservation: number | null;
|
||||
sampled: boolean;
|
||||
};
|
||||
service: {
|
||||
status: string | null;
|
||||
latencyMs: number | null;
|
||||
checkpointKind: string | null;
|
||||
checkpointDurable: boolean | null;
|
||||
executorJobStoreKind: string | null;
|
||||
executorJobStoreDurable: boolean | null;
|
||||
execution: string | null;
|
||||
};
|
||||
checks: OrchestratorCanaryReadinessCheck[];
|
||||
blockers: string[];
|
||||
failureCodes: Array<{ code: string; count: number }>;
|
||||
};
|
||||
|
||||
export type OrchestratorShadowRunDetail = {
|
||||
native: {
|
||||
runId: string;
|
||||
requestId: string;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
status: string;
|
||||
attempts: number;
|
||||
completedAt: number | null;
|
||||
};
|
||||
shadow: OrchestratorShadowRun | null;
|
||||
remote: {
|
||||
available: boolean;
|
||||
state: Record<string, unknown> | null;
|
||||
events: Array<Record<string, unknown>>;
|
||||
error: { code: string; message: string } | null;
|
||||
executorJob: {
|
||||
available: boolean;
|
||||
state: Record<string, unknown> | null;
|
||||
events: Array<Record<string, unknown>>;
|
||||
error: { code: string; message: string } | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user