feat(agents): Phase 8.4-10.5 built-in plugins, bundled skills, coordinator

This commit is contained in:
2026-04-04 23:24:34 +08:00
parent 88955ed550
commit d18167826e
105 changed files with 14780 additions and 15685 deletions

View File

@@ -25,6 +25,123 @@ export interface AgentConfig {
selected_skill_ids?: string[]
}
export interface AgentVisibilityEvent {
event_id: string
event_type: string
timestamp: string
conversation_id?: string | null
agent_id?: string | null
task_id?: string | null
thread_id?: string | null
severity: string
payload: Record<string, unknown>
}
export interface AgentVisibilityVerifier {
conversation_id: string
status?: string | null
summary?: string | null
evidence: Array<Record<string, unknown>>
}
export interface AgentVisibilityTaskSummary {
task_id: string
role?: string | null
owner_agent_id?: string | null
status?: string | null
summary?: string | null
evidence_count: number
}
export interface AgentVisibilityTopologyNode {
agent_id: string
role?: string | null
parent_agent_id?: string | null
source: string
task_count: number
completed_task_count: number
}
export interface AgentVisibilityTopology {
conversation_id: string
root_agent_id?: string | null
current_agent?: string | null
nodes: AgentVisibilityTopologyNode[]
edges: Array<Record<string, string>>
tasks: AgentVisibilityTaskSummary[]
task_hierarchy: Record<string, string[]>
}
export interface AgentVisibilityIsolation {
mode: string
isolation_id?: string | null
workspace_path?: string | null
parent_conversation_id?: string | null
metadata: Record<string, unknown>
}
export interface AgentVisibilityCost {
input_tokens: number
output_tokens: number
total_tokens: number
estimated_cost?: number | null
budget_warning: boolean
currency: string
}
export interface AgentVisibilityCostByAgent {
agent_id: string
input_tokens: number
output_tokens: number
total_tokens: number
estimated_cost?: number | null
budget_warning: boolean
}
export interface AgentVisibilityCostSummary {
conversation_id: string
total: AgentVisibilityCost
thresholds: Record<string, number>
by_agent: AgentVisibilityCostByAgent[]
}
export interface AgentVisibilityToolGovernanceItem {
capability_id: string
tool_name: string
permission_class: string
side_effect_scope: string
supports_retry: boolean
idempotent: boolean
safe_for_parallel_use: boolean
requires_confirmation: boolean
usage_count: number
last_result_preview?: string | null
}
export interface AgentVisibilityToolGovernance {
conversation_id: string
total_tools: number
used_tools: number
items: AgentVisibilityToolGovernanceItem[]
upgrade_candidates: string[]
}
export interface AgentVisibilityRuntimeSummary {
conversation_id: string
execution_mode?: string | null
current_phase?: string | null
current_checkpoint?: string | null
phase_history: Array<Record<string, unknown>>
checkpoint_history: Array<Record<string, unknown>>
verifier: AgentVisibilityVerifier
isolation: AgentVisibilityIsolation
cost: AgentVisibilityCost
topology_node_count: number
active_task_count: number
completed_task_count: number
recent_events: AgentVisibilityEvent[]
}
export const agentApi = {
async getStats(): Promise<AgentStats[]> {
const res = await api.get('/api/agents/stats')
@@ -45,4 +162,39 @@ export const agentApi = {
const res = await api.put(`/api/agents/config/${id}`, data)
return res.data
},
async getRuntimeSummary(conversationId: string): Promise<AgentVisibilityRuntimeSummary> {
const res = await api.get('/api/agents/visibility/runtime-summary', {
params: { conversation_id: conversationId },
})
return res.data
},
async getVisibilityTopology(conversationId: string): Promise<AgentVisibilityTopology> {
const res = await api.get('/api/agents/visibility/topology', {
params: { conversation_id: conversationId },
})
return res.data
},
async getVisibilityVerifier(conversationId: string): Promise<AgentVisibilityVerifier> {
const res = await api.get('/api/agents/visibility/verifier', {
params: { conversation_id: conversationId },
})
return res.data
},
async getVisibilityCost(conversationId: string): Promise<AgentVisibilityCostSummary> {
const res = await api.get('/api/agents/visibility/cost', {
params: { conversation_id: conversationId },
})
return res.data
},
async getVisibilityTools(conversationId: string): Promise<AgentVisibilityToolGovernance> {
const res = await api.get('/api/agents/visibility/tools', {
params: { conversation_id: conversationId },
})
return res.data
},
}

View File

@@ -90,7 +90,10 @@
display: flex;
flex-direction: column;
gap: 12px;
width: 260px;
width: 320px;
max-height: calc(100% - 36px);
overflow: auto;
padding-right: 4px;
}
.hud-panel {
border: 1px solid rgba(0,245,212,0.12);
@@ -120,6 +123,116 @@
color: var(--accent-cyan);
letter-spacing: 0.08em;
}
.runtime-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 12px;
}
.runtime-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.runtime-label {
font-size: 10px;
color: var(--text-dim);
letter-spacing: 0.1em;
}
.runtime-item strong {
font-size: 12px;
color: var(--text-primary);
word-break: break-word;
}
.runtime-note {
margin-top: 10px;
font-size: 11px;
color: var(--text-secondary);
line-height: 1.5;
}
.runtime-meta {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 10px;
color: var(--accent-cyan);
}
.runtime-warning {
color: var(--accent-amber);
}
.runtime-workspace {
margin-top: 8px;
font-size: 10px;
color: var(--text-dim);
word-break: break-all;
}
.stack-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.stack-item {
padding: 8px 10px;
border-radius: 12px;
border: 1px solid rgba(0,245,212,0.08);
background: rgba(10, 18, 30, 0.78);
}
.stack-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 11px;
color: var(--text-primary);
}
.stack-subline {
margin-top: 4px;
font-size: 10px;
color: var(--text-dim);
word-break: break-word;
}
.stack-empty {
font-size: 11px;
color: var(--text-dim);
}
.mini-section + .mini-section {
margin-top: 12px;
}
.mini-title {
margin-bottom: 8px;
font-size: 10px;
letter-spacing: 0.08em;
color: var(--accent-cyan);
}
.event-severity {
text-transform: uppercase;
font-size: 10px;
}
.event-severity.warning {
color: var(--accent-amber);
}
.event-severity.error {
color: var(--accent-red);
}
.threshold-line {
margin-top: 8px;
}
.candidate-list {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.candidate-chip {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 999px;
font-size: 10px;
color: var(--accent-cyan);
border: 1px solid rgba(0,245,212,0.12);
background: rgba(0,245,212,0.06);
}
.canvas-controls {
position: absolute;
right: 20px;

View File

@@ -1,8 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { mount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { useConversationStore } from '@/stores/conversation'
const mocks = vi.hoisted(() => ({
getHierarchyStats: vi.fn(),
getRuntimeSummary: vi.fn(),
getVisibilityTopology: vi.fn(),
getVisibilityVerifier: vi.fn(),
getVisibilityCost: vi.fn(),
getVisibilityTools: vi.fn(),
getConfig: vi.fn(),
updateConfig: vi.fn(),
listSkills: vi.fn(),
@@ -11,6 +18,11 @@ const mocks = vi.hoisted(() => ({
vi.mock('@/api/agent', () => ({
agentApi: {
getHierarchyStats: mocks.getHierarchyStats,
getRuntimeSummary: mocks.getRuntimeSummary,
getVisibilityTopology: mocks.getVisibilityTopology,
getVisibilityVerifier: mocks.getVisibilityVerifier,
getVisibilityCost: mocks.getVisibilityCost,
getVisibilityTools: mocks.getVisibilityTools,
getConfig: mocks.getConfig,
updateConfig: mocks.updateConfig,
},
@@ -69,6 +81,136 @@ const hierarchyStats = {
],
}
const runtimeSummaryFixture = {
conversation_id: 'conv-1',
execution_mode: 'collaboration',
current_phase: 'phase_4_visibility_and_verification',
current_checkpoint: 'visibility.runtime_summary_ready',
phase_history: [],
checkpoint_history: [],
verifier: {
conversation_id: 'conv-1',
status: 'passed',
summary: 'Runtime summary is available.',
evidence: [],
},
isolation: {
mode: 'worktree',
isolation_id: 'iso-1',
workspace_path: '/tmp/jarvis/worktree-1',
parent_conversation_id: 'parent-1',
metadata: { branch: 'jarvis/test-worker' },
},
cost: {
input_tokens: 120,
output_tokens: 80,
total_tokens: 200,
estimated_cost: 0.00156,
budget_warning: true,
currency: 'USD',
},
topology_node_count: 2,
active_task_count: 2,
completed_task_count: 1,
recent_events: [
{
event_id: 'evt-1',
event_type: 'agent.cost.warning',
timestamp: '2026-04-04T10:00:00Z',
severity: 'warning',
payload: {},
},
],
}
const topologyFixture = {
conversation_id: 'conv-1',
root_agent_id: 'master',
current_agent: 'analyst-1234abcd',
nodes: [
{
agent_id: 'master',
role: 'master',
parent_agent_id: null,
source: 'root',
task_count: 0,
completed_task_count: 0,
},
{
agent_id: 'analyst-1234abcd',
role: 'analyst',
parent_agent_id: 'master',
source: 'spawned',
task_count: 2,
completed_task_count: 1,
},
],
edges: [{ parent_agent_id: 'master', child_agent_id: 'analyst-1234abcd' }],
tasks: [],
task_hierarchy: {},
}
const verifierFixture = {
conversation_id: 'conv-1',
status: 'passed',
summary: 'Runtime summary is available.',
evidence: [
{ task_id: 'task-1', status: 'passed' },
],
}
const costFixture = {
conversation_id: 'conv-1',
total: runtimeSummaryFixture.cost,
thresholds: {
total_tokens: 300,
estimated_cost: 0.01,
},
by_agent: [
{
agent_id: 'analyst-1234abcd',
input_tokens: 80,
output_tokens: 70,
total_tokens: 150,
estimated_cost: 0.00129,
budget_warning: true,
},
],
}
const toolGovernanceFixture = {
conversation_id: 'conv-1',
total_tools: 12,
used_tools: 2,
items: [
{
capability_id: 'web_search',
tool_name: 'web_search',
permission_class: 'external',
side_effect_scope: 'network',
supports_retry: true,
idempotent: true,
safe_for_parallel_use: true,
requires_confirmation: false,
usage_count: 1,
last_result_preview: 'ok',
},
{
capability_id: 'create_reminder',
tool_name: 'create_reminder',
permission_class: 'write',
side_effect_scope: 'local_state',
supports_retry: false,
idempotent: false,
safe_for_parallel_use: false,
requires_confirmation: true,
usage_count: 1,
last_result_preview: 'created',
},
],
upgrade_candidates: ['worktree_manager', 'cost_inspector'],
}
const skillFixtures = [
{
id: 'skill-schedule-1',
@@ -124,6 +266,8 @@ describe('agents page pcb command center', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
setActivePinia(createPinia())
useConversationStore().setCurrentConversation('conv-1')
vi.stubGlobal('ResizeObserver', class {
observe() {}
disconnect() {}
@@ -143,6 +287,11 @@ describe('agents page pcb command center', () => {
})),
})
mocks.getHierarchyStats.mockResolvedValue(hierarchyStats)
mocks.getRuntimeSummary.mockResolvedValue(runtimeSummaryFixture)
mocks.getVisibilityTopology.mockResolvedValue(topologyFixture)
mocks.getVisibilityVerifier.mockResolvedValue(verifierFixture)
mocks.getVisibilityCost.mockResolvedValue(costFixture)
mocks.getVisibilityTools.mockResolvedValue(toolGovernanceFixture)
mocks.getConfig.mockImplementation(async (id: string) => ({
id,
name: id === 'schedule_planner' ? 'SCHEDULE PLANNER' : id.toUpperCase(),
@@ -161,6 +310,8 @@ describe('agents page pcb command center', () => {
await Promise.resolve()
await Promise.resolve()
expect(mocks.getRuntimeSummary).toHaveBeenCalledWith('conv-1')
expect(mocks.getVisibilityTopology).toHaveBeenCalledWith('conv-1')
expect(wrapper.find('[data-testid="commander-skills"]').exists()).toBe(false)
const plannerBus = wrapper.get('[data-testid="bus-link-schedule_planner"]')
@@ -367,5 +518,49 @@ describe('agents page pcb command center', () => {
expect(wrapper.get('[data-testid="linked-skills-empty"]').text()).toContain('暂无可关联技能')
})
it('renders runtime summary from the active conversation', async () => {
const wrapper = mount(AgentsPage)
await flushPromises()
await flushPromises()
expect(mocks.getRuntimeSummary).toHaveBeenCalledWith('conv-1')
const runtimeSummary = wrapper.get('[data-testid="runtime-summary"]')
expect(runtimeSummary.text()).toContain('collaboration')
expect(runtimeSummary.text()).toContain('phase_4_visibility_and_verification')
expect(runtimeSummary.text()).toContain('visibility.runtime_summary_ready')
expect(runtimeSummary.text()).toContain('passed')
expect(runtimeSummary.text()).toContain('worktree')
expect(runtimeSummary.text()).toContain('200')
expect(runtimeSummary.text()).toContain('Cost $0.001560')
expect(runtimeSummary.text()).toContain('Tasks 1/2')
expect(runtimeSummary.text()).toContain('Nodes 2')
expect(runtimeSummary.text()).toContain('Budget warning')
expect(wrapper.text()).toContain('/tmp/jarvis/worktree-1')
})
it('renders operator drilldown panels for events topology verifier and tool governance', async () => {
const wrapper = mount(AgentsPage)
await flushPromises()
await flushPromises()
expect(wrapper.get('[data-testid="runtime-events-panel"]').text()).toContain('agent.cost.warning')
expect(wrapper.get('[data-testid="runtime-drilldown-panel"]').text()).toContain('analyst-1234abcd')
expect(wrapper.get('[data-testid="runtime-drilldown-panel"]').text()).toContain('task-1')
expect(wrapper.get('[data-testid="runtime-governance-panel"]').text()).toContain('web_search')
expect(wrapper.get('[data-testid="runtime-governance-panel"]').text()).toContain('worktree_manager')
expect(wrapper.get('[data-testid="runtime-governance-panel"]').text()).toContain('Thresholds: 300 tk / $0.010000')
})
it('shows a prompt when no conversation is selected', async () => {
useConversationStore().setCurrentConversation(null)
const wrapper = mount(AgentsPage)
await flushPromises()
await flushPromises()
expect(mocks.getRuntimeSummary).not.toHaveBeenCalled()
expect(mocks.getVisibilityTopology).not.toHaveBeenCalled()
expect(wrapper.get('[data-testid="runtime-summary"]').text()).toContain('请选择一条会话以查看运行时摘要')
})
})

View File

@@ -1,10 +1,23 @@
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { COMMANDER_SKILLS, DEFAULT_AGENTS, MAIN_AGENT_ORDER, RELATION_LABELS, SUB_COMMANDERS } from '@/data/agents'
import type { Agent, CommanderSkill, MainAgentId, SubCommander } from '@/data/agents'
import { agentApi, type AgentHierarchyStats, type AgentStats } from '@/api/agent'
import {
agentApi,
type AgentHierarchyStats,
type AgentStats,
type AgentVisibilityCostSummary,
type AgentVisibilityRuntimeSummary,
type AgentVisibilityToolGovernance,
type AgentVisibilityTopology,
type AgentVisibilityVerifier,
} from '@/api/agent'
import { skillApi, type Skill } from '@/api/skill'
import { useConversationStore } from '@/stores/conversation'
export function useAgentsPage() {
const conversationStore = useConversationStore()
const { currentConversationId } = storeToRefs(conversationStore)
const NODE_W = 200
const NODE_H = 170
@@ -46,6 +59,59 @@ interface AgentDraft {
selectedSkillIds: string[]
}
interface RuntimeSummaryCard {
executionMode: string
currentPhase: string
currentCheckpoint: string
verifierStatus: string
verifierSummary: string
verifierEvidence: Array<{
label: string
status: string
}>
isolationMode: string
workspacePath: string | null
totalTokens: number
estimatedCost: string
budgetWarning: boolean
activeTaskCount: number
completedTaskCount: number
topologyNodeCount: number
topologyNodes: Array<{
agentId: string
role: string
taskCount: number
completedTaskCount: number
}>
costByAgent: Array<{
agentId: string
totalTokens: number
estimatedCost: string
budgetWarning: boolean
}>
costThresholds: {
totalTokens: number
estimatedCost: string
}
toolGovernance: Array<{
toolName: string
permissionClass: string
sideEffectScope: string
usageCount: number
}>
toolGovernanceTotals: {
totalTools: number
usedTools: number
}
upgradeCandidates: string[]
recentEvents: Array<{
eventId: string
eventType: string
timestamp: string
severity: string
}>
}
const mainAgents = computed(() => MAIN_AGENT_ORDER.map(id => localAgents[id]))
const childAgents = SUB_COMMANDERS
const relationLabels = RELATION_LABELS
@@ -102,11 +168,41 @@ const isPanning = ref(false)
const panStart = reactive({ x: 0, y: 0 })
const panOrigin = reactive({ x: 0, y: 0 })
const connectionStatus = ref<'connected' | 'disconnected'>('disconnected')
const connectionLabel = computed(() => connectionStatus.value === 'connected' ? '瀹炴椂鍚屾' : '绂荤嚎妯″紡')
const connectionLabel = computed(() => connectionStatus.value === 'connected' ? '实时同步' : '离线模式')
const zoomPercent = computed(() => `${Math.round(zoom.value * 100)}%`)
const activeMainId = ref<string | null>(null)
const activeChildId = ref<string | null>(null)
const agentData = reactive<Record<string, AgentRuntimeState>>({})
const runtimeSummary = ref<RuntimeSummaryCard>({
executionMode: 'direct',
currentPhase: 'phase_0_bootstrap',
currentCheckpoint: 'bootstrap.initialized',
verifierStatus: 'unknown',
verifierSummary: '暂无运行时摘要',
verifierEvidence: [],
isolationMode: 'none',
workspacePath: null,
totalTokens: 0,
estimatedCost: '$0.000000',
budgetWarning: false,
activeTaskCount: 0,
completedTaskCount: 0,
topologyNodeCount: 0,
topologyNodes: [],
costByAgent: [],
costThresholds: {
totalTokens: 0,
estimatedCost: '$0.000000',
},
toolGovernance: [],
toolGovernanceTotals: {
totalTools: 0,
usedTools: 0,
},
upgradeCandidates: [],
recentEvents: [],
})
const runtimeConversationId = computed(() => currentConversationId.value)
const localAgents = reactive<Record<string, Agent>>(
Object.fromEntries([
@@ -117,7 +213,7 @@ const localAgents = reactive<Record<string, Agent>>(
role: child.role,
roleKey: child.id,
description: child.description,
systemPrompt: `${child.role}锛?{child.description}`,
systemPrompt: `${child.role}: ${child.description}`,
enabled: true,
})),
].map(agent => [agent.id, { ...agent }]))
@@ -482,7 +578,7 @@ function setRuntimeState(agentId: string, state: AgentStats) {
}
function applyHierarchyStats(stats: AgentHierarchyStats) {
agentData.master = { callCount: 47, currentTask: '鍗忚皟缁勭粐閾捐矾', status: 'active' }
agentData.master = { callCount: 47, currentTask: '协调协作链路', status: 'active' }
let nextMain: string | null = null
let nextChild: string | null = null
@@ -533,14 +629,14 @@ function buildOfflineStats() {
current_task: null,
status: 'active',
sub_commanders: [
{ agent_id: 'schedule_analysis', call_count: 4, current_task: '姊崇悊浠婃棩閲嶇偣', status: 'active' },
{ agent_id: 'schedule_analysis', call_count: 4, current_task: '梳理今日重点', status: 'active' },
{ agent_id: 'schedule_planning', call_count: 9, current_task: null, status: 'idle' },
],
},
{
agent_id: 'executor',
call_count: 8,
current_task: '鍒涘缓鏂囨。',
current_task: '创建文档',
status: 'idle',
sub_commanders: [
{ agent_id: 'executor_tasks', call_count: 8, current_task: null, status: 'idle' },
@@ -571,16 +667,135 @@ function buildOfflineStats() {
} satisfies AgentHierarchyStats
}
function applyRuntimeSummary(
summary: AgentVisibilityRuntimeSummary,
topology?: AgentVisibilityTopology,
verifier?: AgentVisibilityVerifier,
costSummary?: AgentVisibilityCostSummary,
toolGovernance?: AgentVisibilityToolGovernance,
) {
const verifierPayload = verifier || summary.verifier
const costPayload = costSummary || {
conversation_id: summary.conversation_id,
total: summary.cost,
thresholds: {},
by_agent: [],
}
runtimeSummary.value = {
executionMode: summary.execution_mode || 'direct',
currentPhase: summary.current_phase || 'phase_0_bootstrap',
currentCheckpoint: summary.current_checkpoint || 'bootstrap.initialized',
verifierStatus: verifierPayload.status || 'unknown',
verifierSummary: verifierPayload.summary || '暂无 verifier 结论',
verifierEvidence: (verifierPayload.evidence || []).slice(0, 4).map((entry, index) => ({
label: String(entry.task_id || entry.event_type || `evidence-${index}`),
status: String(entry.status || entry.summary || 'available'),
})),
isolationMode: summary.isolation.mode || 'none',
workspacePath: summary.isolation.workspace_path || null,
totalTokens: summary.cost.total_tokens,
estimatedCost: `$${(summary.cost.estimated_cost || 0).toFixed(6)}`,
budgetWarning: Boolean(summary.cost.budget_warning),
activeTaskCount: summary.active_task_count,
completedTaskCount: summary.completed_task_count,
topologyNodeCount: summary.topology_node_count,
topologyNodes: (topology?.nodes || []).slice(0, 5).map(node => ({
agentId: node.agent_id,
role: node.role || 'unknown',
taskCount: node.task_count,
completedTaskCount: node.completed_task_count,
})),
costByAgent: (costPayload.by_agent || []).slice(0, 4).map(item => ({
agentId: item.agent_id,
totalTokens: item.total_tokens,
estimatedCost: `$${(item.estimated_cost || 0).toFixed(6)}`,
budgetWarning: Boolean(item.budget_warning),
})),
costThresholds: {
totalTokens: Number(costPayload.thresholds.total_tokens || 0),
estimatedCost: `$${Number(costPayload.thresholds.estimated_cost || 0).toFixed(6)}`,
},
toolGovernance: (toolGovernance?.items || []).slice(0, 6).map(item => ({
toolName: item.tool_name,
permissionClass: item.permission_class,
sideEffectScope: item.side_effect_scope,
usageCount: item.usage_count,
})),
toolGovernanceTotals: {
totalTools: Number(toolGovernance?.total_tools || 0),
usedTools: Number(toolGovernance?.used_tools || 0),
},
upgradeCandidates: [...(toolGovernance?.upgrade_candidates || [])],
recentEvents: summary.recent_events.map(event => ({
eventId: event.event_id,
eventType: event.event_type,
timestamp: event.timestamp,
severity: event.severity,
})),
}
}
async function refreshStats() {
loading.value = true
try {
const stats = await agentApi.getHierarchyStats()
applyHierarchyStats(stats)
if (runtimeConversationId.value) {
try {
const [summary, topology, verifier, costSummary, tools] = await Promise.all([
agentApi.getRuntimeSummary(runtimeConversationId.value),
agentApi.getVisibilityTopology(runtimeConversationId.value),
agentApi.getVisibilityVerifier(runtimeConversationId.value),
agentApi.getVisibilityCost(runtimeConversationId.value),
agentApi.getVisibilityTools(runtimeConversationId.value),
])
applyRuntimeSummary(summary, topology, verifier, costSummary, tools)
} catch {
runtimeSummary.value = {
...runtimeSummary.value,
verifierSummary: '运行时摘要暂不可用',
}
}
} else {
runtimeSummary.value = {
...runtimeSummary.value,
verifierSummary: '请选择一条会话以查看运行时摘要',
}
}
connectionStatus.value = 'connected'
stopDemoRouteCycle()
} catch {
connectionStatus.value = 'disconnected'
applyHierarchyStats(buildOfflineStats())
runtimeSummary.value = {
executionMode: 'direct',
currentPhase: 'offline_demo',
currentCheckpoint: 'offline.demo_mode',
verifierStatus: 'offline',
verifierSummary: '当前使用离线演示数据',
verifierEvidence: [],
isolationMode: 'none',
workspacePath: null,
totalTokens: 0,
estimatedCost: '$0.000000',
budgetWarning: false,
activeTaskCount: 0,
completedTaskCount: 0,
topologyNodeCount: 0,
topologyNodes: [],
costByAgent: [],
costThresholds: {
totalTokens: 0,
estimatedCost: '$0.000000',
},
toolGovernance: [],
toolGovernanceTotals: {
totalTools: 0,
usedTools: 0,
},
upgradeCandidates: [],
recentEvents: [],
}
startDemoRouteCycle()
} finally {
loading.value = false
@@ -710,6 +925,7 @@ onUnmounted(() => {
selectedNodePackages,
selectedNodeSkills,
agentData,
runtimeSummary,
localAgents,
viewportStyle,
stageStyle,

View File

@@ -38,6 +38,146 @@
<div class="route-main">{{ activeMainRouteLabel }}</div>
<div class="route-child">{{ activeChildRouteLabel }}</div>
</div>
<div class="hud-panel runtime-summary" data-testid="runtime-summary">
<div class="hud-title">RUNTIME SUMMARY</div>
<div class="runtime-grid">
<div class="runtime-item">
<span class="runtime-label">MODE</span>
<strong>{{ runtimeSummary.executionMode }}</strong>
</div>
<div class="runtime-item">
<span class="runtime-label">PHASE</span>
<strong>{{ runtimeSummary.currentPhase }}</strong>
</div>
<div class="runtime-item">
<span class="runtime-label">CHECKPOINT</span>
<strong>{{ runtimeSummary.currentCheckpoint }}</strong>
</div>
<div class="runtime-item">
<span class="runtime-label">VERIFIER</span>
<strong>{{ runtimeSummary.verifierStatus }}</strong>
</div>
<div class="runtime-item">
<span class="runtime-label">ISOLATION</span>
<strong>{{ runtimeSummary.isolationMode }}</strong>
</div>
<div class="runtime-item">
<span class="runtime-label">TOKENS</span>
<strong>{{ runtimeSummary.totalTokens }}</strong>
</div>
</div>
<div class="runtime-note">{{ runtimeSummary.verifierSummary }}</div>
<div class="runtime-meta">
<span>Cost {{ runtimeSummary.estimatedCost }}</span>
<span>Tasks {{ runtimeSummary.completedTaskCount }}/{{ runtimeSummary.activeTaskCount }}</span>
<span>Nodes {{ runtimeSummary.topologyNodeCount }}</span>
<span v-if="runtimeSummary.budgetWarning" class="runtime-warning">Budget warning</span>
</div>
<div v-if="runtimeSummary.workspacePath" class="runtime-workspace">{{ runtimeSummary.workspacePath }}</div>
</div>
<div class="hud-panel runtime-events" data-testid="runtime-events-panel">
<div class="hud-title">RECENT EVENTS</div>
<div v-if="runtimeSummary.recentEvents.length" class="stack-list">
<div
v-for="event in runtimeSummary.recentEvents.slice(0, 5)"
:key="event.eventId"
class="stack-item"
>
<div class="stack-line">
<strong>{{ event.eventType }}</strong>
<span class="event-severity" :class="event.severity">{{ event.severity }}</span>
</div>
<div class="stack-subline">{{ event.timestamp }}</div>
</div>
</div>
<div v-else class="stack-empty">暂无 recent events</div>
</div>
<div class="hud-panel runtime-drilldown" data-testid="runtime-drilldown-panel">
<div class="hud-title">TOPOLOGY & VERIFIER</div>
<div class="mini-section">
<div class="mini-title">Topology</div>
<div v-if="runtimeSummary.topologyNodes.length" class="stack-list">
<div
v-for="node in runtimeSummary.topologyNodes"
:key="node.agentId"
class="stack-item"
>
<div class="stack-line">
<strong>{{ node.agentId }}</strong>
<span>{{ node.role }}</span>
</div>
<div class="stack-subline">Tasks {{ node.completedTaskCount }}/{{ node.taskCount }}</div>
</div>
</div>
<div v-else class="stack-empty">暂无 topology 详情</div>
</div>
<div class="mini-section">
<div class="mini-title">Verifier Evidence</div>
<div v-if="runtimeSummary.verifierEvidence.length" class="stack-list">
<div
v-for="entry in runtimeSummary.verifierEvidence"
:key="entry.label"
class="stack-item"
>
<div class="stack-line">
<strong>{{ entry.label }}</strong>
<span>{{ entry.status }}</span>
</div>
</div>
</div>
<div v-else class="stack-empty">暂无 verifier evidence</div>
</div>
</div>
<div class="hud-panel runtime-governance" data-testid="runtime-governance-panel">
<div class="hud-title">COST & TOOLS</div>
<div class="mini-section">
<div class="mini-title">Cost By Agent</div>
<div v-if="runtimeSummary.costByAgent.length" class="stack-list">
<div
v-for="item in runtimeSummary.costByAgent"
:key="item.agentId"
class="stack-item"
>
<div class="stack-line">
<strong>{{ item.agentId }}</strong>
<span>{{ item.totalTokens }} tk</span>
</div>
<div class="stack-subline">
{{ item.estimatedCost }}
<span v-if="item.budgetWarning" class="runtime-warning">warning</span>
</div>
</div>
</div>
<div v-else class="stack-empty">暂无 cost breakdown</div>
<div class="stack-subline threshold-line">
Thresholds: {{ runtimeSummary.costThresholds.totalTokens }} tk / {{ runtimeSummary.costThresholds.estimatedCost }}
</div>
</div>
<div class="mini-section">
<div class="mini-title">Tool Governance</div>
<div class="stack-subline">Used {{ runtimeSummary.toolGovernanceTotals.usedTools }}/{{ runtimeSummary.toolGovernanceTotals.totalTools }}</div>
<div v-if="runtimeSummary.toolGovernance.length" class="stack-list">
<div
v-for="tool in runtimeSummary.toolGovernance"
:key="tool.toolName"
class="stack-item"
>
<div class="stack-line">
<strong>{{ tool.toolName }}</strong>
<span>{{ tool.usageCount }}x</span>
</div>
<div class="stack-subline">{{ tool.permissionClass }} · {{ tool.sideEffectScope }}</div>
</div>
</div>
<div v-else class="stack-empty">暂无 tool governance 数据</div>
<div v-if="runtimeSummary.upgradeCandidates.length" class="candidate-list">
<span v-for="candidate in runtimeSummary.upgradeCandidates" :key="candidate" class="candidate-chip">{{ candidate }}</span>
</div>
</div>
</div>
</div>
<div class="nodes-viewport" :style="viewportStyle">
@@ -104,7 +244,7 @@
<div class="node-desc">{{ getAgentDesc('master') }}</div>
<div class="node-footer">
<span class="node-stat">
<span class="stat-label">璋冪敤</span>
<span class="stat-label">调用</span>
<span class="stat-val">{{ agentData.master?.callCount || 0 }}</span>
</span>
<span v-if="agentData.master?.currentTask" class="node-task-tag">{{ agentData.master.currentTask }}</span>
@@ -135,7 +275,7 @@
<div class="node-desc">{{ getAgentDesc(agent.id) }}</div>
<div class="node-footer">
<span class="node-stat">
<span class="stat-label">璋冪敤</span>
<span class="stat-label">调用</span>
<span class="stat-val">{{ agentData[agent.id]?.callCount || 0 }}</span>
</span>
<span v-if="agentData[agent.id]?.currentTask" class="node-task-tag">{{ agentData[agent.id]?.currentTask }}</span>
@@ -168,7 +308,7 @@
<div class="node-desc">{{ child.description }}</div>
<div class="node-footer">
<span class="node-stat">
<span class="stat-label">璋冪敤</span>
<span class="stat-label">调用</span>
<span class="stat-val">{{ agentData[child.id]?.callCount || 0 }}</span>
</span>
<span v-if="agentData[child.id]?.currentTask" class="node-task-tag">{{ agentData[child.id]?.currentTask }}</span>
@@ -181,13 +321,13 @@
</div>
<div class="canvas-controls">
<button class="control-chip zoom-chip" @click="zoomOut" title="缂╁皬瑙嗗浘">
<button class="control-chip zoom-chip" @click="zoomOut" title="缩小视图">
<span class="chip-symbol">-</span>
</button>
<button class="control-chip zoom-readout" @click="resetView" title="閲嶇疆瑙嗗浘">
<button class="control-chip zoom-readout" @click="resetView" title="重置视图">
<span class="chip-value">{{ zoomPercent }}</span>
</button>
<button class="control-chip zoom-chip" @click="zoomIn" title="鏀惧ぇ瑙嗗浘">
<button class="control-chip zoom-chip" @click="zoomIn" title="放大视图">
<span class="chip-symbol">+</span>
</button>
</div>
@@ -239,7 +379,7 @@
<span>{{ pkg.stateLabel }}</span>
</span>
</div>
<div v-if="skillsLoading" class="linked-skills-state" data-testid="linked-skills-loading">鍔犺浇鎶鑳戒腑...</div>
<div v-if="skillsLoading" class="linked-skills-state" data-testid="linked-skills-loading">加载技能中...</div>
<div v-else-if="skillsError" class="linked-skills-state linked-skills-error" data-testid="linked-skills-error">{{ skillsError }}</div>
<div v-else-if="saveError" class="linked-skills-state linked-skills-error" data-testid="linked-skills-save-error">{{ saveError }}</div>
<div v-else-if="selectedNodeSkills.length === 0" class="linked-skills-state" data-testid="linked-skills-empty">
@@ -264,7 +404,7 @@
<span class="linked-skill-name">{{ skill.name }}</span>
<span class="linked-skill-agent-type">{{ skill.agent_type }}</span>
</div>
<div class="linked-skill-desc">{{ skill.description || '鏆傛棤鎻忚堪' }}</div>
<div class="linked-skill-desc">{{ skill.description || '暂无描述' }}</div>
<div v-if="skill.tools.length > 0" class="linked-skill-tools">
<span v-for="tool in skill.tools" :key="tool" class="linked-skill-tool">{{ tool }}</span>
</div>
@@ -273,10 +413,10 @@
</div>
</div>
<div class="drawer-actions">
<button class="btn-secondary" data-testid="linked-skills-reset" @click="resetConfig">閲嶇疆</button>
<button class="btn-secondary" data-testid="linked-skills-reset" @click="resetConfig">重置</button>
<button class="btn-primary" data-testid="linked-skills-save" @click="saveConfig" :disabled="saving">
<span v-if="saving" class="btn-loader"></span>
{{ saving ? '淇濆瓨涓?..' : '淇濆瓨閰嶇疆' }}
{{ saving ? '保存中...' : '保存配置' }}
</button>
</div>
</div>
@@ -293,11 +433,11 @@
<div class="modal-body">
<div class="form-group">
<label class="form-label">// AGENT NAME</label>
<input v-model="newAgent.name" type="text" class="form-input" placeholder="渚嬪: CODER" />
<input v-model="newAgent.name" type="text" class="form-input" placeholder="例如: CODER" />
</div>
<div class="form-group">
<label class="form-label">// ROLE KEY (鑻辨枃鍞竴鏍囪瘑)</label>
<input v-model="newAgent.roleKey" type="text" class="form-input" placeholder="渚嬪: coder" />
<label class="form-label">// ROLE KEY (英文唯一标识)</label>
<input v-model="newAgent.roleKey" type="text" class="form-input" placeholder="例如: coder" />
</div>
<div class="form-group">
<label class="form-label">// ROLE</label>
@@ -305,15 +445,15 @@
</div>
<div class="form-group">
<label class="form-label">// DESCRIPTION</label>
<textarea v-model="newAgent.description" class="form-textarea" rows="2" placeholder="鎻忚堪姝?Agent 鐨勮亴璐?.."></textarea>
<textarea v-model="newAgent.description" class="form-textarea" rows="2" placeholder="描述该 Agent 的职责..."></textarea>
</div>
<div class="form-group flex-1">
<label class="form-label">// SYSTEM PROMPT</label>
<textarea v-model="newAgent.systemPrompt" class="form-textarea code-textarea" rows="6" placeholder="杈撳叆绯荤粺鎻愮ず璇?.."></textarea>
<textarea v-model="newAgent.systemPrompt" class="form-textarea code-textarea" rows="6" placeholder="输入系统提示词..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="addModalOpen = false">鍙栨秷</button>
<button class="btn-secondary" @click="addModalOpen = false">取消</button>
<button class="btn-primary" @click="addAgent" :disabled="!newAgent.name || !newAgent.roleKey">创建智能体</button>
</div>
</div>
@@ -360,6 +500,7 @@ const {
selectedNodePackages,
selectedNodeSkills,
agentData,
runtimeSummary,
localAgents,
viewportStyle,
stageStyle,