gentle-pi 4.1.0 → 4.3.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 +352 -128
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +443 -8
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +131 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
package/lib/selection-engine.ts
CHANGED
|
@@ -131,7 +131,10 @@ export function withReverseSpan(row: string, startCu: number, endCu: number): st
|
|
|
131
131
|
// A nested SGR reset (e.g. the cursor block's own, when the cursor
|
|
132
132
|
// sits inside the span) clears reverse for everything after it:
|
|
133
133
|
// re-arm reverse right after it.
|
|
134
|
-
|
|
134
|
+
// Pi 1.2 resolves the APC fake-cursor end to SGR 27 later in
|
|
135
|
+
// the host TUI. Preserve the marker for hardware-cursor suppression,
|
|
136
|
+
// but rearm our selection after it just as after a legacy reset.
|
|
137
|
+
if (opened && cu < endCu && (isSgrReset(seq) || seq === "\x1b_pi:/fc\x07")) out += "\x1b[7m";
|
|
135
138
|
i += seq.length;
|
|
136
139
|
continue;
|
|
137
140
|
}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// The live resolution applies only in `follow` mode (GENTLE_PI_PROFILE_FOLLOW=1,
|
|
17
17
|
// for CI and headless runs), in subagent children (their routing comes from the
|
|
18
18
|
// parent through --model), and when no session id is available. In `follow`
|
|
19
|
-
// mode the orchestrator still
|
|
20
|
-
//
|
|
19
|
+
// mode the orchestrator is still chosen at startup while children follow later
|
|
20
|
+
// defaults: an accepted, documented limitation.
|
|
21
21
|
//
|
|
22
22
|
// The frozen state is in-process only, like the explicit binding of slice 2: it
|
|
23
23
|
// lives on `globalThis` behind a `Symbol.for` key because Pi evaluates this
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
// `gentle-pi.session-profile/v1` entry with origin `local | repo | global` is
|
|
28
28
|
// the integration point with slice 3b-i.
|
|
29
29
|
import { createHash } from "node:crypto";
|
|
30
|
+
import { normalizeModelConfig, type AgentModelConfig } from "./model-routing-authority.ts";
|
|
30
31
|
import { profileRoleEntries, profilesFilePath, readProfilesFileResult } from "./agent-profiles.ts";
|
|
31
32
|
import { readProfilePinStatus, resolveProfilePin, resolveUnversionedProjectProfile } from "./agent-profile-pin.ts";
|
|
32
|
-
import type { AgentModelConfig } from "./model-routing-authority.ts";
|
|
33
33
|
import { readSessionProfileBinding } from "./session-profile-binding.ts";
|
|
34
34
|
import type { WorktreeIdentity, WorktreeResolver } from "./session-worktree-registry.ts";
|
|
35
35
|
|
|
@@ -114,19 +114,14 @@ function isChildSession(env: NodeJS.ProcessEnv): boolean {
|
|
|
114
114
|
* is never part of the routing. `undefined` means no layer has a profile.
|
|
115
115
|
*/
|
|
116
116
|
export function resolveInheritedProfile(options: InheritedProfileOptions): InheritedProfile | undefined {
|
|
117
|
-
// One worktree lookup serves both the pin layers and the non-Git check
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return identity;
|
|
126
|
-
};
|
|
127
|
-
const pin = resolveProfilePin({ cwd: options.cwd, configHome: options.configHome, resolveWorktree });
|
|
128
|
-
if (pin !== undefined) return { name: pin.profile, origin: pin.source, modelProfiles: pin.modelProfiles };
|
|
129
|
-
if (readProfilePinStatus(options.cwd, resolveWorktree) === undefined) {
|
|
117
|
+
// One worktree lookup serves both the pin layers and the non-Git check,
|
|
118
|
+
// whether the caller passes a resolver or the pin module's default runs.
|
|
119
|
+
const status = readProfilePinStatus(options.cwd, options.resolveWorktree);
|
|
120
|
+
if (status !== undefined) {
|
|
121
|
+
const identity: WorktreeIdentity = { root: status.root, commonDir: status.commonDir };
|
|
122
|
+
const pin = resolveProfilePin({ cwd: options.cwd, configHome: options.configHome, resolveWorktree: () => identity });
|
|
123
|
+
if (pin !== undefined) return { name: pin.profile, origin: pin.source, modelProfiles: pin.modelProfiles };
|
|
124
|
+
} else {
|
|
130
125
|
const declaration = resolveUnversionedProjectProfile(options.cwd, options.configHome);
|
|
131
126
|
if (declaration !== undefined) return { name: declaration.profile, origin: "repo", modelProfiles: declaration.modelProfiles };
|
|
132
127
|
}
|
|
@@ -247,6 +242,7 @@ function describe(profile: InheritedProfile | undefined): string {
|
|
|
247
242
|
*/
|
|
248
243
|
export function inheritedProfileDriftNotice(options: SessionProfileOptions): string | undefined {
|
|
249
244
|
const resolution = resolveSessionProfile(options);
|
|
245
|
+
if (resolution.kind === "explicit") return bindingDefinitionDriftNotice(options, resolution);
|
|
250
246
|
if (resolution.kind !== "inherited") return undefined;
|
|
251
247
|
const current = resolveInheritedProfile(options);
|
|
252
248
|
const key = fingerprint(current);
|
|
@@ -259,6 +255,34 @@ export function inheritedProfileDriftNotice(options: SessionProfileOptions): str
|
|
|
259
255
|
return `el Gentleman: the default profile changed to ${describe(current)}, this session keeps ${describe(resolution.profile)}. Press Enter on a profile in /gentle:profiles to adopt it.`;
|
|
260
256
|
}
|
|
261
257
|
|
|
258
|
+
// gentle-shell#2228: an explicit binding re-copies the profile definition at
|
|
259
|
+
// selection, so later edits to that entry in profiles.json no-op silently until
|
|
260
|
+
// the operator re-Enters. The same once-per-change fingerprint machine as the
|
|
261
|
+
// inherited notice names the drift; the comparison never adopts the edit.
|
|
262
|
+
function bindingDefinitionDriftNotice(
|
|
263
|
+
options: SessionProfileOptions,
|
|
264
|
+
resolution: { kind: "explicit"; name: string; modelProfiles: AgentModelConfig },
|
|
265
|
+
): string | undefined {
|
|
266
|
+
const file = readProfilesFileResult(profilesFilePath(options.configHome));
|
|
267
|
+
if (file.status !== "valid") return undefined;
|
|
268
|
+
const definition = file.file.profiles[resolution.name];
|
|
269
|
+
if (definition === undefined) return undefined;
|
|
270
|
+
// Both sides normalize through the store's own route normalizer: a bound
|
|
271
|
+
// snapshot taken from the panel holds normalized routes, while raw string
|
|
272
|
+
// forms from other callers must hash equal to the same definition.
|
|
273
|
+
const normalized = (config: AgentModelConfig) =>
|
|
274
|
+
Object.fromEntries(Object.entries(config).map(([agent, route]) => [agent, normalizeModelConfig(route)]));
|
|
275
|
+
const key = createHash("sha256").update(stableJson({ modelProfiles: normalized(definition) })).digest("hex");
|
|
276
|
+
const current = createHash("sha256").update(stableJson({ modelProfiles: normalized(resolution.modelProfiles) })).digest("hex");
|
|
277
|
+
if (key === current) return undefined;
|
|
278
|
+
const notified = registry().notified;
|
|
279
|
+
const seen = notified.get(options.sessionId!) ?? new Set<string>();
|
|
280
|
+
if (seen.has(key)) return undefined;
|
|
281
|
+
seen.add(key);
|
|
282
|
+
notified.set(options.sessionId!, seen);
|
|
283
|
+
return `el Gentleman: the profile definition changed to "${resolution.name}" (profiles.json), this session keeps the binding from selection. Press Enter on a profile in /gentle:profiles to adopt it.`;
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
/** Testing-only reset of every frozen snapshot and drift memory. */
|
|
263
287
|
export function resetFrozenInheritedProfilesForTesting(): void {
|
|
264
288
|
registry().frozen.clear();
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Standalone v1 codec and pure active-branch replay (gentle-shell#1064).
|
|
2
|
+
// No Pi API, disk access, shared binding store, or orchestrator application.
|
|
3
|
+
import { isValidProfileName } from "./agent-profiles.ts";
|
|
4
|
+
import {
|
|
5
|
+
isSafeAgentName,
|
|
6
|
+
isThinkingLevel,
|
|
7
|
+
normalizeModelId,
|
|
8
|
+
normalizeRoutingEntry,
|
|
9
|
+
type AgentModelConfig,
|
|
10
|
+
type AgentRoutingEntry,
|
|
11
|
+
} from "./model-routing-authority.ts";
|
|
12
|
+
|
|
13
|
+
export const SESSION_PROFILE_CUSTOM_TYPE = "gentle-pi.session-profile/v1";
|
|
14
|
+
const SESSION_PROFILE_FAMILY = "gentle-pi.session-profile/";
|
|
15
|
+
|
|
16
|
+
export type SessionProfileOrigin = "user" | "local" | "repo" | "global";
|
|
17
|
+
|
|
18
|
+
export interface SessionProfileBindPayload {
|
|
19
|
+
kind: "bind";
|
|
20
|
+
origin: SessionProfileOrigin;
|
|
21
|
+
name: string;
|
|
22
|
+
modelProfiles: AgentModelConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SessionProfileClearPayload {
|
|
26
|
+
kind: "clear";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Public structural entry shape only; ordinary messages need no custom fields. */
|
|
30
|
+
export interface SessionProfileEntry {
|
|
31
|
+
type: string;
|
|
32
|
+
customType?: string;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type SessionProfileReadResult =
|
|
37
|
+
| { status: "absent" }
|
|
38
|
+
| { status: "bound"; binding: SessionProfileBindPayload }
|
|
39
|
+
| { status: "cleared" }
|
|
40
|
+
| { status: "invalid" }
|
|
41
|
+
| { status: "unsupported" };
|
|
42
|
+
|
|
43
|
+
export type SessionProfileReplayResult =
|
|
44
|
+
| { status: "absent" }
|
|
45
|
+
| (Exclude<SessionProfileReadResult, { status: "absent" }> & {
|
|
46
|
+
entryIndex: number;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
54
|
+
return Object.hasOwn(value, key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isOrigin(value: unknown): value is SessionProfileOrigin {
|
|
58
|
+
return (
|
|
59
|
+
value === "user" ||
|
|
60
|
+
value === "local" ||
|
|
61
|
+
value === "repo" ||
|
|
62
|
+
value === "global"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Validate known fields before the permissive routing helper can drop them. */
|
|
67
|
+
function readRoute(
|
|
68
|
+
value: unknown,
|
|
69
|
+
encoderInput = false,
|
|
70
|
+
): AgentRoutingEntry | undefined {
|
|
71
|
+
if (typeof value === "string") return normalizeRoutingEntry(value);
|
|
72
|
+
if (!isRecord(value)) return undefined;
|
|
73
|
+
// Typed normalized routes can own optional undefined fields. Omit only those
|
|
74
|
+
// encoder fields; persisted fields and legacy effort retain strict validation.
|
|
75
|
+
const route = encoderInput
|
|
76
|
+
? Object.fromEntries(
|
|
77
|
+
Object.entries(value).filter(
|
|
78
|
+
([key, field]) =>
|
|
79
|
+
!((key === "model" || key === "thinking") && field === undefined),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
: value;
|
|
83
|
+
if (hasOwn(route, "model") && normalizeModelId(route.model) === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (hasOwn(route, "thinking") && !isThinkingLevel(route.thinking))
|
|
86
|
+
return undefined;
|
|
87
|
+
if (hasOwn(route, "effort") && !isThinkingLevel(route.effort))
|
|
88
|
+
return undefined;
|
|
89
|
+
// Project only own known fields. Unknown extras, even an unknown-only route,
|
|
90
|
+
// carry no routing meaning. Thinking wins over legacy effort as elsewhere.
|
|
91
|
+
const known = Object.fromEntries(
|
|
92
|
+
["model", "thinking", "effort"]
|
|
93
|
+
.filter((key) => hasOwn(route, key))
|
|
94
|
+
.map((key) => [key, route[key]]),
|
|
95
|
+
);
|
|
96
|
+
const normalized = normalizeRoutingEntry(known);
|
|
97
|
+
if (!normalized) return undefined;
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(normalized).filter(([, field]) => field !== undefined),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readSnapshot(
|
|
104
|
+
value: unknown,
|
|
105
|
+
encoderInput = false,
|
|
106
|
+
): AgentModelConfig | undefined {
|
|
107
|
+
if (!isRecord(value)) return undefined;
|
|
108
|
+
const entries: Array<[string, AgentRoutingEntry]> = [];
|
|
109
|
+
for (const [name, rawRoute] of Object.entries(value)) {
|
|
110
|
+
// Shared agent-name policy, applied here without the forgiving whole-config
|
|
111
|
+
// normalization that would silently discard a bad route. Output below uses
|
|
112
|
+
// own data properties, never prototype setters.
|
|
113
|
+
if (!isSafeAgentName(name)) return undefined;
|
|
114
|
+
const route = readRoute(rawRoute, encoderInput);
|
|
115
|
+
if (route === undefined) return undefined;
|
|
116
|
+
entries.push([name, route]);
|
|
117
|
+
}
|
|
118
|
+
return Object.fromEntries(entries);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readBind(
|
|
122
|
+
data: Record<string, unknown>,
|
|
123
|
+
encoderInput = false,
|
|
124
|
+
): SessionProfileBindPayload | undefined {
|
|
125
|
+
if (!["origin", "name", "modelProfiles"].every((key) => hasOwn(data, key)))
|
|
126
|
+
return undefined;
|
|
127
|
+
if (!isOrigin(data.origin) || !isValidProfileName(data.name)) return undefined;
|
|
128
|
+
const modelProfiles = readSnapshot(data.modelProfiles, encoderInput);
|
|
129
|
+
if (modelProfiles === undefined) return undefined;
|
|
130
|
+
return { kind: "bind", origin: data.origin, name: data.name, modelProfiles };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Create a detached user selection payload; never appends or claims durability. */
|
|
134
|
+
export function createSessionProfileBind(
|
|
135
|
+
name: string,
|
|
136
|
+
modelProfiles: AgentModelConfig,
|
|
137
|
+
): SessionProfileBindPayload {
|
|
138
|
+
const binding = readBind({ origin: "user", name, modelProfiles }, true);
|
|
139
|
+
if (!binding) throw new TypeError("Invalid session profile binding");
|
|
140
|
+
return binding;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createSessionProfileClear(): SessionProfileClearPayload {
|
|
144
|
+
return { kind: "clear" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Unknown family identifiers are terminal unsupported states, regardless of data. */
|
|
148
|
+
export function readSessionProfileEntry(
|
|
149
|
+
entry: SessionProfileEntry,
|
|
150
|
+
): SessionProfileReadResult {
|
|
151
|
+
if (
|
|
152
|
+
entry.type !== "custom" ||
|
|
153
|
+
typeof entry.customType !== "string" ||
|
|
154
|
+
!entry.customType.startsWith(SESSION_PROFILE_FAMILY)
|
|
155
|
+
)
|
|
156
|
+
return { status: "absent" };
|
|
157
|
+
if (entry.customType !== SESSION_PROFILE_CUSTOM_TYPE)
|
|
158
|
+
return { status: "unsupported" };
|
|
159
|
+
const data = entry.data;
|
|
160
|
+
if (!isRecord(data) || !hasOwn(data, "kind")) return { status: "invalid" };
|
|
161
|
+
if (data.kind === "clear") return { status: "cleared" };
|
|
162
|
+
if (data.kind !== "bind") return { status: "invalid" };
|
|
163
|
+
const binding = readBind(data);
|
|
164
|
+
return binding ? { status: "bound", binding } : { status: "invalid" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Caller MUST supply ALREADY disk-corroborated active-branch entries, in branch
|
|
169
|
+
* order (oldest to newest). This function cannot establish that precondition:
|
|
170
|
+
* it does not read disk, call Pi, enforce append-failure recovery, establish
|
|
171
|
+
* durability, or update the shared map. In-memory getBranch() alone is NOT
|
|
172
|
+
* corroboration. Sibling and future paths must be excluded by the caller.
|
|
173
|
+
*
|
|
174
|
+
* The newest family entry wins even when invalid or unsupported; never revive
|
|
175
|
+
* an older binding. Cleared permits current fallback at a later consumer,
|
|
176
|
+
* whereas invalid/unsupported must remain distinct from absent. entryIndex is
|
|
177
|
+
* the actual supplied branch offset for a later adapter's location warning.
|
|
178
|
+
*/
|
|
179
|
+
export function replaySessionProfileBranch(
|
|
180
|
+
entries: readonly SessionProfileEntry[],
|
|
181
|
+
): SessionProfileReplayResult {
|
|
182
|
+
for (let entryIndex = entries.length - 1; entryIndex >= 0; entryIndex--) {
|
|
183
|
+
const result = readSessionProfileEntry(entries[entryIndex]);
|
|
184
|
+
if (result.status !== "absent") return { ...result, entryIndex };
|
|
185
|
+
}
|
|
186
|
+
return { status: "absent" };
|
|
187
|
+
}
|