Advance human live tests and simplify report evidence

This commit is contained in:
hy committed 2026-03-18 16:26:54 +08:00
1 parent b3fcd23fa0
commit bb3a374b61
12 files changed
+890 -55

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
- 当涉及数据库、LanceDB、表职责、存储分层或检索存储实现时,请阅读 `doc/database.md`
- 当涉及目录规范、文件归位、模块边界或工程结构时,请阅读 `doc/project-structure.md`
- 当涉及 UI 风格、布局规范或组件交互约束时,请阅读 `doc/ui-style.md`
- 当涉及测试相关内容,包括测试目录、raw log、report 规则或历史总数据 / 最新总数据口径时,请阅读 `doc/test.md`;
- 当涉及测试相关内容,包括测试目录、raw log、report 规则、证据展示方式或历史总数据 / 最新总数据口径时,请阅读 `doc/test.md`;
## 2. 注意事项
- 禁止使用 `any` 与 `unknown`
+1 -1
View File
@@ -18,4 +18,4 @@
- [ui-style.md](/Users/test/work-space/NCurator-Desktop/doc/ui-style.md)
- 说明界面设计风格、布局规则和组件使用规范。
- [test.md](/Users/test/work-space/NCurator-Desktop/doc/test.md)
- 说明测试目录、测试用例设计准则,以及 report 中历史总数据 / 最新总数据的统计口径。
- 说明测试目录、测试用例设计准则,以及 report 中证据展示和历史总数据 / 最新总数据的统计口径。
+14 -5
View File
@@ -24,20 +24,24 @@
- [x] 落地 `human-long-task-with-corrections`
- [x] 落地 `human-long-task-approval-branch`
- [x] 落地 `human-live-direct-docx-edit`
- [x] 落地 `human-live-direct-kb-answer`
- [x] 落地 `human-live-evidence-insufficient-safe-stop`
- [x] 落地 `human-live-terminal-docx-handoff`
- [x] 落地 `human-live-long-task-project-brief`
- [x] 跑完 Stage 2 `backend/human` non-live 测试并刷新 `tests/logs/ai/backend/human/{basic,boundary,scenario}/**`
## In Progress
- [ ] 落地 `human-live-direct-kb-answer`
- [ ] 落地 `human-live-kb-docx-bridge`
- [ ] 落地 `human-live-evidence-insufficient-safe-stop`
- [ ] 跑完 Stage 2 backend/human 测试并刷新 `tests/logs/ai/**`
- [ ] 基于新的 human logs 更新 `tests/report/ai/report.md`
- [x] 基于新的 human logs 更新 `tests/report/ai/report.md`
## Blocked
- 当前无新增外部阻塞
- `human-live-kb-docx-bridge` 在 2026-03-18 的 primary `gpt-5` live 运行里仍未形成稳定交付:
- `20260318-150310-015`:`invalid_input`,summary=`524 status code (no body)`,只留下两次 `kb_hybrid_search_chunks`
- 为区分模型/服务问题与编排问题,已在 2026-03-18 补跑 stronger `gpt-5.2`:
- `20260318-150906-789`:在 `kb_hybrid_search_chunks -> kb_list_documents` 后判断剩余步数不足,未进入 docx handoff
- `20260318-151207-536`:`bounded_loop_error`,错误转入多次 `terminal_run_command`,仍未产出 preview artifact
- `human` 阶段 live 结论仍不完整,Stage 2 尚不能仅凭 non-live case 视为完成
## Evidence
@@ -45,6 +49,11 @@
- `doc/ai-workbench-test/plan.md`
- `tests/ai/backend/human/{basic,boundary,scenario,live}/*.test.ts`
- `tests/logs/ai/backend/human/**`
- `tests/logs/ai/backend/human/live/human-live-direct-kb-answer/20260318-145113-697/*`
- `tests/logs/ai/backend/human/live/human-live-evidence-insufficient-safe-stop/20260318-150735-678/*`
- `tests/logs/ai/backend/human/live/human-live-kb-docx-bridge/20260318-150310-015/*`
- `tests/logs/ai/backend/human/live/human-live-kb-docx-bridge/20260318-150906-789/*`
- `tests/logs/ai/backend/human/live/human-live-kb-docx-bridge/20260318-151207-536/*`
## Exit Criteria
+6 -4
View File
@@ -423,7 +423,9 @@ tests/report/<domain>/
- `失败分析`
- `当前评估`
- `下一步建议`
- `证据` 必须列出对应 run 的 `meta.json`、`timeline.jsonl`、`artifacts.json` 路径;如果有关键附件,也应一并列出。
- `证据` 默认只列出能定位该条证据的最小信息,例如 `caseId`、`runId`、`status`。
- 不要在 report 正文里机械展开 `meta.json`、`timeline.jsonl`、`artifacts.json` 这些文件名。
- 只有当失败分析或结论明确依赖某个关键附件时,才额外补充附件名称或用途说明。
- `通过率` 只写当前节点覆盖范围内的统计,不混入其他节点数据。
- `通过率` 固定拆成两组:
- `历史总数据`:`totalRuns`、`passedRuns`、`failedRuns`、`historicalSuccessRate`
@@ -464,7 +466,7 @@ tests/report/<domain>/
##### 证据
- `20260315-101530-112` | passed | `tests/logs/ai/backend/tools/terminal/terminal-basic-command/20260315-101530-112/meta.json` | `tests/logs/ai/backend/tools/terminal/terminal-basic-command/20260315-101530-112/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/terminal-basic-command/20260315-101530-112/artifacts.json`
- `terminal-basic-command` | run=`20260315-101530-112` | status=`passed`
##### 通过率
@@ -499,8 +501,8 @@ tests/report/<domain>/
##### 证据
- `20260315-103002-008` | passed | `tests/logs/ai/e2e/workbench/workbench-plan-artifact-smoke/20260315-103002-008/meta.json` | `tests/logs/ai/e2e/workbench/workbench-plan-artifact-smoke/20260315-103002-008/timeline.jsonl` | `tests/logs/ai/e2e/workbench/workbench-plan-artifact-smoke/20260315-103002-008/artifacts.json`
- artifact references: `tests/logs/ai/e2e/workbench/workbench-plan-artifact-smoke/20260315-103002-008/artifacts/trace/playwright-trace.zip`
- `workbench-plan-artifact-smoke` | run=`20260315-103002-008` | status=`passed`
- 关键附件: playwright trace
##### 通过率
@@ -171,6 +171,261 @@ const toResultData = (value: JsonValue | undefined, key: string): JsonObject | u
return { [key]: value }
}
type DecisionDocumentRef = {
documentName?: string
documentId?: string
}
const clipText = (value: string, maxChars = 120): string => {
const text = value.trim()
if (text.length <= maxChars) return text
return `${text.slice(0, maxChars - 3)}...`
}
const parseJsonValueText = (value: string): JsonValue | undefined => {
const trimmed = value.trim()
if (!trimmed) return undefined
try {
return JSON.parse(trimmed) as JsonValue
} catch {
const arrayStart = trimmed.indexOf('[')
const arrayEnd = trimmed.lastIndexOf(']')
if (arrayStart >= 0 && arrayEnd > arrayStart) {
try {
return JSON.parse(trimmed.slice(arrayStart, arrayEnd + 1)) as JsonValue
} catch {
return undefined
}
}
const objectStart = trimmed.indexOf('{')
const objectEnd = trimmed.lastIndexOf('}')
if (objectStart >= 0 && objectEnd > objectStart) {
try {
return JSON.parse(trimmed.slice(objectStart, objectEnd + 1)) as JsonValue
} catch {
return undefined
}
}
}
return undefined
}
const extractDocumentRefFromRecord = (value: JsonObject): DecisionDocumentRef | null => {
const documentName = toOptionalText(value.document_name) || toOptionalText(value.name)
const documentId = toOptionalText(value.document_id) || toOptionalText(value.id)
if (!documentName && !documentId) return null
return {
documentName,
documentId
}
}
const collectDocumentRefsFromValue = (value: JsonValue | undefined): DecisionDocumentRef[] => {
if (Array.isArray(value)) {
return value.flatMap((item) => collectDocumentRefsFromValue(item))
}
if (!isJsonObject(value)) return []
const refs: DecisionDocumentRef[] = []
const directRef = extractDocumentRefFromRecord(value)
if (directRef) refs.push(directRef)
const raw = toOptionalText(value.raw)
if (raw) {
refs.push(...collectDocumentRefsFromValue(parseJsonValueText(raw)))
}
if (Array.isArray(value.items)) {
refs.push(...collectDocumentRefsFromValue(value.items))
}
return refs
}
const dedupeDocumentRefs = (refs: DecisionDocumentRef[]): DecisionDocumentRef[] => {
const seen = new Set<string>()
const unique: DecisionDocumentRef[] = []
for (const ref of refs) {
const key = `${ref.documentId || ''}::${ref.documentName || ''}`
if (!ref.documentId && !ref.documentName) continue
if (seen.has(key)) continue
seen.add(key)
unique.push(ref)
}
return unique
}
const formatDocumentRef = (ref: DecisionDocumentRef): string | undefined => {
if (ref.documentName && ref.documentId) return `${ref.documentName} (${ref.documentId})`
return ref.documentName || ref.documentId
}
const countValueItems = (value: JsonValue | undefined): number => {
if (Array.isArray(value)) return value.length
if (isJsonObject(value) && Array.isArray(value.items)) return value.items.length
return value === undefined ? 0 : 1
}
const extractPrimaryEvidenceText = (value: JsonValue | undefined): string | undefined => {
if (Array.isArray(value)) {
for (const item of value) {
const nested = extractPrimaryEvidenceText(item)
if (nested) return nested
}
return undefined
}
if (typeof value === 'string') {
const text = value.trim()
return text ? clipText(text) : undefined
}
if (!isJsonObject(value)) return undefined
const directText =
toOptionalText(value.text) || toOptionalText(value.summary) || toOptionalText(value.message)
if (directText) return clipText(directText)
const raw = toOptionalText(value.raw)
if (raw) {
const nested = extractPrimaryEvidenceText(parseJsonValueText(raw))
if (nested) return nested
}
if (Array.isArray(value.items)) {
return extractPrimaryEvidenceText(value.items)
}
return undefined
}
const resolveToolFamily = (toolName: string | undefined): string | undefined => {
const normalized = String(toolName || '').trim()
if (!normalized) return undefined
const separatorIndex = normalized.indexOf('_')
return separatorIndex > 0 ? normalized.slice(0, separatorIndex) : normalized
}
const countTrailingSameToolName = (
rows: Array<{ toolName: string; toolFamily: string; summary: string }>
): number => {
if (rows.length === 0) return 0
let count = 1
for (let index = rows.length - 1; index > 0; index -= 1) {
if (rows[index]?.toolName !== rows[index - 1]?.toolName) break
count += 1
}
return count
}
const countTrailingSameToolFamily = (
rows: Array<{ toolName: string; toolFamily: string; summary: string }>
): number => {
if (rows.length === 0) return 0
let count = 1
for (let index = rows.length - 1; index > 0; index -= 1) {
if (rows[index]?.toolFamily !== rows[index - 1]?.toolFamily) break
count += 1
}
return count
}
const countTrailingSameSummary = (
rows: Array<{ toolName: string; toolFamily: string; summary: string }>
): number => {
if (rows.length === 0) return 0
let count = 1
for (let index = rows.length - 1; index > 0; index -= 1) {
if (rows[index]?.summary !== rows[index - 1]?.summary) break
count += 1
}
return count
}
const buildDecisionStateText = (input: { state: HumanRunState; ctx: AiRunContext }): string => {
const { state, ctx } = input
const remainingSteps = Math.max(0, state.maxSteps - state.steps)
const objective = String(state.input.objective || '')
const recentToolRows = state.stepHistory
.filter(
(step) =>
step.action === 'invoke_tool' && typeof step.toolName === 'string' && step.toolName.length > 0
)
.slice(-4)
.map((step) => ({
toolName: String(step.toolName || ''),
toolFamily: resolveToolFamily(String(step.toolName || '')) || 'unknown',
resultStatus: step.resultStatus || 'unknown',
summary: clipText(String(step.summary || ''))
}))
let lastObservationSignals: JsonValue = {
kind: 'none'
}
if (state.lastObservation?.kind === 'skill_loaded') {
lastObservationSignals = {
kind: 'skill_loaded',
skillId: state.lastObservation.skillId,
activeSkillIds: state.lastObservation.activeSkillIds,
activeToolNames: state.lastObservation.activeToolNames
}
} else if (state.lastObservation?.kind === 'terminal') {
lastObservationSignals = {
kind: 'terminal',
status: state.lastObservation.status,
code: state.lastObservation.code,
summary: clipText(state.lastObservation.summary),
hasData: state.lastObservation.data !== undefined
}
} else if (state.lastObservation?.kind === 'tool_result') {
const uniqueRefs = dedupeDocumentRefs(collectDocumentRefsFromValue(state.lastObservation.data))
const primarySourceRef = uniqueRefs.map((ref) => formatDocumentRef(ref)).find(Boolean)
lastObservationSignals = {
kind: 'tool_result',
toolName: state.lastObservation.toolName,
toolFamily: resolveToolFamily(state.lastObservation.toolName) || 'unknown',
ok: state.lastObservation.ok,
code: state.lastObservation.code,
itemCount: countValueItems(state.lastObservation.data),
uniqueSourceRefCount: uniqueRefs.length,
primarySourceRef: primarySourceRef || 'none',
primaryEvidenceText: extractPrimaryEvidenceText(state.lastObservation.data) || 'none',
summary: clipText(state.lastObservation.summary)
}
}
return toJsonText({
stepBudget: {
currentStep: state.steps + 1,
maxSteps: state.maxSteps,
remainingSteps,
lowBudget: remainingSteps <= 2
},
taskSignals: {
deliveryKind: state.deliveryKind,
workspaceBound: Boolean(ctx.workspace),
selectedDocumentCount: ctx.selectedDocumentIds?.length || 0,
mentionsDocx: /\.docx\b|docx/i.test(objective),
mentionsPreview: /preview|预览/i.test(objective),
mentionsSave: /save|保存|另存/i.test(objective)
},
progressSignals: {
activeSkillCount: state.activeSkillIds.length,
activeToolCount: state.activeTools.length,
recentToolCalls: recentToolRows,
trailingSameToolCount: countTrailingSameToolName(recentToolRows),
trailingSameToolFamilyCount: countTrailingSameToolFamily(recentToolRows),
trailingSameSummaryCount: countTrailingSameSummary(recentToolRows),
recentErrorCount: recentToolRows.filter((row) => row.resultStatus === 'error').length
},
lastObservationSignals
})
}
const buildDecisionPrompt = (input: { state: HumanRunState; ctx: AiRunContext }) => {
const { state } = input
const prompt = buildHumanBrainPrompt({
@@ -181,6 +436,7 @@ const buildDecisionPrompt = (input: { state: HumanRunState; ctx: AiRunContext })
availableSkillCardsText: toJsonText(state.availableSkillCards),
loadedSkillsText: serializeLoadedSkills(state),
activeToolsText: serializeActiveTools(state),
decisionStateText: buildDecisionStateText(input),
lastObservationText: state.lastObservation ? toJsonText(state.lastObservation) : 'none',
stepHistoryText:
state.stepHistory.length > 0
@@ -98,10 +98,14 @@ const deriveLocalSourceStatus = (state: HumanRunState): {
summary: string
} => {
const joined = [state.summary, state.deliveryMarkdown].join('\n')
const mentionsInsufficient =
joined.includes('本地资料不足') ||
joined.includes('是否允许继续使用非本地信息') ||
joined.includes('非本地信息')
const insufficientMarkers = [
'本地资料不足',
'本地证据不足',
'当前本地资料不足',
'当前本地证据不足',
'本地范围不足'
]
const mentionsInsufficient = insufficientMarkers.some((marker) => joined.includes(marker))
if (mentionsInsufficient) {
return {
status: 'insufficient',
+5 -1
View File
@@ -9,6 +9,7 @@ export type HumanBrainPromptInput = {
availableSkillCardsText: string
loadedSkillsText: string
activeToolsText: string
decisionStateText: string
lastObservationText: string
stepHistoryText: string
}
@@ -19,7 +20,9 @@ export const buildHumanBrainPrompt = (input: HumanBrainPromptInput) => {
'【工具边界】只有 active tools 中列出的工具可以 invoke_tool。',
'【完成标准】仅当任务目标已满足且证据充分时才 complete_task;summary 必须写清结论与关键证据。',
'【结果收敛】若任务目标已明确要求 resultData 字段,且最近 observation 已经提供这些字段所需证据,下一步必须 complete_task 并写入 resultData,不要继续为同一证据反复调用 KB 或 shell。',
'【KB 收敛】若 kb_hybrid_search_chunks 的 observation 已经出现唯一的 document_name/document_id,或首条文本已经直接给出答案,不要再调用 kb_list_documents / 再次搜索来“确认同一证据”;应直接 complete_task。',
'【证据收敛】若最近 observation 已经足以唯一支撑当前结论或下一交付,优先 complete_task;不要为了“再次确认同一证据”继续调用等价工具。',
'【无新增信息】若最近连续几步没有带来新信息,优先切换能力、缩小目标或明确资料不足;不要只是在同类工具之间来回试探。',
'【最小路径】优先完成满足 objective 与 delivery kind 的最小交付路径;若任务只要求 answer、handoff 或 preview,不要扩展成非必需的保存、重复检索或额外产物。',
'【阻塞标准】workspace_required/approval_denied 等不可恢复条件必须 block_task。',
'【反循环】最近一步没有带来新信息时,禁止重复相同 toolName + args;应切换 skill、调整参数或收敛。',
'【禁止过度复核】同一文件名、同一命令输出或同一 KB 证据已经足够支撑结论时,不要再做“再次确认”式工具调用。',
@@ -45,6 +48,7 @@ export const buildHumanBrainPrompt = (input: HumanBrainPromptInput) => {
{ label: 'Available Skill Cards', value: input.availableSkillCardsText },
{ label: 'Loaded Skills', value: input.loadedSkillsText },
{ label: 'Active Tools', value: input.activeToolsText },
{ label: 'Decision State', value: input.decisionStateText },
{ label: 'Last Observation', value: input.lastObservationText },
{ label: 'Recent Steps', value: input.stepHistoryText }
])
+14 -9
View File
@@ -4,6 +4,7 @@ import { hasLiveLlmConfigInEnv, loadLiveLlmConfigFromEnv } from '../../../harnes
import type { BackendTestRuntime, TestCaseDefinition } from '../../../harness/backend/test-types'
type AiHumanCaseDefinition<TResult> = Omit<TestCaseDefinition<TResult>, 'domain' | 'layer' | 'suite'>
const HUMAN_LIVE_TEST_TIMEOUT_MS = 300_000
export const defineAiHumanCase = <TResult>(definition: AiHumanCaseDefinition<TResult>): void => {
it(definition.caseId, async () => {
@@ -25,15 +26,19 @@ export const defineAiHumanLiveCase = <TResult>(definition: AiHumanCaseDefinition
const runtimeModelName = loadLiveLlmConfigFromEnv('primary').model_name
it(definition.caseId, async () => {
await runBackendCase({
...definition,
domain: 'ai',
layer: 'backend',
suite: 'human',
runtimeModelName
})
})
it(
definition.caseId,
async () => {
await runBackendCase({
...definition,
domain: 'ai',
layer: 'backend',
suite: 'human',
runtimeModelName
})
},
HUMAN_LIVE_TEST_TIMEOUT_MS
)
}
export const loadPrimaryLiveHumanConfig = () => loadLiveLlmConfigFromEnv('primary')
@@ -0,0 +1,167 @@
import type { DocumentRecord, JsonValue, SearchResult } from '../../../../../src/shared/types'
import { createHumanTestRunner } from '../../../../harness/backend/humanHarness'
import {
defineAiHumanLiveCase,
getEventsByType,
loadPrimaryLiveHumanConfig
} from '../helpers'
const releaseChecklistDocument: DocumentRecord = {
id: 'doc-beta',
name: 'release-checklist.md',
source_type: 'file',
created_at: 1710000000000,
import_status: 2
}
const getFirstPreviewRecord = (
value: JsonValue | undefined
): Record<string, JsonValue> | null => {
if (!value) return null
if (Array.isArray(value)) {
const first = value[0]
if (!first || Array.isArray(first) || typeof first !== 'object') return null
return first as Record<string, JsonValue>
}
if (typeof value !== 'object') return null
return value as Record<string, JsonValue>
}
defineAiHumanLiveCase({
caseId: 'human-live-direct-kb-answer',
caseType: 'live',
groups: ['live'],
description: 'live 模型直接使用本地 KB 回答问题,并在结果里保留可回看的文档引用。',
fixtures: [],
runMode: 'single',
execute: async (runtime) => {
const runner = createHumanTestRunner(
runtime,
{
bindWorkspace: false,
selectedDocumentIds: ['doc-beta'],
liveModelConfig: loadPrimaryLiveHumanConfig(),
documentsStoreOverrides: {
hybridSearch: async () => {
const rows: SearchResult[] = [
{
id: 'chunk-beta-1',
text: 'Approval flow still needs regression coverage before release.',
document_id: 'doc-beta',
document_name: 'release-checklist.md',
source_type: 'file',
_score: 0.91
}
]
return rows
},
vectorSearch: async () => {
const rows: SearchResult[] = [
{
id: 'chunk-beta-1',
text: 'Approval flow still needs regression coverage before release.',
document_id: 'doc-beta',
document_name: 'release-checklist.md',
source_type: 'file',
_score: 0.91
}
]
return rows
},
ftsSearch: async () => {
const rows: SearchResult[] = [
{
id: 'chunk-beta-1',
text: 'Approval flow still needs regression coverage before release.',
document_id: 'doc-beta',
document_name: 'release-checklist.md',
source_type: 'file',
_score: 0.91
}
]
return rows
},
listDocuments: async () => ({
items: [releaseChecklistDocument],
total: 1
}),
getDocumentById: async (id: string) => (id === 'doc-beta' ? releaseChecklistDocument : null)
}
},
{ maxSteps: 6 }
)
const output = await runner.run(
{
taskId: runtime.taskId,
objective:
'请仅使用当前本地资料范围回答 approval flow regression coverage 是否已经完成。先做本地 KB 检索,一旦拿到直接证据就停止,不要重复确认。最终答复要明确说明是否完成,并引用依据文档;答复里保留短语 "regression coverage"。'
},
{
deliveryKind: 'final'
}
)
const toolResults = getEventsByType(runtime, 'tool_call_result')
const firstKbResult = toolResults.find((event) => event.toolName.startsWith('kb_'))
const kbPreview = firstKbResult ? getFirstPreviewRecord(firstKbResult.outputPreview) : null
return {
output,
kbPreview,
toolCalls: getEventsByType(runtime, 'tool_call_started'),
deliveries: getEventsByType(runtime, 'task_delivery_ready')
}
},
assert: async (result, runtime) => {
const joinedOutputText = [
result.output.summary,
result.output.deliveryMarkdown,
JSON.stringify(result.output.resultData || {})
].join('\n')
runtime.assertThat(
result.output.status === 'completed' &&
result.output.resultCode === 'ok' &&
result.output.localSourceStatus === 'used',
{
name: 'human-live-kb-answer-completed',
message: 'live human 应能基于本地 KB 形成完成态问答交付',
data: {
status: result.output.status,
resultCode: result.output.resultCode,
localSourceStatus: result.output.localSourceStatus,
summary: result.output.summary,
resultData: result.output.resultData,
toolNames: result.toolCalls.map((event) => event.toolName)
}
}
)
runtime.assertThat(
Boolean(result.kbPreview) &&
String(result.kbPreview?.document_name || '').includes('release-checklist.md') &&
String(result.kbPreview?.document_id || '').includes('doc-beta'),
{
name: 'human-live-kb-answer-evidence',
message: 'live human 应先拿到可回看的 KB 文档证据'
}
)
runtime.assertThat(
joinedOutputText.includes('regression coverage') &&
(joinedOutputText.includes('release-checklist.md') || joinedOutputText.includes('doc-beta')),
{
name: 'human-live-kb-answer-result-data',
message: 'live human 应在交付中保留答案关键短语和文档引用'
}
)
runtime.assertThat(
result.toolCalls.some((event) => event.toolName.startsWith('kb_')) &&
result.toolCalls.every((event) => event.toolName.startsWith('kb_')) &&
result.deliveries.length === 1,
{
name: 'human-live-kb-answer-events',
message: 'live human 直接问答应只留下 KB 检索和最终交付事件'
}
)
}
})
@@ -0,0 +1,93 @@
import { createHumanTestRunner } from '../../../../harness/backend/humanHarness'
import {
defineAiHumanLiveCase,
getEventsByType,
loadPrimaryLiveHumanConfig
} from '../helpers'
defineAiHumanLiveCase({
caseId: 'human-live-evidence-insufficient-safe-stop',
caseType: 'live',
groups: ['live'],
description: 'live 模型在本地证据不足时应安全收敛,而不是编造结论。',
fixtures: [],
runMode: 'single',
execute: async (runtime) => {
const runner = createHumanTestRunner(
runtime,
{
bindWorkspace: false,
selectedDocumentIds: ['doc-gap'],
liveModelConfig: loadPrimaryLiveHumanConfig(),
documentsStoreOverrides: {
hybridSearch: async () => [],
vectorSearch: async () => [],
ftsSearch: async () => [],
listDocuments: async () => ({
items: [],
total: 0
}),
getDocumentById: async () => null
}
},
{ maxSteps: 6 }
)
const output = await runner.run(
{
taskId: runtime.taskId,
objective:
'请仅使用当前本地资料范围确认 final owner 与 timeline。如果当前本地资料不足,不要猜测,也不要继续重复检索;请直接给出最终答复,答复中必须包含“本地资料不足”和“是否允许继续使用非本地信息”,并在 resultData 中保留 gap_summary 与 next_step。'
},
{
deliveryKind: 'final'
}
)
return {
output,
toolCalls: getEventsByType(runtime, 'tool_call_started'),
toolResults: getEventsByType(runtime, 'tool_call_result'),
deliveries: getEventsByType(runtime, 'task_delivery_ready')
}
},
assert: async (result, runtime) => {
const kbToolResults = result.toolResults.filter((event) => event.toolName.startsWith('kb_'))
const emptyKbResults = kbToolResults.every(
(event) => Array.isArray(event.outputPreview) && event.outputPreview.length === 0
)
runtime.assertThat(
result.output.localSourceStatus === 'insufficient' &&
result.output.deliveryMarkdown.includes('本地资料不足') &&
result.output.deliveryMarkdown.includes('是否允许继续使用非本地信息'),
{
name: 'human-live-safe-stop-status',
message: 'live human 证据不足时应明确说明本地不足,并询问是否允许使用非本地信息'
}
)
runtime.assertThat(
typeof result.output.resultData?.gap_summary === 'string' &&
result.output.resultData.gap_summary.length > 0 &&
typeof result.output.resultData?.next_step === 'string' &&
result.output.resultData.next_step.length > 0,
{
name: 'human-live-safe-stop-result-data',
message: 'live human 安全收敛时应保留结构化缺口与下一步说明'
}
)
runtime.assertThat(
kbToolResults.length >= 1 &&
emptyKbResults &&
result.toolCalls.every((event) => event.toolName.startsWith('kb_')),
{
name: 'human-live-safe-stop-no-hallucination',
message: 'live human 应只基于本地 KB 空结果收敛,不应伪造其他工具证据'
}
)
runtime.assertThat(result.toolCalls.length <= 2 && result.deliveries.length === 1, {
name: 'human-live-safe-stop-no-loop',
message: 'live human 证据不足时不应陷入重复检索循环'
})
}
})
@@ -0,0 +1,233 @@
import fs from 'node:fs/promises'
import type { DocumentRecord, JsonValue, SearchResult } from '../../../../../src/shared/types'
import { createDocxToolHarness, writeTestDocxFile } from '../../../../harness/backend/aiToolHarness'
import { createHumanTestRunner } from '../../../../harness/backend/humanHarness'
import {
defineAiHumanLiveCase,
getEventsByType,
loadPrimaryLiveHumanConfig
} from '../helpers'
const releasePolicyDocument: DocumentRecord = {
id: 'doc-policy',
name: 'release-policy.md',
source_type: 'file',
created_at: 1710000000000,
import_status: 2
}
const fileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.stat(filePath)
return true
} catch {
return false
}
}
const getFirstPreviewRecord = (
value: JsonValue | undefined
): Record<string, JsonValue> | null => {
if (!value) return null
if (Array.isArray(value)) {
const first = value[0]
if (!first || Array.isArray(first) || typeof first !== 'object') return null
return first as Record<string, JsonValue>
}
if (typeof value !== 'object') return null
return value as Record<string, JsonValue>
}
defineAiHumanLiveCase({
caseId: 'human-live-kb-docx-bridge',
caseType: 'live',
groups: ['live'],
description: 'live 模型先检索本地 KB,再把规范要求落到 docx preview 并交付。',
fixtures: [],
runMode: 'single',
execute: async (runtime) => {
await writeTestDocxFile(runtime.workspaceRoot, 'release-check.docx', [
{ type: 'heading', text: 'Release Checklist', level: 1 },
{ type: 'paragraph', text: 'Checklist summary: pending policy sync.' },
{ type: 'paragraph', text: 'Risk note: TBD.' }
])
const runner = createHumanTestRunner(
runtime,
{
selectedDocumentIds: ['doc-policy'],
liveModelConfig: loadPrimaryLiveHumanConfig(),
documentsStoreOverrides: {
hybridSearch: async () => {
const rows: SearchResult[] = [
{
id: 'chunk-policy-1',
text: 'Release reports must mention approval-denied coverage and terminal artifact assertion before sign-off.',
document_id: 'doc-policy',
document_name: 'release-policy.md',
source_type: 'file',
_score: 0.93
}
]
return rows
},
vectorSearch: async () => {
const rows: SearchResult[] = [
{
id: 'chunk-policy-1',
text: 'Release reports must mention approval-denied coverage and terminal artifact assertion before sign-off.',
document_id: 'doc-policy',
document_name: 'release-policy.md',
source_type: 'file',
_score: 0.93
}
]
return rows
},
ftsSearch: async () => {
const rows: SearchResult[] = [
{
id: 'chunk-policy-1',
text: 'Release reports must mention approval-denied coverage and terminal artifact assertion before sign-off.',
document_id: 'doc-policy',
document_name: 'release-policy.md',
source_type: 'file',
_score: 0.93
}
]
return rows
},
listDocuments: async () => ({
items: [releasePolicyDocument],
total: 1
}),
getDocumentById: async (id: string) => (id === 'doc-policy' ? releasePolicyDocument : null)
}
},
{ maxSteps: 8 }
)
const output = await runner.run(
{
taskId: runtime.taskId,
objective:
'请先做一次当前本地发布规范检索,从 release-policy.md 提炼要求后直接修改 release-check.docx。拿到条款后不要再列文档或重复检索,也不需要 save;只需把摘要段改成包含 “approval-denied coverage and terminal artifact assertion before sign-off”,生成可审阅 preview,并以 handoff 形式交付,在交付里说明来源文档。'
},
{
deliveryKind: 'handoff'
}
)
const toolCalls = getEventsByType(runtime, 'tool_call_started')
const toolResults = getEventsByType(runtime, 'tool_call_result')
const fileArtifacts = getEventsByType(runtime, 'file_artifact')
const previewArtifact = fileArtifacts.find((event) => event.artifactRole === 'preview')
const previewPath = previewArtifact?.path || ''
const previewExists = previewPath ? await fileExists(previewPath) : false
const firstKbResult = toolResults.find((event) => event.toolName.startsWith('kb_'))
const kbPreview = firstKbResult ? getFirstPreviewRecord(firstKbResult.outputPreview) : null
if (!previewExists) {
return {
output,
kbPreview,
previewExists,
previewPath,
summarySearchOk: false,
summaryMatchCount: 0,
toolCalls,
fileArtifacts
}
}
const inspectionHarness = createDocxToolHarness(runtime)
try {
const openPreview = await inspectionHarness.invoke<{
ok: boolean
docId: string
}>('docx_open', {
sourcePath: previewPath
})
const summarySearch = await inspectionHarness.invoke<{
ok: boolean
matches: Array<{ blockId: string; text: string }>
}>('docx_search', {
docId: openPreview.docId,
query: 'approval-denied coverage and terminal artifact assertion before sign-off',
mode: 'literal'
})
return {
output,
kbPreview,
previewExists,
previewPath,
summarySearchOk: summarySearch.ok,
summaryMatchCount: summarySearch.matches.length,
toolCalls,
fileArtifacts
}
} finally {
await inspectionHarness.release()
}
},
assert: async (result, runtime) => {
const kbCallIndex = result.toolCalls.findIndex((event) => event.toolName.startsWith('kb_'))
const docxApplyIndex = result.toolCalls.findIndex((event) => event.toolName === 'docx_apply_ops')
const joinedOutputText = [
result.output.summary,
result.output.deliveryMarkdown,
JSON.stringify(result.output.resultData || {})
].join('\n')
runtime.assertThat(
result.output.status === 'completed' &&
result.output.resultCode === 'ok' &&
result.output.localSourceStatus === 'used',
{
name: 'human-live-kb-docx-bridge-completed',
message: 'live human 的 KB -> DOCX bridge 应形成完成态交付',
data: {
status: result.output.status,
resultCode: result.output.resultCode,
localSourceStatus: result.output.localSourceStatus,
summary: result.output.summary,
resultData: result.output.resultData,
previewExists: result.previewExists,
previewPath: result.previewPath,
toolNames: result.toolCalls.map((event) => event.toolName)
}
}
)
runtime.assertThat(
Boolean(result.kbPreview) &&
String(result.kbPreview?.document_name || '').includes('release-policy.md') &&
String(result.kbPreview?.document_id || '').includes('doc-policy'),
{
name: 'human-live-kb-docx-bridge-kb-evidence',
message: 'live human 应先拿到本地发布规范证据'
}
)
runtime.assertThat(
result.previewExists &&
result.summarySearchOk &&
result.summaryMatchCount >= 1 &&
joinedOutputText.includes('release-policy.md'),
{
name: 'human-live-kb-docx-bridge-preview',
message: 'live human 应把 KB 规范写入 preview,并在交付中保留来源文档'
}
)
runtime.assertThat(
kbCallIndex >= 0 &&
docxApplyIndex > kbCallIndex &&
result.toolCalls.some((event) => event.toolName === 'docx_open') &&
result.fileArtifacts.some((event) => event.artifactRole === 'preview'),
{
name: 'human-live-kb-docx-bridge-events',
message: 'live human 应先检索 KB,再进入 docx 编辑和 preview 交付链路'
}
)
}
})
+92 -30
View File
@@ -4,19 +4,22 @@
### 分析范围
- raw logs 范围: 当前仓库内 `tests/logs/ai/backend/tools/**`
- 已分析 scope: `ai/backend/tools/docx/basic`、`ai/backend/tools/docx/boundary`、`ai/backend/tools/docx/scenario`、`ai/backend/tools/terminal/basic`、`ai/backend/tools/terminal/boundary`、`ai/backend/tools/terminal/scenario`、`ai/backend/tools/kb/basic`、`ai/backend/tools/kb/boundary`
- 未纳入本次分析: Stage 2 `ai/backend/human/**` 与旧版 flat scope `ai/backend/tools/docx`、`ai/backend/tools/terminal`、`ai/backend/tools/kb`
- raw logs 范围: 当前仓库内 `tests/logs/ai/backend/tools/**`、`tests/logs/ai/backend/human/**`
- 已分析 scope: `ai/backend/tools/docx/basic`、`ai/backend/tools/docx/boundary`、`ai/backend/tools/docx/scenario`、`ai/backend/tools/terminal/basic`、`ai/backend/tools/terminal/boundary`、`ai/backend/tools/terminal/scenario`、`ai/backend/tools/kb/basic`、`ai/backend/tools/kb/boundary`、`ai/backend/human/basic`、`ai/backend/human/boundary`、`ai/backend/human/scenario`、`ai/backend/human/live`
- 说明: 当前 `ai/backend/human/live` analysis window 只包含本轮补跑并已落日志的 `human-live-direct-kb-answer`、`human-live-evidence-insufficient-safe-stop`、`human-live-kb-docx-bridge`;此前已实现的 `human-live-direct-docx-edit`、`human-live-terminal-docx-handoff`、`human-live-long-task-project-brief` 未在本轮重跑,因此不计入当前 `human/live` 的 latestCaseCount
- 未纳入本次分析: 旧版 flat scope `ai/backend/tools/docx`、`ai/backend/tools/terminal`、`ai/backend/tools/kb`
### 总体结论
- 历史总数据: `40` 次 raw runs,其中 `40` 次 passed、`0` 次 failed,historicalSuccessRate=`100.0%`
- 最新总数据: `20 / 20` 个 case 的最新一轮 run 为 passed,latestSuccessRate=`100.0%`
- 当前结论: report 已重置为 Stage 1 `backend/tools` 口径,当前新的分层 scope 下 `docx / terminal / kb` 三组工具基线都已跑通。
- 历史总数据: `168` 次 raw runs,其中 `147` 次 passed、`21` 次 failed,historicalSuccessRate=`87.5%`
- 最新总数据: `39` 个 case 中有 `38` 个 case 的最新一轮 run 为 passed,latestSuccessRate=`97.4%`
- 当前结论: Stage 1 `backend/tools` 仍保持 `20 / 20 latest passed`;Stage 2 `backend/human` 的 `basic / boundary / scenario` 最新口径为 `16 / 16 passed`,但当前 `live` analysis window 仅 `2 / 3 passed`,`human-live-kb-docx-bridge` 仍未形成稳定 live 交付。
- 解释: `historicalSuccessRate` 已包含 2026-03-18 为诊断 `human-live-kb-docx-bridge` 所做的多轮 live 补跑,因此阶段判断应优先看 latest 口径,而不是把这些诊断性失败等同于稳定回归失败。
### 下一阶段建议
- 先保持当前 Stage 1 `tools` 分层目录和命名稳定,继续把它作为后续 `human` / `entire` 的底层回归基线。
- 保持当前 Stage 1 `tools` 分层目录和 Stage 2 `human` non-live 基线稳定。
- 下一步优先修复 `human-live-kb-docx-bridge` 的 live 收敛与跨工具 handoff,再补跑本轮未覆盖的其他 `human live` case,之后再进入 `context` 阶段。
## backend
@@ -43,16 +46,16 @@
- `boundary`: `4` 次 raw runs,最新 `2 / 2 passed`
- `scenario`: `6` 次 raw runs,最新 `3 / 3 passed`
##### 最新证据
##### 证据
- `tool-docx-search-modes-and-scope` | `20260318-131740-749` | passed | `tests/logs/ai/backend/tools/docx/basic/tool-docx-search-modes-and-scope/20260318-131740-749/meta.json` | `tests/logs/ai/backend/tools/docx/basic/tool-docx-search-modes-and-scope/20260318-131740-749/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/basic/tool-docx-search-modes-and-scope/20260318-131740-749/artifacts.json`
- `tool-docx-structure-read` | `20260318-131740-748` | passed | `tests/logs/ai/backend/tools/docx/basic/tool-docx-structure-read/20260318-131740-748/meta.json` | `tests/logs/ai/backend/tools/docx/basic/tool-docx-structure-read/20260318-131740-748/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/basic/tool-docx-structure-read/20260318-131740-748/artifacts.json`
- `tool-docx-targeting-and-ambiguous-search` | `20260318-131740-749` | passed | `tests/logs/ai/backend/tools/docx/basic/tool-docx-targeting-and-ambiguous-search/20260318-131740-749/meta.json` | `tests/logs/ai/backend/tools/docx/basic/tool-docx-targeting-and-ambiguous-search/20260318-131740-749/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/basic/tool-docx-targeting-and-ambiguous-search/20260318-131740-749/artifacts.json`
- `tool-docx-preview-validate-rollback` | `20260318-131740-749` | passed | `tests/logs/ai/backend/tools/docx/boundary/tool-docx-preview-validate-rollback/20260318-131740-749/meta.json` | `tests/logs/ai/backend/tools/docx/boundary/tool-docx-preview-validate-rollback/20260318-131740-749/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/boundary/tool-docx-preview-validate-rollback/20260318-131740-749/artifacts.json`
- `tool-docx-save-conflict-and-overwrite` | `20260318-131740-749` | passed | `tests/logs/ai/backend/tools/docx/boundary/tool-docx-save-conflict-and-overwrite/20260318-131740-749/meta.json` | `tests/logs/ai/backend/tools/docx/boundary/tool-docx-save-conflict-and-overwrite/20260318-131740-749/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/boundary/tool-docx-save-conflict-and-overwrite/20260318-131740-749/artifacts.json`
- `tool-docx-mixed-structural-edit` | `20260318-131740-763` | passed | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-mixed-structural-edit/20260318-131740-763/meta.json` | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-mixed-structural-edit/20260318-131740-763/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-mixed-structural-edit/20260318-131740-763/artifacts.json`
- `tool-docx-multi-document-session-isolation` | `20260318-131740-749` | passed | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-multi-document-session-isolation/20260318-131740-749/meta.json` | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-multi-document-session-isolation/20260318-131740-749/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-multi-document-session-isolation/20260318-131740-749/artifacts.json`
- `tool-docx-table-and-inline-rewrite` | `20260318-131740-770` | passed | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-table-and-inline-rewrite/20260318-131740-770/meta.json` | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-table-and-inline-rewrite/20260318-131740-770/timeline.jsonl` | `tests/logs/ai/backend/tools/docx/scenario/tool-docx-table-and-inline-rewrite/20260318-131740-770/artifacts.json`
- `tool-docx-search-modes-and-scope` | run=`20260318-131740-749` | status=`passed`
- `tool-docx-structure-read` | run=`20260318-131740-748` | status=`passed`
- `tool-docx-targeting-and-ambiguous-search` | run=`20260318-131740-749` | status=`passed`
- `tool-docx-preview-validate-rollback` | run=`20260318-131740-749` | status=`passed`
- `tool-docx-save-conflict-and-overwrite` | run=`20260318-131740-749` | status=`passed`
- `tool-docx-mixed-structural-edit` | run=`20260318-131740-763` | status=`passed`
- `tool-docx-multi-document-session-isolation` | run=`20260318-131740-749` | status=`passed`
- `tool-docx-table-and-inline-rewrite` | run=`20260318-131740-770` | status=`passed`
##### 失败分析
@@ -83,15 +86,15 @@
- `boundary`: `6` 次 raw runs,最新 `3 / 3 passed`
- `scenario`: `4` 次 raw runs,最新 `2 / 2 passed`
##### 最新证据
##### 证据
- `tool-terminal-path-resolution` | `20260318-131741-370` | passed | `tests/logs/ai/backend/tools/terminal/basic/tool-terminal-path-resolution/20260318-131741-370/meta.json` | `tests/logs/ai/backend/tools/terminal/basic/tool-terminal-path-resolution/20260318-131741-370/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/basic/tool-terminal-path-resolution/20260318-131741-370/artifacts.json`
- `tool-terminal-workspace-material-discovery` | `20260318-131741-389` | passed | `tests/logs/ai/backend/tools/terminal/basic/tool-terminal-workspace-material-discovery/20260318-131741-389/meta.json` | `tests/logs/ai/backend/tools/terminal/basic/tool-terminal-workspace-material-discovery/20260318-131741-389/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/basic/tool-terminal-workspace-material-discovery/20260318-131741-389/artifacts.json`
- `tool-terminal-approval-and-boundary` | `20260318-131741-430` | passed | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-approval-and-boundary/20260318-131741-430/meta.json` | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-approval-and-boundary/20260318-131741-430/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-approval-and-boundary/20260318-131741-430/artifacts.json`
- `tool-terminal-approval-denied-follow-up` | `20260318-131741-505` | passed | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-approval-denied-follow-up/20260318-131741-505/meta.json` | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-approval-denied-follow-up/20260318-131741-505/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-approval-denied-follow-up/20260318-131741-505/artifacts.json`
- `tool-terminal-failure-and-long-output` | `20260318-131740-773` | passed | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-failure-and-long-output/20260318-131740-773/meta.json` | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-failure-and-long-output/20260318-131740-773/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/boundary/tool-terminal-failure-and-long-output/20260318-131740-773/artifacts.json`
- `tool-terminal-artifact-producing-command` | `20260318-131741-439` | passed | `tests/logs/ai/backend/tools/terminal/scenario/tool-terminal-artifact-producing-command/20260318-131741-439/meta.json` | `tests/logs/ai/backend/tools/terminal/scenario/tool-terminal-artifact-producing-command/20260318-131741-439/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/scenario/tool-terminal-artifact-producing-command/20260318-131741-439/artifacts.json`
- `tool-terminal-batch-material-summary` | `20260318-131741-439` | passed | `tests/logs/ai/backend/tools/terminal/scenario/tool-terminal-batch-material-summary/20260318-131741-439/meta.json` | `tests/logs/ai/backend/tools/terminal/scenario/tool-terminal-batch-material-summary/20260318-131741-439/timeline.jsonl` | `tests/logs/ai/backend/tools/terminal/scenario/tool-terminal-batch-material-summary/20260318-131741-439/artifacts.json`
- `tool-terminal-path-resolution` | run=`20260318-131741-370` | status=`passed`
- `tool-terminal-workspace-material-discovery` | run=`20260318-131741-389` | status=`passed`
- `tool-terminal-approval-and-boundary` | run=`20260318-131741-430` | status=`passed`
- `tool-terminal-approval-denied-follow-up` | run=`20260318-131741-505` | status=`passed`
- `tool-terminal-failure-and-long-output` | run=`20260318-131740-773` | status=`passed`
- `tool-terminal-artifact-producing-command` | run=`20260318-131741-439` | status=`passed`
- `tool-terminal-batch-material-summary` | run=`20260318-131741-439` | status=`passed`
##### 失败分析
@@ -121,13 +124,13 @@
- `basic`: `8` 次 raw runs,最新 `4 / 4 passed`
- `boundary`: `2` 次 raw runs,最新 `1 / 1 passed`
##### 最新证据
##### 证据
- `tool-kb-fts-search` | `20260318-131741-574` | passed | `tests/logs/ai/backend/tools/kb/basic/tool-kb-fts-search/20260318-131741-574/meta.json` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-fts-search/20260318-131741-574/timeline.jsonl` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-fts-search/20260318-131741-574/artifacts.json`
- `tool-kb-hybrid-search` | `20260318-131741-664` | passed | `tests/logs/ai/backend/tools/kb/basic/tool-kb-hybrid-search/20260318-131741-664/meta.json` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-hybrid-search/20260318-131741-664/timeline.jsonl` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-hybrid-search/20260318-131741-664/artifacts.json`
- `tool-kb-list-documents-and-scope-filter` | `20260318-131741-814` | passed | `tests/logs/ai/backend/tools/kb/basic/tool-kb-list-documents-and-scope-filter/20260318-131741-814/meta.json` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-list-documents-and-scope-filter/20260318-131741-814/timeline.jsonl` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-list-documents-and-scope-filter/20260318-131741-814/artifacts.json`
- `tool-kb-vector-search` | `20260318-131741-823` | passed | `tests/logs/ai/backend/tools/kb/basic/tool-kb-vector-search/20260318-131741-823/meta.json` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-vector-search/20260318-131741-823/timeline.jsonl` | `tests/logs/ai/backend/tools/kb/basic/tool-kb-vector-search/20260318-131741-823/artifacts.json`
- `tool-kb-empty-scope-and-no-result` | `20260318-131741-542` | passed | `tests/logs/ai/backend/tools/kb/boundary/tool-kb-empty-scope-and-no-result/20260318-131741-542/meta.json` | `tests/logs/ai/backend/tools/kb/boundary/tool-kb-empty-scope-and-no-result/20260318-131741-542/timeline.jsonl` | `tests/logs/ai/backend/tools/kb/boundary/tool-kb-empty-scope-and-no-result/20260318-131741-542/artifacts.json`
- `tool-kb-fts-search` | run=`20260318-131741-574` | status=`passed`
- `tool-kb-hybrid-search` | run=`20260318-131741-664` | status=`passed`
- `tool-kb-list-documents-and-scope-filter` | run=`20260318-131741-814` | status=`passed`
- `tool-kb-vector-search` | run=`20260318-131741-823` | status=`passed`
- `tool-kb-empty-scope-and-no-result` | run=`20260318-131741-542` | status=`passed`
##### 失败分析
@@ -136,3 +139,62 @@
##### 当前评估
- `kb` 已覆盖 `hybrid / vector / fts / list_documents` 与 `empty scope / no-result`,且当前最新口径为 `5 / 5 passed`。
### human
##### 通过率
- 历史总数据:
- totalRuns: 128
- passedRuns: 107
- failedRuns: 21
- historicalSuccessRate: 83.6%
- 最新总数据:
- latestCaseCount: 19
- latestPassedCases: 18
- latestFailedCases: 1
- latestSuccessRate: 94.7%
##### 分层统计
- `basic`: `8` 次 raw runs,最新 `2 / 2 passed`
- `boundary`: `24` 次 raw runs,最新 `6 / 6 passed`
- `scenario`: `33` 次 raw runs,最新 `8 / 8 passed`
- `live`: `11` 次 raw runs,最新 `2 / 3 passed`
##### 证据
- `human-direct-docx-edit` | run=`20260318-151550-605` | status=`passed`
- `human-direct-kb-answer` | run=`20260318-151551-706` | status=`passed`
- `human-approval-blocked-recovery` | run=`20260318-151551-755` | status=`passed`
- `human-evidence-insufficient-safe-stop` | run=`20260318-151551-710` | status=`passed`
- `human-no-redundant-recheck` | run=`20260318-151551-803` | status=`passed`
- `human-partial-delivery-on-budget-limit` | run=`20260318-151551-723` | status=`passed`
- `human-tool-failure-recovery` | run=`20260318-151550-618` | status=`passed`
- `human-workspace-blocked-recovery` | run=`20260318-151551-679` | status=`passed`
- `human-kb-docx-bridge` | run=`20260318-151550-641` | status=`passed`
- `human-long-task-approval-branch` | run=`20260318-151551-710` | status=`passed`
- `human-long-task-project-brief` | run=`20260318-151550-604` | status=`passed`
- `human-long-task-with-corrections` | run=`20260318-151550-604` | status=`passed`
- `human-multi-source-merge-delivery` | run=`20260318-151550-604` | status=`passed`
- `human-multi-turn-refinement` | run=`20260318-151550-605` | status=`passed`
- `human-review-handoff-structured-delivery` | run=`20260318-151550-605` | status=`passed`
- `human-terminal-docx-handoff` | run=`20260318-151550-606` | status=`passed`
- `human-live-direct-kb-answer` | run=`20260318-145113-697` | status=`passed`
- `human-live-evidence-insufficient-safe-stop` | run=`20260318-150735-678` | status=`passed`
- `human-live-kb-docx-bridge` | run=`20260318-151207-536` | status=`failed`
##### 失败分析
- 当前 latest 口径下唯一 failed case 是 `human-live-kb-docx-bridge`。
- 2026-03-18 的 primary `gpt-5` 最新诊断 run `20260318-150310-015` 返回 `invalid_input`,summary=`524 status code (no body)`,事件只留下两次 `kb_hybrid_search_chunks`,说明链路在 live 模型侧已出现上游不稳定。
- 2026-03-18 的 stronger `gpt-5.2` 诊断 run `20260318-150906-789` 没有触发 524,但在 `kb_hybrid_search_chunks -> kb_list_documents` 后直接判定步数不足,未进入 `docx-editing` handoff。
- 2026-03-18 的更收敛 objective 诊断 run `20260318-151207-536` 仍然失败,表现为 `bounded_loop_error`,工具轨迹错误转入四次 `terminal_run_command` 加一次 `kb_hybrid_search_chunks`,没有产出 preview artifact。
- 因此,当前 bridge 失败同时暴露两类问题:一是 primary live 服务侧不稳定;二是 human 在 `KB -> DOCX` 这类跨工具任务上的 live 路由和收敛仍不稳定。
##### 当前评估
- `basic / boundary / scenario` 当前 latest 全通过,说明 Stage 2 的脚本化回归基线已经稳定。
- 新增补齐的 `human-live-direct-kb-answer` 与 `human-live-evidence-insufficient-safe-stop` 已形成 live 正向证据,其中前者还促成了 `KB` 收敛提示和 `localSourceStatus` 判定逻辑的修正。
- 当前 `human/live` latestCaseCount 只有 `3`,因为本轮没有重跑此前已实现的 `human-live-direct-docx-edit`、`human-live-terminal-docx-handoff`、`human-live-long-task-project-brief`;因此 Stage 2 live 结论目前仍只能算“部分补齐 + 明确暴露 bridge 缺口”。
- 在 `human-live-kb-docx-bridge` 稳定前,不建议把 Stage 2 视为可关闭阶段。