Merge pull request 'feat: add $usage field to adapter protocol' (#80) from feat/74-usage-in-protocol into main
CI / check (push) Successful in 1m41s

feat: add $usage field to adapter protocol (#80)
This commit was merged in pull request #80.
This commit is contained in:
2026-06-04 22:14:12 +00:00
15 changed files with 290 additions and 6 deletions
+8 -1
View File
@@ -82,7 +82,13 @@ async function runBuiltinWithMessages(
if (loopResult.turnCount === 0) {
log("5RWTK9NB", "no turns produced, returning empty output");
return { output: "", detailHash: "", sessionId: session.sessionId, assembledPrompt: "" };
return {
output: "",
detailHash: "",
sessionId: session.sessionId,
assembledPrompt: "",
usage: null,
};
}
// Read jsonl → persist turns to CAS → store detail
@@ -99,6 +105,7 @@ async function runBuiltinWithMessages(
detailHash,
sessionId: session.sessionId,
assembledPrompt: "",
usage: null,
};
}
@@ -145,7 +145,7 @@ async function processClaudeOutput(
);
}
return { output, detailHash, sessionId, assembledPrompt };
return { output, detailHash, sessionId, assembledPrompt, usage: null };
}
// Truly unparseable output - provide enhanced error message
+2 -2
View File
@@ -118,7 +118,7 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
await setCachedSessionId(ctx.threadId, ctx.role, sessionId, ctx.storageRoot);
}
return { output: text, detailHash, sessionId, assembledPrompt: fullPrompt };
return { output: text, detailHash, sessionId, assembledPrompt: fullPrompt, usage: null };
}
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
@@ -149,7 +149,7 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
// so the agent sees the full conversation history (crucial for retries).
const { text, sessionId } = await client.prompt(message);
const { detailHash } = await storePromptResult(store, sessionId);
return { output: text, detailHash, sessionId, assembledPrompt: "" };
return { output: text, detailHash, sessionId, assembledPrompt: "", usage: null };
}
const agentMain = createAgent({
+1
View File
@@ -103,6 +103,7 @@ export function createMockAgent(mockDataPath: string): () => Promise<void> {
detailHash,
sessionId,
assembledPrompt: "",
usage: null,
};
lastResult = result;
return result;
@@ -118,6 +118,7 @@ async function createTestStep(
completedAtMs: Date.now() + 1000,
assembledPrompt: null,
cwd: "/tmp",
usage: null,
};
return store.cas.put(schemas.stepNode, stepPayload);
}
@@ -96,6 +96,7 @@ describe("protocol types", () => {
completedAtMs: 2000,
assembledPrompt: null,
cwd: "/test/path",
usage: null,
};
expect(record.startedAtMs).toBe(1000);
expect(record.completedAtMs).toBe(2000);
@@ -110,6 +111,7 @@ describe("protocol types", () => {
agent: "uwf-test",
timestamp: 123,
durationMs: 5000,
usage: null,
};
expect(entry.durationMs).toBe(5000);
});
+1
View File
@@ -66,6 +66,7 @@ export async function cmdStepList(
agent: item.payload.agent,
timestamp: item.timestamp,
durationMs: item.payload.completedAtMs - item.payload.startedAtMs,
usage: item.payload.usage ?? null,
});
}
@@ -27,6 +27,7 @@ describe("Protocol types for thread/edge location", () => {
completedAtMs: Date.now() + 1000,
assembledPrompt: null,
cwd: "/home/user/project",
usage: null,
};
expect(record.cwd).toBe("/home/user/project");
+1
View File
@@ -44,6 +44,7 @@ export type {
ThreadStatus,
ThreadStepsOutput,
ThreadsIndex,
Usage,
WorkflowConfig,
WorkflowName,
WorkflowPayload,
+16
View File
@@ -91,6 +91,22 @@ export const STEP_NODE_SCHEMA: JSONSchema = {
assembledPrompt: {
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
},
usage: {
anyOf: [
{
type: "object",
required: ["turns", "inputTokens", "outputTokens", "duration"],
properties: {
turns: { type: "integer" },
inputTokens: { type: "integer" },
outputTokens: { type: "integer" },
duration: { type: "number" },
},
additionalProperties: false,
},
{ type: "null" },
],
},
},
additionalProperties: false,
};
+12
View File
@@ -22,6 +22,17 @@ export type StepRecord = {
cwd: string;
/** CAS ref to the fully assembled prompt sent to the agent. null for legacy steps. */
assembledPrompt: CasRef | null;
/** Token usage statistics reported by the agent adapter. null for legacy steps. */
usage: Usage | null;
};
/** Token usage statistics reported by agent adapters. */
export type Usage = {
turns: number;
inputTokens: number;
outputTokens: number;
/** Wall-clock duration in seconds. */
duration: number;
};
// ── 4.2 Workflow 定义 ───────────────────────────────────────────────
@@ -131,6 +142,7 @@ export type StepEntry = {
agent: string;
timestamp: number;
durationMs: number;
usage: Usage | null;
};
/** uwf thread steps — start entry */
+1
View File
@@ -132,6 +132,7 @@ async function buildHistory(
completedAtMs: step.completedAtMs,
cwd: step.cwd ?? "",
assembledPrompt: step.assembledPrompt ?? null,
usage: step.usage ?? null,
content,
});
}
+8 -1
View File
@@ -1,5 +1,5 @@
import { getSchema, validate } from "@ocas/core";
import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol";
import type { CasRef, StepNodePayload, ThreadId, Usage } from "@united-workforce/protocol";
import { config as loadDotenv } from "dotenv";
import { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
import { buildContextWithMeta } from "./context.js";
@@ -65,6 +65,7 @@ async function writeStepNode(options: {
startedAtMs: number;
completedAtMs: number;
assembledPromptHash: CasRef | null;
usage: Usage | null;
}): Promise<CasRef> {
const payload: StepNodePayload = {
start: options.startHash,
@@ -78,6 +79,7 @@ async function writeStepNode(options: {
completedAtMs: options.completedAtMs,
cwd: process.cwd(),
assembledPrompt: options.assembledPromptHash,
usage: options.usage,
};
const hash = await options.store.cas.put(options.schemas.stepNode, payload);
const node = options.store.cas.get(hash);
@@ -117,6 +119,7 @@ async function persistStep(options: {
startedAtMs: number;
completedAtMs: number;
assembledPromptHash: CasRef | null;
usage: Usage | null;
}): Promise<CasRef> {
const { store, schemas, chain, headHash } = options.ctx.meta;
return writeStepNode({
@@ -132,6 +135,7 @@ async function persistStep(options: {
startedAtMs: options.startedAtMs,
completedAtMs: options.completedAtMs,
assembledPromptHash: options.assembledPromptHash,
usage: options.usage,
});
}
@@ -200,6 +204,7 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
);
}
const completedAtMs = Date.now();
const usage = agentResult.usage;
// Store the assembled prompt in CAS for later inspection via `step read --prompt`
const promptText = agentResult.assembledPrompt;
@@ -220,6 +225,7 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
startedAtMs,
completedAtMs,
assembledPromptHash,
usage,
});
const adapterOutput: AdapterOutput = {
@@ -230,6 +236,7 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
body: extracted.body,
startedAtMs,
completedAtMs,
usage,
};
process.stdout.write(`${JSON.stringify(adapterOutput)}\n`);
};
+9 -1
View File
@@ -1,5 +1,10 @@
import type { Store } from "@ocas/core";
import type { ModeratorContext, ThreadId, WorkflowPayload } from "@united-workforce/protocol";
import type {
ModeratorContext,
ThreadId,
Usage,
WorkflowPayload,
} from "@united-workforce/protocol";
export type AgentContext = ModeratorContext & {
threadId: ThreadId;
@@ -33,6 +38,8 @@ export type AgentRunResult = {
sessionId: string;
/** The fully assembled prompt that was sent to the agent. */
assembledPrompt: string;
/** Token usage statistics for this run. null when the adapter does not report usage. */
usage: Usage | null;
};
export type AgentContinueFn = (
@@ -51,6 +58,7 @@ export type AdapterOutput = {
body: string;
startedAtMs: number;
completedAtMs: number;
usage: Usage | null;
};
export type AgentOptions = {