From 5ed6f68e4b1172e864b063f49d16ec9d4b97e6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Sat, 6 Jun 2026 00:40:12 +0000 Subject: [PATCH] feat: inject thread progress into agent prompt (#127) Agents now receive a Thread Progress section showing current step number and role visit count, eliminating tool calls to count turns. - util-agent: new buildThreadProgress() helper - agent-hermes: inject before continuation/first-visit prompt - agent-claude-code: same injection point Fixes #127 --- .changeset/inject-thread-progress.md | 12 ++++ packages/agent-claude-code/src/claude-code.ts | 5 ++ packages/agent-hermes/src/hermes.ts | 4 ++ .../__tests__/build-thread-progress.test.ts | 59 +++++++++++++++++++ .../util-agent/src/build-thread-progress.ts | 27 +++++++++ packages/util-agent/src/index.ts | 1 + 6 files changed, 108 insertions(+) create mode 100644 .changeset/inject-thread-progress.md create mode 100644 packages/util-agent/__tests__/build-thread-progress.test.ts create mode 100644 packages/util-agent/src/build-thread-progress.ts diff --git a/.changeset/inject-thread-progress.md b/.changeset/inject-thread-progress.md new file mode 100644 index 0000000..1c0d2dc --- /dev/null +++ b/.changeset/inject-thread-progress.md @@ -0,0 +1,12 @@ +--- +"@united-workforce/agent-hermes": patch +"@united-workforce/agent-claude-code": patch +"@united-workforce/util-agent": patch +--- + +feat: inject thread progress into agent prompt (#127) + +Agents now receive a "Thread Progress" section in their prompt showing the +current step number and how many times the current role has spoken before. +This eliminates the need for agents to make tool calls (terminal, delegate_task) +just to count their own turn history. diff --git a/packages/agent-claude-code/src/claude-code.ts b/packages/agent-claude-code/src/claude-code.ts index 5bc81a3..72be905 100644 --- a/packages/agent-claude-code/src/claude-code.ts +++ b/packages/agent-claude-code/src/claude-code.ts @@ -7,6 +7,7 @@ import { type AgentRunResult, buildContinuationPrompt, buildRolePrompt, + buildThreadProgress, createAgent, getCachedSessionId, setCachedSessionId, @@ -27,6 +28,10 @@ export function buildClaudeCodePrompt(ctx: AgentContext): string { if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") { parts.push(ctx.outputFormatInstruction, ""); } + + // Inject thread progress so the agent knows step count and role visit count + parts.push(buildThreadProgress(ctx.steps, ctx.role), ""); + parts.push(rolePrompt, "", "## Task", ctx.start.prompt); if (!ctx.isFirstVisit) { diff --git a/packages/agent-hermes/src/hermes.ts b/packages/agent-hermes/src/hermes.ts index c666ac8..e144642 100644 --- a/packages/agent-hermes/src/hermes.ts +++ b/packages/agent-hermes/src/hermes.ts @@ -6,6 +6,7 @@ import { type AgentRunResult, buildContinuationPrompt, buildRolePrompt, + buildThreadProgress, createAgent, } from "@united-workforce/util-agent"; import type { AcpUsage } from "./acp-client.js"; @@ -60,6 +61,9 @@ export function buildHermesPrompt(ctx: AgentContext): string { parts.push(ctx.outputFormatInstruction, ""); } + // Inject thread progress so the agent knows step count and role visit count + parts.push(buildThreadProgress(ctx.steps, ctx.role), ""); + if (!ctx.isFirstVisit) { // Re-entry: show only steps since last visit, meta only parts.push(buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt)); diff --git a/packages/util-agent/__tests__/build-thread-progress.test.ts b/packages/util-agent/__tests__/build-thread-progress.test.ts new file mode 100644 index 0000000..40b3197 --- /dev/null +++ b/packages/util-agent/__tests__/build-thread-progress.test.ts @@ -0,0 +1,59 @@ +import type { StepContext } from "@united-workforce/protocol"; +import { describe, expect, test } from "vitest"; +import { buildThreadProgress } from "../src/build-thread-progress.js"; + +function makeStep(role: string): StepContext { + return { + role, + output: {}, + detail: "0000000000000" as string, + agent: "uwf-mock", + edgePrompt: "", + startedAtMs: 0, + completedAtMs: 0, + cwd: "", + assembledPrompt: null, + usage: null, + content: null, + }; +} + +describe("buildThreadProgress", () => { + test("first step of thread", () => { + const result = buildThreadProgress([], "proponent"); + expect(result).toContain("## Thread Progress"); + expect(result).toContain("first step"); + expect(result).toContain("first time"); + expect(result).toContain("proponent"); + }); + + test("second step, role not seen before", () => { + const steps = [makeStep("opponent")]; + const result = buildThreadProgress(steps, "proponent"); + expect(result).toContain("Thread step 2"); + expect(result).toContain("spoken 0 times"); + }); + + test("role has spoken once before", () => { + const steps = [makeStep("proponent"), makeStep("opponent")]; + const result = buildThreadProgress(steps, "proponent"); + expect(result).toContain("Thread step 3"); + expect(result).toContain("spoken 1 time before"); + // singular "time" not "times" + expect(result).not.toContain("1 times"); + }); + + test("role has spoken multiple times", () => { + const steps = [ + makeStep("proponent"), + makeStep("opponent"), + makeStep("proponent"), + makeStep("opponent"), + makeStep("proponent"), + makeStep("opponent"), + ]; + const result = buildThreadProgress(steps, "proponent"); + expect(result).toContain("Thread step 7"); + expect(result).toContain("spoken 3 times"); + }); +}); diff --git a/packages/util-agent/src/build-thread-progress.ts b/packages/util-agent/src/build-thread-progress.ts new file mode 100644 index 0000000..546c980 --- /dev/null +++ b/packages/util-agent/src/build-thread-progress.ts @@ -0,0 +1,27 @@ +import type { StepContext } from "@united-workforce/protocol"; + +/** + * Build a compact thread-progress summary so the agent knows where it is + * in the conversation without making tool calls to count steps. + * + * Example output: + * ## Thread Progress + * Thread step 6. You (proponent) have spoken 2 times before this turn. + */ +export function buildThreadProgress(steps: StepContext[], role: string): string { + const totalSteps = steps.length; + const roleVisits = steps.filter((s) => s.role === role).length; + + const parts = [`## Thread Progress`]; + if (totalSteps === 0) { + parts.push( + `This is the first step of the thread. You (${role}) are speaking for the first time.`, + ); + } else { + parts.push( + `Thread step ${totalSteps + 1}. You (${role}) have spoken ${roleVisits} time${roleVisits === 1 ? "" : "s"} before this turn.`, + ); + } + + return parts.join("\n"); +} diff --git a/packages/util-agent/src/index.ts b/packages/util-agent/src/index.ts index dfdfcdc..db0573a 100644 --- a/packages/util-agent/src/index.ts +++ b/packages/util-agent/src/index.ts @@ -1,6 +1,7 @@ export { buildContinuationPrompt } from "./build-continuation-prompt.js"; export { buildOutputFormatInstruction } from "./build-output-format-instruction.js"; export { buildRolePrompt } from "./build-role-prompt.js"; +export { buildThreadProgress } from "./build-thread-progress.js"; export type { BuildContextMeta } from "./context.js"; export { buildContext, buildContextWithMeta } from "./context.js"; export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";