Skip to content

Commit 5841096

Browse files
edwinyjlimgewenyu99claude
authored
feat(ai-observability): agent-driven AI Observability program (#922)
Co-authored-by: Vincent (Wen Yu) Ge <vincent@posthog.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent cf92070 commit 5841096

17 files changed

Lines changed: 334 additions & 12 deletions

File tree

bin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Wizard } from './src/wizard';
3737
import { basicIntegrationCommand } from './src/commands/basic-integration';
3838
import { mcpCommand } from './src/commands/mcp';
3939
import { mcpAnalyticsCommand } from './src/commands/mcp-analytics';
40+
import { aiObservabilityCommand } from './src/commands/ai-observability';
4041
import { auditCommand } from './src/commands/audit';
4142
import { doctorCommand } from './src/commands/doctor';
4243
import { migrateCommand } from './src/commands/migrate';
@@ -67,6 +68,7 @@ function resolveInstallDir(): string {
6768
Wizard.use(basicIntegrationCommand)
6869
.use(mcpCommand)
6970
.use(mcpAnalyticsCommand)
71+
.use(aiObservabilityCommand)
7072
.use(cliCommand)
7173
.use(auditCommand)
7274
.use(doctorCommand)

e2e-harness/__tests__/__snapshots__/e2e-flow-snapshot.test.ts.snap

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`e2e flow snapshot — ai-observability > walks intro → health → auth → run → outro → skills 1`] = `
4+
{
5+
"profile": {
6+
"ask": "first",
7+
"healthCheck": "dismiss",
8+
"mcp": "skip",
9+
"setup": "first",
10+
"skills": "delete",
11+
"slack": "skip",
12+
},
13+
"program": "ai-observability",
14+
"trace": [
15+
{
16+
"action": "confirm_setup",
17+
"screen": "ai-observability-intro",
18+
},
19+
{
20+
"action": "dismiss_outage",
21+
"screen": "health-check",
22+
},
23+
{
24+
"action": "(external)",
25+
"screen": "auth",
26+
},
27+
{
28+
"action": "(external)",
29+
"screen": "run",
30+
},
31+
{
32+
"action": "dismiss_outro",
33+
"screen": "outro",
34+
},
35+
{
36+
"action": "keep_skills",
37+
"screen": "keep-skills",
38+
},
39+
],
40+
}
41+
`;
42+
343
exports[`e2e flow snapshot — posthog-integration > Next.js (with a setup question) walks a stable path 1`] = `
444
{
545
"profile": {

e2e-harness/__tests__/e2e-flow-snapshot.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@ import { profileFor } from '../profiles';
3434
*/
3535
function traceFlow(
3636
integration: Integration,
37+
program: (typeof Program)[keyof typeof Program] = Program.PostHogIntegration,
3738
): Array<{ screen: string; action: string }> {
38-
const store = new WizardStore(Program.PostHogIntegration);
39+
const store = new WizardStore(program);
3940
setUI(new InkUI(store));
4041
const session = buildSession({ installDir: '/tmp/e2e-snap', ci: true });
4142
session.integration = integration;
4243
session.frameworkConfig = FRAMEWORK_REGISTRY[integration];
4344
store.session = session;
4445

4546
const driver = new WizardCiDriver(store);
46-
const profile = profileFor(Program.PostHogIntegration);
47+
const profile = profileFor(program);
4748

4849
const trace: Array<{ screen: string; action: string }> = [];
4950
for (let guard = 0; guard < 40; guard++) {
@@ -95,3 +96,13 @@ describe('e2e flow snapshot — posthog-integration', () => {
9596
}).toMatchSnapshot();
9697
});
9798
});
99+
100+
describe('e2e flow snapshot — ai-observability', () => {
101+
it('walks intro → health → auth → run → outro → skills', () => {
102+
expect({
103+
program: 'ai-observability',
104+
profile: profileFor(Program.AiObservability),
105+
trace: traceFlow(Integration.javascriptNode, Program.AiObservability),
106+
}).toMatchSnapshot();
107+
});
108+
});

e2e-harness/action-registry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export const NO_ACTION_SCREENS: ReadonlySet<ScreenName> = new Set<ScreenName>([
7373
ScreenId.SourceMapsDetect,
7474
ScreenId.SourceMapsOutro,
7575
ScreenId.AuditOutro,
76+
ScreenId.SelfDrivingIntegrationCheck,
77+
ScreenId.SelfDrivingIntegrationDetect,
78+
ScreenId.SelfDrivingHandoff,
7679
Overlay.ManagedSettings,
7780
Overlay.AuthError,
7881
Overlay.SessionTimeout,
@@ -96,6 +99,7 @@ export const ACTION_REGISTRY: Partial<Record<ScreenName, DriverAction[]>> = {
9699
[ScreenId.SourceMapsIntro]: [confirmSetupAction],
97100
[ScreenId.MigrationIntro]: [confirmSetupAction],
98101
[ScreenId.AgentSkillIntro]: [confirmSetupAction],
102+
[ScreenId.AiObservabilityIntro]: [confirmSetupAction],
99103
[ScreenId.AuditIntro]: [confirmSetupAction],
100104
[ScreenId.DoctorIntro]: [confirmSetupAction],
101105
[ScreenId.WarehouseIntro]: [confirmSetupAction],

e2e-harness/e2e-profile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export function decideE2eAction(
9191
case ScreenId.RevenueIntro:
9292
case ScreenId.MigrationIntro:
9393
case ScreenId.AgentSkillIntro:
94+
case ScreenId.AiObservabilityIntro:
9495
case ScreenId.AuditIntro:
9596
case ScreenId.SourceMapsIntro:
9697
case ScreenId.DoctorIntro:

e2e-harness/profiles.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ import {
1616
type WizardE2eVariation,
1717
} from './e2e-profile.js';
1818
import posthogIntegrationE2e from '@lib/programs/posthog-integration/test/e2e.json';
19+
import aiObservabilityE2e from '@lib/programs/ai-observability/test/e2e.json';
1920

2021
const PROFILES: Partial<Record<ProgramId, WizardE2eProfile>> = {
2122
[Program.PostHogIntegration]:
2223
posthogIntegrationE2e.profile as WizardE2eProfile,
24+
[Program.AiObservability]: aiObservabilityE2e.profile as WizardE2eProfile,
2325
};
2426

2527
const VARIATIONS: Partial<Record<ProgramId, WizardE2eVariation[]>> = {
2628
[Program.PostHogIntegration]:
2729
posthogIntegrationE2e.variations as WizardE2eVariation[],
30+
[Program.AiObservability]:
31+
aiObservabilityE2e.variations as WizardE2eVariation[],
2832
};
2933

3034
/** The e2e profile for a program, or the happy-path default if none is set. */

scripts/tui-host.no-jest.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ import fs from 'fs';
1717
import net from 'net';
1818
import { startTUI } from '@ui/tui/start-tui';
1919
import { VERSION } from '@lib/version';
20-
import { Program } from '@lib/programs/program-registry';
20+
import {
21+
Program,
22+
getProgramConfig,
23+
type ProgramId,
24+
} from '@lib/programs/program-registry';
2125
import type { Harness, Sequence } from '@lib/constants';
2226
import { buildSession } from '@lib/wizard-session';
23-
import { posthogIntegrationConfig } from '@lib/programs/posthog-integration';
2427
import { runAgent } from '@lib/agent/agent-runner';
2528
import { getOrAskForProjectData } from '@utils/setup-utils';
2629
import { logToFile } from '@utils/debug';
@@ -43,13 +46,22 @@ async function main() {
4346
).trim();
4447
const projectId = process.env.PROJECT_ID!;
4548

46-
const { store } = startTUI(VERSION, Program.PostHogIntegration);
49+
// Which program to drive — PROGRAM env from the workbench e2e runner;
50+
// defaults to the integration flow. getProgramConfig throws on unknown ids.
51+
const programId = (process.env.PROGRAM ||
52+
Program.PostHogIntegration) as ProgramId;
53+
const programConfig = getProgramConfig(programId);
54+
55+
const { store } = startTUI(VERSION, programId);
4756
store.session = buildSession({
4857
installDir: process.env.APP_DIR!,
4958
ci: true,
5059
apiKey,
5160
projectId,
5261
region: 'us',
62+
// Local skills + MCP (context-mill dev server on :8765, MCP on :8787) —
63+
// same env-backed flag the bin's --local-mcp reads.
64+
localMcp: process.env.POSTHOG_WIZARD_LOCAL_MCP === 'true',
5365
// Switchboard variation overrides (see e2e.json `variations`), threaded by
5466
// the snapshot driver as one run per variation. Empty ⇒ resolved default.
5567
harness: (process.env.SNAP_HARNESS || undefined) as Harness | undefined,
@@ -66,7 +78,7 @@ async function main() {
6678
ci: true,
6779
apiKey,
6880
projectId: Number(projectId),
69-
programId: Program.PostHogIntegration,
81+
programId,
7082
});
7183
store.setCredentials({
7284
accessToken: d.accessToken,
@@ -76,12 +88,12 @@ async function main() {
7688
});
7789
};
7890

79-
// Pass the intro and health-check gates and run the real integration agent.
91+
// Pass the intro and health-check gates and run the program's real agent.
8092
// The auth and run screens never advance on their own; this is what moves them.
8193
const runIntegration = async () => {
8294
await store.getGate('intro');
8395
await store.getGate('health-check');
84-
await runAgent(posthogIntegrationConfig, store.session);
96+
await runAgent(programConfig, store.session);
8597
};
8698

8799
if (process.env.MODE === 'serve') return serve();
@@ -166,7 +178,7 @@ async function main() {
166178
// ---- CI route: self-drive the fixed profile, snapshot each screen ----
167179
async function fixed() {
168180
const CTRL = process.env.SNAP_CTRL!;
169-
const profile: WizardE2eProfile = profileFor(Program.PostHogIntegration);
181+
const profile: WizardE2eProfile = profileFor(programId);
170182
const screenPath: string[] = [];
171183
// Snapshot on key moments — a screen change, a task-list update, or a
172184
// runPhase change — so the run screen's progression (the agent working) is

src/commands/ai-observability.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { aiObservabilityConfig } from '@lib/programs/ai-observability/index';
2+
3+
import type { Command } from './command';
4+
import { nativeCommandFactory } from './factories/native-command-factory';
5+
6+
/**
7+
* `wizard ai-observability` — flat skill command, wire AI Observability into a
8+
* project today.
9+
*
10+
* Installs the OpenTelemetry SDK, the PostHog span processor, and the
11+
* provider-specific instrumentation so LLM calls emit `$ai_generation` events.
12+
* The `ai-observability` context-mill skill has one variant per (LLM provider ×
13+
* language); the agent picks the right one at run time by scanning the
14+
* project's manifest and (when ambiguous) asking the user via `wizard_ask`.
15+
* Stays flat while a single "add AIO to a project" flow is the only action.
16+
*/
17+
export const aiObservabilityCommand: Command = nativeCommandFactory(
18+
aiObservabilityConfig,
19+
);

src/lib/agent/runner/__tests__/switchboard.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
GPT5_6_LUNA_MODEL,
2121
GPT5_6_SOL_MODEL,
2222
GPT5_6_TERRA_MODEL,
23+
SONNET_5_MODEL,
2324
Harness,
2425
Sequence,
2526
WIZARD_ORCHESTRATOR_FLAG_KEY,
@@ -60,11 +61,23 @@ describe('switchboard PROGRAM_BINDINGS', () => {
6061
// Pins today's behavior: the seam changes nothing until a binding is moved.
6162
it('resolves every program, unflagged, to the same default binding', () => {
6263
for (const program of PROGRAM_IDS) {
64+
if (program === 'ai-observability') continue; // pinned below
6365
expect(resolveBinding({ program, flags: {} })).toEqual(DEFAULT_RESOLVED);
6466
}
6567
});
6668

6769
runBindingCases([
70+
{
71+
name: 'binds ai-observability to anthropic + sonnet 5',
72+
ctx: { program: 'ai-observability', flags: {} },
73+
binding: {
74+
sequence: Sequence.linear,
75+
harness: Harness.anthropic,
76+
model: SONNET_5_MODEL,
77+
thinkingLevel: undefined,
78+
},
79+
trace: { harness: 'binding', model: 'binding', sequence: 'binding' },
80+
},
6881
{
6982
name: 'falls back to DEFAULT_BINDING for an unmapped program',
7083
ctx: { program: 'not-a-program', flags: {} },
@@ -187,8 +200,13 @@ describe('switchboard composed clamp', () => {
187200
trace: {},
188201
};
189202
// Only the orchestrator flag is on → harness/model stay at the binding
190-
// default, and the composed clamp holds the sequence at linear.
191-
expect(resolveBinding(ctx)).toEqual(DEFAULT_RESOLVED);
203+
// (sonnet 5 for ai-observability, the default elsewhere), and the
204+
// composed clamp holds the sequence at linear.
205+
expect(resolveBinding(ctx)).toEqual(
206+
program === 'ai-observability'
207+
? { ...DEFAULT_RESOLVED, model: SONNET_5_MODEL }
208+
: DEFAULT_RESOLVED,
209+
);
192210
expect(ctx.trace?.sequence).toBe('composed');
193211
}
194212
});

src/lib/agent/runner/switchboard/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
* Model ids are gateway strings — add new ones as constants in `@lib/constants`.
88
*/
99

10-
import { DEFAULT_AGENT_MODEL, Harness, Sequence } from '@lib/constants';
10+
import {
11+
DEFAULT_AGENT_MODEL,
12+
SONNET_5_MODEL,
13+
Harness,
14+
Sequence,
15+
} from '@lib/constants';
1116
import type { ProgramId } from '@lib/programs/program-registry';
1217
import { resolveHarness } from './harness';
1318
import type { EffortLevel } from './models';
@@ -130,6 +135,11 @@ export const PROGRAM_BINDINGS: Partial<Record<ProgramId, ProgramBinding>> = {
130135
'mcp-remove': DEFAULT_BINDING,
131136
'mcp-tutorial': DEFAULT_BINDING,
132137
'mcp-analytics': DEFAULT_BINDING,
138+
'ai-observability': {
139+
sequence: Sequence.linear,
140+
harness: Harness.anthropic,
141+
model: SONNET_5_MODEL,
142+
},
133143
slack: DEFAULT_BINDING,
134144
};
135145

0 commit comments

Comments
 (0)