diff --git a/packages/ai/test/openai-responses-history-payload.test.ts b/packages/ai/test/openai-responses-history-payload.test.ts index fc5322112..78a59f19e 100644 --- a/packages/ai/test/openai-responses-history-payload.test.ts +++ b/packages/ai/test/openai-responses-history-payload.test.ts @@ -26,7 +26,10 @@ function createCodexToken(accountId: string): string { * is exercised by its own targeted tests; these history-replay tests assert raw * payload shape and should stay independent of it. */ -function getOpenAIReasoningModel(provider: Provider, id: string): Model<"openai-responses"> { +function getOpenAIReasoningModel( + provider: Parameters[0], + id: string, +): Model<"openai-responses"> { const base = getBundledModel(provider, id) as Model<"openai-responses">; return { ...base, name: "Reasoning Mini" }; } diff --git a/packages/coding-agent/src/goals/runtime.ts b/packages/coding-agent/src/goals/runtime.ts index a892f6c8d..9b8c9e400 100644 --- a/packages/coding-agent/src/goals/runtime.ts +++ b/packages/coding-agent/src/goals/runtime.ts @@ -162,6 +162,11 @@ export class GoalRuntime { return this.#host.now?.() ?? Date.now(); } + #hasAccountingState(): boolean { + const state = this.#host.getState(); + return Boolean(state?.enabled && isAccountingStatus(state.goal)); + } + async #withAccounting(fn: () => Promise | T): Promise { const previous = this.#accountingTail; const { promise, resolve } = Promise.withResolvers(); @@ -225,31 +230,44 @@ export class GoalRuntime { async onToolCompleted(toolName: string): Promise { if (toolName === "goal") return; + if (!this.#hasAccountingState()) return; await this.flushUsage("allowed"); } async onGoalToolCompleted(): Promise { + if (!this.#hasAccountingState()) return; await this.flushUsage("suppressed"); } async onAgentEnd(options?: { turnCompleted?: boolean; currentUsage?: GoalTokenUsage }): Promise { + if (!this.#hasAccountingState()) { + this.#turnSnapshot = undefined; + return; + } await this.flushUsage("suppressed", options?.currentUsage); this.#turnSnapshot = undefined; } async onTaskAborted(options?: { reason?: "interrupted" | "internal" }): Promise { + const state = this.#host.getState(); + const needsAccounting = state?.enabled && isAccountingStatus(state.goal); + const needsPause = options?.reason === "interrupted" && state?.enabled && state.goal.status === "active"; + if (!needsAccounting && !needsPause) { + this.#turnSnapshot = undefined; + return; + } await this.#withAccounting(async () => { await this.#flushUsageLocked("suppressed"); this.#turnSnapshot = undefined; if (options?.reason !== "interrupted") return; - const state = this.#getStateClone(); - if (!state?.enabled || state.goal.status !== "active") return; - state.enabled = false; - state.goal.status = "paused"; - state.goal.updatedAt = this.#now(); + const cloned = this.#getStateClone(); + if (!cloned?.enabled || cloned.goal.status !== "active") return; + cloned.enabled = false; + cloned.goal.status = "paused"; + cloned.goal.updatedAt = this.#now(); this.#clearActiveAccounting(); this.#budgetReportedFor = undefined; - await this.#commitState(state, { persist: "goal_paused" }); + await this.#commitState(cloned, { persist: "goal_paused" }); }); } diff --git a/packages/coding-agent/test/goals/goal-mode-integration.test.ts b/packages/coding-agent/test/goals/goal-mode-integration.test.ts index 8583ed29f..5c31dcbb6 100644 --- a/packages/coding-agent/test/goals/goal-mode-integration.test.ts +++ b/packages/coding-agent/test/goals/goal-mode-integration.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; -import { Agent, type AgentTool } from "@oh-my-pi/pi-agent-core"; +import { Agent } from "@oh-my-pi/pi-agent-core"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { GoalTool } from "@oh-my-pi/pi-coding-agent/goals/tools/goal-tool"; @@ -9,7 +9,7 @@ import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; +import { createTools, type Tool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import { TempDir } from "@oh-my-pi/pi-utils"; function createToolSession(cwd: string, settings: Settings, overrides: Partial = {}): ToolSession { @@ -51,7 +51,7 @@ async function createGoalHarness(): Promise { }); const bootstrapToolSession = createToolSession(tempDir.path(), settings); const initialTools = await createTools(bootstrapToolSession, ["read"]); - const toolRegistry = new Map(initialTools.map(tool => [tool.name, tool] as const)); + const toolRegistry = new Map(initialTools.map(tool => [tool.name, tool] as const)); const session = new AgentSession({ agent: new Agent({ @@ -73,7 +73,7 @@ async function createGoalHarness(): Promise { getGoalModeState: () => session.getGoalModeState(), getGoalRuntime: () => session.goalRuntime, }); - toolRegistry.set("goal", new GoalTool(toolSession)); + toolRegistry.set("goal", new GoalTool(toolSession) as unknown as Tool); return { tempDir,