@posthog/agent 2.3.519 → 2.3.524

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);