@posthog/agent 1.30.0 → 2.0.1

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.
Files changed (144) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +172 -1203
  35. package/dist/index.js +3704 -6826
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +4451 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +4507 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +66 -14
  51. package/src/acp-extensions.ts +93 -61
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -611
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +97 -734
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +51 -154
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/dist/templates/plan-template.md +0 -41
  119. package/src/adapters/claude/claude.ts +0 -1543
  120. package/src/adapters/claude/mcp-server.ts +0 -810
  121. package/src/adapters/claude/utils.ts +0 -267
  122. package/src/agents/execution.ts +0 -37
  123. package/src/agents/planning.ts +0 -60
  124. package/src/agents/research.ts +0 -160
  125. package/src/file-manager.ts +0 -306
  126. package/src/git-manager.ts +0 -577
  127. package/src/prompt-builder.ts +0 -499
  128. package/src/schemas.ts +0 -241
  129. package/src/session-store.ts +0 -259
  130. package/src/task-manager.ts +0 -163
  131. package/src/template-manager.ts +0 -236
  132. package/src/templates/plan-template.md +0 -41
  133. package/src/todo-manager.ts +0 -180
  134. package/src/tools/registry.ts +0 -129
  135. package/src/tools/types.ts +0 -127
  136. package/src/utils/tapped-stream.ts +0 -60
  137. package/src/workflow/config.ts +0 -53
  138. package/src/workflow/steps/build.ts +0 -135
  139. package/src/workflow/steps/finalize.ts +0 -241
  140. package/src/workflow/steps/plan.ts +0 -167
  141. package/src/workflow/steps/research.ts +0 -223
  142. package/src/workflow/types.ts +0 -62
  143. package/src/workflow/utils.ts +0 -53
  144. package/src/worktree-manager.ts +0 -928
