Skip to content
Closed
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
6 changes: 5 additions & 1 deletion products/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ Hosts:
- `apps/code`: Electron desktop host.
- `apps/web`: web host and portability smoke test.
- `apps/mobile`: React Native host.
- `apps/cli`: thin shell over `@posthog/cli`.

Executable packages own a `bin` rather than an `apps/*` host shell. They boot the same packages a host does, without a UI:

- `packages/cli`: headless CLI (`@posthog/code-cli`, bin `posthog-code-cli`) for one-shot agent runs over the in-process ACP connection.
- `packages/harness`: `@posthog/harness` (bin `harness`, `hog`), which spawns the pi.dev coding agent against the PostHog LLM gateway.

## Rules

Expand Down
5 changes: 2 additions & 3 deletions products/desktop/knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@
"@vitest/coverage-v8"
]
},
"apps/cli": {
"entry": ["src/cli.ts"],
"project": ["src/**/*.ts", "bin/**/*.ts"],
"packages/cli": {
"project": ["src/**/*.ts"],
"includeEntryExports": true
},
"packages/agent": {
Expand Down
4 changes: 4 additions & 0 deletions products/desktop/packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@
"types": "./dist/execution-mode.d.ts",
"import": "./dist/execution-mode.js"
},
"./unattended-permission-policy": {
"types": "./dist/unattended-permission-policy.d.ts",
"import": "./dist/unattended-permission-policy.js"
},
"./resume": {
"types": "./dist/resume.d.ts",
"import": "./dist/resume.js"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const claudeAgentConstructor = vi.fn();

vi.mock("./claude/claude-agent", () => ({
ClaudeAcpAgent: class {
constructor(...args: unknown[]) {
claudeAgentConstructor(...args);
}
// Called by the connection's cleanup.
async closeSession(): Promise<void> {}
},
}));

const { createAcpConnection } = await import("./acp-connection");
const { Logger } = await import("../utils/logger");

/**
* The Claude adapter's own diagnostics include whole payloads: the expanded body
* of a slash command, raw tool inputs, the subprocess's stderr. A host logger
* usually carries an `onLog` that persists or transmits what it receives (on
* desktop, electron-log's file and OTLP transports; on cloud, the run log and
* the user's task feed), so forwarding is opt-in.
*/
describe("createAcpConnection adapter log forwarding", () => {
beforeEach(() => {
claudeAgentConstructor.mockClear();
});

function adapterOptions(): { logger?: unknown } {
// AgentSideConnection builds the agent eagerly, so one call is recorded by
// the time createAcpConnection returns.
expect(claudeAgentConstructor).toHaveBeenCalledTimes(1);
return claudeAgentConstructor.mock.calls[0][1] as { logger?: unknown };
}

it("withholds the host logger from the adapter by default", async () => {
const connection = createAcpConnection({
adapter: "claude",
logger: new Logger({ debug: true, onLog: () => {} }),
});
try {
expect(adapterOptions().logger).toBeUndefined();
} finally {
await connection.cleanup();
}
});

it("passes a scoped child logger when the host opts in", async () => {
const connection = createAcpConnection({
adapter: "claude",
logger: new Logger({ debug: true, onLog: () => {} }),
forwardAdapterLogs: true,
});
try {
expect(adapterOptions().logger).toBeInstanceOf(Logger);
} finally {
await connection.cleanup();
}
});

it("passes no logger when the host opts in without supplying one", async () => {
const connection = createAcpConnection({
adapter: "claude",
forwardAdapterLogs: true,
});
try {
expect(adapterOptions().logger).toBeUndefined();
} finally {
await connection.cleanup();
}
});
});
12 changes: 12 additions & 0 deletions products/desktop/packages/agent/src/adapters/acp-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export type AcpConnectionConfig = {
/** Deployment environment - "local" for desktop, "cloud" for cloud sandbox */
deviceType?: "local" | "cloud";
logger?: Logger;
/**
* Route the Claude adapter's own diagnostics through `logger` instead of its
* console fallback. Off by default: adapter-internal logging includes whole
* payloads (expanded slash-command output, tool inputs, subprocess stderr),
* and a host `logger` typically carries an `onLog` that persists or transmits
* what it receives. Only a host whose sink is the operator's own terminal
* should turn this on.
*/
forwardAdapterLogs?: boolean;
processCallbacks?: ProcessSpawnedCallback;
codexOptions?: CodexOptions;
codexModels?: ReadonlyArray<ModelInfo>;
Expand Down Expand Up @@ -119,6 +128,9 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection {
onStructuredOutput: config.onStructuredOutput,
posthogApiConfig: resolveEnricherApiConfig(config),
gatewayEnv: config.claudeGatewayEnv,
logger: config.forwardAdapterLogs
? config.logger?.child("ClaudeAcpAgent")
: undefined,
});
return agent;
}, agentStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ export interface ClaudeAcpAgentOptions {
posthogApiConfig?: PostHogAPIConfig;
/** Explicit gateway config — avoids global process.env mutation across concurrent sessions. */
gatewayEnv?: GatewayEnv;
/** Injected logger; defaults to a console logger with debug enabled. */
logger?: Logger;
}

export class ClaudeAcpAgent extends BaseAcpAgent {
Expand All @@ -279,7 +281,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
this.toolUseCache = {};
this.emittedToolCalls = new Set();
this.toolUseStreamCache = new Map();
this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
this.logger =
options?.logger ??
new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
this.enrichment = createEnrichment(options?.posthogApiConfig, this.logger);
}

Expand Down Expand Up @@ -2278,6 +2282,17 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
settingsManager.getSettings().model,
]);
modelOptions.currentModelId = resolvedModelId;
// A requested id that isn't available falls back silently, which reads as
// "the model I asked for" to a caller that can't see the allowed set (a
// headless CLI run, a scripted session). Say so.
const requestedModelId = meta?.model?.trim();
if (requestedModelId && requestedModelId !== resolvedModelId) {
this.logger.warn("Requested model is unavailable; using another", {
requested: requestedModelId,
resolved: resolvedModelId,
available: modelOptions.options.map((opt) => opt.value),
});
}
session.modelId = resolvedModelId;
session.lastContextWindowSize =
meta?.contextWindow === "200k"
Expand Down
3 changes: 3 additions & 0 deletions products/desktop/packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class Agent {
private sessionLogWriter?: SessionLogWriter;
private posthogApiConfig?: AgentConfig["posthog"];
private enricherEnabled: boolean;
private forwardAdapterLogs: boolean;

constructor(config: AgentConfig) {
this.logger = new Logger({
Expand All @@ -38,6 +39,7 @@ export class Agent {
this.posthogApiConfig = config.posthog;
}
this.enricherEnabled = config.enricher?.enabled !== false;
this.forwardAdapterLogs = config.forwardAdapterLogs === true;

if (config.posthog && !config.skipLogPersistence) {
this.sessionLogWriter = new SessionLogWriter({
Expand Down Expand Up @@ -132,6 +134,7 @@ export class Agent {
taskId,
deviceType: "local",
logger: this.logger,
forwardAdapterLogs: this.forwardAdapterLogs,
processCallbacks: options.processCallbacks,
onStructuredOutput: options.onStructuredOutput,
codexModels,
Expand Down
7 changes: 7 additions & 0 deletions products/desktop/packages/agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ export interface AgentConfig {
enricher?: { enabled?: boolean };
debug?: boolean;
onLog?: OnLogCallback;
/**
* Send the adapter's own diagnostics to `onLog` too. Off by default because
* those lines carry whole payloads (expanded slash-command output, tool
* inputs, subprocess stderr) and most hosts persist or transmit what `onLog`
* receives. Turn it on only when the sink is the operator's own terminal.
*/
forwardAdapterLogs?: boolean;
}

// Device info for tracking where work happens
Expand Down
Loading
Loading