Skip to content

Commit 80dce42

Browse files
committed
feat(agent-core-v2): enforce telemetry domain coverage in the event registry
1 parent 5f0aa7f commit 80dce42

5 files changed

Lines changed: 278 additions & 3 deletions

File tree

‎.agents/skills/agent-core-dev/telemetry.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Telemetry is a **layer-1 root** domain (alongside `log`): the facade lives at `A
77
## Where things live
88

99
- `src/app/telemetry/telemetry.ts`: contract — `ITelemetryService` (facade), `ITelemetryAppender` (destination), `TelemetryProperties`, `nullTelemetryAppender`, and `TelemetryServiceOptions`.
10-
- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent<P>` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent<P>`.
10+
- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / domain / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent<P>` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent<P>`.
11+
- `src/app/telemetry/coverage.ts`: domain coverage map — `telemetryDomainExemptions` (domains that intentionally emit nothing, with reasons) and `telemetryDomainKnownGaps` (domains with zero coverage whose events are planned). `events.test.ts` walks `src/` and requires every domain to own an event, be exempted, or be listed as a known gap; adding a domain without a telemetry decision fails the test.
1112
- `src/app/telemetry/telemetryService.ts`: `TelemetryService` impl + `registerScopedService(LifecycleScope.App, …)`.
1213
- `src/app/telemetry/agentTelemetryContext.ts` + `agentTelemetryContextService.ts`: `IAgentTelemetryContextService` — Agent-scoped mutable request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) snapshot into turn telemetry at launch. Agent identity (`agent_id`) is not part of it — identity is bound by the Agent-scoped `ITelemetryService` view.
1314
- `src/app/telemetry/consoleAppender.ts`: `ConsoleAppender` — echoes events to a log function (dev / debug).
@@ -27,7 +28,7 @@ constructor(@ITelemetryService private readonly telemetry: ITelemetryService) {}
2728
this.telemetry.track2('cron_fired', { task_id: taskId, coalesced_count: 0, stale: false, buffered: false, recurring: true });
2829
```
2930

30-
`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent<P>({ owner, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent<P>` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
31+
`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent<P>({ owner, domain, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent<P>` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. `domain` is the owning `src/` directory path (`agent/loop`, `wire`) or the pseudo-domain `host` for events the host app emits; `coverage.ts` plus `events.test.ts` require every source domain to own an event, be exempted, or be listed as a known gap. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
3132

3233
`TelemetryService.track` merges the bound context into the properties and fans the event out to every registered appender. A single throwing appender is isolated via `onUnexpectedError` and never blocks the rest.
3334

