gentle-pi 2.4.0 → 2.6.0

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 (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -0,0 +1,113 @@
1
+ import { findPackageJSON } from "node:module";
2
+
3
+ // Catalog-name privacy classification ONLY. No identity issuance, route evidence,
4
+ // registry reads, SDK hooks, auth resolution, network, or injected catalogs.
5
+ export interface PiCatalogName {
6
+ readonly classification: "catalog_public" | "custom" | "unknown";
7
+ readonly modelId: string;
8
+ }
9
+
10
+ const UNKNOWN: PiCatalogName = Object.freeze({ classification: "unknown", modelId: "unknown" });
11
+ const CUSTOM: PiCatalogName = Object.freeze({ classification: "custom", modelId: "custom" });
12
+ // Initial provider coverage matches the accumulator. Other providers are custom;
13
+ // additions require deliberate review. These are providers, not model-ID lists.
14
+ const CATALOGS = ["anthropic", "openai", "openai-codex", "google", "google-vertex", "amazon-bedrock", "openrouter"] as const;
15
+ const MAX_MODELS = 4096;
16
+ const MAX_LOAD_ATTEMPTS = 3;
17
+
18
+ function object(value: unknown): value is Record<string, unknown> {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+
22
+ function text(value: unknown, max: number): value is string {
23
+ return typeof value === "string" && value.length > 0 && value.length <= max;
24
+ }
25
+
26
+ function key(provider: string, modelId: string): string {
27
+ return JSON.stringify([provider, modelId]);
28
+ }
29
+
30
+ async function loadCatalog(): Promise<ReadonlyMap<string, PiCatalogName>> {
31
+ // Node >=22.19 supports findPackageJSON. Verify ESM lookup from this module
32
+ // selects Pi's own pi-ai package; fail rather than silently use another copy.
33
+ const pi = import.meta.resolve("@earendil-works/pi-coding-agent");
34
+ const ownPackage = findPackageJSON("@earendil-works/pi-ai", import.meta.url);
35
+ if (!ownPackage || ownPackage !== findPackageJSON("@earendil-works/pi-ai", pi)) {
36
+ throw new Error("Pi catalog dependency mismatch");
37
+ }
38
+ const entries = new Map<string, PiCatalogName>();
39
+ let count = 0;
40
+ for (const provider of CATALOGS) {
41
+ // pi-ai 0.85.1 providers/* has an import-only export condition: use ESM,
42
+ // never require.resolve. Generated modules load packaged JSON, not registry
43
+ // overrides. Specifiers and export names come only from the fixed list.
44
+ const module = await import(`@earendil-works/pi-ai/providers/${provider}.models`);
45
+ const models: unknown = module[`${provider.replaceAll("-", "_").toUpperCase()}_MODELS`];
46
+ if (!object(models)) throw new Error("Invalid packaged model catalog");
47
+ for (const model of Object.values(models)) {
48
+ if (++count > MAX_MODELS) throw new RangeError("Packaged model catalog exceeds capacity");
49
+ if (!object(model) || model.provider !== provider || !text(model.id, 128)) {
50
+ throw new Error("Invalid packaged model name");
51
+ }
52
+ entries.set(key(provider, model.id), Object.freeze({ classification: "catalog_public", modelId: model.id }));
53
+ }
54
+ }
55
+ return entries;
56
+ }
57
+
58
+ export function createPiCatalogNameLookup(
59
+ load: () => Promise<ReadonlyMap<string, PiCatalogName>> = loadCatalog,
60
+ maxAttempts = MAX_LOAD_ATTEMPTS,
61
+ ): { lookup(input: unknown): Promise<PiCatalogName>; classify(input: unknown): PiCatalogName } {
62
+ let catalogPromise: Promise<ReadonlyMap<string, PiCatalogName>> | undefined;
63
+ let loadedCatalog: ReadonlyMap<string, PiCatalogName> | undefined;
64
+ let attempts = 0;
65
+ const classify = (input: unknown): PiCatalogName => {
66
+ if (!object(input) || !text(input.provider, 32) || !text(input.modelId, 128)) return UNKNOWN;
67
+ if (!CATALOGS.includes(input.provider as typeof CATALOGS[number])) return CUSTOM;
68
+ return loadedCatalog ? loadedCatalog.get(key(input.provider, input.modelId)) ?? CUSTOM : UNKNOWN;
69
+ };
70
+ const lookup = async (input: unknown): Promise<PiCatalogName> => {
71
+ if (!object(input) || !text(input.provider, 32) || !text(input.modelId, 128)) return UNKNOWN;
72
+ if (!CATALOGS.includes(input.provider as typeof CATALOGS[number])) return CUSTOM;
73
+ if (!loadedCatalog) {
74
+ if (!catalogPromise) {
75
+ if (attempts >= maxAttempts) throw new Error("Pi catalog load attempts exhausted");
76
+ attempts++;
77
+ catalogPromise = load().then(catalog => {
78
+ loadedCatalog = catalog;
79
+ return catalog;
80
+ }, error => {
81
+ catalogPromise = undefined;
82
+ throw error;
83
+ });
84
+ }
85
+ await catalogPromise;
86
+ }
87
+ return classify(input);
88
+ };
89
+ return { lookup, classify };
90
+ }
91
+
92
+ const catalogLookup = createPiCatalogNameLookup();
93
+
94
+ /**
95
+ * Returns only a privacy-safe catalog name, NOT the model actually dispatched.
96
+ * Matching a public ID remains a public-name fact even on a custom endpoint;
97
+ * origin/API/endpoint strings are ignored, never treated as provenance evidence.
98
+ * Missing/malformed names are unknown; non-catalog names/aliases are custom.
99
+ * No modelVersion is invented. This labels a caller-observed selection only;
100
+ * actual dispatch/response identity requires separate future host evidence.
101
+ * One cached snapshot retains <=4096 names of <=128 code units; no input is kept.
102
+ * Missing/incompatible dependencies and catalog overflow reject, never skip.
103
+ */
104
+ export async function lookupPiCatalogName(input: unknown): Promise<PiCatalogName> {
105
+ return catalogLookup.lookup(input);
106
+ }
107
+
108
+ /** Pure lookup after async catalog loading; unknown until initialization succeeds.
109
+ * Always recheck membership, never trust a caller's classification/public label.
110
+ */
111
+ export function classifyPiCatalogName(input: unknown): PiCatalogName {
112
+ return catalogLookup.classify(input);
113
+ }
@@ -0,0 +1,51 @@
1
+ import { gentleAiDevBinaryOverrideConfigured, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
2
+ import { createNodeExecFileAdapter, type ExecFileAdapter } from "./native-review-cli.ts";
3
+
4
+ export interface RuntimeMetricsPolicyDeps { resolve?: () => string; exec?: ExecFileAdapter }
5
+
6
+ export function runtimeMetricsEnvAllows(env: NodeJS.ProcessEnv): boolean {
7
+ // Unknown nonempty spellings veto too: never weaken a native environment veto.
8
+ const truthy = (value: string | undefined) => !["", "0", "false", "no", "off"].includes(value?.trim().toLowerCase() ?? "");
9
+ return !truthy(env.DO_NOT_TRACK) && !truthy(env.CI) && !truthy(env.GITHUB_ACTIONS)
10
+ && env.GENTLE_AI_TELEMETRY?.trim() !== "0";
11
+ }
12
+
13
+ function publishedBinary(): string {
14
+ // This feature must not select an unpublished development binary or install one.
15
+ if (gentleAiDevBinaryOverrideConfigured()) throw new Error("Policy binary unavailable");
16
+ return resolveGentleAiBinary();
17
+ }
18
+
19
+ /** Only a validated boolean crosses this boundary; never raw stdout or errors.
20
+ * The shared adapter and outer timer bound asynchronous process waiting, NOT
21
+ * end-to-end time: the supported resolver synchronously reads/hashes the binary
22
+ * on every check. It exposes no supported validation cache. Keep verification
23
+ * intact rather than cache an unchecked path or invent stat-based trust.
24
+ * No retries, status/preview fallback, enrollment, installation or polling.
25
+ */
26
+ export async function readRuntimeMetricsPolicy(cwd: string,
27
+ { resolve = publishedBinary, exec = createNodeExecFileAdapter() }: RuntimeMetricsPolicyDeps = {}): Promise<boolean> {
28
+ const controller = new AbortController();
29
+ let timer: ReturnType<typeof setTimeout> | undefined;
30
+ try {
31
+ const deadline = new Promise<false>(done => {
32
+ timer = setTimeout(() => { controller.abort(); done(false); }, 1000);
33
+ });
34
+ const probe = async () => {
35
+ const result = await exec({ file: resolve(), arguments: ["telemetry", "policy", "--json"],
36
+ cwd, timeoutMs: 1000, maxBufferBytes: 2048, signal: controller.signal });
37
+ if (result.exitCode !== 0 || result.signal !== null || result.timedOut !== false
38
+ || result.outputLimitExceeded !== false || result.stderr !== ""
39
+ || typeof result.stdout !== "string" || Buffer.byteLength(result.stdout) > 2048) return false;
40
+ const value: unknown = JSON.parse(result.stdout);
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
42
+ const row = value as Record<string, unknown>;
43
+ return Object.keys(row).sort().join(",") === "enabled,operation,reason,schema,source"
44
+ && row.schema === "gentle-ai.telemetry-policy/v1" && row.operation === "policy"
45
+ && typeof row.enabled === "boolean" && row.enabled && row.reason === "enabled"
46
+ && ["DO_NOT_TRACK", "GENTLE_AI_TELEMETRY", "CI", "state", "default"].includes(row.source as string);
47
+ };
48
+ return await Promise.race([probe(), deadline]);
49
+ } catch { return false; }
50
+ finally { clearTimeout(timer); }
51
+ }
@@ -0,0 +1,255 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { classifyPiCatalogName } from "./runtime-metrics-pi-identity.ts";
3
+
4
+ const runtimeSchema = JSON.parse(readFileSync(new URL("../contracts/telemetry/runtime-aggregate-v1.schema.json", import.meta.url), "utf8"));
5
+
6
+ // Pure local accounting, not a telemetry transport or Pi event adapter.
7
+ // Callers supply finalized assistant responses and authoritative classifications.
8
+ // Never infer executor, usage availability, or measured timings from SDK defaults.
9
+ const EXECUTORS = ["orchestrator", "worker", "reviewer", "unknown"] as const;
10
+ const PROVIDERS = ["anthropic", "openai", "openai-codex", "google", "google-vertex", "amazon-bedrock", "openrouter", "custom", "unknown"] as const;
11
+ // Stable families, not model IDs/versions: new models need no catalog update.
12
+ // Callers map known native metadata to families; private aliases stay custom.
13
+ const FAMILIES = ["claude", "gpt", "o-series", "gemini", "llama", "qwen", "deepseek", "kimi", "custom", "unknown"] as const;
14
+ // Pi 0.85.1 docs/models.md, Thinking Level Map. These are selected Pi levels,
15
+ // not inferred provider effort or a claim that each model supports every level.
16
+ export const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max", "not_selected", "unsupported", "unavailable"] as const;
17
+ const ERRORS = ["none", "aborted", "rate_limit", "authentication", "network", "provider", "unknown"] as const;
18
+ const TOKEN_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "reasoning", "totalTokens"] as const;
19
+ declare const agentClassBrand: unique symbol;
20
+ export type AgentClass = string & { readonly [agentClassBrand]: "AgentClass" };
21
+
22
+ function agentClasses(): readonly AgentClass[] {
23
+ const values: unknown = runtimeSchema?.$defs?.row?.properties?.agent_class?.enum;
24
+ if (!Array.isArray(values) || !values.length || values.some(value => typeof value !== "string")) {
25
+ throw new Error("Invalid runtime telemetry agent_class schema");
26
+ }
27
+ return Object.freeze([...values]) as readonly AgentClass[];
28
+ }
29
+
30
+ // The mirrored transport contract is the runtime source of truth. Keeping this
31
+ // data-driven lets packaged agent updates follow the closed enum without a
32
+ // second name registry drifting in TypeScript.
33
+ export const AGENT_CLASSES = agentClasses();
34
+ export function parseAgentClass(value: unknown): AgentClass | undefined {
35
+ return typeof value === "string" && (AGENT_CLASSES as readonly string[]).includes(value) ? value as AgentClass : undefined;
36
+ }
37
+ function requiredAgentClass(value: string): AgentClass {
38
+ const parsed = parseAgentClass(value);
39
+ if (!parsed) throw new Error(`Runtime telemetry schema is missing required agent_class ${value}`);
40
+ return parsed;
41
+ }
42
+ export const UNKNOWN_AGENT_CLASS = requiredAgentClass("unknown");
43
+ export const ORCHESTRATOR_AGENT_CLASS = requiredAgentClass("orchestrator");
44
+
45
+ function registeredModels(): ReadonlySet<string> {
46
+ const rules: unknown = runtimeSchema?.$defs?.model?.oneOf;
47
+ if (!Array.isArray(rules) || !rules.length) throw new Error("Invalid runtime telemetry model schema");
48
+ const values = (rule: unknown, field: "provider" | "id"): string[] => {
49
+ if (!object(rule)) throw new Error("Invalid runtime telemetry model schema");
50
+ const properties = rule.properties;
51
+ if (!object(properties) || !object(properties[field])) throw new Error("Invalid runtime telemetry model schema");
52
+ const property = properties[field];
53
+ const candidates = typeof property.const === "string" ? [property.const] : property.enum;
54
+ if (!Array.isArray(candidates) || !candidates.length || candidates.some(value => typeof value !== "string")) {
55
+ throw new Error("Invalid runtime telemetry model schema");
56
+ }
57
+ return candidates as string[];
58
+ };
59
+ const models = new Set<string>();
60
+ for (const rule of rules) for (const provider of values(rule, "provider")) {
61
+ for (const id of values(rule, "id")) models.add(JSON.stringify([provider, id]));
62
+ }
63
+ return models;
64
+ }
65
+
66
+ const REGISTERED_MODELS = registeredModels();
67
+
68
+ /** The mirrored transport registry is authoritative before the optional Pi
69
+ * catalog. Unknown registry pairs still pass through the existing privacy
70
+ * classifier, so private IDs can only become custom/unknown.
71
+ */
72
+ export function classifyRuntimeModelId(provider: unknown, modelId: unknown,
73
+ classifyModel: typeof classifyPiCatalogName = classifyPiCatalogName): string {
74
+ if (typeof provider === "string" && typeof modelId === "string"
75
+ && REGISTERED_MODELS.has(JSON.stringify([provider, modelId]))) return modelId;
76
+ return classifyModel({ provider, modelId }).modelId;
77
+ }
78
+
79
+ type Missing = { state: "unavailable" | "unsupported" };
80
+ export type TokenMeasurement = Missing | { state: "reported"; value: number };
81
+ export type DurationMeasurement = Missing | { state: "measured"; value: number };
82
+ export interface FinalResponse {
83
+ kind: "final_assistant_response";
84
+ /** Local dedupe only: 1..128 UTF-16 code units; never exported. */
85
+ responseId: string;
86
+ /** Caller-observed selected SDK model ID, never dispatched/response identity.
87
+ * Catalog must be loaded before accounting; only public catalog names survive.
88
+ */
89
+ selectedModelId?: string;
90
+ /** Selection namespace, independent from observed response provider.
91
+ * Omission preserves legacy same-provider callers; adapters must pass it explicitly.
92
+ */
93
+ selectedProvider?: typeof PROVIDERS[number];
94
+ agentClass?: AgentClass;
95
+ /** Public catalog names from SDK response metadata, never endpoint proof. */
96
+ observedModelId?: string;
97
+ responseModelId?: string;
98
+ providerThinkingLevel?: typeof EFFORTS[number];
99
+ executor: typeof EXECUTORS[number];
100
+ provider: typeof PROVIDERS[number];
101
+ modelFamily: typeof FAMILIES[number];
102
+ effort: typeof EFFORTS[number];
103
+ error: typeof ERRORS[number];
104
+ /** Separate native counters; do not add cached tokens into input here. */
105
+ tokens: Record<"input" | "output" | "cacheRead" | "cacheWrite", TokenMeasurement>
106
+ & Partial<Record<"reasoning" | "totalTokens", TokenMeasurement>>;
107
+ /** Request start to response headers; not first token or full response. */
108
+ responseHeadersMs: DurationMeasurement;
109
+ /** Same request start to completed response; only explicitly measured values. */
110
+ fullResponseMs: DurationMeasurement;
111
+ }
112
+
113
+ interface TokenTotals { reported: number; unavailable: number; unsupported: number; sum: number }
114
+ interface DurationTotals { measured: number; unavailable: number; unsupported: number; sum: number }
115
+ export interface RuntimeMetricBucket {
116
+ hostAgent: "pi";
117
+ agentClass: AgentClass;
118
+ observedModelId: string;
119
+ responseModelId: string;
120
+ providerThinkingLevel: typeof EFFORTS[number];
121
+ selectedModelId: string;
122
+ selectedProvider: FinalResponse["provider"];
123
+ executor: FinalResponse["executor"];
124
+ provider: FinalResponse["provider"];
125
+ modelFamily: FinalResponse["modelFamily"];
126
+ effort: FinalResponse["effort"];
127
+ error: FinalResponse["error"];
128
+ responses: number;
129
+ tokens: Record<typeof TOKEN_FIELDS[number], TokenTotals>;
130
+ responseHeadersMs: DurationTotals;
131
+ fullResponseMs: DurationTotals;
132
+ }
133
+
134
+ function object(value: unknown): value is Record<string, unknown> {
135
+ return value !== null && typeof value === "object" && !Array.isArray(value);
136
+ }
137
+
138
+ function member<T extends string>(values: readonly T[], value: unknown): value is T {
139
+ return typeof value === "string" && values.includes(value as T);
140
+ }
141
+
142
+ function category<T extends string>(values: readonly T[], value: unknown, fallback: T): T {
143
+ return member(values, value) ? value : fallback;
144
+ }
145
+
146
+ function measurement(value: unknown, present: "reported" | "measured"): boolean {
147
+ if (!object(value)) return false;
148
+ if (value.state === "unavailable" || value.state === "unsupported") return !("value" in value);
149
+ if (value.state !== present || typeof value.value !== "number") return false;
150
+ const n = value.value;
151
+ // Hard ceilings keep every sum finite/exact for integer counters, even at capacity.
152
+ return Number.isFinite(n) && n >= 0 && (present === "reported"
153
+ ? Number.isSafeInteger(n) && n <= 1_000_000_000
154
+ : n <= 86_400_000);
155
+ }
156
+
157
+ export function validRuntimeResponse(value: unknown): value is FinalResponse {
158
+ if (!object(value) || value.kind !== "final_assistant_response") return false;
159
+ if (typeof value.responseId !== "string" || value.responseId.length < 1 || value.responseId.length > 128) return false;
160
+ if (!member(EFFORTS, value.effort) || !member(ERRORS, value.error)) return false;
161
+ const tokens = value.tokens;
162
+ if (!object(tokens) || !TOKEN_FIELDS.every(key => measurement(tokens[key] === undefined && ["reasoning", "totalTokens"].includes(key)
163
+ ? { state: "unavailable" } : tokens[key], "reported"))) return false;
164
+ if (!measurement(value.responseHeadersMs, "measured") || !measurement(value.fullResponseMs, "measured")) return false;
165
+ const headers = value.responseHeadersMs as DurationMeasurement;
166
+ const full = value.fullResponseMs as DurationMeasurement;
167
+ return headers.state !== "measured" || full.state !== "measured" || headers.value <= full.value;
168
+ }
169
+
170
+ function tokenTotals(): TokenTotals {
171
+ return { reported: 0, unavailable: 0, unsupported: 0, sum: 0 };
172
+ }
173
+
174
+ function durationTotals(): DurationTotals {
175
+ return { measured: 0, unavailable: 0, unsupported: 0, sum: 0 };
176
+ }
177
+
178
+ function addDuration(totals: DurationTotals, value: DurationMeasurement): void {
179
+ totals[value.state] += 1;
180
+ if (value.state === "measured") totals.sum += value.value;
181
+ }
182
+
183
+ /**
184
+ * At most 1024 accepted responses/IDs, 64 dimension buckets, and 128 code units
185
+ * per ID. No eviction: once capacity is reached, new records are rejected
186
+ * atomically (including existing buckets); accepted IDs remain deduplicated.
187
+ * Invalid/rejected IDs are not reserved. A new instance starts a new accounting
188
+ * window with NO cross-instance/lifetime dedupe guarantee. No reset/flush API:
189
+ * window ownership and delivery remain future work. Runtime consumption stays
190
+ * separate from deterministic SDD/RDD counts; no closure attribution or bridge.
191
+ * Snapshots contain only closed dimensions and bounded numeric aggregates.
192
+ */
193
+ export class RuntimeMetrics {
194
+ #ids = new Set<string>();
195
+ #buckets = new Map<string, RuntimeMetricBucket>();
196
+ #maxResponses: number;
197
+ #maxBuckets: number;
198
+ #classifyModel: typeof classifyPiCatalogName;
199
+
200
+ constructor({ maxResponses = 1024, maxBuckets = 64, classifyModel = classifyPiCatalogName }:
201
+ { maxResponses?: number; maxBuckets?: number; classifyModel?: typeof classifyPiCatalogName } = {}) {
202
+ if (!Number.isInteger(maxResponses) || maxResponses < 1 || maxResponses > 1024
203
+ || !Number.isInteger(maxBuckets) || maxBuckets < 1 || maxBuckets > 64) {
204
+ throw new RangeError("Invalid runtime metrics capacity");
205
+ }
206
+ this.#maxResponses = maxResponses;
207
+ this.#maxBuckets = maxBuckets;
208
+ this.#classifyModel = classifyModel;
209
+ }
210
+
211
+ record(response: FinalResponse): "recorded" | "duplicate" | "invalid" | "capacity" {
212
+ if (!validRuntimeResponse(response)) return "invalid";
213
+ if (this.#ids.has(response.responseId)) return "duplicate";
214
+ const selectedProvider = response.selectedProvider ?? response.provider;
215
+ const dimensions = {
216
+ hostAgent: "pi" as const,
217
+ agentClass: category(AGENT_CLASSES, response.agentClass, UNKNOWN_AGENT_CLASS),
218
+ observedModelId: classifyRuntimeModelId(response.provider, response.observedModelId, this.#classifyModel),
219
+ responseModelId: classifyRuntimeModelId(response.provider, response.responseModelId, this.#classifyModel),
220
+ providerThinkingLevel: category(EFFORTS, response.providerThinkingLevel, "unavailable"),
221
+ selectedModelId: classifyRuntimeModelId(selectedProvider, response.selectedModelId, this.#classifyModel),
222
+ selectedProvider: category(PROVIDERS, selectedProvider, typeof selectedProvider === "string" && selectedProvider ? "custom" : "unknown"),
223
+ executor: category(EXECUTORS, response.executor, "unknown"),
224
+ provider: category(PROVIDERS, response.provider, typeof response.provider === "string" && response.provider ? "custom" : "unknown"),
225
+ modelFamily: category(FAMILIES, response.modelFamily, typeof response.modelFamily === "string" && response.modelFamily ? "custom" : "unknown"),
226
+ effort: response.effort,
227
+ error: response.error,
228
+ };
229
+ const key = JSON.stringify(dimensions);
230
+ let bucket = this.#buckets.get(key);
231
+ if (this.#ids.size >= this.#maxResponses || (!bucket && this.#buckets.size >= this.#maxBuckets)) return "capacity";
232
+ if (!bucket) {
233
+ bucket = {
234
+ ...dimensions, responses: 0,
235
+ tokens: { input: tokenTotals(), output: tokenTotals(), cacheRead: tokenTotals(), cacheWrite: tokenTotals(), reasoning: tokenTotals(), totalTokens: tokenTotals() },
236
+ responseHeadersMs: durationTotals(), fullResponseMs: durationTotals(),
237
+ };
238
+ this.#buckets.set(key, bucket);
239
+ }
240
+ this.#ids.add(response.responseId);
241
+ bucket.responses += 1;
242
+ for (const field of TOKEN_FIELDS) {
243
+ const value = response.tokens[field] ?? { state: "unavailable" as const };
244
+ bucket.tokens[field][value.state] += 1;
245
+ if (value.state === "reported") bucket.tokens[field].sum += value.value;
246
+ }
247
+ addDuration(bucket.responseHeadersMs, response.responseHeadersMs);
248
+ addDuration(bucket.fullResponseMs, response.fullResponseMs);
249
+ return "recorded";
250
+ }
251
+
252
+ snapshot(): RuntimeMetricBucket[] {
253
+ return structuredClone([...this.#buckets.values()]);
254
+ }
255
+ }