@posthog/agent 2.3.520 → 2.3.526

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,12 +36,45 @@ import {
36
36
  import type { PermissionMode } from "../../execution-mode";
37
37
  import type { Logger } from "../../utils/logger";
38
38
  import type { CodexSessionState } from "./session-state";
39
+ import {
40
+ STRUCTURED_OUTPUT_MCP_NAME,
41
+ STRUCTURED_OUTPUT_TOOL_NAME,
42
+ } from "./structured-output-constants";
39
43
 
40
44
  export interface CodexClientCallbacks {
41
45
  /** Called when a usage_update session notification is received */
42
46
  onUsageUpdate?: (update: Record<string, unknown>) => void;
43
47
  /** When set, Read responses are annotated with PostHog enrichment before reaching codex-acp. */
44
48
  enrichmentDeps?: FileEnrichmentDeps;
49
+ /**
50
+ * Called once per session when the agent completes the injected
51
+ * `create_output` MCP tool. Matches the Claude adapter's structured
52
+ * output delivery.
53
+ */
54
+ onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;
55
+ }
56
+
57
+ /**
58
+ * Tool calls for our injected MCP server surface in ACP `tool_call` /
59
+ * `tool_call_update` notifications. The `title` from codex-acp can be
60
+ * either the bare tool name or prefixed (`mcp__<server>__<tool>`); match
61
+ * both forms but require the server name on prefixed titles so an unrelated
62
+ * user tool happening to contain `create_output` doesn't trigger us.
63
+ */
64
+ function isStructuredOutputToolCall(title: string | undefined | null): boolean {
65
+ if (!title) return false;
66
+ if (title === STRUCTURED_OUTPUT_TOOL_NAME) return true;
67
+ return (
68
+ title.includes(STRUCTURED_OUTPUT_MCP_NAME) &&
69
+ title.includes(STRUCTURED_OUTPUT_TOOL_NAME)
70
+ );
71
+ }
72
+
73
+ function toRecord(value: unknown): Record<string, unknown> | null {
74
+ if (value && typeof value === "object" && !Array.isArray(value)) {
75
+ return value as Record<string, unknown>;
76
+ }
77
+ return null;
45
78
  }
46
79
 
47
80
  const AUTO_APPROVED_KINDS: Record<PermissionMode, Set<ToolKind>> = {
@@ -96,6 +129,14 @@ export function createCodexClient(
96
129
  callbacks?: CodexClientCallbacks,
97
130
  ): Client {
98
131
  const terminalHandles = new Map<string, TerminalHandle>();
132
+ // Track rawInput across tool_call → tool_call_update → completed so we can
133
+ // fire onStructuredOutput exactly once per tool call id. Entries stay in
134
+ // the map after firing with `fired: true` so a re-emitted completion
135
+ // (if codex-acp ever resends one) is a no-op.
136
+ const structuredOutputState = new Map<
137
+ string,
138
+ { rawInput?: Record<string, unknown>; fired: boolean }
139
+ >();
99
140
 
100
141
  return {
101
142
  async requestPermission(
@@ -125,6 +166,33 @@ export function createCodexClient(
125
166
 
126
167
  async sessionUpdate(params: SessionNotification): Promise<void> {
127
168
  const update = params.update as Record<string, unknown> | undefined;
169
+
170
+ if (
171
+ callbacks?.onStructuredOutput &&
172
+ (update?.sessionUpdate === "tool_call" ||
173
+ update?.sessionUpdate === "tool_call_update")
174
+ ) {
175
+ const toolCallId = update.toolCallId as string | undefined;
176
+ const title = update.title as string | undefined;
177
+ if (toolCallId && isStructuredOutputToolCall(title)) {
178
+ const entry = structuredOutputState.get(toolCallId) ?? {
179
+ fired: false,
180
+ };
181
+ const rawInput = toRecord(update.rawInput);
182
+ if (rawInput) entry.rawInput = rawInput;
183
+ structuredOutputState.set(toolCallId, entry);
184
+
185
+ if (update.status === "completed" && !entry.fired && entry.rawInput) {
186
+ entry.fired = true;
187
+ try {
188
+ await callbacks.onStructuredOutput(entry.rawInput);
189
+ } catch (err) {
190
+ logger.warn("onStructuredOutput callback threw", { error: err });
191
+ }
192
+ }
193
+ }
194
+ }
195
+
128
196
  if (update?.sessionUpdate === "usage_update") {
129
197
  const used = update.used as number | undefined;
130
198
  const size = update.size as number | undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared identifiers for the injected structured-output MCP server.
3
+ * Imported by codex-agent.ts (server config), codex-client.ts (tool-call
4
+ * matching), and structured-output-mcp-server.ts (tool registration) so the
5
+ * three stay in sync.
6
+ */
7
+
8
+ export const STRUCTURED_OUTPUT_MCP_NAME = "posthog_output";
9
+ export const STRUCTURED_OUTPUT_TOOL_NAME = "create_output";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Standalone stdio MCP server for structured output in the Codex adapter.
3
+ *
4
+ * Spawned by codex-acp as an MCP server process. Reads the JSON schema
5
+ * from the POSTHOG_OUTPUT_SCHEMA env var (base64-encoded) and registers
6
+ * a tool whose Zod shape McpServer.tool() validates on each call.
7
+ *
8
+ * Usage:
9
+ * POSTHOG_OUTPUT_SCHEMA=<base64> node structured-output-mcp-server.js
10
+ */
11
+
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import {
16
+ STRUCTURED_OUTPUT_MCP_NAME,
17
+ STRUCTURED_OUTPUT_TOOL_NAME,
18
+ } from "./structured-output-constants";
19
+
20
+ function die(message: string): never {
21
+ process.stderr.write(`[structured-output-mcp-server] ${message}\n`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const schemaEnv = process.env.POSTHOG_OUTPUT_SCHEMA;
26
+ if (!schemaEnv) {
27
+ die("POSTHOG_OUTPUT_SCHEMA env var is required");
28
+ }
29
+
30
+ let jsonSchema: Record<string, unknown>;
31
+ try {
32
+ jsonSchema = JSON.parse(Buffer.from(schemaEnv, "base64").toString("utf-8"));
33
+ } catch (err) {
34
+ die(`Failed to parse POSTHOG_OUTPUT_SCHEMA as base64-encoded JSON: ${err}`);
35
+ }
36
+
37
+ const zodType = z.fromJSONSchema(jsonSchema);
38
+ if (!(zodType instanceof z.ZodObject)) {
39
+ die(
40
+ `POSTHOG_OUTPUT_SCHEMA must describe a JSON object schema (got ${zodType.constructor.name})`,
41
+ );
42
+ }
43
+ // McpServer.tool() expects a mutable ZodRawShape
44
+ const zodShape = { ...zodType.shape } as z.ZodRawShape;
45
+
46
+ const server = new McpServer({
47
+ name: STRUCTURED_OUTPUT_MCP_NAME,
48
+ version: "1.0.0",
49
+ });
50
+
51
+ server.tool(
52
+ STRUCTURED_OUTPUT_TOOL_NAME,
53
+ "Submit the structured output for this task. Call this tool with the required fields to deliver your final result.",
54
+ zodShape,
55
+ async () => {
56
+ // McpServer.tool() validates `args` against `zodShape` before invoking
57
+ // this handler, so reaching this point means the input is valid. The
58
+ // parent process captures the validated output by intercepting the
59
+ // tool call in the ACP stream.
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text" as const,
64
+ text: "Output submitted successfully.",
65
+ },
66
+ ],
67
+ };
68
+ },
69
+ );
70
+
71
+ const transport = new StdioServerTransport();
72
+ await server.connect(transport);
@@ -300,7 +300,7 @@ export class HandoffCheckpointTracker {
300
300
  checkpoint: GitHandoffCheckpoint,
301
301
  uploads: Uploads,
302
302
  ): void {
303
- this.logger.info("Captured handoff checkpoint", {
303
+ this.logger.debug("Captured handoff checkpoint", {
304
304
  branch: checkpoint.branch,
305
305
  head: checkpoint.head?.slice(0, 7),
306
306
  totalBytes: this.sumRawBytes(uploads.pack, uploads.index),
@@ -312,7 +312,7 @@ export class HandoffCheckpointTracker {
312
312
  _downloads: Downloads,
313
313
  totalBytes: number,
314
314
  ): void {
315
- this.logger.info("Applied handoff checkpoint", {
315
+ this.logger.debug("Applied handoff checkpoint", {
316
316
  branch: checkpoint.branch,
317
317
  head: checkpoint.head?.slice(0, 7),
318
318
  totalBytes,
@@ -588,7 +588,7 @@ export class AgentServer {
588
588
  switch (method) {
589
589
  case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
590
590
  case "user_message": {
591
- this.logger.info("Received user_message command", {
591
+ this.logger.debug("Received user_message command", {
592
592
  hasContent:
593
593
  typeof params.content === "string"
594
594
  ? params.content.trim().length > 0
@@ -608,7 +608,7 @@ export class AgentServer {
608
608
  if (prompt.length === 0) {
609
609
  throw new Error("User message cannot be empty");
610
610
  }
611
- this.logger.info("Built user_message prompt", {
611
+ this.logger.debug("Built user_message prompt", {
612
612
  blockTypes: prompt.map((block) => block.type),
613
613
  });
614
614
  const promptPreview = promptBlocksToText(prompt);
@@ -718,7 +718,7 @@ export class AgentServer {
718
718
  ? params.mcpServers
719
719
  : [];
720
720
 
721
- this.logger.info("Refresh session requested", {
721
+ this.logger.debug("Refresh session requested", {
722
722
  serverCount: mcpServers.length,
723
723
  });
724
724
 
@@ -1191,7 +1191,7 @@ export class AgentServer {
1191
1191
  this.resumeState.latestGitCheckpoint,
1192
1192
  );
1193
1193
  checkpointApplied = true;
1194
- this.logger.info("Git checkpoint applied", {
1194
+ this.logger.debug("Git checkpoint applied", {
1195
1195
  branch: this.resumeState.latestGitCheckpoint.branch,
1196
1196
  head: this.resumeState.latestGitCheckpoint.head,
1197
1197
  packBytes: metrics.packBytes,
@@ -1314,7 +1314,7 @@ export class AgentServer {
1314
1314
  taskId: taskRun.task,
1315
1315
  runId: taskRun.id,
1316
1316
  });
1317
- this.logger.info("Built pending user prompt", {
1317
+ this.logger.debug("Built pending user prompt", {
1318
1318
  hasMessage: typeof message === "string" && message.trim().length > 0,
1319
1319
  requestedArtifactCount: artifactIds.length,
1320
1320
  blockTypes: prompt.map((block) => block.type),