@@ -0,0 +1,43 @@
1
+ import { IS_ROOT } from "./utils/common.js";
2
+
3
+ export interface ModeInfo {
4
+ id: TwigExecutionMode;
5
+ name: string;
6
+ description: string;
7
+ }
8
+
9
+ const MODES: ModeInfo[] = [
10
+ {
11
+ id: "default",
12
+ name: "Always Ask",
13
+ description: "Prompts for permission on first use of each tool",
14
+ },
15
+ {
16
+ id: "acceptEdits",
17
+ name: "Accept Edits",
18
+ description: "Automatically accepts file edit permissions for the session",
19
+ },
20
+ {
21
+ id: "plan",
22
+ name: "Plan Mode",
23
+ description: "Claude can analyze but not modify files or execute commands",
24
+ },
25
+ {
26
+ id: "bypassPermissions",
27
+ name: "Bypass Permissions",
28
+ description: "Skips all permission prompts",
29
+ },
30
+ ];
31
+
32
+ export const TWIG_EXECUTION_MODES = [
33
+ "default",
34
+ "acceptEdits",
35
+ "plan",
36
+ "bypassPermissions",
37
+ ] as const;
38
+
39
+ export type TwigExecutionMode = (typeof TWIG_EXECUTION_MODES)[number];
40
+
41
+ export function getAvailableModes(): ModeInfo[] {
42
+ return IS_ROOT ? MODES.filter((m) => m.id !== "bypassPermissions") : MODES;
43
+ }
@@ -0,0 +1,135 @@
1
+ export interface GatewayModel {
2
+ id: string;
3
+ owned_by: string;
4
+ context_window: number;
5
+ supports_streaming: boolean;
6
+ supports_vision: boolean;
7
+ }
8
+
9
+ interface GatewayModelsResponse {
10
+ object: "list";
11
+ data: GatewayModel[];
12
+ }
13
+
14
+ export interface FetchGatewayModelsOptions {
15
+ gatewayUrl: string;
16
+ }
17
+
18
+ export const DEFAULT_GATEWAY_MODEL = "claude-opus-4-6";
19
+
20
+ export const BLOCKED_MODELS = new Set(["gpt-5-mini", "openai/gpt-5-mini"]);
21
+
22
+ type ArrayModelsResponse =
23
+ | {
24
+ data?: Array<{ id?: string; owned_by?: string }>;
25
+ models?: Array<{ id?: string; owned_by?: string }>;
26
+ }
27
+ | Array<{ id?: string; owned_by?: string }>;
28
+
29
+ export async function fetchGatewayModels(
30
+ options?: FetchGatewayModelsOptions,
31
+ ): Promise<GatewayModel[]> {
32
+ const gatewayUrl = options?.gatewayUrl ?? process.env.ANTHROPIC_BASE_URL;
33
+ if (!gatewayUrl) {
34
+ return [];
35
+ }
36
+
37
+ const modelsUrl = `${gatewayUrl}/v1/models`;
38
+
39
+ try {
40
+ const response = await fetch(modelsUrl);
41
+
42
+ if (!response.ok) {
43
+ return [];
44
+ }
45
+
46
+ const data = (await response.json()) as GatewayModelsResponse;
47
+ const models = data.data ?? [];
48
+ return models.filter((m) => !BLOCKED_MODELS.has(m.id));
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ export function isAnthropicModel(model: GatewayModel): boolean {
55
+ if (model.owned_by) {
56
+ return model.owned_by === "anthropic";
57
+ }
58
+ return model.id.startsWith("claude-") || model.id.startsWith("anthropic/");
59
+ }
60
+
61
+ export async function fetchArrayModelIds(
62
+ options?: FetchGatewayModelsOptions,
63
+ ): Promise<string[]> {
64
+ const models = await fetchArrayModels(options);
65
+ return models.map((model) => model.id);
66
+ }
67
+
68
+ export interface ArrayModelInfo {
69
+ id: string;
70
+ owned_by?: string;
71
+ }
72
+
73
+ export async function fetchArrayModels(
74
+ options?: FetchGatewayModelsOptions,
75
+ ): Promise<ArrayModelInfo[]> {
76
+ const gatewayUrl = options?.gatewayUrl ?? process.env.ANTHROPIC_BASE_URL;
77
+ if (!gatewayUrl) {
78
+ return [];
79
+ }
80
+
81
+ try {
82
+ const base = new URL(gatewayUrl);
83
+ base.pathname = "/array/v1/models";
84
+ base.search = "";
85
+ base.hash = "";
86
+ const response = await fetch(base.toString());
87
+ if (!response.ok) {
88
+ return [];
89
+ }
90
+ const data = (await response.json()) as ArrayModelsResponse;
91
+ const models = Array.isArray(data)
92
+ ? data
93
+ : (data.data ?? data.models ?? []);
94
+ const results: ArrayModelInfo[] = [];
95
+ for (const model of models) {
96
+ const id = model?.id ? String(model.id) : "";
97
+ if (!id) continue;
98
+ results.push({ id, owned_by: model?.owned_by });
99
+ }
100
+ return results;
101
+ } catch {
102
+ return [];
103
+ }
104
+ }
105
+
106
+ const PROVIDER_NAMES: Record<string, string> = {
107
+ anthropic: "Anthropic",
108
+ openai: "OpenAI",
109
+ "google-vertex": "Gemini",
110
+ };
111
+
112
+ export function getProviderName(ownedBy: string): string {
113
+ return PROVIDER_NAMES[ownedBy] ?? ownedBy;
114
+ }
115
+
116
+ const PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"];
117
+
118
+ export function formatGatewayModelName(model: GatewayModel): string {
119
+ let cleanId = model.id;
120
+ for (const prefix of PROVIDER_PREFIXES) {
121
+ if (cleanId.startsWith(prefix)) {
122
+ cleanId = cleanId.slice(prefix.length);
123
+ break;
124
+ }
125
+ }
126
+
127
+ cleanId = cleanId.replace(/(\d)-(\d)/g, "$1.$2");
128
+
129
+ const words = cleanId.split(/[-_]/).map((word) => {
130
+ if (word.match(/^[0-9.]+$/)) return word;
131
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
132
+ });
133
+
134
+ return words.join(" ");
135
+ }
package/src/index.ts ADDED
@@ -0,0 +1,79 @@
1
+ export type {
2
+ BranchCreatedPayload,
3
+ CompactBoundaryPayload,
4
+ ConsoleNotificationPayload,
5
+ ErrorNotificationPayload,
6
+ ModeChangePayload,
7
+ PostHogNotificationPayload,
8
+ PostHogNotificationType,
9
+ RunStartedPayload,
10
+ SdkSessionPayload,
11
+ SessionResumePayload,
12
+ StatusPayload,
13
+ TaskCompletePayload,
14
+ TaskNotificationPayload,
15
+ TreeSnapshotPayload,
16
+ UserMessagePayload,
17
+ } from "./acp-extensions.js";
18
+ export { POSTHOG_NOTIFICATIONS } from "./acp-extensions.js";
19
+ export type {
20
+ AcpConnection,
21
+ AcpConnectionConfig,
22
+ AgentAdapter,
23
+ InProcessAcpConnection,
24
+ } from "./adapters/acp-connection.js";
25
+ export { createAcpConnection } from "./adapters/acp-connection.js";
26
+ export {
27
+ fetchMcpToolMetadata,
28
+ isMcpToolReadOnly,
29
+ } from "./adapters/claude/mcp/tool-metadata.js";
30
+ export type { CodexProcessOptions } from "./adapters/codex/spawn.js";
31
+ export { Agent } from "./agent.js";
32
+ export {
33
+ type ArrayModelInfo,
34
+ BLOCKED_MODELS,
35
+ DEFAULT_GATEWAY_MODEL,
36
+ type FetchGatewayModelsOptions,
37
+ fetchArrayModels,
38
+ fetchGatewayModels,
39
+ formatGatewayModelName,
40
+ type GatewayModel,
41
+ getProviderName,
42
+ isAnthropicModel,
43
+ } from "./gateway-models.js";
44
+ export type { OtelLogConfig, SessionContext } from "./otel-log-writer.js";
45
+ export { OtelLogWriter } from "./otel-log-writer.js";
46
+ export { PostHogAPIClient } from "./posthog-api.js";
47
+ export type {
48
+ ConversationTurn,
49
+ ResumeConfig,
50
+ ResumeState,
51
+ ToolCallInfo,
52
+ } from "./resume.js";
53
+ export { conversationToPromptHistory, resumeFromLog } from "./resume.js";
54
+ export type { SessionLogWriterOptions } from "./session-log-writer.js";
55
+ export { SessionLogWriter } from "./session-log-writer.js";
56
+ export type { TreeSnapshot, TreeTrackerConfig } from "./tree-tracker.js";
57
+ export {
58
+ isCommitOnRemote,
59
+ TreeTracker,
60
+ validateForCloudHandoff,
61
+ } from "./tree-tracker.js";
62
+ export type {
63
+ AgentConfig,
64
+ AgentMode,
65
+ DeviceInfo,
66
+ FileChange,
67
+ FileStatus,
68
+ LogLevel,
69
+ OnLogCallback,
70
+ OtelTransportConfig,
71
+ StoredEntry,
72
+ StoredNotification,
73
+ Task,
74
+ TaskRun,
75
+ TreeSnapshotEvent,
76
+ } from "./types.js";
77
+ export { getLlmGatewayUrl } from "./utils/gateway.js";
78
+ export type { LoggerConfig } from "./utils/logger.js";
79
+ export { Logger } from "./utils/logger.js";
@@ -0,0 +1,105 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { OtelLogWriter } from "./otel-log-writer.js";
3
+ import type { StoredNotification } from "./types.js";
4
+
5
+ // Mock the OTEL exporter
6
+ const mockExport = vi.fn((_logs, callback) => {
7
+ callback({ code: 0 }); // Success
8
+ });
9
+
10
+ vi.mock("@opentelemetry/exporter-logs-otlp-http", () => ({
11
+ OTLPLogExporter: vi.fn().mockImplementation(() => ({
12
+ export: mockExport,
13
+ shutdown: vi.fn().mockResolvedValue(undefined),
14
+ })),
15
+ }));
16
+
17
+ describe("OtelLogWriter", () => {
18
+ let writer: OtelLogWriter;
19
+
20
+ beforeEach(() => {
21
+ mockExport.mockClear();
22
+ // Session context (taskId, runId) is now passed in constructor as resource attributes
23
+ writer = new OtelLogWriter(
24
+ {
25
+ posthogHost: "https://us.i.posthog.com",
26
+ apiKey: "phc_test_key",
27
+ flushIntervalMs: 100,
28
+ },
29
+ {
30
+ taskId: "task-123",
31
+ runId: "run-456",
32
+ },
33
+ );
34
+ });
35
+
36
+ afterEach(async () => {
37
+ await writer.shutdown();
38
+ });
39
+
40
+ it("should emit a log entry with event_type as regular attribute", async () => {
41
+ const notification: StoredNotification = {
42
+ type: "notification",
43
+ timestamp: new Date().toISOString(),
44
+ notification: {
45
+ jsonrpc: "2.0",
46
+ method: "_posthog/test_event",
47
+ params: { foo: "bar" },
48
+ },
49
+ };
50
+
51
+ // taskId and runId are now resource attributes set in constructor,
52
+ // only notification is passed per-emit
53
+ writer.emit({ notification });
54
+
55
+ // Force flush to trigger export
56
+ await writer.flush();
57
+
58
+ // Verify export was called
59
+ expect(mockExport).toHaveBeenCalled();
60
+
61
+ // Get the logs that were exported
62
+ const exportedLogs = mockExport.mock.calls[0][0];
63
+ expect(exportedLogs.length).toBe(1);
64
+
65
+ const log = exportedLogs[0];
66
+ // task_id and run_id are now resource attributes, not regular attributes
67
+ expect(log.attributes.task_id).toBeUndefined();
68
+ expect(log.attributes.run_id).toBeUndefined();
69
+ // event_type is still a regular attribute (varies per log entry)
70
+ expect(log.attributes.event_type).toBe("_posthog/test_event");
71
+ expect(log.body).toBe(JSON.stringify(notification));
72
+
73
+ // Verify resource attributes contain task_id and run_id
74
+ expect(log.resource.attributes.task_id).toBe("task-123");
75
+ expect(log.resource.attributes.run_id).toBe("run-456");
76
+ expect(log.resource.attributes["service.name"]).toBe("twig-agent");
77
+ });
78
+
79
+ it("should batch multiple log entries", async () => {
80
+ const makeNotification = (method: string): StoredNotification => ({
81
+ type: "notification",
82
+ timestamp: new Date().toISOString(),
83
+ notification: {
84
+ jsonrpc: "2.0",
85
+ method,
86
+ },
87
+ });
88
+
89
+ writer.emit({ notification: makeNotification("event_1") });
90
+ writer.emit({ notification: makeNotification("event_2") });
91
+ writer.emit({ notification: makeNotification("event_3") });
92
+
93
+ await writer.flush();
94
+
95
+ expect(mockExport).toHaveBeenCalled();
96
+ const exportedLogs = mockExport.mock.calls[0][0];
97
+ expect(exportedLogs.length).toBe(3);
98
+
99
+ // All logs should share the same resource attributes
100
+ for (const log of exportedLogs) {
101
+ expect(log.resource.attributes.task_id).toBe("task-123");
102
+ expect(log.resource.attributes.run_id).toBe("run-456");
103
+ }
104
+ });
105
+ });
@@ -0,0 +1,94 @@
1
+ import { SeverityNumber } from "@opentelemetry/api-logs";
2
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
3
+ import { resourceFromAttributes } from "@opentelemetry/resources";
4
+ import {
5
+ BatchLogRecordProcessor,
6
+ LoggerProvider,
7
+ } from "@opentelemetry/sdk-logs";
8
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
9
+ import type { StoredNotification } from "./types.js";
10
+ import type { Logger } from "./utils/logger.js";
11
+
12
+ export interface OtelLogConfig {
13
+ /** PostHog ingest host, e.g., "https://us.i.posthog.com" */
14
+ posthogHost: string;
15
+ /** Project API key, e.g., "phc_xxx" */
16
+ apiKey: string;
17
+ /** Batch flush interval in ms (default: 500) */
18
+ flushIntervalMs?: number;
19
+ /** Override the logs endpoint path (default: /i/v1/agent-logs) */
20
+ logsPath?: string;
21
+ }
22
+
23
+ /**
24
+ * Session context for resource attributes.
25
+ * These are set once per OTEL logger instance and indexed via resource_fingerprint
26
+ */
27
+ export interface SessionContext {
28
+ /** Parent task grouping - all runs for a task share this */
29
+ taskId: string;
30
+ /** Primary conversation identifier - all events in a run share this */
31
+ runId: string;
32
+ /** Deployment environment - "local" for desktop, "cloud" for cloud sandbox */
33
+ deviceType?: "local" | "cloud";
34
+ }
35
+
36
+ export class OtelLogWriter {
37
+ private loggerProvider: LoggerProvider;
38
+ private logger: ReturnType<LoggerProvider["getLogger"]>;
39
+
40
+ constructor(
41
+ config: OtelLogConfig,
42
+ sessionContext: SessionContext,
43
+ _debugLogger?: Logger,
44
+ ) {
45
+ const logsPath = config.logsPath ?? "/i/v1/agent-logs";
46
+ const exporter = new OTLPLogExporter({
47
+ url: `${config.posthogHost}${logsPath}`,
48
+ headers: { Authorization: `Bearer ${config.apiKey}` },
49
+ });
50
+
51
+ const processor = new BatchLogRecordProcessor(exporter, {
52
+ scheduledDelayMillis: config.flushIntervalMs ?? 500,
53
+ });
54
+
55
+ // Resource attributes are set ONCE per session and indexed via resource_fingerprint
56
+ // So we have fast queries by run_id/task_id in PostHog Logs UI
57
+ this.loggerProvider = new LoggerProvider({
58
+ resource: resourceFromAttributes({
59
+ [ATTR_SERVICE_NAME]: "twig-agent",
60
+ run_id: sessionContext.runId,
61
+ task_id: sessionContext.taskId,
62
+ device_type: sessionContext.deviceType ?? "local",
63
+ }),
64
+ processors: [processor],
65
+ });
66
+
67
+ this.logger = this.loggerProvider.getLogger("agent-session");
68
+ }
69
+
70
+ /**
71
+ * Emit an agent event to PostHog Logs via OTEL.
72
+ */
73
+ emit(entry: { notification: StoredNotification }): void {
74
+ const { notification } = entry;
75
+ const eventType = notification.notification.method;
76
+
77
+ this.logger.emit({
78
+ severityNumber: SeverityNumber.INFO,
79
+ severityText: "INFO",
80
+ body: JSON.stringify(notification),
81
+ attributes: {
82
+ event_type: eventType,
83
+ },
84
+ });
85
+ }
86
+
87
+ async flush(): Promise<void> {
88
+ await this.loggerProvider.forceFlush();
89
+ }
90
+
91
+ async shutdown(): Promise<void> {
92
+ await this.loggerProvider.shutdown();
93
+ }
94
+ }