feat: inject thread progress into agent prompt (#127)
CI / check (pull_request) Successful in 1m42s
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
Reference in New Issue
Block a user