Kaynağa Gözat

fix(agent): 章节工作流结果不再被 6000 字截断

run_chapter_workflow 返回的是终稿交付物,头尾压缩会丢掉正文中段,外层模型只能按残片另写一章。read_* 证据类工具仍按原限额压缩。

Co-authored-by: Cursor <cursoragent@cursor.com>
darknessomi 3 hafta önce
ebeveyn
işleme
1141de84e8

+ 58 - 0
src/lib/agent/runner.spec.ts

@@ -1035,6 +1035,64 @@ describe("AgentRunner", () => {
     expect(compressedToolMessage).toContain("结尾")
   })
 
+  it("sends the full run_chapter_workflow result to the model instead of compressing it", async () => {
+    const longResult = [
+      "章节工作流完成。",
+      "是否返修:是",
+      `任务书:${"开头承接".repeat(400)}`,
+      "",
+      "最终正文:",
+      `${"陈远的手还压在西线地图上。".repeat(80)}\n中间正文\n${"空袭窗口正在关闭。".repeat(80)}`,
+    ].join("\n")
+    const tool: Tool = {
+      name: "run_chapter_workflow",
+      description: "workflow",
+      category: "action",
+      permission: "auto",
+      executeTimeoutMs: 0,
+      parameters: {},
+      execute: vi.fn().mockResolvedValue(longResult),
+    }
+    registry.register(tool)
+
+    let injectedToolMessage = ""
+    let callCount = 0
+    mockStreamChat.mockImplementation(async (_config: unknown, messages: AgentMessage[], cb: StreamCallbacks) => {
+      callCount++
+      if (callCount === 1) {
+        cb.onToolCallDelta?.({ index: 0, id: "workflow_1", name: "run_chapter_workflow" })
+        cb.onToolCallDelta?.({ index: 0, arguments: "{}" })
+        cb.onDone()
+      } else {
+        injectedToolMessage = String(messages[messages.length - 1].content)
+        cb.onToken("已输出")
+        cb.onDone()
+      }
+    })
+
+    const config: AgentConfig = {
+      maxRounds: 3,
+      tools: [tool],
+      systemPrompt: "",
+      llmConfig: mockLlmConfig,
+      toolResultContextLimit: 1200,
+    }
+    const result = await runner.run(
+      config,
+      registry,
+      [systemMsg, userMsg],
+      { onText: vi.fn(), onToolCall: vi.fn(), onToolResult: vi.fn(), onToolError: vi.fn(), onDone: vi.fn(), onError: vi.fn() },
+      undefined,
+    )
+
+    expect(longResult.length).toBeGreaterThan(1200)
+    expect(result.toolCalls[0].result).toBe(longResult)
+    expect(injectedToolMessage).toContain(longResult)
+    expect(injectedToolMessage).toContain("陈远的手还压在西线地图上")
+    expect(injectedToolMessage).toContain("中间正文")
+    expect(injectedToolMessage).not.toContain("已压缩给模型使用")
+  })
+
   it("每轮模型请求保留任务契约并压缩内部工作消息", async () => {
     const untrimmed = [
       { role: "system" as const, content: "系统规则".repeat(120) },

+ 19 - 0
src/lib/agent/tool-evidence-ledger.spec.ts

@@ -18,4 +18,23 @@ describe("ToolEvidenceLedger", () => {
 
     expect(ledger.format("read_chapter", { chapter: 2 }, "第二章")).toContain("第二章")
   })
+
+  it("章节工作流终稿不按证据限额截断", () => {
+    const ledger = new ToolEvidenceLedger(300)
+    const result = [
+      "章节工作流完成。",
+      "是否返修:否",
+      `任务书:${"必须完成项".repeat(120)}`,
+      "",
+      "最终正文:",
+      "陈远的手还压在西线地图上。".repeat(80),
+    ].join("\n")
+
+    const formatted = ledger.format("run_chapter_workflow", { chapterNumber: 240 }, result)
+
+    expect(formatted).toContain("最终正文:")
+    expect(formatted).toContain("陈远的手还压在西线地图上")
+    expect(formatted).not.toContain("已压缩给模型使用")
+    expect(formatted).toContain(result)
+  })
 })

+ 19 - 1
src/lib/agent/tool-result.spec.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from "vitest"
-import { formatToolResultForModel } from "./tool-result"
+import { formatToolResultForModel, keepsFullToolResultForModel } from "./tool-result"
 
 describe("formatToolResultForModel", () => {
   it("returns short tool results unchanged", () => {
@@ -16,4 +16,22 @@ describe("formatToolResultForModel", () => {
     expect(compressed).toContain("开头内容")
     expect(compressed).toContain("结尾内容")
   })
+
+  it("does not truncate run_chapter_workflow deliverable even when over the limit", () => {
+    const chapterBody = "陈远的手还压在西线地图上。".repeat(80)
+    const result = [
+      "章节工作流完成。",
+      "是否返修:是",
+      `任务书:${"场景验收标准".repeat(800)}`,
+      "",
+      "最终正文:",
+      chapterBody,
+    ].join("\n")
+
+    expect(keepsFullToolResultForModel("run_chapter_workflow")).toBe(true)
+    expect(result.length).toBeGreaterThan(300)
+    expect(formatToolResultForModel("run_chapter_workflow", result, 300)).toBe(result)
+    expect(formatToolResultForModel("run_chapter_workflow", result, 300)).not.toContain("已压缩给模型使用")
+    expect(formatToolResultForModel("run_chapter_workflow", result, 300)).toContain("陈远的手还压在西线地图上")
+  })
 })

+ 11 - 0
src/lib/agent/tool-result.ts

@@ -2,15 +2,26 @@ const TOOL_ERROR_PREFIX = /^\s*错误\s*[::]/
 
 export const DEFAULT_TOOL_RESULT_CONTEXT_LIMIT = 6000
 
+/**
+ * 这些工具的返回值就是给用户的交付物(章节终稿),不是给模型当「证据摘录」的资料。
+ * 头尾截断会先切掉任务书、丢掉正文中段,外层模型只能按残片另写一章。
+ */
+const FULL_TOOL_RESULT_FOR_MODEL = new Set(["run_chapter_workflow"])
+
 export function isToolErrorResult(result: string): boolean {
   return TOOL_ERROR_PREFIX.test(result)
 }
 
+export function keepsFullToolResultForModel(toolName: string): boolean {
+  return FULL_TOOL_RESULT_FOR_MODEL.has(toolName)
+}
+
 export function formatToolResultForModel(
   toolName: string,
   result: string,
   limit = DEFAULT_TOOL_RESULT_CONTEXT_LIMIT,
 ): string {
+  if (keepsFullToolResultForModel(toolName)) return result
   if (result.length <= limit) return result
 
   const safeLimit = Math.max(200, limit)