@posthog/agent 2.0.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.
- package/LICENSE +1 -1
- package/README.md +221 -219
- package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
- package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
- package/dist/adapters/claude/permissions/permission-options.js +117 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
- package/dist/adapters/claude/questions/utils.d.ts +132 -0
- package/dist/adapters/claude/questions/utils.js +63 -0
- package/dist/adapters/claude/questions/utils.js.map +1 -0
- package/dist/adapters/claude/tools.d.ts +18 -0
- package/dist/adapters/claude/tools.js +95 -0
- package/dist/adapters/claude/tools.js.map +1 -0
- package/dist/agent-DBQY1BfC.d.ts +123 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +3656 -0
- package/dist/agent.js.map +1 -0
- package/dist/claude-cli/cli.js +3695 -2746
- package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
- package/dist/gateway-models.d.ts +24 -0
- package/dist/gateway-models.js +93 -0
- package/dist/gateway-models.js.map +1 -0
- package/dist/index.d.ts +170 -1157
- package/dist/index.js +3252 -5074
- package/dist/index.js.map +1 -1
- package/dist/logger-DDBiMOOD.d.ts +24 -0
- package/dist/posthog-api.d.ts +40 -0
- package/dist/posthog-api.js +175 -0
- package/dist/posthog-api.js.map +1 -0
- package/dist/server/agent-server.d.ts +41 -0
- package/dist/server/agent-server.js +4451 -0
- package/dist/server/agent-server.js.map +1 -0
- package/dist/server/bin.d.ts +1 -0
- package/dist/server/bin.js +4507 -0
- package/dist/server/bin.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -14
- package/src/acp-extensions.ts +98 -16
- package/src/adapters/acp-connection.ts +494 -0
- package/src/adapters/base-acp-agent.ts +150 -0
- package/src/adapters/claude/claude-agent.ts +596 -0
- package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
- package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
- package/src/adapters/claude/hooks.ts +64 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
- package/src/adapters/claude/permissions/permission-options.ts +103 -0
- package/src/adapters/claude/plan/utils.ts +56 -0
- package/src/adapters/claude/questions/utils.ts +92 -0
- package/src/adapters/claude/session/commands.ts +38 -0
- package/src/adapters/claude/session/mcp-config.ts +37 -0
- package/src/adapters/claude/session/models.ts +12 -0
- package/src/adapters/claude/session/options.ts +236 -0
- package/src/adapters/claude/tool-meta.ts +143 -0
- package/src/adapters/claude/tools.ts +53 -688
- package/src/adapters/claude/types.ts +61 -0
- package/src/adapters/codex/spawn.ts +130 -0
- package/src/agent.ts +96 -587
- package/src/execution-mode.ts +43 -0
- package/src/gateway-models.ts +135 -0
- package/src/index.ts +79 -0
- package/src/otel-log-writer.test.ts +105 -0
- package/src/otel-log-writer.ts +94 -0
- package/src/posthog-api.ts +75 -235
- package/src/resume.ts +115 -0
- package/src/sagas/apply-snapshot-saga.test.ts +690 -0
- package/src/sagas/apply-snapshot-saga.ts +88 -0
- package/src/sagas/capture-tree-saga.test.ts +892 -0
- package/src/sagas/capture-tree-saga.ts +141 -0
- package/src/sagas/resume-saga.test.ts +558 -0
- package/src/sagas/resume-saga.ts +332 -0
- package/src/sagas/test-fixtures.ts +250 -0
- package/src/server/agent-server.test.ts +220 -0
- package/src/server/agent-server.ts +748 -0
- package/src/server/bin.ts +88 -0
- package/src/server/jwt.ts +65 -0
- package/src/server/schemas.ts +47 -0
- package/src/server/types.ts +13 -0
- package/src/server/utils/retry.test.ts +122 -0
- package/src/server/utils/retry.ts +61 -0
- package/src/server/utils/sse-parser.test.ts +93 -0
- package/src/server/utils/sse-parser.ts +46 -0
- package/src/session-log-writer.test.ts +140 -0
- package/src/session-log-writer.ts +137 -0
- package/src/test/assertions.ts +114 -0
- package/src/test/controllers/sse-controller.ts +107 -0
- package/src/test/fixtures/api.ts +111 -0
- package/src/test/fixtures/config.ts +33 -0
- package/src/test/fixtures/notifications.ts +92 -0
- package/src/test/mocks/claude-sdk.ts +251 -0
- package/src/test/mocks/msw-handlers.ts +48 -0
- package/src/test/setup.ts +114 -0
- package/src/test/wait.ts +41 -0
- package/src/tree-tracker.ts +173 -0
- package/src/types.ts +54 -137
- package/src/utils/acp-content.ts +58 -0
- package/src/utils/async-mutex.test.ts +104 -0
- package/src/utils/async-mutex.ts +31 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/gateway.ts +9 -6
- package/src/utils/logger.ts +0 -30
- package/src/utils/streams.ts +220 -0
- package/CLAUDE.md +0 -331
- package/src/adapters/claude/claude.ts +0 -1947
- package/src/adapters/claude/mcp-server.ts +0 -810
- package/src/adapters/claude/utils.ts +0 -267
- package/src/adapters/connection.ts +0 -95
- package/src/file-manager.ts +0 -273
- package/src/git-manager.ts +0 -577
- package/src/schemas.ts +0 -241
- package/src/session-store.ts +0 -259
- package/src/task-manager.ts +0 -163
- package/src/todo-manager.ts +0 -180
- package/src/tools/registry.ts +0 -134
- package/src/tools/types.ts +0 -133
- package/src/utils/tapped-stream.ts +0 -60
- package/src/worktree-manager.ts +0 -974
|
@@ -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
|
+
}
|