Files
openclaw/extensions/copilot/src/usage-bridge.ts
Ramrajprabu f3cfd752d3 feat(copilot): add GitHub Copilot agent runtime
Adds the opt-in bundled GitHub Copilot agent runtime, pinned SDK install path, docs/inventory, SDK/tool/sandbox/auth wiring, and replay/tool-safety fixes.

Verification:
- Local: git diff --check; fnm exec --using 24.15.0 pnpm tsgo:extensions; fnm exec --using 24.15.0 pnpm check:test-types; fnm exec --using 24.15.0 pnpm build.
- Autoreview local: clean for the replay-safety fix; branch autoreview engine returned empty output twice, so local autoreview plus local/Crabbox/CI proof was used.
- Crabbox focused Copilot: run_2c0db9f48a4a, 19 files / 485 tests passed.
- Crabbox additional boundary shard: run_26a246a1aa24, prompt snapshots and plugin SDK boundary/export checks passed.
- Crabbox live Copilot: run_d128e4048b4e, real gpt-4.1 turn with live_echo phase-1-green and clean session-file check.
- GitHub checks: green on head 7cc8657e0d, including Dependency Guard after exact-head approval.

Co-authored-by: Ramraj Balasubramanian <ramrajba@microsoft.com>
2026-05-29 05:15:22 +01:00

84 lines
2.3 KiB
TypeScript

import type { AgentMessage, NormalizedUsage } from "openclaw/plugin-sdk/agent-harness-runtime";
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
type AssistantUsage = NonNullable<AssistantMessage["usage"]>;
type CopilotUsageSource = {
cacheReadTokens?: unknown;
cacheWriteTokens?: unknown;
inputTokens?: unknown;
outputTokens?: unknown;
};
export type CopilotUsageSnapshot = NormalizedUsage;
function isCopilotUsageSource(data: unknown): data is CopilotUsageSource {
return typeof data === "object" && data !== null;
}
function buildZeroCost(): AssistantUsage["cost"] {
return {
cacheRead: 0,
cacheWrite: 0,
input: 0,
output: 0,
total: 0,
};
}
function coerceTokenCount(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value)
? Math.max(0, Math.trunc(value))
: undefined;
}
export function normalizeCopilotUsage(data: unknown): NormalizedUsage | undefined {
if (!isCopilotUsageSource(data)) {
return undefined;
}
// SDK usage events only expose these four fields. Keep coercion identical to
// the prior event-bridge implementation so invalid object-shaped events still
// overwrite state with the legacy all-zero snapshot.
const input = coerceTokenCount(data.inputTokens);
const output = coerceTokenCount(data.outputTokens);
const cacheRead = coerceTokenCount(data.cacheReadTokens);
const cacheWrite = coerceTokenCount(data.cacheWriteTokens);
const total = (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
return {
cacheRead,
cacheWrite,
input,
output,
total,
};
}
export function buildCopilotAssistantUsage(params: {
usage?: NormalizedUsage;
fallbackOutputTokens?: unknown;
}): AssistantMessage["usage"] {
const usage =
params.usage ?? normalizeCopilotUsage({ outputTokens: params.fallbackOutputTokens });
return {
cacheRead: usage?.cacheRead ?? 0,
cacheWrite: usage?.cacheWrite ?? 0,
cost: buildZeroCost(),
input: usage?.input ?? 0,
output: usage?.output ?? 0,
totalTokens: usage?.total ?? 0,
};
}
export function deriveCopilotUsageTotal(usage?: NormalizedUsage): number | undefined {
if (!usage) {
return undefined;
}
return (
(usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)
);
}