feat(v3): T26 两级智能体(大模型规划/审查 + 本地小模型执行,D7)
- run_dual 编排:规划者两阶段 JSON(plan/review,失败回喂重试一次再降级),
redo 时裁决意见回喂执行者,交接上限 agent.max_handoffs(默认 2)
- 交接文档 agent_runs/{id}/handoff.json(智能体版交流文本:instructions/acceptance/exchanges)
- /agent 新增 executor_pool_id;规划者==执行者条目拒绝;整体 token_cap 覆盖两级调用
- ToolLoop 增 emit_final 开关(内层循环不发终态,防前端 SSE 提前收口)
- 前端:执行者选择器 + phase/message 事件渲染(阶段徽标 + 双色消息卡)
- 测试 +3(done/redo/执行者故障),全量 277 passed
- fix(tests): test_config_get_put_reset 增加设置备份/恢复隔离,防止清掉用户真实配置
This commit is contained in:
+20
-5
@@ -311,7 +311,7 @@ export async function listPoolModels(id: string) {
|
||||
// ── 智能体(工具调用) ───────────────────────────────────────────────────────
|
||||
|
||||
export interface AgentEvent {
|
||||
type: 'round' | 'tool_call' | 'tool_result' | 'usage' | 'final'
|
||||
type: 'round' | 'tool_call' | 'tool_result' | 'usage' | 'final' | 'phase' | 'message'
|
||||
ts?: number
|
||||
round?: number
|
||||
name?: string
|
||||
@@ -322,6 +322,12 @@ export interface AgentEvent {
|
||||
completion_tokens?: number
|
||||
reason?: string
|
||||
error?: string
|
||||
// 两级模式(D7)
|
||||
phase?: 'plan' | 'execute' | 'review'
|
||||
handoff?: number
|
||||
model?: string
|
||||
role?: 'planner' | 'executor'
|
||||
content?: string
|
||||
}
|
||||
|
||||
export interface AgentStatus {
|
||||
@@ -337,12 +343,21 @@ export interface AgentStatus {
|
||||
started_at?: number
|
||||
finished_at?: number
|
||||
workspace?: string
|
||||
executor_model?: string
|
||||
mode?: 'single' | 'dual'
|
||||
}
|
||||
|
||||
/** POST /agent:提交智能体任务(workspace 可选:选中工作目录) */
|
||||
export async function startAgent(task: string, poolId?: string, workspace?: string) {
|
||||
const { data } = await http.post<{ request_id: string; status: string; model: string; workspace?: string }>(
|
||||
'/agent', { task, pool_id: poolId || undefined, workspace: workspace || undefined })
|
||||
/** POST /agent:提交智能体任务(executorPoolId 可选:两级模式的执行者/本地小模型) */
|
||||
export async function startAgent(task: string, poolId?: string, workspace?: string, executorPoolId?: string) {
|
||||
const { data } = await http.post<{
|
||||
request_id: string; status: string; model: string
|
||||
workspace?: string; mode?: 'single' | 'dual'; executor_model?: string
|
||||
}>('/agent', {
|
||||
task,
|
||||
pool_id: poolId || undefined,
|
||||
workspace: workspace || undefined,
|
||||
executor_pool_id: executorPoolId || undefined,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@
|
||||
<option value="">默认模型(模型池 Agent 角色 / Architect 设置)</option>
|
||||
<option v-for="e in agentModels" :key="e.id" :value="e.id">{{ e.name }}({{ e.model }})</option>
|
||||
</select>
|
||||
<select v-model="selectedExecutorId" class="model-select executor-select"
|
||||
title="两级模式:大模型拆解/审查,本地小模型执行工具轮">
|
||||
<option value="">单模型模式(规划者全程包办)</option>
|
||||
<option v-for="e in executorCandidates" :key="e.id" :value="e.id">
|
||||
🔧 执行者:{{ e.name }}({{ e.model }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="task-bar">
|
||||
<textarea v-model="task" class="task-input" rows="3"
|
||||
@@ -66,6 +73,17 @@
|
||||
<template v-for="(ev, i) in events" :key="i">
|
||||
<div v-if="ev.type === 'round'" class="ev-round">— 第 {{ ev.round }} 轮 —</div>
|
||||
|
||||
<div v-else-if="ev.type === 'phase'" class="ev-phase" :class="'ph-' + ev.phase">
|
||||
{{ phaseLabel(ev) }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="ev.type === 'message'" class="ev-msg">
|
||||
<div class="ev-title">
|
||||
{{ ev.role === 'planner' ? '🧠 规划者' : '🔧 执行者' }}{{ ev.handoff ? `(第 ${ev.handoff} 轮交接)` : '' }}
|
||||
</div>
|
||||
<pre class="ev-msgbody" :class="ev.role === 'planner' ? 'msg-planner' : 'msg-executor'">{{ ev.content }}</pre>
|
||||
</div>
|
||||
|
||||
<div v-else-if="ev.type === 'tool_call' && ev.name === 'edit_file'" class="ev-card">
|
||||
<div class="ev-title">✏️ 精确编辑 <b>{{ (ev.arguments as any)?.path }}</b></div>
|
||||
<pre class="diff-old">- {{ (ev.arguments as any)?.old_string }}</pre>
|
||||
@@ -171,6 +189,9 @@ const manualPath = ref('')
|
||||
const createIfMissing = ref(false)
|
||||
const allowShell = ref(false)
|
||||
|
||||
// 两级模式
|
||||
const selectedExecutorId = ref('')
|
||||
|
||||
const wsFiles = ref<{ name: string; type: string; size?: number }[]>([])
|
||||
const loadingWs = ref(false)
|
||||
const filePreview = ref<{ path: string; content: string; truncated: boolean } | null>(null)
|
||||
@@ -180,6 +201,18 @@ let _watch: ReturnType<typeof watchAgent> | null = null
|
||||
|
||||
const agentModels = computed(() => poolEntries.value.filter(e => e.enabled))
|
||||
|
||||
const executorCandidates = computed(() =>
|
||||
poolEntries.value
|
||||
.filter(e => e.enabled && e.backend !== 'mock')
|
||||
.sort((a, b) => (a.backend === 'llama_server' ? -1 : 0) - (b.backend === 'llama_server' ? -1 : 0)))
|
||||
|
||||
function phaseLabel(ev: AgentEvent) {
|
||||
const m = ev.model ? ` · ${ev.model}` : ''
|
||||
if (ev.phase === 'plan') return `🧠 规划者 · 拆解任务${m}`
|
||||
if (ev.phase === 'execute') return `🔧 执行者 · 工具执行(第 ${ev.handoff} 轮交接)${m}`
|
||||
return `🔍 规划者 · 审查裁决${m}`
|
||||
}
|
||||
|
||||
const finalResponse = computed(() => ((statusInfo.value as any)?.response || ''))
|
||||
|
||||
const statusLine = computed(() => {
|
||||
@@ -281,7 +314,8 @@ async function run() {
|
||||
events.value = []
|
||||
statusInfo.value = null
|
||||
try {
|
||||
const init = await startAgent(task.value.trim(), selectedPoolId.value, selectedRoot.value)
|
||||
const init = await startAgent(task.value.trim(), selectedPoolId.value, selectedRoot.value,
|
||||
selectedExecutorId.value)
|
||||
if (init.workspace) selectedRoot.value = init.workspace
|
||||
_watch = watchAgent(init.request_id)
|
||||
_watch.subscribe({
|
||||
@@ -481,6 +515,37 @@ onMounted(async () => {
|
||||
font-size: 12px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.ev-phase {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
padding: 4px 14px;
|
||||
margin: 12px auto 8px;
|
||||
width: fit-content;
|
||||
max-width: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ph-plan { color: #1e40af; background: #dbeafe; }
|
||||
.ph-execute { color: #166534; background: #dcfce7; }
|
||||
.ph-review { color: #7c2d12; background: #ffedd5; }
|
||||
.ev-msg { margin-bottom: 8px; }
|
||||
.ev-msgbody {
|
||||
margin: 4px 0 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.msg-planner { background: #eff6ff; color: #1e3a8a; border: 1px solid #bfdbfe; }
|
||||
.msg-executor { background: #f0fdf4; color: #14532d; border: 1px solid #bbf7d0; }
|
||||
.executor-select { flex: 1; max-width: 340px; }
|
||||
.ev-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
Reference in New Issue
Block a user