@@ -79,7 +80,7 @@ telemetry.addAppender(new CloudAppender({ // production
7980

8081
`addAppender` returns an `IDisposable` that removes the appender when disposed. `setAppender(appender)` resets to a single appender (mainly for tests). `removeAppender(appender)` drops one.
8182

82-
> There is no production bootstrap wired yet — `TelemetryService` defaults to `[nullTelemetryAppender]`, so `track(...)` is a no-op until `addAppender` is called at startup.
83+
> Production bootstrap exists in three places: kap-server (`packages/kap-server/src/services/telemetry.ts`), the v2 print runner (`apps/kimi-code/src/cli/v2/run-v2-print.ts`), and the node-sdk v2 client (`installEngineTelemetry` in `packages/node-sdk/src/sdk-rpc-client-v2.ts`) — each attaches a `CloudAppender` (or the host client) at startup. Without `addAppender`, `TelemetryService` defaults to `[nullTelemetryAppender]` and `track(...)` is a no-op.
8384
8485
## Lifecycle
8586

‎packages/agent-core-v2/AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Business events go through `ITelemetryService.track2` — never the low-level `t
4848
- **Naming**: event names and property keys are snake_case (`tool_call`, `duration_ms`). Durations, counts, and sizes carry a unit suffix (`_ms` / `_count` / `_bytes`). Use specific names (`error_type`, not `error`).
4949
- **Privacy**: never register user content, prompts, or file paths as properties. `CloudAppender` redacts URLs, emails, tokens, and absolute paths from string values before events leave the process, but that is a safety net, not a license.
5050
- **Stability**: registered event names and property keys are wire data consumed by dashboards — treat renames as breaking changes.
51+
- **Coverage**: every event declares its owning `domain` — a `src/` directory path (`agent/loop`, `wire`) or the pseudo-domain `host` for events the host app emits. `src/app/telemetry/coverage.ts` records the domains that intentionally emit nothing (`telemetryDomainExemptions`, with reasons) and the domains whose gaps are known and tracked (`telemetryDomainKnownGaps`, with the planned events). `events.test.ts` walks `src/` and fails unless every domain owns an event, is exempted, or is a known gap — adding a domain without a telemetry decision breaks the test, so make the decision explicit.
5152
- The registry is the single source of truth; `test/app/telemetry/events.test.ts` enforces the naming conventions.
5253

5354
## Persistence
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
export const telemetryCoverageTierRoots = ['app', 'session', 'agent', 'workspace', 'features'] as const;
2+
3+
export const telemetryPseudoDomains = ['host'] as const;
4+
5+
export const telemetryDomainExemptions: Readonly<Record<string, string>> = {
6+
'app/agentIdentity': 'Config-derived snapshot built once; no IO, state machine, or failure branches.',
7+
'app/agentProfileCatalog':
8+
'Type definitions, pure functions, and static built-in profile loading; no IO or user decisions.',
9+
'app/authLegacy':
10+
'Read-only aggregate facade over config and oauth status; authentication events belong to app/auth.',
11+
'app/bashParser': 'Stateless parse adapter over tree-sitter-bash; no IO or lifecycle.',
12+
'app/bootstrap': 'Pure value carriers (path derivation, env reads) and scope creation functions.',
13+
'app/edit': 'Thin file-edit adapter; failures return to the Edit tool and are covered by tool_call.',
14+
'app/event': 'Pure pub/sub plumbing with no business semantics.',
15+
'app/feature': 'Thin DI unit management shell; no user-perceivable behavior.',
16+
'app/flag': 'In-memory flag resolution (env > config > default); no IO or lifecycle.',
17+
'app/gateway': 'Thin delegate to prompt/loop services; turn events are emitted by the owning domains.',
18+
'app/hostFolderBrowser': 'Stateless readdir proxy; errors map to typed RPC errors.',
19+
'app/mcpRegistry': 'Read-only aggregation of config and plugin queries; no writes or failure branches.',
20+
'app/projectLocalConfig': 'Interface declaration only; implementation lives in persistence backends.',
21+
'app/remoteControl': 'Flag definition registration only; no runtime logic.',
22+
'app/sessionManager':
23+
'Resume failures funnel to session_load_failed via sessionLookup; lifecycle events are owned by workspace/sessionLifecycle.',
24+
'app/sessionLegacy': 'Read-only aggregate proxy; resume failures are covered by session_load_failed.',
25+
'app/state': 'StateRegistry subclass with no business logic.',
26+
'app/task': 'Generic task-handle primitive; background task events live in agent/task.',
27+
'app/telemetry': 'Telemetry infrastructure itself; it cannot instrument itself.',
28+
'app/workspaceAliases': 'Stateless resolution proxy over IWorkspaceService and the session index.',
29+
'app/workspaceSessions': 'Read-only aggregate facade over workspace aliases and the session index.',
30+
'session/approval': 'Thin delegate to features/interaction; resolution events are emitted by agent/toolApproval.',
31+
'session/mcp': 'Type seeds and a merged view; connection events are emitted by workspace/workspaceMcp.',
32+
'session/question':
33+
'Thin delegate; resolution events are emitted by the ask-user-question tool (agent/tools).',
34+
'session/sessionActivity': 'Pure in-memory fold of already-instrumented turn and activity events.',
35+
'session/sessionAgentProfileCatalog':
36+
'In-memory registry merge projection; diagnostics go through log and inspect surfaces.',
37+
'session/sessionContext': 'Pure data carrier (sessionId/workspaceId/cwd) plus seed factory.',
38+
'session/sessionInstructions': 'Interface declaration and DI seed helper only.',
39+
'session/sessionLog':
40+
'Thin adapter over FileLogWriter; it is the logging substrate telemetry itself relies on.',
41+
'session/sessionToolPolicy': 'Simple disabled-tools preference persistence; changes broadcast via onDidChange.',
42+
'session/sessionToolPolicyGate': 'No-op implementation with no behavior to observe.',
43+
'session/state': 'StateRegistry subclass with no business logic.',
44+
'session/tokenCounting':
45+
'Pure in-memory token estimation bookkeeping; size signals are covered by loop and compaction events.',
46+
'session/usage':
47+
'Pure in-memory usage accumulator; consumption signals are covered by llmRequester and loop events.',
48+
'session/workspaceInfo': 'Interface declaration and scope seed factory only.',
49+
'agent/activityView':
50+
'Read-only projection of loop, task, compaction, and approval events that are instrumented at the source.',
51+
'agent/agentContext': 'In-memory agent model lease registry; anomalies route to onUnexpectedError.',
52+
'agent/command': 'Thin dispatcher over command contributions; real work is instrumented by owning domains.',
53+
'agent/contextMemory':
54+
'In-memory history mutation API; lifecycle operations are instrumented by owner domains (undo, compaction, loop).',
55+
'agent/interruptionReminder':
56+
'Fixed-text reminder injection on user_cancelled; the interrupt itself is covered by turn_interrupted.',
57+
'agent/modeMutex':
58+
'Mode mutual-exclusion wiring over the event bus; mode transitions are owned by features/plan, features/swarm, and features/tower.',
59+
'agent/permissionPolicy': 'Stateless policy evaluation chain; decisions are recorded by permissionGate.',
60+
'agent/permissionRules': 'Thin state wrapper; approval persistence is recorded by permission_approval_result.',
61+
'agent/plugin': 'Reminder reconcile and render logic; plugin install/enable events belong to app/plugin.',
62+
'agent/replayBuilder': 'Pure type definitions.',
63+
'agent/scopeContext': 'Stateless plumbing (scope key and frozen context factory).',
64+
'agent/state': 'StateRegistry subclass for replayable key bookkeeping.',
65+
'agent/tokenCounting': 'Contracts and wire event definitions only; counting logic lives in session/tokenCounting.',
66+
'agent/toolActivation': 'Tool registration bookkeeping; policy-blocked calls surface via tool_call.',
67+
'agent/toolPolicy': 'Pure policy evaluation; guard interceptions are recorded via tool_call.',
68+
'agent/toolRegistry': 'In-memory Map registry with no IO or failure degradation.',
69+
'workspace/state': 'StateRegistry subclass with no business logic.',
70+
'workspace/workspaceContext': 'Pure data interface plus seed factory.',
71+
'workspace/workspaceDirs': 'Thin state holder over project-local config; failures propagate to session creation.',
72+
'workspace/workspaceGit': 'Pass-through proxy to IGitService; git subprocess telemetry belongs to app/git.',
73+
'workspace/workspaceInstructions':
74+
'AGENTS.md snapshot loader with fs watch; reload failures degrade to prior content with log.warn.',
75+
'workspace/workspaceMcpConfig':
76+
'Config aggregation with fingerprint diff; connection outcomes are covered by workspaceMcp events.',
77+
'features/dateChange': 'Date disclosure computation and reminder injection; no IO or decisions.',
78+
'features/debugEvents': 'Read-only introspection for the /api/v1/debug surface.',
79+
'features/tokenCounting': 'Pure feature assembly; counting logic lives in session/tokenCounting.',
80+
'features/usage': 'Pure feature assembly; usage logic lives in session/usage.',
81+
_base: 'DI kernel and base utilities below the telemetry layer; activation failures surface as sticky Failed units.',
82+
debug: 'Read-only introspection views for the /api/v1/debug surface.',
83+
kosong:
84+
'LLM HTTP errors translate and bubble to api_error in agent/llmRequester; instrumenting here would double-count.',
85+
os: 'Host capability implementations; failures bubble to caller domains (mcp_failed, tool_call, fs fallbacks).',
86+
runtime:
87+
'Runtime registry and host shells; failures throw typed RuntimeError to callers and state changes publish via onDidChange.',
88+
state: 'State definitions and the event dispatcher pipeline; restore failures are covered by session_load_failed.',
89+
tool: 'Stateless tool utilities (path access, args validation, output accumulation); rejections surface via tool_call.',
90+
};
91+
92+
export const telemetryDomainKnownGaps: Readonly<Record<string, string>> = {
93+
'app/auth':
94+
'Login funnel: oauth_login_finished (provider, status, duration_ms), oauth_models_refresh_finished, auth_ensure_ready_failed.',
95+
'app/capability': 'Install funnel: capability_install_started / capability_install_ended (outcome, duration_ms).',
96+
'app/config': 'Config health: config_load_failed, config_persist_blocked, config_migration_applied.',
97+
'app/file': 'Upload health: file_saved (outcome, size_bytes), file_blob_missing (index/blob divergence).',
98+
'app/git': 'Subprocess health: git_spawn_failed, git_command_timeout, git_command_duration.',
99+
'app/kosongConfig': 'Provider config: config_persist_failed, provider_models_refreshed, provider_catalog_import.',
100+
'app/mcpConfig': 'Credential store: mcp_oauth_store_read_failed (silent credential loss).',
101+
'app/mcpManagement': 'Server management: mcp_server_test, mcp_auth_flow_completed, mcp_server_config_mutated.',
102+
'app/plugin': 'Plugin lifecycle: plugin_install, plugin_reload, plugin_update_check.',
103+
'app/sessionExport': 'Export health: session_export (success, duration_ms, entries_count).',
104+
'app/sessionIndex':
105+
'Read model health: session_index_degraded, session_index_projected, session_index_mirror_give_up.',
106+
'app/web': 'Managed fetch fallback: web_fetch_fallback (silent local degradation).',
107+
'app/workspace':
108+
'Workspace lifecycle: workspace_created, workspace_deleted, workspace_catalog_rebuilt, workspace_root_invalid.',
109+
'session/agentLifecycle': 'Creation failure: agent_create_failed (stage, error_type).',
110+
'session/sessionMetadata': 'Metadata health: session_meta_load_failed, session_meta_migrated.',
111+
'session/sessionTitle':
112+
'Title generation: session_title_generated, session_title_generation_failed (experiment evaluation).',
113+
'session/terminal': 'Terminal lifecycle: terminal_spawn_failed, terminal_exited.',
114+
'session/workspaceContext': 'Security boundary: workspace_path_denied (path escape attempts).',
115+
'agent/blob': 'Media storage: blob_read_failed (silent media loss), blob_offloaded.',
116+
'agent/mcp': 'MCP tool calls: mcp_tool_reconnect, mcp_tool_name_collision.',
117+
'agent/pluginCommand': 'Adoption: plugin_command (plugin_id, command_name).',
118+
'agent/runtime': 'Runtime lifecycle: agent_runtime_failed (phase), agent_runtime_restored (duration_ms).',
119+
'agent/runtimeBinding': 'Binding decisions: agent_runtime_binding_changed, agent_runtime_binding_rejected.',
120+
'agent/shellCommand':
121+
'Execution bypasses tool_call: shell_command_finished (duration_ms, is_error, backgrounded).',
122+
'agent/stepRetry': 'Retry behavior: turn_step_retrying, turn_step_retry_exhausted.',
123+
'agent/toolResultTruncation':
124+
'Truncation: tool_result_truncated (size distribution), tool_result_spill_save_failed.',
125+
'agent/toolSelect': 'Dynamic tool loading: tool_select_load (to_load_count, unknown_count).',
126+
'agent/userTool': 'Adoption: user_tool_registered.',
127+
'workspace/workspaceAgentProfileLoader': 'Profile loading: agent_profile_load_failed (source, fatal).',
128+
'workspace/workspaceInstance': 'Materialization: workspace_materialized (duration_ms), workspace_materialize_failed.',
129+
'workspace/workspaceTrust':
130+
'Trust decisions: workspace_trust_changed, workspace_trust_read_failed (fail-closed silently).',
131+
'features/btw': 'Adoption: btw_started.',
132+
'features/externalHooks':
133+
'Hook execution: external_hook_executed (outcome), external_hook_blocked (security decisions).',
134+
'features/interaction': 'Orphan interactions: interaction_cancelled (kind, reason, pending_duration_ms).',
135+
'features/reminder': 'Injection health: reminder_provider_failed (silent context-injection failure).',
136+
'features/sessionInit': '/init run: session_init (outcome, duration_ms).',
137+
'features/staleGuard': 'Guard hits: stale_guard_blocked (reason).',
138+
'features/swarm':
139+
'Batch runs: agent_swarm_batch_finished (outcome distribution), agent_swarm_rate_limit_mode_entered.',
140+
'features/todo': 'Reminder strategy: todo_list_reminder_shown.',
141+
'features/tower':
142+
'Tower governance: tower_mode_entered, tower_spawn_denied, tower_rate_limit_paused, tower_worktree_escape_denied, tower_worktree_setup_warning.',
143+
mcpCore: 'Runtime connection health: mcp_server_dropped, mcp_oauth_refresh_failed.',
144+
persistence: 'Store health: query_store_rebuilt (silent corruption recovery).',
145+
program: 'Generation failures: program_generation_failed (stage).',
146+
};

0 commit comments

Comments
 (0)