gentle-pi 2.5.0 → 2.6.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/README.md +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +67 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +795 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +121 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +288 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +197 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +174 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +308 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +288 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +95 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1454 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +23 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +207 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +196 -0
- package/tests/runtime-metrics-model.test.ts +76 -0
- package/tests/runtime-metrics-native.test.ts +275 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +187 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const runtimeSchema = JSON.parse(readFileSync(new URL("../contracts/telemetry/runtime-aggregate-v1.schema.json", import.meta.url), "utf8"));
|
|
4
|
+
|
|
5
|
+
// Pure local accounting, not a telemetry transport or Pi event adapter.
|
|
6
|
+
// Callers supply finalized assistant responses and authoritative classifications.
|
|
7
|
+
// Never infer executor, usage availability, or measured timings from SDK defaults.
|
|
8
|
+
const EXECUTORS = ["orchestrator", "worker", "reviewer", "unknown"] as const;
|
|
9
|
+
// Stable families, not model IDs/versions: new models need no catalog update.
|
|
10
|
+
// Callers map known native metadata to families; private aliases stay custom.
|
|
11
|
+
const FAMILIES = ["claude", "gpt", "o-series", "gemini", "llama", "qwen", "deepseek", "kimi", "custom", "unknown"] as const;
|
|
12
|
+
// Pi 0.85.1 docs/models.md, Thinking Level Map. These are selected Pi levels,
|
|
13
|
+
// not inferred provider effort or a claim that each model supports every level.
|
|
14
|
+
export const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max", "not_selected", "unsupported", "unavailable"] as const;
|
|
15
|
+
const ERRORS = ["none", "aborted", "rate_limit", "authentication", "network", "provider", "unknown"] as const;
|
|
16
|
+
const TOKEN_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "reasoning", "totalTokens"] as const;
|
|
17
|
+
declare const agentClassBrand: unique symbol;
|
|
18
|
+
export type AgentClass = string & { readonly [agentClassBrand]: "AgentClass" };
|
|
19
|
+
|
|
20
|
+
function agentClasses(): readonly AgentClass[] {
|
|
21
|
+
const values: unknown = runtimeSchema?.$defs?.row?.properties?.agent_class?.enum;
|
|
22
|
+
if (!Array.isArray(values) || !values.length || values.some(value => typeof value !== "string")) {
|
|
23
|
+
throw new Error("Invalid runtime telemetry agent_class schema");
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze([...values]) as readonly AgentClass[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The mirrored transport contract is the runtime source of truth. Keeping this
|
|
29
|
+
// data-driven lets packaged agent updates follow the closed enum without a
|
|
30
|
+
// second name registry drifting in TypeScript.
|
|
31
|
+
export const AGENT_CLASSES = agentClasses();
|
|
32
|
+
export function parseAgentClass(value: unknown): AgentClass | undefined {
|
|
33
|
+
return typeof value === "string" && (AGENT_CLASSES as readonly string[]).includes(value) ? value as AgentClass : undefined;
|
|
34
|
+
}
|
|
35
|
+
function requiredAgentClass(value: string): AgentClass {
|
|
36
|
+
const parsed = parseAgentClass(value);
|
|
37
|
+
if (!parsed) throw new Error(`Runtime telemetry schema is missing required agent_class ${value}`);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
export const UNKNOWN_AGENT_CLASS = requiredAgentClass("unknown");
|
|
41
|
+
export const ORCHESTRATOR_AGENT_CLASS = requiredAgentClass("orchestrator");
|
|
42
|
+
|
|
43
|
+
interface ModelFieldRule { pattern: RegExp; maxLength: number }
|
|
44
|
+
|
|
45
|
+
function modelFieldRule(field: "provider" | "id"): ModelFieldRule {
|
|
46
|
+
// The length cap lives on $defs.model.properties.<field>; the shape pattern
|
|
47
|
+
// lives only on the first (public-pattern) branch of $defs.model.anyOf, next
|
|
48
|
+
// to the unknown/custom/opencode sentinel branches. Both are mirrored,
|
|
49
|
+
// byte-for-byte, from the Gentle AI transport schema.
|
|
50
|
+
const property: unknown = runtimeSchema?.$defs?.model?.properties?.[field];
|
|
51
|
+
if (!object(property) || typeof property.maxLength !== "number") {
|
|
52
|
+
throw new Error(`Invalid runtime telemetry model ${field} schema`);
|
|
53
|
+
}
|
|
54
|
+
const branches: unknown = runtimeSchema?.$defs?.model?.anyOf;
|
|
55
|
+
const patternProperty = Array.isArray(branches) && object(branches[0]) && object(branches[0].properties)
|
|
56
|
+
? branches[0].properties[field] : undefined;
|
|
57
|
+
if (!object(patternProperty) || typeof patternProperty.pattern !== "string") {
|
|
58
|
+
throw new Error(`Invalid runtime telemetry model ${field} schema`);
|
|
59
|
+
}
|
|
60
|
+
return { pattern: new RegExp(patternProperty.pattern), maxLength: property.maxLength };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Schema-driven, not a hardcoded TypeScript regex: the mirrored transport
|
|
64
|
+
// contract owns the open family-pattern rules for provider/id shape. A
|
|
65
|
+
// companion Gentle AI change keeps the Go side on the same patterns.
|
|
66
|
+
const MODEL_PROVIDER_RULE = modelFieldRule("provider");
|
|
67
|
+
const MODEL_ID_RULE = modelFieldRule("id");
|
|
68
|
+
|
|
69
|
+
/** Provider-only normalization, independent of any specific model id: trims,
|
|
70
|
+
* lowercases, and keeps the slug only when it matches the schema provider
|
|
71
|
+
* pattern within its maxLength. Non-string/empty input is "unknown"; a
|
|
72
|
+
* non-conforming non-empty string is "custom". Shared by the standalone
|
|
73
|
+
* provider dimension and by normalizeRuntimeModel's own provider handling.
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeRuntimeProvider(value: unknown): string {
|
|
76
|
+
if (typeof value !== "string") return "unknown";
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (!trimmed) return "unknown";
|
|
79
|
+
const lower = trimmed.toLowerCase();
|
|
80
|
+
return lower.length <= MODEL_PROVIDER_RULE.maxLength && MODEL_PROVIDER_RULE.pattern.test(lower) ? lower : "custom";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Generic, schema-driven family-pattern normalizer for a (provider, id)
|
|
84
|
+
* selection or response pair (gentle-pi#968 / gentle-ai#4536). Open-weight
|
|
85
|
+
* models on arbitrary providers are reported by name; private aliases and
|
|
86
|
+
* fine-tunes stay custom. Rules, identical to the mirrored Go/schema side:
|
|
87
|
+
* - a non-string or empty provider or id fails closed to unknown/unknown;
|
|
88
|
+
* - id: trim, keep the LAST "/"-separated segment, lowercase; public only
|
|
89
|
+
* when it matches the schema id pattern within its maxLength, otherwise
|
|
90
|
+
* "custom";
|
|
91
|
+
* - provider: trim, lowercase; kept when it matches the schema provider
|
|
92
|
+
* pattern, otherwise "custom";
|
|
93
|
+
* - when the id is not public the provider becomes "custom" too, except
|
|
94
|
+
* "opencode", which stays opencode/custom;
|
|
95
|
+
* - the literal unknown/unknown and custom/custom pairs pass through.
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeRuntimeModel(provider: unknown, id: unknown): { provider: string; id: string } {
|
|
98
|
+
if (typeof provider !== "string" || typeof id !== "string") return { provider: "unknown", id: "unknown" };
|
|
99
|
+
const providerTrimmed = provider.trim();
|
|
100
|
+
const idTrimmed = id.trim();
|
|
101
|
+
if (!providerTrimmed || !idTrimmed) return { provider: "unknown", id: "unknown" };
|
|
102
|
+
const providerLower = providerTrimmed.toLowerCase();
|
|
103
|
+
const idSegment = idTrimmed.split("/").pop() ?? "";
|
|
104
|
+
const idLower = idSegment.toLowerCase();
|
|
105
|
+
if (providerLower === "unknown" && idLower === "unknown") return { provider: "unknown", id: "unknown" };
|
|
106
|
+
if (providerLower === "custom" && idLower === "custom") return { provider: "custom", id: "custom" };
|
|
107
|
+
const idPublic = idLower.length <= MODEL_ID_RULE.maxLength && MODEL_ID_RULE.pattern.test(idLower);
|
|
108
|
+
const id_ = idPublic ? idLower : "custom";
|
|
109
|
+
let providerResult = normalizeRuntimeProvider(providerTrimmed);
|
|
110
|
+
if (!idPublic) providerResult = providerResult === "opencode" ? "opencode" : "custom";
|
|
111
|
+
return { provider: providerResult, id: id_ };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Thin wrapper over normalizeRuntimeModel for callers that only track the
|
|
115
|
+
* id dimension (the provider is used only to decide the closed unknown/empty
|
|
116
|
+
* fast path; a valid non-empty provider string never changes the id result).
|
|
117
|
+
*/
|
|
118
|
+
export function classifyRuntimeModelId(provider: unknown, modelId: unknown): string {
|
|
119
|
+
return normalizeRuntimeModel(provider, modelId).id;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** True only for an id already in its normalized public form: it matches the
|
|
123
|
+
* schema id pattern within its maxLength (which, by construction, also
|
|
124
|
+
* excludes the "unknown"/"custom" sentinels, since neither matches any
|
|
125
|
+
* recognized family prefix). Used to pick the strongest available evidence
|
|
126
|
+
* tier among already-classified id dimensions (response/selected/observed).
|
|
127
|
+
*/
|
|
128
|
+
export function isPublicRuntimeModelId(value: unknown): value is string {
|
|
129
|
+
return typeof value === "string" && value.length > 0 && value.length <= MODEL_ID_RULE.maxLength && MODEL_ID_RULE.pattern.test(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type Missing = { state: "unavailable" | "unsupported" };
|
|
133
|
+
export type TokenMeasurement = Missing | { state: "reported"; value: number };
|
|
134
|
+
export type DurationMeasurement = Missing | { state: "measured"; value: number };
|
|
135
|
+
export interface FinalResponse {
|
|
136
|
+
kind: "final_assistant_response";
|
|
137
|
+
/** Local dedupe only: 1..128 UTF-16 code units; never exported. */
|
|
138
|
+
responseId: string;
|
|
139
|
+
/** Caller-observed selected SDK model ID, never dispatched/response identity.
|
|
140
|
+
* Normalized through normalizeRuntimeModel at record time; only an id
|
|
141
|
+
* matching the schema family pattern survives as a public name.
|
|
142
|
+
*/
|
|
143
|
+
selectedModelId?: string;
|
|
144
|
+
/** Selection namespace, independent from observed response provider.
|
|
145
|
+
* Omission preserves legacy same-provider callers; adapters must pass it explicitly.
|
|
146
|
+
*/
|
|
147
|
+
selectedProvider?: string;
|
|
148
|
+
agentClass?: AgentClass;
|
|
149
|
+
/** SDK-observed model id from response metadata, never endpoint proof.
|
|
150
|
+
* Normalized like selectedModelId; only a schema family match survives.
|
|
151
|
+
*/
|
|
152
|
+
observedModelId?: string;
|
|
153
|
+
responseModelId?: string;
|
|
154
|
+
providerThinkingLevel?: typeof EFFORTS[number];
|
|
155
|
+
executor: typeof EXECUTORS[number];
|
|
156
|
+
provider: string;
|
|
157
|
+
modelFamily: typeof FAMILIES[number];
|
|
158
|
+
effort: typeof EFFORTS[number];
|
|
159
|
+
error: typeof ERRORS[number];
|
|
160
|
+
/** Separate native counters; do not add cached tokens into input here. */
|
|
161
|
+
tokens: Record<"input" | "output" | "cacheRead" | "cacheWrite", TokenMeasurement>
|
|
162
|
+
& Partial<Record<"reasoning" | "totalTokens", TokenMeasurement>>;
|
|
163
|
+
/** Request start to response headers; not first token or full response. */
|
|
164
|
+
responseHeadersMs: DurationMeasurement;
|
|
165
|
+
/** Same request start to completed response; only explicitly measured values. */
|
|
166
|
+
fullResponseMs: DurationMeasurement;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface TokenTotals { reported: number; unavailable: number; unsupported: number; sum: number }
|
|
170
|
+
interface DurationTotals { measured: number; unavailable: number; unsupported: number; sum: number }
|
|
171
|
+
export interface RuntimeMetricBucket {
|
|
172
|
+
hostAgent: "pi";
|
|
173
|
+
agentClass: AgentClass;
|
|
174
|
+
observedModelId: string;
|
|
175
|
+
responseModelId: string;
|
|
176
|
+
providerThinkingLevel: typeof EFFORTS[number];
|
|
177
|
+
selectedModelId: string;
|
|
178
|
+
selectedProvider: FinalResponse["provider"];
|
|
179
|
+
executor: FinalResponse["executor"];
|
|
180
|
+
provider: FinalResponse["provider"];
|
|
181
|
+
modelFamily: FinalResponse["modelFamily"];
|
|
182
|
+
effort: FinalResponse["effort"];
|
|
183
|
+
error: FinalResponse["error"];
|
|
184
|
+
responses: number;
|
|
185
|
+
tokens: Record<typeof TOKEN_FIELDS[number], TokenTotals>;
|
|
186
|
+
responseHeadersMs: DurationTotals;
|
|
187
|
+
fullResponseMs: DurationTotals;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
191
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function member<T extends string>(values: readonly T[], value: unknown): value is T {
|
|
195
|
+
return typeof value === "string" && values.includes(value as T);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function category<T extends string>(values: readonly T[], value: unknown, fallback: T): T {
|
|
199
|
+
return member(values, value) ? value : fallback;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function measurement(value: unknown, present: "reported" | "measured"): boolean {
|
|
203
|
+
if (!object(value)) return false;
|
|
204
|
+
if (value.state === "unavailable" || value.state === "unsupported") return !("value" in value);
|
|
205
|
+
if (value.state !== present || typeof value.value !== "number") return false;
|
|
206
|
+
const n = value.value;
|
|
207
|
+
// Hard ceilings keep every sum finite/exact for integer counters, even at capacity.
|
|
208
|
+
return Number.isFinite(n) && n >= 0 && (present === "reported"
|
|
209
|
+
? Number.isSafeInteger(n) && n <= 1_000_000_000
|
|
210
|
+
: n <= 86_400_000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function validRuntimeResponse(value: unknown): value is FinalResponse {
|
|
214
|
+
if (!object(value) || value.kind !== "final_assistant_response") return false;
|
|
215
|
+
if (typeof value.responseId !== "string" || value.responseId.length < 1 || value.responseId.length > 128) return false;
|
|
216
|
+
if (!member(EFFORTS, value.effort) || !member(ERRORS, value.error)) return false;
|
|
217
|
+
const tokens = value.tokens;
|
|
218
|
+
if (!object(tokens) || !TOKEN_FIELDS.every(key => measurement(tokens[key] === undefined && ["reasoning", "totalTokens"].includes(key)
|
|
219
|
+
? { state: "unavailable" } : tokens[key], "reported"))) return false;
|
|
220
|
+
if (!measurement(value.responseHeadersMs, "measured") || !measurement(value.fullResponseMs, "measured")) return false;
|
|
221
|
+
const headers = value.responseHeadersMs as DurationMeasurement;
|
|
222
|
+
const full = value.fullResponseMs as DurationMeasurement;
|
|
223
|
+
return headers.state !== "measured" || full.state !== "measured" || headers.value <= full.value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function tokenTotals(): TokenTotals {
|
|
227
|
+
return { reported: 0, unavailable: 0, unsupported: 0, sum: 0 };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function durationTotals(): DurationTotals {
|
|
231
|
+
return { measured: 0, unavailable: 0, unsupported: 0, sum: 0 };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function addDuration(totals: DurationTotals, value: DurationMeasurement): void {
|
|
235
|
+
totals[value.state] += 1;
|
|
236
|
+
if (value.state === "measured") totals.sum += value.value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* At most 1024 accepted responses/IDs, 64 dimension buckets, and 128 code units
|
|
241
|
+
* per ID. No eviction: once capacity is reached, new records are rejected
|
|
242
|
+
* atomically (including existing buckets); accepted IDs remain deduplicated.
|
|
243
|
+
* Invalid/rejected IDs are not reserved. A new instance starts a new accounting
|
|
244
|
+
* window with NO cross-instance/lifetime dedupe guarantee. No reset/flush API:
|
|
245
|
+
* window ownership and delivery remain future work. Runtime consumption stays
|
|
246
|
+
* separate from deterministic SDD/RDD counts; no closure attribution or bridge.
|
|
247
|
+
* Snapshots contain only closed dimensions and bounded numeric aggregates.
|
|
248
|
+
*/
|
|
249
|
+
export class RuntimeMetrics {
|
|
250
|
+
#ids = new Set<string>();
|
|
251
|
+
#buckets = new Map<string, RuntimeMetricBucket>();
|
|
252
|
+
#maxResponses: number;
|
|
253
|
+
#maxBuckets: number;
|
|
254
|
+
|
|
255
|
+
constructor({ maxResponses = 1024, maxBuckets = 64 }: { maxResponses?: number; maxBuckets?: number } = {}) {
|
|
256
|
+
if (!Number.isInteger(maxResponses) || maxResponses < 1 || maxResponses > 1024
|
|
257
|
+
|| !Number.isInteger(maxBuckets) || maxBuckets < 1 || maxBuckets > 64) {
|
|
258
|
+
throw new RangeError("Invalid runtime metrics capacity");
|
|
259
|
+
}
|
|
260
|
+
this.#maxResponses = maxResponses;
|
|
261
|
+
this.#maxBuckets = maxBuckets;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
record(response: FinalResponse): "recorded" | "duplicate" | "invalid" | "capacity" {
|
|
265
|
+
if (!validRuntimeResponse(response)) return "invalid";
|
|
266
|
+
if (this.#ids.has(response.responseId)) return "duplicate";
|
|
267
|
+
const selectedProvider = response.selectedProvider ?? response.provider;
|
|
268
|
+
const dimensions = {
|
|
269
|
+
hostAgent: "pi" as const,
|
|
270
|
+
agentClass: category(AGENT_CLASSES, response.agentClass, UNKNOWN_AGENT_CLASS),
|
|
271
|
+
observedModelId: classifyRuntimeModelId(response.provider, response.observedModelId),
|
|
272
|
+
responseModelId: classifyRuntimeModelId(response.provider, response.responseModelId),
|
|
273
|
+
providerThinkingLevel: category(EFFORTS, response.providerThinkingLevel, "unavailable"),
|
|
274
|
+
selectedModelId: classifyRuntimeModelId(selectedProvider, response.selectedModelId),
|
|
275
|
+
selectedProvider: normalizeRuntimeProvider(selectedProvider),
|
|
276
|
+
executor: category(EXECUTORS, response.executor, "unknown"),
|
|
277
|
+
provider: normalizeRuntimeProvider(response.provider),
|
|
278
|
+
modelFamily: category(FAMILIES, response.modelFamily, typeof response.modelFamily === "string" && response.modelFamily ? "custom" : "unknown"),
|
|
279
|
+
effort: response.effort,
|
|
280
|
+
error: response.error,
|
|
281
|
+
};
|
|
282
|
+
const key = JSON.stringify(dimensions);
|
|
283
|
+
let bucket = this.#buckets.get(key);
|
|
284
|
+
if (this.#ids.size >= this.#maxResponses || (!bucket && this.#buckets.size >= this.#maxBuckets)) return "capacity";
|
|
285
|
+
if (!bucket) {
|
|
286
|
+
bucket = {
|
|
287
|
+
...dimensions, responses: 0,
|
|
288
|
+
tokens: { input: tokenTotals(), output: tokenTotals(), cacheRead: tokenTotals(), cacheWrite: tokenTotals(), reasoning: tokenTotals(), totalTokens: tokenTotals() },
|
|
289
|
+
responseHeadersMs: durationTotals(), fullResponseMs: durationTotals(),
|
|
290
|
+
};
|
|
291
|
+
this.#buckets.set(key, bucket);
|
|
292
|
+
}
|
|
293
|
+
this.#ids.add(response.responseId);
|
|
294
|
+
bucket.responses += 1;
|
|
295
|
+
for (const field of TOKEN_FIELDS) {
|
|
296
|
+
const value = response.tokens[field] ?? { state: "unavailable" as const };
|
|
297
|
+
bucket.tokens[field][value.state] += 1;
|
|
298
|
+
if (value.state === "reported") bucket.tokens[field].sum += value.value;
|
|
299
|
+
}
|
|
300
|
+
addDuration(bucket.responseHeadersMs, response.responseHeadersMs);
|
|
301
|
+
addDuration(bucket.fullResponseMs, response.fullResponseMs);
|
|
302
|
+
return "recorded";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
snapshot(): RuntimeMetricBucket[] {
|
|
306
|
+
return structuredClone([...this.#buckets.values()]);
|
|
307
|
+
}
|
|
308
|
+
}
|