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
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:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -44,6 +44,7 @@ export type {
|
||||
ThreadStatus,
|
||||
ThreadStepsOutput,
|
||||
ThreadsIndex,
|
||||
Usage,
|
||||
WorkflowConfig,
|
||||
WorkflowName,
|
||||
WorkflowPayload,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -132,6 +132,7 @@ async function buildHistory(
|
||||
completedAtMs: step.completedAtMs,
|
||||
cwd: step.cwd ?? "",
|
||||
assembledPrompt: step.assembledPrompt ?? null,
|
||||
usage: step.usage ?? null,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user