@gajae-code/coding-agent 0.17.6 → 0.17.7
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/CHANGELOG.md +34 -0
- package/dist/types/cli/public-command-errors.d.ts +2 -0
- package/dist/types/commands/sdk.d.ts +7 -1
- package/dist/types/config/model-profile-ownership.d.ts +74 -0
- package/dist/types/config/model-registry.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +4 -1
- package/dist/types/sdk/lifecycle/service.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +5 -1
- package/dist/types/sdk/session-directory.d.ts +1 -0
- package/dist/types/sdk/session-list.d.ts +11 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +18 -0
- package/dist/types/task/delegation-hint.d.ts +53 -0
- package/examples/README.md +3 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/hooks/README.md +1 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/src/cli/config-cli.ts +2 -0
- package/src/cli/public-command-errors.ts +6 -0
- package/src/commands/sdk.ts +361 -117
- package/src/config/model-profile-ownership.ts +303 -0
- package/src/config/model-registry.ts +53 -14
- package/src/config/settings-schema.ts +81 -0
- package/src/internal-urls/docs-index.generated.ts +18 -17
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/selector-controller.ts +3 -29
- package/src/sdk/broker/broker.ts +120 -23
- package/src/sdk/broker/lifecycle.ts +89 -13
- package/src/sdk/cli/session-cli.ts +45 -9
- package/src/sdk/host/control/dispatch.ts +6 -0
- package/src/sdk/lifecycle/service.ts +186 -3
- package/src/sdk/lifecycle-session.ts +35 -8
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/protocol/operation-registry.ts +5 -1
- package/src/sdk/session-directory.ts +1 -1
- package/src/sdk/session-list.ts +47 -0
- package/src/sdk/session.ts +290 -114
- package/src/sdk/startup-capability.ts +2 -1
- package/src/session/agent-session.ts +102 -4
- package/src/session/internal/managed-session-scope.ts +81 -17
- package/src/session/session-manager.ts +70 -11
- package/src/setup/model-onboarding-guidance.ts +26 -0
- package/src/task/delegation-hint.ts +578 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import * as util from "node:util";
|
|
2
|
+
import { resolveModelProfileName } from "./model-profile-contract";
|
|
3
|
+
import type { SettingsAtomicPatch } from "./settings";
|
|
4
|
+
|
|
5
|
+
const OWNERSHIP_PROJECTION_PATHS = new Set(["modelProfile.ownership", "modelProfile.default"]);
|
|
6
|
+
|
|
7
|
+
export type ModelProfileOwnershipMarker =
|
|
8
|
+
| { kind: "inherit" }
|
|
9
|
+
| { kind: "cleared" }
|
|
10
|
+
| { kind: "profile"; profile: string };
|
|
11
|
+
|
|
12
|
+
export interface DurableModelProfileOwnership {
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
version: number;
|
|
15
|
+
marker: ModelProfileOwnershipMarker;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ModelProfileOwnershipSettings {
|
|
19
|
+
getGlobal(path: "modelProfile.ownership" | "modelProfile.default"): unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DurableModelProfileOwnershipCommit {
|
|
23
|
+
ownership: DurableModelProfileOwnership;
|
|
24
|
+
wrote: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DurableModelProfileOwnershipStore extends ModelProfileOwnershipSettings {
|
|
28
|
+
commitAtomicBatchWithCurrent(
|
|
29
|
+
buildPatches: (
|
|
30
|
+
current: Readonly<Record<string, unknown>>,
|
|
31
|
+
) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
|
|
32
|
+
): Promise<unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const MODEL_PROFILE_OWNERSHIP_ENTRY = "model_profile_ownership";
|
|
36
|
+
|
|
37
|
+
export function validateModelProfileOwnershipMarker(value: unknown): ModelProfileOwnershipMarker | undefined {
|
|
38
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
39
|
+
const marker = value as Record<string, unknown>;
|
|
40
|
+
if (marker.kind === "inherit" || marker.kind === "cleared") {
|
|
41
|
+
return Object.keys(marker).length === 1 ? { kind: marker.kind } : undefined;
|
|
42
|
+
}
|
|
43
|
+
if (marker.kind === "profile" && typeof marker.profile === "string" && marker.profile.trim() !== "") {
|
|
44
|
+
return Object.keys(marker).length === 2 ? { kind: "profile", profile: marker.profile } : undefined;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateDurableModelProfileOwnership(value: unknown): DurableModelProfileOwnership | undefined {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
51
|
+
const record = value as Record<string, unknown>;
|
|
52
|
+
const marker = validateModelProfileOwnershipMarker(record.marker);
|
|
53
|
+
if (
|
|
54
|
+
record.schemaVersion !== 1 ||
|
|
55
|
+
!Number.isSafeInteger(record.version) ||
|
|
56
|
+
(record.version as number) < 0 ||
|
|
57
|
+
!marker ||
|
|
58
|
+
Object.keys(record).some(key => !["schemaVersion", "version", "marker"].includes(key))
|
|
59
|
+
) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return { schemaVersion: 1, version: record.version as number, marker };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function markersEqual(left: ModelProfileOwnershipMarker, right: ModelProfileOwnershipMarker): boolean {
|
|
66
|
+
if (left.kind !== right.kind) return false;
|
|
67
|
+
return left.kind !== "profile" || (right.kind === "profile" && left.profile === right.profile);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function rawValueAtPath(raw: Readonly<Record<string, unknown>>, path: string): unknown {
|
|
71
|
+
const dotted = raw[path];
|
|
72
|
+
if (dotted !== undefined) return dotted;
|
|
73
|
+
let current: unknown = raw;
|
|
74
|
+
for (const segment of path.split(".")) {
|
|
75
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return undefined;
|
|
76
|
+
current = (current as Record<string, unknown>)[segment];
|
|
77
|
+
}
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extraPatchesAlreadyApplied(
|
|
82
|
+
current: Readonly<Record<string, unknown>>,
|
|
83
|
+
patches: readonly SettingsAtomicPatch[],
|
|
84
|
+
): boolean {
|
|
85
|
+
return patches.every(patch => {
|
|
86
|
+
const actual = rawValueAtPath(current, patch.path);
|
|
87
|
+
return patch.op === "unset" ? actual === undefined : util.isDeepStrictEqual(actual, patch.value);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertLegacyDefaultMatchesOwnership(marker: ModelProfileOwnershipMarker, legacyProfile: unknown): void {
|
|
92
|
+
const expected = marker.kind === "profile" ? marker.profile : undefined;
|
|
93
|
+
if (legacyProfile !== expected) throw new InvalidModelProfileOwnershipError();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class InvalidModelProfileOwnershipError extends Error {
|
|
97
|
+
readonly code = "model_profile_ownership_invalid";
|
|
98
|
+
|
|
99
|
+
constructor() {
|
|
100
|
+
super("Persisted model-profile ownership state is invalid; refusing to infer an owner.");
|
|
101
|
+
this.name = "InvalidModelProfileOwnershipError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class UnresolvedModelProfileOwnershipError extends Error {
|
|
106
|
+
readonly code = "model_profile_unresolved";
|
|
107
|
+
|
|
108
|
+
constructor(readonly profileName: string) {
|
|
109
|
+
super(
|
|
110
|
+
`Model profile "${profileName}" is referenced by this session but no longer exists. Choose a replacement profile explicitly.`,
|
|
111
|
+
);
|
|
112
|
+
this.name = "UnresolvedModelProfileOwnershipError";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class ModelProfileReplacementRequiredError extends Error {
|
|
117
|
+
readonly code = "model_profile_replacement_required";
|
|
118
|
+
|
|
119
|
+
constructor(readonly profileName: string) {
|
|
120
|
+
super(`Choose a replacement profile before deleting the active profile "${profileName}".`);
|
|
121
|
+
this.name = "ModelProfileReplacementRequiredError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class ModelProfileOwnershipConflictError extends Error {
|
|
126
|
+
readonly code = "model_profile_ownership_conflict";
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
readonly expectedVersion: number,
|
|
130
|
+
readonly actualVersion: number,
|
|
131
|
+
readonly actualMarker?: ModelProfileOwnershipMarker,
|
|
132
|
+
) {
|
|
133
|
+
super(
|
|
134
|
+
`Model-profile ownership changed concurrently (expected version ${expectedVersion}, found ${actualVersion}).`,
|
|
135
|
+
);
|
|
136
|
+
this.name = "ModelProfileOwnershipConflictError";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class ModelProfileApplyCommittedError extends Error {
|
|
141
|
+
readonly code = "model_profile_apply_committed";
|
|
142
|
+
|
|
143
|
+
constructor(
|
|
144
|
+
readonly profileName: string,
|
|
145
|
+
readonly committedVersion: number,
|
|
146
|
+
cause: unknown,
|
|
147
|
+
) {
|
|
148
|
+
super(
|
|
149
|
+
`Model-profile ownership for "${profileName}" was durably committed at version ${committedVersion}, but could not be applied. The requesting session is fail-closed; the durable version was not compensated.`,
|
|
150
|
+
{ cause },
|
|
151
|
+
);
|
|
152
|
+
this.name = "ModelProfileApplyCommittedError";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ProfileOwnershipChangedEvent {
|
|
157
|
+
type: "profile_ownership_changed";
|
|
158
|
+
transitionId: string;
|
|
159
|
+
source: "session" | "durable" | "recovery";
|
|
160
|
+
oldMarker: ModelProfileOwnershipMarker;
|
|
161
|
+
newMarker: ModelProfileOwnershipMarker;
|
|
162
|
+
oldSessionId: string;
|
|
163
|
+
sessionId: string;
|
|
164
|
+
observedDurableVersion: number;
|
|
165
|
+
committedDurableVersion?: number;
|
|
166
|
+
outcome: "committed" | "failed" | "reconciled";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function readDurableModelProfileOwnership(
|
|
170
|
+
settings: ModelProfileOwnershipSettings,
|
|
171
|
+
): DurableModelProfileOwnership {
|
|
172
|
+
const configured = settings.getGlobal("modelProfile.ownership");
|
|
173
|
+
if (configured !== undefined) {
|
|
174
|
+
const validated = validateDurableModelProfileOwnership(configured);
|
|
175
|
+
if (!validated) throw new InvalidModelProfileOwnershipError();
|
|
176
|
+
assertLegacyDefaultMatchesOwnership(validated.marker, settings.getGlobal("modelProfile.default"));
|
|
177
|
+
return validated;
|
|
178
|
+
}
|
|
179
|
+
const legacyProfile = settings.getGlobal("modelProfile.default");
|
|
180
|
+
return {
|
|
181
|
+
schemaVersion: 1,
|
|
182
|
+
version: 0,
|
|
183
|
+
marker:
|
|
184
|
+
typeof legacyProfile === "string" && legacyProfile.trim() !== ""
|
|
185
|
+
? { kind: "profile", profile: legacyProfile }
|
|
186
|
+
: { kind: "inherit" },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function readDurableModelProfileOwnershipFromRaw(
|
|
191
|
+
raw: Readonly<Record<string, unknown>>,
|
|
192
|
+
): DurableModelProfileOwnership {
|
|
193
|
+
const configured = rawValueAtPath(raw, "modelProfile.ownership");
|
|
194
|
+
if (configured !== undefined) {
|
|
195
|
+
const validated = validateDurableModelProfileOwnership(configured);
|
|
196
|
+
if (!validated) throw new InvalidModelProfileOwnershipError();
|
|
197
|
+
assertLegacyDefaultMatchesOwnership(validated.marker, rawValueAtPath(raw, "modelProfile.default"));
|
|
198
|
+
return validated;
|
|
199
|
+
}
|
|
200
|
+
const legacyProfile = rawValueAtPath(raw, "modelProfile.default");
|
|
201
|
+
return {
|
|
202
|
+
schemaVersion: 1,
|
|
203
|
+
version: 0,
|
|
204
|
+
marker:
|
|
205
|
+
typeof legacyProfile === "string" && legacyProfile.trim() !== ""
|
|
206
|
+
? { kind: "profile", profile: legacyProfile }
|
|
207
|
+
: { kind: "inherit" },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveEffectiveModelProfileMarker(
|
|
212
|
+
sessionMarker: ModelProfileOwnershipMarker | undefined,
|
|
213
|
+
durable: DurableModelProfileOwnership,
|
|
214
|
+
): ModelProfileOwnershipMarker {
|
|
215
|
+
const session = sessionMarker ?? { kind: "inherit" as const };
|
|
216
|
+
return session.kind === "inherit" ? durable.marker : session;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resolveOwnedModelProfileName(
|
|
220
|
+
marker: ModelProfileOwnershipMarker,
|
|
221
|
+
profiles: ReadonlyMap<string, unknown>,
|
|
222
|
+
): string | undefined {
|
|
223
|
+
if (marker.kind !== "profile") return undefined;
|
|
224
|
+
const resolvedName = resolveModelProfileName(marker.profile, profiles);
|
|
225
|
+
if (!profiles.has(resolvedName)) throw new UnresolvedModelProfileOwnershipError(marker.profile);
|
|
226
|
+
return resolvedName;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function nextDurableModelProfileOwnership(
|
|
230
|
+
current: DurableModelProfileOwnership,
|
|
231
|
+
marker: ModelProfileOwnershipMarker,
|
|
232
|
+
): DurableModelProfileOwnership {
|
|
233
|
+
if (current.version === Number.MAX_SAFE_INTEGER)
|
|
234
|
+
throw new RangeError("Model-profile ownership version is exhausted.");
|
|
235
|
+
return { schemaVersion: 1, version: current.version + 1, marker };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function commitDurableModelProfileOwnershipWithResult(
|
|
239
|
+
settings: DurableModelProfileOwnershipStore,
|
|
240
|
+
marker: ModelProfileOwnershipMarker,
|
|
241
|
+
extraPatches: readonly SettingsAtomicPatch[] = [],
|
|
242
|
+
validateCurrent?: () => Promise<void> | void,
|
|
243
|
+
observedOwnership?: DurableModelProfileOwnership,
|
|
244
|
+
): Promise<DurableModelProfileOwnershipCommit> {
|
|
245
|
+
const requestedMarker = validateModelProfileOwnershipMarker(marker);
|
|
246
|
+
if (!requestedMarker) throw new InvalidModelProfileOwnershipError();
|
|
247
|
+
if (extraPatches.some(patch => OWNERSHIP_PROJECTION_PATHS.has(patch.path))) {
|
|
248
|
+
throw new Error("Durable ownership extra patches cannot target modelProfile.ownership or modelProfile.default.");
|
|
249
|
+
}
|
|
250
|
+
const expected = observedOwnership ?? readDurableModelProfileOwnership(settings);
|
|
251
|
+
let result = expected;
|
|
252
|
+
let wrote = false;
|
|
253
|
+
await settings.commitAtomicBatchWithCurrent(async current => {
|
|
254
|
+
const actual = readDurableModelProfileOwnershipFromRaw(current);
|
|
255
|
+
if (actual.version !== expected.version || !markersEqual(actual.marker, expected.marker)) {
|
|
256
|
+
throw new ModelProfileOwnershipConflictError(expected.version, actual.version, actual.marker);
|
|
257
|
+
}
|
|
258
|
+
await validateCurrent?.();
|
|
259
|
+
if (
|
|
260
|
+
actual.version > 0 &&
|
|
261
|
+
markersEqual(actual.marker, requestedMarker) &&
|
|
262
|
+
extraPatchesAlreadyApplied(current, extraPatches)
|
|
263
|
+
) {
|
|
264
|
+
result = actual;
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
result = nextDurableModelProfileOwnership(actual, requestedMarker);
|
|
268
|
+
wrote = true;
|
|
269
|
+
return [
|
|
270
|
+
{ path: "modelProfile.ownership", op: "set", value: result },
|
|
271
|
+
requestedMarker.kind === "profile"
|
|
272
|
+
? { path: "modelProfile.default", op: "set", value: requestedMarker.profile }
|
|
273
|
+
: { path: "modelProfile.default", op: "unset" },
|
|
274
|
+
...extraPatches,
|
|
275
|
+
];
|
|
276
|
+
});
|
|
277
|
+
return { ownership: result, wrote };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function commitDurableModelProfileOwnership(
|
|
281
|
+
settings: DurableModelProfileOwnershipStore,
|
|
282
|
+
marker: ModelProfileOwnershipMarker,
|
|
283
|
+
extraPatches: readonly SettingsAtomicPatch[] = [],
|
|
284
|
+
validateCurrent?: () => Promise<void> | void,
|
|
285
|
+
observedOwnership?: DurableModelProfileOwnership,
|
|
286
|
+
): Promise<DurableModelProfileOwnership> {
|
|
287
|
+
return (
|
|
288
|
+
await commitDurableModelProfileOwnershipWithResult(
|
|
289
|
+
settings,
|
|
290
|
+
marker,
|
|
291
|
+
extraPatches,
|
|
292
|
+
validateCurrent,
|
|
293
|
+
observedOwnership,
|
|
294
|
+
)
|
|
295
|
+
).ownership;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function modelProfileOwnershipMarkersEqual(
|
|
299
|
+
left: ModelProfileOwnershipMarker | undefined,
|
|
300
|
+
right: ModelProfileOwnershipMarker | undefined,
|
|
301
|
+
): boolean {
|
|
302
|
+
return markersEqual(left ?? { kind: "inherit" }, right ?? { kind: "inherit" });
|
|
303
|
+
}
|
|
@@ -87,6 +87,11 @@ import {
|
|
|
87
87
|
refreshModelPresetRegistry,
|
|
88
88
|
refreshModelPresetRegistryInBackground,
|
|
89
89
|
} from "./model-preset-registry";
|
|
90
|
+
import {
|
|
91
|
+
ModelProfileReplacementRequiredError,
|
|
92
|
+
readDurableModelProfileOwnershipFromRaw,
|
|
93
|
+
UnresolvedModelProfileOwnershipError,
|
|
94
|
+
} from "./model-profile-ownership";
|
|
90
95
|
|
|
91
96
|
export type { ProviderDiscoveryState, ProviderDiscoveryStatus } from "./model-discovery-manager";
|
|
92
97
|
|
|
@@ -1747,6 +1752,7 @@ export class ModelRegistry {
|
|
|
1747
1752
|
#configError: ConfigError | undefined = undefined;
|
|
1748
1753
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
1749
1754
|
#settings: Pick<Settings, "get" | "getGlobal">;
|
|
1755
|
+
#ownershipSettings: Pick<Settings, "commitAtomicBatchWithCurrent"> | undefined;
|
|
1750
1756
|
readonly #authStorageConfigOwner: object = {};
|
|
1751
1757
|
#disposeAuthStorageFallbackResolver: (() => void) | undefined;
|
|
1752
1758
|
#lastStaticLoadMtime: number | null = null;
|
|
@@ -1795,10 +1801,15 @@ export class ModelRegistry {
|
|
|
1795
1801
|
constructor(
|
|
1796
1802
|
readonly authStorage: AuthStorage,
|
|
1797
1803
|
modelsPath?: string,
|
|
1798
|
-
registrySettings?: Pick<Settings, "get" | "getGlobal"
|
|
1804
|
+
registrySettings?: Pick<Settings, "get" | "getGlobal"> & Partial<Pick<Settings, "commitAtomicBatchWithCurrent">>,
|
|
1799
1805
|
modelPresetRegistryDependencies: ModelPresetRegistryDependencies = {},
|
|
1800
1806
|
) {
|
|
1801
1807
|
this.#settings = registrySettings ?? settings;
|
|
1808
|
+
this.#ownershipSettings = registrySettings
|
|
1809
|
+
? typeof registrySettings.commitAtomicBatchWithCurrent === "function"
|
|
1810
|
+
? (registrySettings as Pick<Settings, "commitAtomicBatchWithCurrent">)
|
|
1811
|
+
: undefined
|
|
1812
|
+
: settings;
|
|
1802
1813
|
const configuredAgentDir = path.resolve(modelPresetRegistryDependencies.agentDir ?? getAgentDir());
|
|
1803
1814
|
this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
|
|
1804
1815
|
this.#modelPresetRegistryAgentDir = modelPresetRegistryDependencies.agentDir
|
|
@@ -3005,6 +3016,18 @@ export class ModelRegistry {
|
|
|
3005
3016
|
return new Map(this.#modelProfiles);
|
|
3006
3017
|
}
|
|
3007
3018
|
|
|
3019
|
+
assertCurrentModelProfileExists(name: string): void {
|
|
3020
|
+
const loaded = this.#modelsConfigFile.tryLoad();
|
|
3021
|
+
if (loaded.status === "error") {
|
|
3022
|
+
throw new Error(
|
|
3023
|
+
`Cannot validate durable model-profile ownership because ${this.#modelsConfigFile.path()} is invalid.`,
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
if (mergeModelProfiles(loaded.value?.profiles).has(name)) return;
|
|
3027
|
+
if (this.#modelProfiles.get(name)?.source === "registry") return;
|
|
3028
|
+
throw new UnresolvedModelProfileOwnershipError(name);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3008
3031
|
getModelProfile(name: string): ModelProfileDefinition | undefined {
|
|
3009
3032
|
return this.#modelProfiles.get(name);
|
|
3010
3033
|
}
|
|
@@ -3090,20 +3113,36 @@ export class ModelRegistry {
|
|
|
3090
3113
|
async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
|
|
3091
3114
|
const normalizedName = name.trim();
|
|
3092
3115
|
if (!normalizedName) throw new Error("Profile name is required.");
|
|
3093
|
-
const
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3116
|
+
const ownershipSettings = this.#ownershipSettings;
|
|
3117
|
+
if (!ownershipSettings) {
|
|
3118
|
+
throw new Error("Profile deletion requires a settings store with ownership transaction support.");
|
|
3119
|
+
}
|
|
3120
|
+
let deletedProfile: ModelProfileConfig | undefined;
|
|
3121
|
+
await ownershipSettings.commitAtomicBatchWithCurrent(async currentSettings => {
|
|
3122
|
+
const durableOwnership = readDurableModelProfileOwnershipFromRaw(currentSettings);
|
|
3123
|
+
if (durableOwnership.marker.kind === "profile" && durableOwnership.marker.profile === normalizedName) {
|
|
3124
|
+
throw new ModelProfileReplacementRequiredError(normalizedName);
|
|
3125
|
+
}
|
|
3126
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
|
|
3127
|
+
const nextProfiles = { ...(current.profiles ?? {}) };
|
|
3128
|
+
delete nextProfiles[normalizedName];
|
|
3129
|
+
const checkedConfig = ModelsConfigSchema.safeParse({
|
|
3130
|
+
...current,
|
|
3131
|
+
profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
|
|
3132
|
+
});
|
|
3133
|
+
if (!checkedConfig.success) {
|
|
3134
|
+
const first = checkedConfig.error.issues[0];
|
|
3135
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
3136
|
+
throw new Error(
|
|
3137
|
+
`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`,
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
3141
|
+
deletedProfile = profile;
|
|
3142
|
+
return [];
|
|
3099
3143
|
});
|
|
3100
|
-
if (!
|
|
3101
|
-
|
|
3102
|
-
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
3103
|
-
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
3104
|
-
}
|
|
3105
|
-
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
3106
|
-
return profile;
|
|
3144
|
+
if (!deletedProfile) throw new Error(`Model profile "${normalizedName}" was not deleted.`);
|
|
3145
|
+
return deletedProfile;
|
|
3107
3146
|
}
|
|
3108
3147
|
|
|
3109
3148
|
#loadCustomProfileForMutation(
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
validateAutoroutingProvenance,
|
|
17
17
|
validateAutoroutingSetup,
|
|
18
18
|
} from "./autorouting-contract";
|
|
19
|
+
import { type DurableModelProfileOwnership, validateDurableModelProfileOwnership } from "./model-profile-ownership";
|
|
19
20
|
import type { ModelSelectorValue } from "./model-selector-value";
|
|
20
21
|
import { UPDATE_CHANNELS } from "./update-channel";
|
|
21
22
|
|
|
@@ -369,6 +370,42 @@ const AUTOROUTING_PROVENANCE_JSON_SCHEMA: JsonSchemaObject = {
|
|
|
369
370
|
required: ["schema", "source", "declarationFingerprint", "tiersFingerprint"],
|
|
370
371
|
};
|
|
371
372
|
|
|
373
|
+
const MODEL_PROFILE_OWNERSHIP_JSON_SCHEMA: JsonSchemaObject = {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
schemaVersion: { type: "integer", const: 1 },
|
|
377
|
+
version: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
|
378
|
+
marker: {
|
|
379
|
+
type: "object",
|
|
380
|
+
oneOf: [
|
|
381
|
+
{
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: { kind: { type: "string", const: "inherit" } },
|
|
384
|
+
required: ["kind"],
|
|
385
|
+
additionalProperties: false,
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: { kind: { type: "string", const: "cleared" } },
|
|
390
|
+
required: ["kind"],
|
|
391
|
+
additionalProperties: false,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
type: "object",
|
|
395
|
+
properties: {
|
|
396
|
+
kind: { type: "string", const: "profile" },
|
|
397
|
+
profile: { type: "string", minLength: 1 },
|
|
398
|
+
},
|
|
399
|
+
required: ["kind", "profile"],
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
required: ["schemaVersion", "version", "marker"],
|
|
406
|
+
additionalProperties: false,
|
|
407
|
+
};
|
|
408
|
+
|
|
372
409
|
// SDK prompt deadline defaults. The `sdk.promptDeadlineMs` /
|
|
373
410
|
// `sdk.promptMaxRuntimeMs` schema entries below and the SDK bus/host lease
|
|
374
411
|
// fallbacks read these same constants, so a Settings lookup that misses — no
|
|
@@ -684,6 +721,23 @@ export const SETTINGS_SCHEMA = {
|
|
|
684
721
|
options: "runtime",
|
|
685
722
|
},
|
|
686
723
|
},
|
|
724
|
+
"modelProfile.ownership": {
|
|
725
|
+
type: "optional-object",
|
|
726
|
+
default: undefined,
|
|
727
|
+
jsonSchema: MODEL_PROFILE_OWNERSHIP_JSON_SCHEMA,
|
|
728
|
+
validate: (value: unknown) =>
|
|
729
|
+
validateDurableModelProfileOwnership(value)
|
|
730
|
+
? []
|
|
731
|
+
: [
|
|
732
|
+
{
|
|
733
|
+
path: "modelProfile.ownership",
|
|
734
|
+
code: "config_invalid",
|
|
735
|
+
reason: "config_invalid",
|
|
736
|
+
detail: "Expected a versioned ownership record with a valid inherit, cleared, or profile marker.",
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
_value: undefined as unknown as DurableModelProfileOwnership,
|
|
740
|
+
},
|
|
687
741
|
"modelProfile.proxyProvider": {
|
|
688
742
|
type: "string",
|
|
689
743
|
default: undefined,
|
|
@@ -3636,6 +3690,25 @@ export const SETTINGS_SCHEMA = {
|
|
|
3636
3690
|
"Encourage the agent to delegate work to subagents unless changes are trivial (on by default when executor/planner run on a different provider than the default role)",
|
|
3637
3691
|
},
|
|
3638
3692
|
},
|
|
3693
|
+
"task.delegationHint.mode": {
|
|
3694
|
+
type: "enum",
|
|
3695
|
+
values: ["off", "hint"] as const,
|
|
3696
|
+
default: "off",
|
|
3697
|
+
ui: {
|
|
3698
|
+
tab: "tasks",
|
|
3699
|
+
label: "Delegation Decision Hint",
|
|
3700
|
+
description:
|
|
3701
|
+
"Opt in to transient local-only Kev advice after observable edit activity. Notices never alter prompts, task arguments, or saved sessions.",
|
|
3702
|
+
options: [
|
|
3703
|
+
{ value: "off", label: "Off", description: "Do not request decision hints (default)" },
|
|
3704
|
+
{
|
|
3705
|
+
value: "hint",
|
|
3706
|
+
label: "Hint",
|
|
3707
|
+
description: "Show ephemeral user-facing advice from the local Kev service",
|
|
3708
|
+
},
|
|
3709
|
+
],
|
|
3710
|
+
},
|
|
3711
|
+
},
|
|
3639
3712
|
|
|
3640
3713
|
"task.simple": {
|
|
3641
3714
|
type: "enum",
|
|
@@ -4303,6 +4376,14 @@ export function validateSettingPatch(patch: Record<string, unknown>): Array<{ pa
|
|
|
4303
4376
|
const issues: Array<{ path: string; detail: string }> = [];
|
|
4304
4377
|
const knownPaths = new Set(Object.keys(SETTINGS_SCHEMA));
|
|
4305
4378
|
for (const [path, value] of Object.entries(patch)) {
|
|
4379
|
+
// Keep the legacy projection writable until every durable writer uses ownership CAS.
|
|
4380
|
+
if (path === "modelProfile.ownership") {
|
|
4381
|
+
issues.push({
|
|
4382
|
+
path,
|
|
4383
|
+
detail: "The versioned model-profile ownership record is internal and cannot be patched generically.",
|
|
4384
|
+
});
|
|
4385
|
+
continue;
|
|
4386
|
+
}
|
|
4306
4387
|
const definition = SETTINGS_SCHEMA[path as SettingPath];
|
|
4307
4388
|
if (!definition) {
|
|
4308
4389
|
const recordParent = [...knownPaths].find(known => known !== path && path.startsWith(`${known}.`));
|