Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/agent/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ const COMMANDS: readonly CommandEntry[] = Object.freeze([
"Enables verbose SDK event logging (same as HYPERAGENT_DEBUG=1).\n" +
"Shows every event the agent receives from the Copilot SDK.",
},
{
completion: "/tokens",
help: "Show session token usage summary",
detail:
"Displays cumulative input/output/cache tokens, request count,\n" +
"and turn count for the current session. Also shown on exit.",
},
{
completion: "/reasoning conversation ",
help: "Set conversation reasoning effort (low|medium|high|xhigh)",
Expand Down
7 changes: 7 additions & 0 deletions src/agent/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,13 @@ export function registerEventHandler(
cost?: number;
duration?: number;
};

// Accumulate session totals
state.totalInputTokens += usageData.inputTokens ?? 0;
state.totalOutputTokens += usageData.outputTokens ?? 0;
state.totalCacheReadTokens += usageData.cacheReadTokens ?? 0;
state.totalRequests += usageData.cost ?? 0;
Comment thread
simongdavies marked this conversation as resolved.
Outdated

// Ensure stats appear on a new line — streamed
// message_delta writes don't end with \n.
if (state.streamedContent) {
Expand Down
20 changes: 20 additions & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import {
renderReasoningTransition,
printUsageStats,
printExtendedReasoningNotice,
formatTokenSummary,
} from "./llm-output.js";

// ── Session Timing ───────────────────────────────────────────────────
Expand Down Expand Up @@ -145,6 +146,19 @@ function formatSessionDuration(): string {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}

/**
* Print session token summary to console.
* Called at all exit points (interactive, --prompt, SIGINT).
*/
function printExitTokenSummary(): void {
if (state.totalRequests === 0) return; // No LLM calls made
const lines = formatTokenSummary(state);
Comment thread
simongdavies marked this conversation as resolved.
Outdated
console.log(); // blank line before summary
for (const line of lines) {
console.log(` ${line}`);
}
}

// ── Paths ────────────────────────────────────────────────────────────

const __agentFilename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -4175,6 +4189,9 @@ function buildSessionConfig() {
// Guidance is stored in state.lastGuidance and re-injected on
// every turn so it survives compaction.
onUserPromptSubmitted: async (input: { prompt: string }) => {
// Track turn count
state.totalTurns++;

// Capture prompt and reset per-prompt tracking flags
state.currentUserPrompt = input.prompt;
state.hasCalledListModules = false;
Expand Down Expand Up @@ -4865,6 +4882,7 @@ async function main(): Promise<void> {
await processMessage(session, "continue");
}
}
printExitTokenSummary();
console.log(`\n✅ Prompt completed. (${formatSessionDuration()})\n`);
cleanupCtrlR?.();
rl.close();
Expand Down Expand Up @@ -4899,6 +4917,7 @@ async function main(): Promise<void> {
// Exit — either bare 'exit' or '/exit'
const lower = trimmed.toLowerCase();
if (lower === "exit" || lower === "/exit") {
printExitTokenSummary();
console.log(`\n👋 Goodbye! (session: ${formatSessionDuration()})\n`);
break;
}
Expand Down Expand Up @@ -5105,6 +5124,7 @@ const SHUTDOWN_TIMEOUT_MS = 5_000;

// Graceful shutdown — clean up on SIGINT (Ctrl+C)
process.on("SIGINT", async () => {
printExitTokenSummary();
console.log(`\n\n👋 Goodbye! (session: ${formatSessionDuration()})\n`);

// Stop transcript synchronously — async won't complete before exit
Expand Down
31 changes: 31 additions & 0 deletions src/agent/llm-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ export function printUsageStats(stats: string, indent: string): void {
console.log(`${indent}${C.dim("📊 " + stats)}`);
}

/**
* Format a session token summary for /tokens command or exit display.
* Returns an array of lines (without leading newline).
*/
export function formatTokenSummary(state: {
totalInputTokens: number;
totalOutputTokens: number;
totalCacheReadTokens: number;
totalRequests: number;
totalTurns: number;
}): string[] {
const total = state.totalInputTokens + state.totalOutputTokens;
const lines: string[] = [];
lines.push(`${C.label("Token Usage")} ${C.dim("(session total)")}`);
lines.push(
` Input: ${state.totalInputTokens.toLocaleString()} tokens`,
);
lines.push(
` Output: ${state.totalOutputTokens.toLocaleString()} tokens`,
);
if (state.totalCacheReadTokens > 0) {
lines.push(
` Cache read: ${state.totalCacheReadTokens.toLocaleString()} tokens`,
);
}
lines.push(` Total: ${total.toLocaleString()} tokens`);
lines.push(` Requests: ${state.totalRequests}`);
lines.push(` Turns: ${state.totalTurns}`);
Comment thread
simongdavies marked this conversation as resolved.
Outdated
return lines;
}

// ── Reasoning Rendering ──────────────────────────────────────────────

/**
Expand Down
2 changes: 2 additions & 0 deletions src/agent/module-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ export async function saveModule(
description,
author,
mutable,
// Keep sourceHash in sync so the validator doesn't flag a mismatch
sourceHash: currentHash.slice(0, 23), // "sha256:" + 16 hex chars
Comment thread
simongdavies marked this conversation as resolved.
Outdated
metadataCache: {
extractedFromHash: currentHash,
exports,
Expand Down
10 changes: 10 additions & 0 deletions src/agent/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ export async function handleSlashCommand(
console.log();
return true;

case "/tokens": {
const { formatTokenSummary } = await import("./llm-output.js");
const lines = formatTokenSummary(state);
for (const line of lines) {
console.log(` ${line}`);
}
console.log();
return true;
}
Comment thread
simongdavies marked this conversation as resolved.

case "/reasoning": {
// /reasoning conversation <level> — set conversation reasoning effort
// /reasoning audit <level> — set audit reasoning effort (min: medium)
Expand Down
24 changes: 24 additions & 0 deletions src/agent/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,23 @@ export interface AgentState {
* additionalContext on every onUserPromptSubmitted.
*/
lastGuidance: string | null;

// ── Token Tracking ───────────────────────────────────────────────

/** Cumulative input tokens across all LLM requests this session. */
totalInputTokens: number;

/** Cumulative output tokens across all LLM requests this session. */
totalOutputTokens: number;

/** Cumulative cache-read tokens across all LLM requests this session. */
totalCacheReadTokens: number;

/** Total number of LLM API requests this session. */
Comment thread
simongdavies marked this conversation as resolved.
Outdated
totalRequests: number;

/** Number of user turns (messages sent) this session. */
totalTurns: number;
}

// ── Factory ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -317,5 +334,12 @@ export function createAgentState(
hasCalledListModules: false,
modulesInspected: new Set<string>(),
lastGuidance: null,

// Token tracking
totalInputTokens: 0,
totalOutputTokens: 0,
totalCacheReadTokens: 0,
totalRequests: 0,
totalTurns: 0,
};
}
6 changes: 6 additions & 0 deletions src/agent/system-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ DISCOVERY (never guess — always check):
Do NOT guess parameter names — they are ALL listed in typeDefinitions.
For specific function details, call module_info(name, "functionName").

BUILDING REUSABLE MODULES:
When you identify a missing capability (like a format library), don't just
describe the gap — use register_module to build a reusable module that fills
it. Modules persist across sessions and compound in value over time.
Import your module with: import { fn } from "ha:<name>"

PLUGINS: Require explicit enable via manage_plugin.
Host plugin functions return values directly (not Promises).
You CAN use async/await — it works — but await on a plugin call
Expand Down
Loading