feat(agent): fall back to the container backend when pi-native fails

Dev starts on pi-native and keeps pi-agent available for a failed turn. Latency tuning stays deferred.
This commit is contained in:
weichen
2026-09-21 15:29:33 +00:00
parent f3b1209207
commit 127cfccccc
8 changed files with 124 additions and 8 deletions
+2 -2
View File
@@ -10,13 +10,13 @@
3. **上传解析提示 / 去重 / CoW 收口**:MinerU 解析状态轮询闭环(pending→running→done)、重复文件「已复用」提示、失败原因+重新解析、CAS 不可变 CoW 契约测试,已合入 `dev`(代码 `99905571`、证据 `74587ccf`、收口 `a0464eb5`)。见 [报告](../test-reports/2026-09-21-upload-parse-ux-dedup-test-report.md)。test/main 未晋升。前序任务执行状态卡收口见 [报告](../test-reports/2026-09-21-taskrun-status-card-persistence-test-report.md)(含二轮修复:completed 步骤 label 冻结为"正在…(NN%)"的收口归一化 + dev 历史回填,已合入 `dev@4b303247` 并真实 PI 验收)。
4. **MinerU 真实 PDF 二轮验收**:用户反馈「PDF 内容未被理解」由两因叠加——(A) 首轮在 worktree 内跑 `env-service dev rebuild` 令挂载漂到 worktree `data/dev`,真实历史 blob 全部读不到;(B) Agent 轻量 API 硬限 20 页,28 页真实文件失败。首轮结论作废。二轮从**主目录** rebuild 修复挂载,`internal/mineru` 在 `-30003` 时按 20 页窗分块解析并重编全局页锚;`ReadRefBytes` 失败改为 `slog.Warn`。已合入 `dev@362b68a0`。真实 `bd_intake`(项目 6532)`completed`,工作区 `materials_text` 注入 29152 字节中文正文。见 [报告](../test-reports/2026-09-21-mineru-doc-parsing-test-report.md) 第八节。
5. **材料切块不再看体积**:项目 6579 `doc_proj6579_bd_intake` 已确认候选 535,正式正文不再含「二进制压缩流」。成功解析的材料一律写入 `material_chunks`,≤60KB 仍全文注入。见 [报告](../test-reports/2026-09-21-force-material-chunk-index-test-report.md)。已合入 `dev` 并从主目录 rebuild。test/main 未晋升。
6. **红线**:`pi-native` 对比未过 P95 门槛,默认 `AGENT_BACKEND` 保持 `pi-agent`,未合入、未部署。禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读;SWBOOT-10 保持 fail-closed stub;D7 对话 ADR 仍待合规签字(追踪表留存类别已定为 `record`,不授权物理删除);**不改 `production_ready`**;模块解耦批次 A–G 与回溯实施切片未单独认领前保持 `planned`;**合入 dev 后必须回主目录跑 `env-service.sh dev rebuild`,禁止任何 worktree 内运行 `env-service`**(脚本 ROOT_DIR 取自脚本所在目录树,会带走 bind mount,导致 `/app/data` 漂到 worktree 空目录)。
6. **红线**:dev 启用 `pi-native`,失败回退容器 `pi-agent`;时延优化后置。test/main 仍默认 `pi-agent`。禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读;SWBOOT-10 保持 fail-closed stub;D7 对话 ADR 仍待合规签字(追踪表留存类别已定为 `record`,不授权物理删除);**不改 `production_ready`**;模块解耦批次 A–G 与回溯实施切片未单独认领前保持 `planned`;**合入 dev 后必须回主目录跑 `env-service.sh dev rebuild`,禁止任何 worktree 内运行 `env-service`**(脚本 ROOT_DIR 取自脚本所在目录树,会带走 bind mount,导致 `/app/data` 漂到 worktree 空目录)。
## 并行认领
| owner | task | branch | worktree | mutex | status |
|-------|------|--------|----------|-------|--------|
| Cursor | 2026-09-21-go-agentx-runtime | feature/go-agentx-runtime-2026-09-21 | .worktrees/go-agentx-runtime-2026-09-21 | agent-backend-swap | blocked |
| Cursor | 2026-09-21-go-agentx-runtime | feature/go-agentx-runtime-2026-09-21 | .worktrees/go-agentx-runtime-2026-09-21 | agent-backend-swap | in_progress |
| Cursor | 2026-09-21-force-material-chunk-index | feature/force-material-chunk-index-2026-09-21 | .worktrees/force-material-chunk-index-2026-09-21 | bizdev+materialindex | done |
| Cursor | 2026-09-21-workflow-scope-weighting | feature/workflow-scope-weighting-2026-09-21 | .worktrees/workflow-scope-weighting-2026-09-21 | intent+web+docgov | done |
| Cursor | 2026-09-21-upload-parse-ux-dedup | feature/upload-parse-ux-2026-09-21 | .worktrees/upload-parse-ux-2026-09-21 | server+web+storage | done |
@@ -1,6 +1,6 @@
---
title: "Go 原生 PI Agent 运行时(pi-native 并存验证)"
status: blocked
status: in_progress
created: 2026-09-21
updated: 2026-09-21
owner: Cursor
@@ -63,4 +63,4 @@ mutex: agent-backend-swap
远端同步状态:创建时主目录为本地 `dev`,ahead of `origin/dev`,未为创建任务执行 fetch/pull。
对比未过 P95 门槛,状态 `blocked`,默认后端不改。差距见 `.doc/audit/2026-09-21-go-agentx-runtime-latency-gap.md`。`deployment=pending`,`cleanup=pending`(未合入,worktree 保留)。短测试曾对 `127.0.0.1:5433` 执行 goose,dev schema 现为 62,表 `agentx_workspace_registry` 已存在;迁移文件仍只在本分支。
对比未过 P95 门槛后,用户决定先不把时延作为切换条件:dev 启用 `pi-native`,失败时在进程内回退到容器 `pi-agent`。test/main 的 compose 默认仍是 `pi-agent`。`deployment` 在主目录 rebuild 之后再记。
+1 -1
View File
@@ -66,7 +66,7 @@
| 阶段回溯与影响闭环 | 五表追踪索引、影响报告、L1 文档修订、L2 阶段再打开、待复核软标记、对话修订提案 | done | deferred_enhancement(不挡首发) | [设计](../system-design/2026-09-20-stage-backtrack-and-impact-loop-design.md);[Track T](../remediation/2026-09-20-capability-enablement-impact-plan.md);[对话化报告](../test-reports/2026-09-21-trace-panel-to-chat-test-report.md) | 已合入 dev。常驻「需求与影响」面板已退役,新待复核与下游影响改走已有项目会话。dev schema 59。test/main 未升。阶段推进确认门禁仍在。不签 D7 对话 ADR,不改 `production_ready` | `trace-index` |
| PI change-report 叙事 | 只读消费 diff+lineage 的 PI 叙事;失败全量降级 fallback | partial | deferred_enhancement | [Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[pu7](../ops/long-term/2026-09-16-pu7-pi-narrative-contract.md);[脚手架](../test-reports/2026-09-16-pu7-pi-narrative-test-report.md);[W3 真 PI](../test-reports/2026-09-17-w3-pi-change-report-narrative-test-report.md);[2026-09-20 复核](../ops/research/2026-09-20-capability-enablement-impact-research.md);[最小化输入](../ops/short-term/2026-09-20-pi-narrative-clipped-diff.md) | flag 默认 off。`facts.json` 含截断后的 hunk 前后文和影响说明,以及完整 hunk/impact 计数与省略数;摘要 digest 仍不含正文。叙事请求已带 `tool_profile=reasoning`。运行中的 runner 尚未切到 `--no-tools`。不打开开关 | `docgov` |
| 前端工作台 | 三栏 + 块文档 + SSE;错误卡/`TaskRunCard` 竖向进度/busy;版本冲突信封;demo seed 默认关;终止 preview | done | non_blocker | [workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[双标签](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png);[demo](../test-reports/2026-09-16-demo-seed-pollution-test-report.md);[B4](../test-reports/2026-09-16-frontend-gate-b4-test-report.md)(项目 6363);[TaskRunCard](../test-reports/2026-09-21-taskrun-status-card-persistence-test-report.md) | **before**:空「缺口」却 partial → **after**:本行 done。其他 409 消费方信封复用另列 hardening backlog | `web` |
| PI Runner / 配置链 | pi-agent + MiniMax + typed-result-v1;分支并存 pi-native,默认未切 | done | non_blocker | [piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md);[typed-result](../test-reports/2026-09-15-pi-typed-result-v1-test-report.md);[pi-native 对比](../test-reports/2026-09-21-go-agentx-runtime-test-report.md) | 容器链路仍是默认。pi-native 真实双跑成功率 1.00、容器 0.33、P0 为 0,P95 16.6s 对 6.9s,未过 1.3 倍,不切 AGENT_BACKEND。cursor-cli 已从该分支移除,容器服务保留 | `pi-agent` |
| PI Runner / 配置链 | pi-agent + MiniMax + typed-result-v1;dev 默认 pi-native,失败回退 pi-agent | done | non_blocker | [piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md);[typed-result](../test-reports/2026-09-15-pi-typed-result-v1-test-report.md);[pi-native 对比](../test-reports/2026-09-21-go-agentx-runtime-test-report.md) | 用户决定时延后置。dev compose 默认 pi-native,进程内失败再调用容器 pi-agent。test/main 默认仍是 pi-agent。cursor-cli 已移除 | `pi-agent` |
| 三环境运维 | dev/test/main + 共享 KB;对话/业务库隔离 | done | non_blocker | [PROJECT.md](../../PROJECT.md) §4;CURRENT 环境快照 dev/test schema **48**、main **47** | — | `infra` |
| CI / E2E | PR CI、PG 集成、治理 PG、已签真实 PI 门禁;**nightly** schedule/`workflow_dispatch` + infra/product 分类 receipt | done | 加固(阻止**下一次**晋升,不自动否定已发布) | [BUG-08](../test-reports/2026-09-16-ci-governance-pg-bug08-test-report.md);[PI 三工作流](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md);[W1 green](../test-reports/2026-09-17-w1-ci-e2e-green-nightly-test-report.md) + [receipt `6e2476f0…`](../assets/promotion-receipts/6e2476f0f2e408e111fe06ca7c31a38294a340b3.json) | **before**:partial(FullNone product_fail / secrets 文案过时)→ **after**:secrets 已配;duplicate goose 00048 已清;FullNone+U8 nightly `verdict=pass` | `ci` |
| Artifact 同 digest 晋升 | 候选 commit 一次构建、按 digest 晋升、promotion receipt | done | 加固 | [计划 PU5B](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md) | **关闭**:same-image pin(`candidate-images-*` + `PINNED_IMAGE_MODE` + promote `--require-digests`)。**声明**:单机 retag/digest pin;远程 registry 推送另立任务 | `infra` |
+2 -1
View File
@@ -30,7 +30,8 @@ KB_DB_PORT=5432
# 5. Agent 后端与 Runner 端口
PI_AGENT_RUNNER_PORT=18865
AGENT_BACKEND=pi-agent
AGENT_BACKEND=pi-native
# pi-native 失败时进程内回退到容器 pi-agent,无需改此值。
AGENT_TIMEOUT=60m
# 6. Pi-Agent / MiniMax / Tavily / Runner 密钥统一读取根 .env
+4 -1
View File
@@ -80,7 +80,10 @@ services:
HTTP_ADDR: ":${HTTP_PORT:-8080}"
HTTP_PORT: "${HTTP_PORT:-8080}"
HTTP_HOST: "0.0.0.0"
AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent}
AGENT_BACKEND: ${AGENT_BACKEND:-pi-native}
AGENTX_API_BASE: ${MINIMAX_ENDPOINT_URL:-https://api.minimaxi.com/v1}
AGENTX_API_KEY: ${MINIMAX_API_KEY:-}
AGENTX_MODEL: ${MINIMAX_MODEL_ID:-MiniMax-M2.7-highspeed}
AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m}
MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8}
DATA_DIR: /app/data
+4 -1
View File
@@ -46,7 +46,10 @@ func NewBackend(name string, cfg *config.Config) Backend {
case "pi-agent":
inner = &PiAgentBackend{cfg: cfg}
case "pi-native":
inner = &PiNativeBackend{cfg: cfg}
inner = &fallbackBackend{
primary: NewPiNativeBackend(cfg),
fallback: &PiAgentBackend{cfg: cfg},
}
default:
inner = &StubBackend{}
}
+46
View File
@@ -0,0 +1,46 @@
package agent
import (
"context"
"strings"
)
// fallbackBackend runs pi-native first. A failed turn is retried on the container pi-agent.
type fallbackBackend struct {
primary Backend
fallback Backend
}
func (f *fallbackBackend) Name() string {
if f.primary == nil {
return "pi-native"
}
return f.primary.Name()
}
func (f *fallbackBackend) Execute(ctx context.Context, req Request) (*Result, error) {
res, err := f.primary.Execute(ctx, req)
if ctx.Err() != nil || succeeded(res, err) || f.fallback == nil {
return res, err
}
fb, fbErr := f.fallback.Execute(ctx, req)
if !succeeded(fb, fbErr) {
return res, err
}
note := "pi-native failed; served by pi-agent"
if res != nil && strings.TrimSpace(res.ErrorReason) != "" {
note += ": " + strings.TrimSpace(res.ErrorReason)
} else if err != nil {
note += ": " + err.Error()
}
if strings.TrimSpace(fb.Logs) == "" {
fb.Logs = note
} else {
fb.Logs = note + "\n" + fb.Logs
}
return fb, nil
}
func succeeded(res *Result, err error) bool {
return err == nil && res != nil && res.Success
}
+63
View File
@@ -0,0 +1,63 @@
package agent
import (
"context"
"errors"
"strings"
"testing"
)
type scriptBackend struct {
name string
res *Result
err error
calls int
}
func (s *scriptBackend) Name() string { return s.name }
func (s *scriptBackend) Execute(ctx context.Context, req Request) (*Result, error) {
s.calls++
return s.res, s.err
}
func TestFallbackKeepsNativeSuccess(t *testing.T) {
primary := &scriptBackend{name: "pi-native", res: &Result{Success: true, Logs: "native"}}
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: true}}
got, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(context.Background(), Request{})
if err != nil || !got.Success || got.Logs != "native" || spare.calls != 0 {
t.Fatalf("got %#v err=%v spare=%d", got, err, spare.calls)
}
}
func TestFallbackUsesContainerAfterNativeFailure(t *testing.T) {
primary := &scriptBackend{name: "pi-native", res: &Result{Success: false, ErrorReason: "missing target deliverable"}}
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: true, Logs: "container"}}
got, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(context.Background(), Request{})
if err != nil || !got.Success || spare.calls != 1 {
t.Fatalf("got %#v err=%v calls=%d", got, err, spare.calls)
}
if !strings.Contains(got.Logs, "served by pi-agent") || !strings.Contains(got.Logs, "missing target deliverable") {
t.Fatalf("logs %q", got.Logs)
}
}
func TestFallbackReturnsNativeWhenContainerAlsoFails(t *testing.T) {
primary := &scriptBackend{name: "pi-native", err: errors.New("native down")}
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: false, ErrorReason: "runner down"}}
got, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(context.Background(), Request{})
if err == nil || err.Error() != "native down" || got != nil || spare.calls != 1 {
t.Fatalf("got %#v err=%v", got, err)
}
}
func TestFallbackStopsWhenContextCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
primary := &scriptBackend{name: "pi-native", err: context.Canceled}
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: true}}
_, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(ctx, Request{})
if !errors.Is(err, context.Canceled) || spare.calls != 0 {
t.Fatalf("err=%v spare=%d", err, spare.calls)
}
}