Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion artifacts/architecture-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ interface Agent {

// Optional
postLaunchSetup?(session: Session): Promise<void>;
estimateCost?(session: Session): Promise<CostEstimate>;
}
```

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
session.activitySignal = createActivitySignal("probe_failure", { source: "native" });
}

// Enrich with agent session info (summary, cost, native restore metadata).
// Enrich with lightweight agent session info (summary and native restore metadata).
await persistAgentSessionInfo();
}
}
Expand Down
12 changes: 2 additions & 10 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ export interface Session {
/** Runtime handle for communicating with the session */
runtimeHandle: RuntimeHandle | null;

/** Agent session info (summary, cost, etc.) */
/** Agent session info (summary and native restore metadata) */
agentInfo: AgentSessionInfo | null;

/** When the session was created */
Expand Down Expand Up @@ -515,7 +515,7 @@ export interface Agent {
*/
isProcessRunning(handle: RuntimeHandle): Promise<ProcessProbeResult>;

/** Extract information from agent's internal data (summary, cost, session ID) */
/** Extract lightweight information from agent-owned metadata (summary and native session ID) */
getSessionInfo(session: Session): Promise<AgentSessionInfo | null>;

/**
Expand Down Expand Up @@ -636,14 +636,6 @@ export interface AgentSessionInfo {
agentSessionId: string | null;
/** Agent-owned metadata worth persisting for later restore. */
metadata?: Record<string, string>;
/** Estimated cost so far */
cost?: CostEstimate;
}

export interface CostEstimate {
inputTokens: number;
outputTokens: number;
estimatedCostUsd: number;
}

// =============================================================================
Expand Down
38 changes: 29 additions & 9 deletions packages/plugins/agent-aider/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
type AgentLaunchConfig,
} from "@aoagents/ao-core";

// Mock fs/promises for getSessionInfo tests (readFile for .aider.chat.history.md)
// Mock fs/promises for getSessionInfo tests
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>;
return {
...actual,
readFile: vi.fn().mockRejectedValue(new Error("ENOENT")),
open: vi.fn().mockRejectedValue(new Error("ENOENT")),
};
});

Expand Down Expand Up @@ -125,6 +126,24 @@ function mockTmuxWithProcess(processName: string, found = true) {
});
}

function makeFakeFileHandle(content: string) {
const buf = Buffer.from(content, "utf-8");
return {
read: vi
.fn()
.mockImplementation(
(buffer: Buffer, offset: number, length: number, position: number | null) => {
const start = position ?? 0;
if (start >= buf.length) return Promise.resolve({ bytesRead: 0, buffer });
const bytesToCopy = Math.min(length, buf.length - start);
buf.copy(buffer, offset, start, start + bytesToCopy);
return Promise.resolve({ bytesRead: bytesToCopy, buffer });
},
),
close: vi.fn().mockResolvedValue(undefined),
};
}

beforeEach(() => {
vi.clearAllMocks();
});
Expand Down Expand Up @@ -412,28 +431,29 @@ describe("getSessionInfo", () => {
});

it("returns null when no chat history file exists", async () => {
const { readFile } = await import("node:fs/promises");
vi.mocked(readFile).mockRejectedValueOnce(new Error("ENOENT"));
const { open } = await import("node:fs/promises");
vi.mocked(open).mockRejectedValueOnce(new Error("ENOENT"));
expect(await agent.getSessionInfo(makeSession())).toBeNull();
});

it("extracts summary from chat history file", async () => {
const { readFile } = await import("node:fs/promises");
vi.mocked(readFile).mockResolvedValueOnce(
"# aider chat started\n\n#### Fix the login bug in auth.ts\n\nSome response here...\n",
const { open } = await import("node:fs/promises");
vi.mocked(open).mockResolvedValueOnce(
makeFakeFileHandle(
"# aider chat started\n\n#### Fix the login bug in auth.ts\n\nSome response here...\n",
) as never,
);
const info = await agent.getSessionInfo(makeSession());
expect(info).not.toBeNull();
expect(info!.summary).toBe("Fix the login bug in auth.ts");
expect(info!.summaryIsFallback).toBe(true);
expect(info!.agentSessionId).toBeNull();
expect(info!.cost).toBeUndefined();
});

it("truncates long summaries to 120 chars", async () => {
const { readFile } = await import("node:fs/promises");
const { open } = await import("node:fs/promises");
const longMsg = "A".repeat(200);
vi.mocked(readFile).mockResolvedValueOnce(`#### ${longMsg}\n`);
vi.mocked(open).mockResolvedValueOnce(makeFakeFileHandle(`#### ${longMsg}\n`) as never);
const info = await agent.getSessionInfo(makeSession());
expect(info!.summary).toHaveLength(123); // 120 + "..."
expect(info!.summary!.endsWith("...")).toBe(true);
Expand Down
13 changes: 10 additions & 3 deletions packages/plugins/agent-aider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "@aoagents/ao-core";
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { stat, access, readFile } from "node:fs/promises";
import { stat, access, open } from "node:fs/promises";
import { join } from "node:path";
import { constants, readFileSync } from "node:fs";

Expand Down Expand Up @@ -59,7 +59,15 @@ async function getChatHistoryMtime(workspacePath: string): Promise<Date | null>
async function extractAiderSummary(workspacePath: string): Promise<string | null> {
try {
const chatFile = join(workspacePath, ".aider.chat.history.md");
const content = await readFile(chatFile, "utf-8");
const handle = await open(chatFile, "r");
let content: string;
try {
const buffer = Buffer.allocUnsafe(64 * 1024);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
content = buffer.subarray(0, bytesRead).toString("utf-8");
} finally {
await handle.close();
}

// Aider chat history uses "#### " prefix for user messages
for (const line of content.split("\n")) {
Expand Down Expand Up @@ -277,7 +285,6 @@ function createAiderAgent(): Agent {
summary,
summaryIsFallback: true,
agentSessionId: null,
// Aider doesn't expose token/cost data
};
},

Expand Down
Loading
Loading