gentle-pi 2.6.0 → 2.6.2
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 +183 -943
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +9 -8
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +3 -3
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +7 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json +2 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json +2 -2
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +8 -6
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/docs/assets/brand/gentle-pi-banner.svg +33 -0
- package/docs/assets/brand/terminal-divider.svg +17 -0
- package/docs/assets/diagrams/agent-orchestration.svg +19 -0
- package/docs/assets/diagrams/gentleman-workflow.svg +15 -0
- package/docs/assets/diagrams/native-review.svg +16 -0
- package/docs/assets/diagrams/sdd-cycle.svg +14 -0
- package/docs/assets/features/gentle-shell.png +0 -0
- package/docs/gentle-shell.md +151 -0
- package/docs/readme-reference.md +868 -0
- package/extensions/gentle-agents.ts +4 -5
- package/extensions/gentle-ai.ts +65 -22
- package/extensions/runtime-metrics.ts +10 -19
- package/lib/agents-history.ts +7 -1
- package/lib/native-review-cli.ts +14 -0
- package/lib/runtime-metrics-children.ts +3 -5
- package/lib/runtime-metrics-native.ts +17 -9
- package/lib/runtime-metrics.ts +95 -42
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +14 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/types-baseline.json +1 -5
- package/scripts/verify-package-files.mjs +3 -3
- package/tests/fixtures/runtime-metrics-native-batches.json +1 -1
- package/tests/gentle-agents.test.ts +22 -6
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +3 -2
- package/tests/native-review-capability-contract.test.ts +24 -1
- package/tests/package-manifest.test.ts +19 -18
- package/tests/review-authority-recovery-docs.test.ts +13 -13
- package/tests/review-controller-native-routing.test.ts +49 -0
- package/tests/review-ledger-contract.test.ts +7 -5
- package/tests/runtime-metrics-children.test.ts +4 -3
- package/tests/runtime-metrics-extension.test.ts +40 -31
- package/tests/runtime-metrics-model.test.ts +76 -0
- package/tests/runtime-metrics-native.test.ts +73 -7
- package/tests/runtime-metrics.test.ts +22 -19
- package/tests/sdd-managed-runtime-settlement.test.ts +37 -0
- package/tests/sdd-selection-transport.test.ts +57 -0
- package/tests/skill-collision-prefixes.test.ts +2 -2
- package/lib/runtime-metrics-pi-identity.ts +0 -113
- package/tests/runtime-metrics-pi-identity.test.ts +0 -113
|
@@ -30,7 +30,6 @@ import { openInExternalEditor } from "./gentle-shell.ts";
|
|
|
30
30
|
import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
|
|
31
31
|
import { assertResearchCheckpoint, parseResearchPersistence, RESEARCH_PERSISTENCE_ENTRY, canonicalArtifactPath, researchAgent, renderResearchCapabilities, RESEARCH_CHILD_TOOLS_ENV, RESEARCH_SELECTION_ENV, RESEARCH_ARTIFACT_ENV, parseResearchArtifactIntent, researchArtifactCall, researchArtifactReadback, type ResearchArtifactIntent, type ResearchWriteIdentity } from "../lib/sdd-research-capabilities.ts";
|
|
32
32
|
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
|
|
33
|
-
import { lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
|
|
34
33
|
import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
|
|
35
34
|
|
|
36
35
|
// Gentle Agents: subagents as isolated `pi --mode rpc` children, a task
|
|
@@ -198,9 +197,12 @@ export async function admitManagedRemediation(request: TaskRequest, input: unkno
|
|
|
198
197
|
task.error = "Managed remediation lacks complete passing planned-command evidence";
|
|
199
198
|
}
|
|
200
199
|
if (payload.outcome === "passed" && state.settlement && state.settlement.state !== "blocked") task.status = TASK_STATUS.COMPLETED;
|
|
201
|
-
if (!state.settlement
|
|
200
|
+
if (!state.settlement) {
|
|
202
201
|
task.status = TASK_STATUS.FAILED;
|
|
203
202
|
task.error = "Native remediation settlement unresolved; retain exact history for reconciliation";
|
|
203
|
+
} else if (state.settlement.state === "blocked") {
|
|
204
|
+
task.status = TASK_STATUS.FAILED;
|
|
205
|
+
task.error = `Native remediation settlement blocked(${state.settlement.reason ?? "unspecified"}); current native admission decides any later attempt`;
|
|
204
206
|
}
|
|
205
207
|
await persist(task);
|
|
206
208
|
},
|
|
@@ -244,7 +246,6 @@ export interface AgentsDeps extends RunnerDeps {
|
|
|
244
246
|
runtimeMetricsPolicy?: RuntimeMetricsPolicyDeps;
|
|
245
247
|
metricsNow?: () => number;
|
|
246
248
|
metricsSchedule?: RunnerDeps["schedule"];
|
|
247
|
-
lookupPiCatalogName?: typeof lookupPiCatalogName;
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
|
|
@@ -633,7 +634,6 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
633
634
|
const stoppingTaskIds = new Set<string>();
|
|
634
635
|
const yieldedTaskIds = new Set<string>();
|
|
635
636
|
const metricsNow = deps.metricsNow ?? (() => performance.now());
|
|
636
|
-
const catalogLookup = deps.lookupPiCatalogName ?? lookupPiCatalogName;
|
|
637
637
|
let metricsOwner = {};
|
|
638
638
|
const metricTasks = new Map<string, { selection?: LaunchSelection; started: number; launched: boolean; finished: boolean; current(): boolean; valid(): boolean }>();
|
|
639
639
|
const unsubscribeMetrics = pi.events.on(CHILD_METRICS_REVOKED, id => {
|
|
@@ -1121,7 +1121,6 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1121
1121
|
onLaunch: () => { metrics.launched = true; request.onLaunch?.(); },
|
|
1122
1122
|
...(observe ? { canCollectResponseObservations: metrics.valid, prepareResponseObservations: async () => {
|
|
1123
1123
|
if (metrics.finished || owner !== metricsOwner || request.parentSessionId !== activeSessionId() || !runtimeMetricsEnvAllows(deps.env)) return false;
|
|
1124
|
-
void catalogLookup({ provider: "openai", modelId: "gpt-4o" }).catch(() => {});
|
|
1125
1124
|
if (!metrics.valid()) return false;
|
|
1126
1125
|
metrics.selection = launchSelection(request.agent, request.model, request.thinking);
|
|
1127
1126
|
metrics.started = metricsNow();
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1839,7 +1839,15 @@ async function resolveSelectedNativeSddChangeStartup(
|
|
|
1839
1839
|
}
|
|
1840
1840
|
if (selection.phase === "remediate") {
|
|
1841
1841
|
if (status.nextRecommended !== "remediate" || status.remediationState?.failedEvidenceRevision !== selection.failedEvidenceRevision) throw new Error("Stale remediation selection");
|
|
1842
|
-
} else if (status.nextRecommended !== selection.phase || status.dependencies[selection.phase] !== "ready" || status.blockedReasons.length > 0) {
|
|
1842
|
+
} else if (status.nextRecommended !== selection.phase || status.dependencies[selection.phase] !== "ready" || (status.blockedReasons.length > 0 && selection.phase !== "verify")) {
|
|
1843
|
+
// Native's contract gates terminal, archive, and apply work on a
|
|
1844
|
+
// non-empty `blockedReasons`, and it deliberately keeps the `verify`
|
|
1845
|
+
// route runnable, because the blocker can name the evidence refresh
|
|
1846
|
+
// that is its own remedy ("failed verification evidence is incomplete;
|
|
1847
|
+
// rerun SDD verification", gentle-ai#3538). Vetoing that route made the
|
|
1848
|
+
// native-recommended phase unreachable (gentle-pi#972). The other
|
|
1849
|
+
// phases still fail closed, and every blocker stays in the injected
|
|
1850
|
+
// status for reporting.
|
|
1843
1851
|
throw new Error(`SDD selection native status blocks phase ${selection.phase}; it cannot execute.`);
|
|
1844
1852
|
}
|
|
1845
1853
|
return { selection, status };
|
|
@@ -5601,6 +5609,33 @@ function completeNativeStart(
|
|
|
5601
5609
|
};
|
|
5602
5610
|
}
|
|
5603
5611
|
|
|
5612
|
+
// gentle-ai#4003: every Pi-side teardown step that fails after the native
|
|
5613
|
+
// burn is deferred cleanup, not a failed acknowledgement. Only the
|
|
5614
|
+
// already-sanitized CandidateViewError surface is relayed; anything else is
|
|
5615
|
+
// reduced to the step's fixed code so no path or command text reaches the
|
|
5616
|
+
// caller. The candidate-view hint is out-of-band on purpose: no controller
|
|
5617
|
+
// operation exposes a cleanup-only retry, and replaying acknowledge-approved
|
|
5618
|
+
// would hit the already-burned lineage.
|
|
5619
|
+
const POST_BURN_CLEANUP = {
|
|
5620
|
+
candidateView: { code: "candidate-view-cleanup-failed", nextAction: "retry-candidate-view-cleanup-or-remove-the-view-out-of-band" },
|
|
5621
|
+
retainedSelection: { code: "retained-selection-cleanup-failed", nextAction: "retained-selection-clears-on-the-next-terminal-status" },
|
|
5622
|
+
} as const;
|
|
5623
|
+
|
|
5624
|
+
function deferredPostBurnCleanup(step: (typeof POST_BURN_CLEANUP)[keyof typeof POST_BURN_CLEANUP], cleanup: () => void): Record<string, unknown> | undefined {
|
|
5625
|
+
try {
|
|
5626
|
+
cleanup();
|
|
5627
|
+
return undefined;
|
|
5628
|
+
} catch (error) {
|
|
5629
|
+
return {
|
|
5630
|
+
status: "deferred",
|
|
5631
|
+
diagnostics: error instanceof CandidateViewError
|
|
5632
|
+
? { code: error.reason, message: error.message }
|
|
5633
|
+
: { code: step.code },
|
|
5634
|
+
next_action: step.nextAction,
|
|
5635
|
+
};
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
|
|
5604
5639
|
function nativeOperationFailure(operation: ReviewControllerOperation | "gentle_review_capture", error: unknown): Record<string, unknown> {
|
|
5605
5640
|
const value = error as { mutationOutcome?: unknown; nextAction?: unknown; diagnostics?: unknown; auditRecord?: unknown; launchAttempted?: unknown; candidateViewPreNative?: unknown; failureEnvelope?: { raw?: unknown; mutationOutcome?: unknown; replayability?: unknown; nextAction?: unknown; code?: unknown; continuation?: { command?: unknown } } };
|
|
5606
5641
|
if (isRecord(value.failureEnvelope) && isRecord(value.failureEnvelope.raw)) {
|
|
@@ -7335,42 +7370,50 @@ async function executeReviewControllerOperation(
|
|
|
7335
7370
|
} catch (error) {
|
|
7336
7371
|
return nativeOperationFailure(parameters.operation, error);
|
|
7337
7372
|
}
|
|
7373
|
+
let acknowledged: NativeReviewAcknowledgeApprovedOutcome | void;
|
|
7338
7374
|
try {
|
|
7339
7375
|
// gentle-ai #3947: the burn answers with one review-acknowledged/v1
|
|
7340
7376
|
// envelope bound to exactly this lineage, target, and revision, and
|
|
7341
7377
|
// the burn is reported from that envelope, never from a later
|
|
7342
7378
|
// STATUS. Every published release up to v2.5.0-rc.3 still burns in
|
|
7343
7379
|
// silence, and that result stays byte-identical.
|
|
7344
|
-
|
|
7380
|
+
acknowledged = await acknowledgementCli.acknowledgeApproved({
|
|
7345
7381
|
argumentTokens,
|
|
7346
7382
|
cwd: defaultCwd,
|
|
7347
7383
|
binding: { lineageId: parameters.lineageId, targetIdentity: status.targetIdentity, revision: status.authority.revision },
|
|
7348
7384
|
...(signal === undefined ? {} : { signal }),
|
|
7349
7385
|
});
|
|
7350
|
-
clearRetainedNativeUntrackedSelection(retainedUntrackedSelections, defaultCwd, parameters.lineageId);
|
|
7351
|
-
// The registry owns restoring writability of its 0555 views before
|
|
7352
|
-
// removal; a terminal approved cleanup keeps the lineage projection.
|
|
7353
|
-
candidateViews?.cleanupTerminal(parameters.lineageId, "approved", defaultCwd);
|
|
7354
|
-
// gentle-pi#668: `closed` is never auto-derived or recorded here --
|
|
7355
|
-
// a parent that wants the on-path passes nativeReviewOutcome:
|
|
7356
|
-
// "closed" explicitly on its next assess call for this candidate.
|
|
7357
|
-
return {
|
|
7358
|
-
operation: parameters.operation,
|
|
7359
|
-
status: "closed",
|
|
7360
|
-
outcome: "native-approved-acknowledgement-completed",
|
|
7361
|
-
lineage_id: parameters.lineageId,
|
|
7362
|
-
target_identity: status.targetIdentity,
|
|
7363
|
-
...(acknowledged === undefined ? {} : { consumed_revision: acknowledged.consumedRevision }),
|
|
7364
|
-
authority: "burned",
|
|
7365
|
-
...(acknowledged === undefined ? {} : { burn_evidence: acknowledged.schema }),
|
|
7366
|
-
delivery: "ordinary-repository-policy",
|
|
7367
|
-
mutation_performed: true,
|
|
7368
|
-
mutation_outcome: "committed",
|
|
7369
|
-
};
|
|
7370
7386
|
} catch (error) {
|
|
7371
7387
|
if (!nativeMutationRequiresStatus(error)) return nativeOperationFailure(parameters.operation, error);
|
|
7372
7388
|
return await reconcileNativeMutationFailure(parameters.operation, error, acknowledgementCli, target, retainedUntrackedSelections);
|
|
7373
7389
|
}
|
|
7390
|
+
// gentle-ai#4003: from here the native burn is the committed authority
|
|
7391
|
+
// outcome. Both Pi-side teardown steps run outside the mutation-result
|
|
7392
|
+
// try/catch and each one is guarded on its own, so a cleanup failure is
|
|
7393
|
+
// reported as deferred cleanup and never as a failed acknowledgement
|
|
7394
|
+
// that would invite a replay of a burned operation.
|
|
7395
|
+
const retainedSelectionCleanup = deferredPostBurnCleanup(POST_BURN_CLEANUP.retainedSelection, () => clearRetainedNativeUntrackedSelection(retainedUntrackedSelections, defaultCwd, parameters.lineageId));
|
|
7396
|
+
// The registry owns restoring writability of its 0555 views before
|
|
7397
|
+
// removal; a terminal approved cleanup keeps the lineage projection.
|
|
7398
|
+
const candidateViewCleanup = deferredPostBurnCleanup(POST_BURN_CLEANUP.candidateView, () => candidateViews?.cleanupTerminal(parameters.lineageId, "approved", defaultCwd));
|
|
7399
|
+
// gentle-pi#668: `closed` is never auto-derived or recorded here --
|
|
7400
|
+
// a parent that wants the on-path passes nativeReviewOutcome:
|
|
7401
|
+
// "closed" explicitly on its next assess call for this candidate.
|
|
7402
|
+
return {
|
|
7403
|
+
operation: parameters.operation,
|
|
7404
|
+
status: "closed",
|
|
7405
|
+
outcome: "native-approved-acknowledgement-completed",
|
|
7406
|
+
lineage_id: parameters.lineageId,
|
|
7407
|
+
target_identity: status.targetIdentity,
|
|
7408
|
+
...(acknowledged === undefined ? {} : { consumed_revision: acknowledged.consumedRevision }),
|
|
7409
|
+
authority: "burned",
|
|
7410
|
+
...(acknowledged === undefined ? {} : { burn_evidence: acknowledged.schema }),
|
|
7411
|
+
delivery: "ordinary-repository-policy",
|
|
7412
|
+
mutation_performed: true,
|
|
7413
|
+
mutation_outcome: "committed",
|
|
7414
|
+
...(retainedSelectionCleanup === undefined ? {} : { retained_selection_cleanup: retainedSelectionCleanup }),
|
|
7415
|
+
...(candidateViewCleanup === undefined ? {} : { candidate_view_cleanup: candidateViewCleanup }),
|
|
7416
|
+
};
|
|
7374
7417
|
}
|
|
7375
7418
|
if (parameters.operation === REVIEW_CONTROLLER_OPERATION.ANSWER_CONSENT) {
|
|
7376
7419
|
const input = parseControllerJson(requiredControllerString(parameters, "input"), parameters.operation);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { EFFORTS, ORCHESTRATOR_AGENT_CLASS, RuntimeMetrics, UNKNOWN_AGENT_CLASS, type FinalResponse, type TokenMeasurement } from "../lib/runtime-metrics.ts";
|
|
3
|
-
import { classifyPiCatalogName, lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
|
|
4
3
|
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, snapshotChildEvent, type ChildLaunchBucket } from "../lib/runtime-metrics-children.ts";
|
|
5
4
|
import { RuntimeMetricsAttempt } from "../lib/runtime-metrics-delivery.ts";
|
|
6
5
|
import { sendNativeRuntimeEvent, type NativeRuntimeTransportDeps } from "../lib/runtime-metrics-native.ts";
|
|
@@ -12,10 +11,8 @@ import { runtimeMetricsEnvAllows } from "../lib/runtime-metrics-policy.ts";
|
|
|
12
11
|
* Pi hooks lack request correlation: latency and SDK-zero presence are unknown.
|
|
13
12
|
*/
|
|
14
13
|
export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
{ lookup?: typeof lookupPiCatalogName; classify?: typeof classifyPiCatalogName; native?: NativeRuntimeTransportDeps;
|
|
18
|
-
send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
|
|
14
|
+
{ native, send = sendNativeRuntimeEvent, now = () => performance.now(), shutdownWaitMs = 1500 }:
|
|
15
|
+
{ native?: NativeRuntimeTransportDeps; send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
|
|
19
16
|
const allows = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && runtimeMetricsEnvAllows(env);
|
|
20
17
|
if (!allows()) return;
|
|
21
18
|
type Selection = Pick<FinalResponse, "selectedModelId" | "selectedProvider" | "effort">;
|
|
@@ -24,7 +21,7 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
|
24
21
|
let requestSeen = false;
|
|
25
22
|
let ambiguous = false;
|
|
26
23
|
let live: { id: string; started: number; ctx: ExtensionContext; attempt: RuntimeMetricsAttempt;
|
|
27
|
-
seen: WeakSet<object>; children: Set<string
|
|
24
|
+
seen: WeakSet<object>; children: Set<string> } | undefined;
|
|
28
25
|
const missing = { state: "unavailable" } as const;
|
|
29
26
|
function invalidate() { selection = undefined; ambiguous = true; }
|
|
30
27
|
function dispose() { live?.attempt.dispose(); live = undefined; active = false; invalidate(); }
|
|
@@ -32,15 +29,10 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
|
32
29
|
if (!allows() || live?.id !== ctx.sessionManager.getSessionId()) dispose();
|
|
33
30
|
return live;
|
|
34
31
|
}
|
|
35
|
-
function refreshCatalog(owner: NonNullable<typeof live>, input: { provider: unknown; modelId: unknown }) {
|
|
36
|
-
void lookup(input).then(result => {
|
|
37
|
-
if (live === owner && result.classification === "catalog_public") owner.catalog = true;
|
|
38
|
-
}, () => { /* A later classification callback may retry the bounded load. */ });
|
|
39
|
-
}
|
|
40
32
|
function submit(owner: NonNullable<typeof live>, responses: FinalResponse[], launches?: ChildLaunchBucket[]) {
|
|
41
33
|
if (!responses.length || live !== owner || !current(owner.ctx)) return;
|
|
42
34
|
// Ephemeral event-local accounting only; source IDs never enter these rows.
|
|
43
|
-
const metrics = new RuntimeMetrics(
|
|
35
|
+
const metrics = new RuntimeMetrics();
|
|
44
36
|
for (const [index, row] of responses.entries()) metrics.record({ ...row, responseId: String(index) });
|
|
45
37
|
const rows = metrics.snapshot();
|
|
46
38
|
if (!rows.length) return;
|
|
@@ -71,10 +63,8 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
|
71
63
|
pi.on("session_start", (_event, ctx) => {
|
|
72
64
|
dispose();
|
|
73
65
|
if (!allows()) return;
|
|
74
|
-
|
|
75
|
-
attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>()
|
|
76
|
-
live = owner;
|
|
77
|
-
refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
|
|
66
|
+
live = { id: ctx.sessionManager.getSessionId(), started: now(), ctx,
|
|
67
|
+
attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>() };
|
|
78
68
|
});
|
|
79
69
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
80
70
|
const teardown = () => { dispose(); offChild(); offRevoke(); };
|
|
@@ -95,8 +85,10 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
|
95
85
|
if (!active || ambiguous) return;
|
|
96
86
|
let effort: FinalResponse["effort"] = "unavailable";
|
|
97
87
|
try { const value = pi.getThinkingLevel(); if (EFFORTS.includes(value)) effort = value; } catch { /* No evidence. */ }
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
// No catalog gate: the selection is taken as-is (bounded to 128 chars) and
|
|
89
|
+
// the schema-driven normalizer decides what actually leaves the machine.
|
|
90
|
+
selection = { selectedProvider: typeof ctx.model?.provider === "string" && ctx.model.provider.length <= 128 ? ctx.model.provider : undefined,
|
|
91
|
+
selectedModelId: typeof ctx.model?.id === "string" && ctx.model.id.length <= 128 ? ctx.model.id : undefined, effort };
|
|
100
92
|
});
|
|
101
93
|
function token(value: unknown): TokenMeasurement {
|
|
102
94
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000
|
|
@@ -109,7 +101,6 @@ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
|
109
101
|
if (!owner || message.role !== "assistant" || message.stopReason === "pending" || message.stopReason === "deferred"
|
|
110
102
|
|| owner.seen.has(message)) return;
|
|
111
103
|
owner.seen.add(message);
|
|
112
|
-
refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
|
|
113
104
|
const selected = active && !ambiguous ? selection : undefined;
|
|
114
105
|
submit(owner, [{ kind: "final_assistant_response", responseId: "0",
|
|
115
106
|
selectedProvider: selected?.selectedProvider ?? "unknown", selectedModelId: selected?.selectedModelId,
|
package/lib/agents-history.ts
CHANGED
|
@@ -16,7 +16,13 @@ export function remediationUnresolved(task: TaskRecord): boolean {
|
|
|
16
16
|
const state = task.sddRemediation;
|
|
17
17
|
if (!state) return false;
|
|
18
18
|
if (state.acquireUncertain || state.settlementUncertain) return true;
|
|
19
|
-
|
|
19
|
+
// A received settlement is a definite native outcome, whatever its state
|
|
20
|
+
// (including "blocked"): it is terminal task history, never local
|
|
21
|
+
// ambiguity. Native admission is the sole authority over any later
|
|
22
|
+
// attempt for the same cwd/change; only genuinely uncertain outcomes, or
|
|
23
|
+
// no settlement at all with a still-retained token/claimed actor, are
|
|
24
|
+
// unresolved.
|
|
25
|
+
if (state.settlement) return false;
|
|
20
26
|
return !!state.token || !!state.actorClaimed || !["blocked", "complete"].includes(state.acquireResult?.state ?? "");
|
|
21
27
|
}
|
|
22
28
|
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -966,6 +966,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
966
966
|
// so this row repeats 2.7.0. riskEvidence and hint remain dark because
|
|
967
967
|
// neither is proven to reach the negotiated START path Pi consumes.
|
|
968
968
|
"2.8.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
969
|
+
// v2.8.1 only changed runtime telemetry model attribution (gentle-ai#4536);
|
|
970
|
+
// the closed fields Pi consumes did not change between 2.8.0 and 2.8.1, so
|
|
971
|
+
// this row repeats 2.8.0 exactly. riskEvidence and hint remain dark
|
|
972
|
+
// because neither is proven to reach the negotiated START path Pi consumes.
|
|
973
|
+
"2.8.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
974
|
+
// v2.8.2 shipped OpenCode SDD preflight plugin fixes, community-tools RTK
|
|
975
|
+
// acquisition, and Claude Code Stop telemetry. The provider contract semver
|
|
976
|
+
// stays 1.2.0; the same pin re-mirrors bundle bytes that had drifted under
|
|
977
|
+
// that semver (lens inspection.status "unavailable", targeted-validator
|
|
978
|
+
// regressions/inspection members, seven Pi stop reason codes). None of
|
|
979
|
+
// those touch the closed START/STATUS fields this row negotiates, so it
|
|
980
|
+
// repeats 2.8.1 exactly. riskEvidence and hint remain dark because neither
|
|
981
|
+
// is proven to reach the negotiated START path Pi consumes.
|
|
982
|
+
"2.8.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
969
983
|
});
|
|
970
984
|
|
|
971
985
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -2,14 +2,12 @@ import { readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { parseAgentDefinition, type AgentDefinition, type ModelRef } from "./agents-config.ts";
|
|
3
3
|
import type { ChildObservationSnapshot } from "./agents-runner.ts";
|
|
4
4
|
import { FINISHED_STATUSES, type TaskStatus } from "./agents-protocol.ts";
|
|
5
|
-
import { classifyRuntimeModelId, EFFORTS, parseAgentClass, RuntimeMetrics, UNKNOWN_AGENT_CLASS, validRuntimeResponse, type AgentClass, type FinalResponse, type RuntimeMetricBucket } from "./runtime-metrics.ts";
|
|
6
|
-
import { classifyPiCatalogName } from "./runtime-metrics-pi-identity.ts";
|
|
5
|
+
import { classifyRuntimeModelId, EFFORTS, normalizeRuntimeProvider, parseAgentClass, RuntimeMetrics, UNKNOWN_AGENT_CLASS, validRuntimeResponse, type AgentClass, type FinalResponse, type RuntimeMetricBucket } from "./runtime-metrics.ts";
|
|
7
6
|
export const CHILD_METRICS_EVENT = "gentle:runtime-metrics:child/v1";
|
|
8
7
|
// Local revocation notification invalidates active observations. Contains only
|
|
9
8
|
// the local session join, never policy output.
|
|
10
9
|
export const CHILD_METRICS_REVOKED = "gentle:runtime-metrics:revoked/v1";
|
|
11
10
|
const missing = { state: "unavailable" } as const;
|
|
12
|
-
const providers = ["anthropic", "openai", "openai-codex", "google", "google-vertex", "amazon-bedrock", "openrouter", "custom", "unknown"];
|
|
13
11
|
const tokenFields = ["input", "output", "cacheRead", "cacheWrite", "reasoning", "totalTokens"] as const;
|
|
14
12
|
/** Recognize only names from this package's fixed assets and the transport's
|
|
15
13
|
* closed agent_class enum. Customized packaged agents retain their schema name;
|
|
@@ -44,11 +42,11 @@ export function classifyBuiltinAgent(agent: AgentDefinition): AgentClass {
|
|
|
44
42
|
} catch { return UNKNOWN_AGENT_CLASS; }
|
|
45
43
|
}
|
|
46
44
|
function provider(value: unknown): FinalResponse["provider"] {
|
|
47
|
-
return
|
|
45
|
+
return normalizeRuntimeProvider(value);
|
|
48
46
|
}
|
|
49
47
|
function modelId(namespace: unknown, value: unknown): string {
|
|
50
48
|
if (value === "unknown" || value === undefined) return "unknown";
|
|
51
|
-
return classifyRuntimeModelId(namespace, value
|
|
49
|
+
return classifyRuntimeModelId(namespace, value);
|
|
52
50
|
}
|
|
53
51
|
function effort(value: unknown): FinalResponse["effort"] {
|
|
54
52
|
return EFFORTS.includes(value as FinalResponse["effort"]) ? value as FinalResponse["effort"] : "unavailable";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
|
|
2
2
|
import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
3
3
|
import { runtimeMetricsEnvAllows } from "./runtime-metrics-policy.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { isPublicRuntimeModelId, normalizeRuntimeModel, type RuntimeMetricBucket } from "./runtime-metrics.ts";
|
|
5
5
|
import type { ChildLaunchBucket } from "./runtime-metrics-children.ts";
|
|
6
6
|
|
|
7
7
|
import schema from "../contracts/telemetry/runtime-aggregate-v1.schema.json" with { type: "json" };
|
|
@@ -10,11 +10,11 @@ export type NativeRuntimeResult = "stored" | "duplicate" | "discarded" | "disabl
|
|
|
10
10
|
export const NATIVE_SEND_ACK_SCHEMA = "gentle-ai.telemetry-runtime-send/v1";
|
|
11
11
|
const MAX_METRIC = schema.$defs.count.maximum;
|
|
12
12
|
const count = (value: number) => Number.isSafeInteger(value) && value >= 0 && value <= MAX_METRIC;
|
|
13
|
+
// Re-derives through the same schema-driven normalizer used at record time so
|
|
14
|
+
// an already-classified (provider, id) pair is re-validated against the
|
|
15
|
+
// transport's anyOf shape before it ever leaves the machine.
|
|
13
16
|
function publicModel(provider: string, id: string) {
|
|
14
|
-
|
|
15
|
-
if (schema.$defs.model.oneOf.some(({ properties }) => matches(properties.provider, provider) && matches(properties.id, id))) return { provider, id };
|
|
16
|
-
const kind = !id || id === "unknown" ? "unknown" : "custom";
|
|
17
|
-
return { provider: kind, id: kind };
|
|
17
|
+
return normalizeRuntimeModel(provider, id);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** Closed field projection of one event, not a session snapshot. Oversized or
|
|
@@ -34,14 +34,22 @@ export function encodeNativeRuntimeEvent(source: readonly RuntimeMetricBucket[],
|
|
|
34
34
|
};
|
|
35
35
|
const agentClass = schema.$defs.row.properties.agent_class.enum.includes(row.agentClass) ? row.agentClass : "unknown";
|
|
36
36
|
const effort = (value: string) => schema.$defs.effort.enum.includes(value) ? value : "unavailable";
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Evidence order: an actually-dispatched response model wins; otherwise
|
|
38
|
+
// the caller's own selection; otherwise the SDK-observed model the
|
|
39
|
+
// selection never captured (still labeled "selected" evidence, since
|
|
40
|
+
// the transport has no separate "observed" category). Never response
|
|
41
|
+
// evidence unless the response model itself normalizes to a public id.
|
|
42
|
+
const response = isPublicRuntimeModelId(row.responseModelId);
|
|
43
|
+
const selected = !response && isPublicRuntimeModelId(row.selectedModelId);
|
|
44
|
+
const observed = !response && !selected && isPublicRuntimeModelId(row.observedModelId);
|
|
39
45
|
const model = response ? publicModel(row.provider, row.responseModelId)
|
|
40
|
-
: selected ? publicModel(row.selectedProvider, row.selectedModelId)
|
|
46
|
+
: selected ? publicModel(row.selectedProvider, row.selectedModelId)
|
|
47
|
+
: observed ? publicModel(row.provider, row.observedModelId)
|
|
48
|
+
: { provider: "unknown", id: "unknown" };
|
|
41
49
|
const time = row.fullResponseMs;
|
|
42
50
|
if (!count(time.measured) || !Number.isFinite(time.sum) || time.sum < 0 || time.sum > MAX_METRIC
|
|
43
51
|
|| (!time.measured && time.sum !== 0)) throw new Error("Invalid duration");
|
|
44
|
-
return { model, model_evidence: response ? "response" : selected ? "selected" : "unknown",
|
|
52
|
+
return { model, model_evidence: response ? "response" : (selected || observed) ? "selected" : "unknown",
|
|
45
53
|
agent_kind: agentClass === "orchestrator" || agentClass === "unknown" ? agentClass : "built_in", agent_class: agentClass,
|
|
46
54
|
selected_effort: effort(row.effort), effective_effort: effort(row.providerThinkingLevel),
|
|
47
55
|
launches: null, responses: row.responses,
|
package/lib/runtime-metrics.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { classifyPiCatalogName } from "./runtime-metrics-pi-identity.ts";
|
|
3
2
|
|
|
4
3
|
const runtimeSchema = JSON.parse(readFileSync(new URL("../contracts/telemetry/runtime-aggregate-v1.schema.json", import.meta.url), "utf8"));
|
|
5
4
|
|
|
@@ -7,7 +6,6 @@ const runtimeSchema = JSON.parse(readFileSync(new URL("../contracts/telemetry/ru
|
|
|
7
6
|
// Callers supply finalized assistant responses and authoritative classifications.
|
|
8
7
|
// Never infer executor, usage availability, or measured timings from SDK defaults.
|
|
9
8
|
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
9
|
// Stable families, not model IDs/versions: new models need no catalog update.
|
|
12
10
|
// Callers map known native metadata to families; private aliases stay custom.
|
|
13
11
|
const FAMILIES = ["claude", "gpt", "o-series", "gemini", "llama", "qwen", "deepseek", "kimi", "custom", "unknown"] as const;
|
|
@@ -42,38 +40,93 @@ function requiredAgentClass(value: string): AgentClass {
|
|
|
42
40
|
export const UNKNOWN_AGENT_CLASS = requiredAgentClass("unknown");
|
|
43
41
|
export const ORCHESTRATOR_AGENT_CLASS = requiredAgentClass("orchestrator");
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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]));
|
|
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`);
|
|
62
53
|
}
|
|
63
|
-
|
|
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 };
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
|
|
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");
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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.
|
|
71
74
|
*/
|
|
72
|
-
export function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
130
|
}
|
|
78
131
|
|
|
79
132
|
type Missing = { state: "unavailable" | "unsupported" };
|
|
@@ -84,20 +137,23 @@ export interface FinalResponse {
|
|
|
84
137
|
/** Local dedupe only: 1..128 UTF-16 code units; never exported. */
|
|
85
138
|
responseId: string;
|
|
86
139
|
/** Caller-observed selected SDK model ID, never dispatched/response identity.
|
|
87
|
-
*
|
|
140
|
+
* Normalized through normalizeRuntimeModel at record time; only an id
|
|
141
|
+
* matching the schema family pattern survives as a public name.
|
|
88
142
|
*/
|
|
89
143
|
selectedModelId?: string;
|
|
90
144
|
/** Selection namespace, independent from observed response provider.
|
|
91
145
|
* Omission preserves legacy same-provider callers; adapters must pass it explicitly.
|
|
92
146
|
*/
|
|
93
|
-
selectedProvider?:
|
|
147
|
+
selectedProvider?: string;
|
|
94
148
|
agentClass?: AgentClass;
|
|
95
|
-
/**
|
|
149
|
+
/** SDK-observed model id from response metadata, never endpoint proof.
|
|
150
|
+
* Normalized like selectedModelId; only a schema family match survives.
|
|
151
|
+
*/
|
|
96
152
|
observedModelId?: string;
|
|
97
153
|
responseModelId?: string;
|
|
98
154
|
providerThinkingLevel?: typeof EFFORTS[number];
|
|
99
155
|
executor: typeof EXECUTORS[number];
|
|
100
|
-
provider:
|
|
156
|
+
provider: string;
|
|
101
157
|
modelFamily: typeof FAMILIES[number];
|
|
102
158
|
effort: typeof EFFORTS[number];
|
|
103
159
|
error: typeof ERRORS[number];
|
|
@@ -195,17 +251,14 @@ export class RuntimeMetrics {
|
|
|
195
251
|
#buckets = new Map<string, RuntimeMetricBucket>();
|
|
196
252
|
#maxResponses: number;
|
|
197
253
|
#maxBuckets: number;
|
|
198
|
-
#classifyModel: typeof classifyPiCatalogName;
|
|
199
254
|
|
|
200
|
-
constructor({ maxResponses = 1024, maxBuckets = 64
|
|
201
|
-
{ maxResponses?: number; maxBuckets?: number; classifyModel?: typeof classifyPiCatalogName } = {}) {
|
|
255
|
+
constructor({ maxResponses = 1024, maxBuckets = 64 }: { maxResponses?: number; maxBuckets?: number } = {}) {
|
|
202
256
|
if (!Number.isInteger(maxResponses) || maxResponses < 1 || maxResponses > 1024
|
|
203
257
|
|| !Number.isInteger(maxBuckets) || maxBuckets < 1 || maxBuckets > 64) {
|
|
204
258
|
throw new RangeError("Invalid runtime metrics capacity");
|
|
205
259
|
}
|
|
206
260
|
this.#maxResponses = maxResponses;
|
|
207
261
|
this.#maxBuckets = maxBuckets;
|
|
208
|
-
this.#classifyModel = classifyModel;
|
|
209
262
|
}
|
|
210
263
|
|
|
211
264
|
record(response: FinalResponse): "recorded" | "duplicate" | "invalid" | "capacity" {
|
|
@@ -215,13 +268,13 @@ export class RuntimeMetrics {
|
|
|
215
268
|
const dimensions = {
|
|
216
269
|
hostAgent: "pi" as const,
|
|
217
270
|
agentClass: category(AGENT_CLASSES, response.agentClass, UNKNOWN_AGENT_CLASS),
|
|
218
|
-
observedModelId: classifyRuntimeModelId(response.provider, response.observedModelId
|
|
219
|
-
responseModelId: classifyRuntimeModelId(response.provider, response.responseModelId
|
|
271
|
+
observedModelId: classifyRuntimeModelId(response.provider, response.observedModelId),
|
|
272
|
+
responseModelId: classifyRuntimeModelId(response.provider, response.responseModelId),
|
|
220
273
|
providerThinkingLevel: category(EFFORTS, response.providerThinkingLevel, "unavailable"),
|
|
221
|
-
selectedModelId: classifyRuntimeModelId(selectedProvider, response.selectedModelId
|
|
222
|
-
selectedProvider:
|
|
274
|
+
selectedModelId: classifyRuntimeModelId(selectedProvider, response.selectedModelId),
|
|
275
|
+
selectedProvider: normalizeRuntimeProvider(selectedProvider),
|
|
223
276
|
executor: category(EXECUTORS, response.executor, "unknown"),
|
|
224
|
-
provider:
|
|
277
|
+
provider: normalizeRuntimeProvider(response.provider),
|
|
225
278
|
modelFamily: category(FAMILIES, response.modelFamily, typeof response.modelFamily === "string" && response.modelFamily ? "custom" : "unknown"),
|
|
226
279
|
effort: response.effort,
|
|
227
280
|
error: response.error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -967,6 +967,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
967
967
|
// so this row repeats 2.7.0. riskEvidence and hint remain dark because
|
|
968
968
|
// neither is proven to reach the negotiated START path Pi consumes.
|
|
969
969
|
"2.8.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
970
|
+
// v2.8.1 only changed runtime telemetry model attribution (gentle-ai#4536);
|
|
971
|
+
// the closed fields Pi consumes did not change between 2.8.0 and 2.8.1, so
|
|
972
|
+
// this row repeats 2.8.0 exactly. riskEvidence and hint remain dark
|
|
973
|
+
// because neither is proven to reach the negotiated START path Pi consumes.
|
|
974
|
+
"2.8.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
975
|
+
// v2.8.2 shipped OpenCode SDD preflight plugin fixes, community-tools RTK
|
|
976
|
+
// acquisition, and Claude Code Stop telemetry. The provider contract semver
|
|
977
|
+
// stays 1.2.0; the same pin re-mirrors bundle bytes that had drifted under
|
|
978
|
+
// that semver (lens inspection.status "unavailable", targeted-validator
|
|
979
|
+
// regressions/inspection members, seven Pi stop reason codes). None of
|
|
980
|
+
// those touch the closed START/STATUS fields this row negotiates, so it
|
|
981
|
+
// repeats 2.8.1 exactly. riskEvidence and hint remain dark because neither
|
|
982
|
+
// is proven to reach the negotiated START path Pi consumes.
|
|
983
|
+
"2.8.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
970
984
|
});
|
|
971
985
|
|
|
972
986
|
|