diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ProgressLedgerTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ProgressLedgerTool.java index 50a018a2..41a1b8e4 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ProgressLedgerTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ProgressLedgerTool.java @@ -70,8 +70,23 @@ public class ProgressLedgerTool { } try { ProgressLedger updated = service.upsert(conversationId, stepKey, label, parsed, note); - return "Recorded " + stepKey + " → " + parsed.wireValue() - + ". Ledger now has " + updated.size() + " entries."; + // Return the freshly-rendered snapshot in the tool result so the + // model immediately sees its own update reflected in the + // canonical view it will be reading next iteration. Without this + // positive-feedback loop the model treats progress_update as a + // fire-and-forget side effect and stops calling it after the + // first few transitions (observed: round-4 dropped to 3 calls in + // 27 minutes of work). The snapshot is also what the runtime + // injects pre-LLM-call, so echoing it here keeps the two views + // identical. + StringBuilder out = new StringBuilder(256); + out.append("✓ Recorded ").append(stepKey).append(" → ").append(parsed.wireValue()) + .append(" (").append(updated.size()).append(" entries total).\n\n"); + String snapshot = updated.renderSnapshot(); + if (snapshot != null) { + out.append(snapshot); + } + return out.toString(); } catch (Exception e) { log.warn("progress_update failed for conv={} key={}: {}", conversationId, stepKey, e.getMessage()); return "Error: failed to persist progress entry — " + e.getMessage(); diff --git a/mateclaw-server/src/test/java/vip/mate/tool/builtin/ProgressLedgerToolTest.java b/mateclaw-server/src/test/java/vip/mate/tool/builtin/ProgressLedgerToolTest.java index 98b7446a..4c0f7743 100644 --- a/mateclaw-server/src/test/java/vip/mate/tool/builtin/ProgressLedgerToolTest.java +++ b/mateclaw-server/src/test/java/vip/mate/tool/builtin/ProgressLedgerToolTest.java @@ -37,7 +37,7 @@ class ProgressLedgerToolTest { } @Test - @DisplayName("Happy path: valid args persist + return entry count.") + @DisplayName("Happy path: tool result includes the full rendered snapshot for positive feedback.") void happyPath() { ToolExecutionContext.set("conv-1", "admin"); ProgressLedgerService service = mock(ProgressLedgerService.class); @@ -51,7 +51,13 @@ class ProgressLedgerToolTest { ProgressLedgerTool tool = new ProgressLedgerTool(service); String out = tool.progress_update("step_a", "Step A", "in_progress", "starting now", null); - assertEquals("Recorded step_a → in_progress. Ledger now has 1 entries.", out); + // Header line confirms the write so the model has a clear ack. + assertTrue(out.startsWith("✓ Recorded step_a → in_progress (1 entries total)"), out); + // The rendered snapshot must be appended so the model sees its own + // update reflected in the same view the runtime injects each turn. + assertTrue(out.contains("当前任务进度"), "expected snapshot in tool result: " + out); + assertTrue(out.contains("Step A"), "expected entry label in snapshot: " + out); + assertTrue(out.contains("`step_a`"), "expected bracketed key in snapshot: " + out); verify(service, times(1)).upsert(any(), any(), any(), any(), any()); }