gentle-pi 2.5.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.
- 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 +65 -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 +799 -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 +130 -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 +283 -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 +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -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 +283 -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 +99 -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 +1460 -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 +12 -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 +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -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,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
|
+
}
|