feat: add $usage field to adapter protocol
CI / check (pull_request) Successful in 2m28s

- Add Usage type to protocol (turns, inputTokens, outputTokens, duration)
- Add usage to StepRecord, StepNodePayload, StepEntry, STEP_NODE_SCHEMA
- Thread usage through util-agent extract pipeline (writeStepNode → persistStep → createAgent)
- All adapters return usage: null as placeholder (mock, hermes, claude-code, builtin)
- 746 tests pass, no breaking changes (usage not in schema required array)

Fixes #74
Refs #68
This commit is contained in:
2026-06-04 15:41:07 +00:00
parent 17f7f44c43
commit 99f40c2488
15 changed files with 290 additions and 6 deletions
+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 = {