feat: inject thread progress into agent prompt (#127)
CI / check (pull_request) Successful in 1m42s

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
This commit is contained in:
2026-06-06 00:40:12 +00:00
parent 1ed0bf1f76
commit 5ed6f68e4b
6 changed files with 108 additions and 0 deletions
+12
View File
@@ -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.
@@ -7,6 +7,7 @@ import {
type AgentRunResult, type AgentRunResult,
buildContinuationPrompt, buildContinuationPrompt,
buildRolePrompt, buildRolePrompt,
buildThreadProgress,
createAgent, createAgent,
getCachedSessionId, getCachedSessionId,
setCachedSessionId, setCachedSessionId,
@@ -27,6 +28,10 @@ export function buildClaudeCodePrompt(ctx: AgentContext): string {
if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") { if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") {
parts.push(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); parts.push(rolePrompt, "", "## Task", ctx.start.prompt);
if (!ctx.isFirstVisit) { if (!ctx.isFirstVisit) {
+4
View File
@@ -6,6 +6,7 @@ import {
type AgentRunResult, type AgentRunResult,
buildContinuationPrompt, buildContinuationPrompt,
buildRolePrompt, buildRolePrompt,
buildThreadProgress,
createAgent, createAgent,
} from "@united-workforce/util-agent"; } from "@united-workforce/util-agent";
import type { AcpUsage } from "./acp-client.js"; import type { AcpUsage } from "./acp-client.js";
@@ -60,6 +61,9 @@ export function buildHermesPrompt(ctx: AgentContext): string {
parts.push(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), "");
if (!ctx.isFirstVisit) { if (!ctx.isFirstVisit) {
// Re-entry: show only steps since last visit, meta only // Re-entry: show only steps since last visit, meta only
parts.push(buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt)); parts.push(buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt));
@@ -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");
});
});
@@ -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");
}
+1
View File
@@ -1,6 +1,7 @@
export { buildContinuationPrompt } from "./build-continuation-prompt.js"; export { buildContinuationPrompt } from "./build-continuation-prompt.js";
export { buildOutputFormatInstruction } from "./build-output-format-instruction.js"; export { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
export { buildRolePrompt } from "./build-role-prompt.js"; export { buildRolePrompt } from "./build-role-prompt.js";
export { buildThreadProgress } from "./build-thread-progress.js";
export type { BuildContextMeta } from "./context.js"; export type { BuildContextMeta } from "./context.js";
export { buildContext, buildContextWithMeta } from "./context.js"; export { buildContext, buildContextWithMeta } from "./context.js";
export type { ExtractResult, ResolvedLlmProvider } from "./extract.js"; export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";