@dsh-enhanced/assistant-skills 0.1.31 → 0.1.34
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 +56 -9
- package/lib/definition.d.ts +53 -2
- package/lib/definition.d.ts.map +1 -1
- package/lib/definition.js +96 -0
- package/lib/definition.js.map +1 -1
- package/lib/delegated-arm.d.ts +47 -0
- package/lib/delegated-arm.d.ts.map +1 -0
- package/lib/delegated-arm.js +26 -0
- package/lib/delegated-arm.js.map +1 -0
- package/lib/external-holdout.js +1 -1
- package/lib/external-holdout.js.map +1 -1
- package/lib/holdout-authority.d.ts +1 -122
- package/lib/holdout-authority.d.ts.map +1 -1
- package/lib/holdout-authority.js +5 -262
- package/lib/holdout-authority.js.map +1 -1
- package/lib/holdout-cli.d.ts.map +1 -1
- package/lib/holdout-cli.js +2 -0
- package/lib/holdout-cli.js.map +1 -1
- package/lib/holdout-qualification.d.ts +11 -3
- package/lib/holdout-qualification.d.ts.map +1 -1
- package/lib/holdout-qualification.js +19 -9
- package/lib/holdout-qualification.js.map +1 -1
- package/lib/index.d.ts +10 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/prospective-holdout.d.ts +1 -34
- package/lib/prospective-holdout.d.ts.map +1 -1
- package/lib/prospective-holdout.js +4 -253
- package/lib/prospective-holdout.js.map +1 -1
- package/lib/repair-agent.d.ts +11 -2
- package/lib/repair-agent.d.ts.map +1 -1
- package/lib/repair-agent.js +178 -17
- package/lib/repair-agent.js.map +1 -1
- package/lib/repair-process.d.ts +20 -0
- package/lib/repair-process.d.ts.map +1 -0
- package/lib/repair-process.js +92 -0
- package/lib/repair-process.js.map +1 -0
- package/lib/repair-profile.d.ts +6 -2
- package/lib/repair-profile.d.ts.map +1 -1
- package/lib/repair-profile.js +10 -4
- package/lib/repair-profile.js.map +1 -1
- package/lib/repair-runtime.d.ts +2 -0
- package/lib/repair-runtime.d.ts.map +1 -1
- package/lib/repair-runtime.js +7 -1
- package/lib/repair-runtime.js.map +1 -1
- package/lib/repair-workspace-path.d.ts +17 -0
- package/lib/repair-workspace-path.d.ts.map +1 -0
- package/lib/repair-workspace-path.js +84 -0
- package/lib/repair-workspace-path.js.map +1 -0
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +17 -2
- package/lib/replay.js.map +1 -1
- package/lib/service.d.ts +271 -2
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +473 -35
- package/lib/service.js.map +1 -1
- package/lib/store.d.ts +47 -0
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +262 -6
- package/lib/store.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/watch-proof.js +2 -2
- package/lib/watch-proof.js.map +1 -1
- package/package.json +8 -8
package/lib/service.js
CHANGED
|
@@ -5,7 +5,7 @@ import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
|
|
|
5
5
|
import Schema from '@deepseek-ai/schemastery';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
-
import { createDefinition, fileObservationSteps, instantiate } from './definition.js';
|
|
8
|
+
import { compensationDirective, createDefinition, evaluatePreconditions, fileObservationSteps, instantiate } from './definition.js';
|
|
9
9
|
import { captureFailureCandidateProvenance, captureRunExpansions } from './capture-expansion.js';
|
|
10
10
|
import { validateComparisonProfiles, SkillComparator } from './comparison.js';
|
|
11
11
|
import { watchBindingCurrent, watchObservation, watchObservationResult, watchObservationRevision } from './watch-proof.js';
|
|
@@ -18,6 +18,7 @@ import { RepairContinuationRuntime } from './repair-runtime.js';
|
|
|
18
18
|
import { OwnerRepairAgentRuntime } from './repair-agent.js';
|
|
19
19
|
import { enqueueRepairFeedback } from './repair-feedback.js';
|
|
20
20
|
import { SkillStore } from './store.js';
|
|
21
|
+
import { awaitBenchmarkSignal, benchmarkArmDefinition, sameBenchmarkArm } from './delegated-arm.js';
|
|
21
22
|
export const Config = Schema.object({
|
|
22
23
|
databasePath: Schema.string().default(join(homedir(), '.dsh', 'assistant-skills.sqlite')),
|
|
23
24
|
allowedTools: Schema.array(Schema.string()).default(['read', 'write', 'edit']),
|
|
@@ -53,6 +54,14 @@ function exactArguments(value, allowed) {
|
|
|
53
54
|
return undefined;
|
|
54
55
|
return args;
|
|
55
56
|
}
|
|
57
|
+
function repairModelRoute(value) {
|
|
58
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
59
|
+
return false;
|
|
60
|
+
const route = value;
|
|
61
|
+
return typeof route.provider === 'string' && route.provider.length > 0 && route.provider.length <= 256 && !route.provider.includes('\0')
|
|
62
|
+
&& typeof route.model === 'string' && route.model.length > 0 && route.model.length <= 256 && !route.model.includes('\0')
|
|
63
|
+
&& (route.reasoningEffort === undefined || typeof route.reasoningEffort === 'string' && route.reasoningEffort.length > 0 && route.reasoningEffort.length <= 256 && !route.reasoningEffort.includes('\0'));
|
|
64
|
+
}
|
|
56
65
|
function failureLocator(value) {
|
|
57
66
|
const locator = plainArguments(value);
|
|
58
67
|
return locator !== undefined && Object.keys(locator).length === 2 && Object.hasOwn(locator, 'sessionId') && Object.hasOwn(locator, 'goalId')
|
|
@@ -91,6 +100,37 @@ function failureWindow(input) {
|
|
|
91
100
|
.sort((left, right) => compare(left.sessionId, right.sessionId) || compare(left.goalId, right.goalId));
|
|
92
101
|
return { failures: Object.freeze(canonical.map(locator => Object.freeze(locator))), minimumOccurrences: minimumOccurrences };
|
|
93
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Bounded window of model-suggested owner-root success locators for one
|
|
105
|
+
* autonomous growth deposit. The locator shape ({sessionId,goalId}) is
|
|
106
|
+
* identical to a failure locator; only its meaning differs. Discovery only —
|
|
107
|
+
* every locator still has to pass an independent owner verified source read.
|
|
108
|
+
*/
|
|
109
|
+
function successWindow(input) {
|
|
110
|
+
const raw = input.successLocators;
|
|
111
|
+
const minimumOccurrences = input.minimumOccurrences ?? 1;
|
|
112
|
+
if (!Array.isArray(raw) || Object.getPrototypeOf(raw) !== Array.prototype || raw.length < 1 || raw.length > 32
|
|
113
|
+
|| Object.getOwnPropertySymbols(raw).length !== 0 || Reflect.ownKeys(raw).length !== raw.length + 1
|
|
114
|
+
|| !Number.isSafeInteger(minimumOccurrences) || minimumOccurrences < 1 || minimumOccurrences > 32
|
|
115
|
+
|| minimumOccurrences > raw.length) {
|
|
116
|
+
throw new Error('assistant-skills: invalid bounded success locator window');
|
|
117
|
+
}
|
|
118
|
+
const descriptors = Object.getOwnPropertyDescriptors(raw), successes = [];
|
|
119
|
+
for (let index = 0; index < raw.length; index++) {
|
|
120
|
+
const descriptor = descriptors[String(index)];
|
|
121
|
+
if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor) || !failureLocator(descriptor.value)) {
|
|
122
|
+
throw new Error('assistant-skills: invalid bounded success locator window');
|
|
123
|
+
}
|
|
124
|
+
successes.push(descriptor.value);
|
|
125
|
+
}
|
|
126
|
+
const identities = successes.map(locator => acceptanceDigest(locator));
|
|
127
|
+
if (new Set(identities).size !== identities.length)
|
|
128
|
+
throw new Error('assistant-skills: duplicate success locator');
|
|
129
|
+
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
130
|
+
const canonical = successes.map(locator => ({ sessionId: locator.sessionId, goalId: locator.goalId }))
|
|
131
|
+
.sort((left, right) => compare(left.sessionId, right.sessionId) || compare(left.goalId, right.goalId));
|
|
132
|
+
return { successes: Object.freeze(canonical.map(locator => Object.freeze(locator))), minimumOccurrences: minimumOccurrences };
|
|
133
|
+
}
|
|
94
134
|
function publicDefinition(definition) {
|
|
95
135
|
const source = { digest: acceptanceDigest(definition.source), goalDefinitionDigest: definition.source.goal.definition.digest, stepCount: definition.source.steps.length,
|
|
96
136
|
failedObservationCount: definition.source.failedObservations?.length ?? 0, segmentCount: definition.source.segments?.length ?? 0 };
|
|
@@ -188,6 +228,17 @@ function trialProofSteps(steps, candidateId, trialRunId, goalId, invocationId, i
|
|
|
188
228
|
/** Fixed tool compositions run in the original native Goal; no new AgentLoop or scheduler. */
|
|
189
229
|
export class AssistantSkillsService extends Service {
|
|
190
230
|
static Config = Config;
|
|
231
|
+
static { Object.defineProperty(this, 'name', { value: 'dsh-enhanced-assistant-skills' }); }
|
|
232
|
+
static #benchmarkCapabilities = new WeakMap();
|
|
233
|
+
static #benchmarkRecipients = new WeakMap();
|
|
234
|
+
#benchmarkIdentity = Object.freeze({});
|
|
235
|
+
/** A Host identity only; it carries no permission and survives fresh Cordis trace proxies. */
|
|
236
|
+
benchmarkRecipientIdentity = () => this.#benchmarkIdentity;
|
|
237
|
+
#issuedBenchmarkArms = new Set();
|
|
238
|
+
#issuedBenchmarkCells = new Set();
|
|
239
|
+
#mountedBenchmarkArms = new Map();
|
|
240
|
+
#benchmarkRuns = new Set();
|
|
241
|
+
#benchmarkGeneration = 0;
|
|
191
242
|
#store;
|
|
192
243
|
#allowed;
|
|
193
244
|
#duration;
|
|
@@ -226,18 +277,37 @@ export class AssistantSkillsService extends Service {
|
|
|
226
277
|
this.#externalHoldouts = validateExternalHoldoutProfiles(config.externalHoldouts ?? []);
|
|
227
278
|
this.#repairProfiles = validateRepairProfiles(config.repairProfiles ?? [], this.#externalHoldouts);
|
|
228
279
|
this.#store = new SkillStore(config.databasePath ?? join(homedir(), '.dsh', 'assistant-skills.sqlite'));
|
|
229
|
-
|
|
280
|
+
AssistantSkillsService.#benchmarkRecipients.set(this.#benchmarkIdentity, this);
|
|
281
|
+
ctx.effect(() => async () => { this.#active = false; this.#lifecycle.abort(); this.#mountedBenchmarkArms.clear(); await Promise.allSettled(this.#benchmarkRuns); await Promise.all([...this.#repairStops].map(stop => stop())); await Promise.allSettled(this.#comparing); await Promise.allSettled(this.#captureTasks); await Promise.allSettled([...this.#comparators.values()].map(async (value) => (await value).close())); this.#store.close(); }, 'assistant-skills.store');
|
|
282
|
+
ctx.inject(['assistantGoals', 'assistantPolicy', 'assistantDelivery'], runtime => {
|
|
283
|
+
this.#benchmarkGeneration++;
|
|
284
|
+
runtime.effect(() => () => {
|
|
285
|
+
this.#benchmarkGeneration++;
|
|
286
|
+
for (const controller of this.#issuedBenchmarkArms)
|
|
287
|
+
controller.abort();
|
|
288
|
+
this.#issuedBenchmarkArms.clear();
|
|
289
|
+
for (const arm of this.#mountedBenchmarkArms.values())
|
|
290
|
+
arm.revoke();
|
|
291
|
+
this.#mountedBenchmarkArms.clear();
|
|
292
|
+
}, 'assistant-skills.benchmark-service-generation');
|
|
293
|
+
});
|
|
230
294
|
if (this.#repairProfiles.length > 0)
|
|
231
295
|
this.#installRepairRuntime(ctx);
|
|
232
296
|
ctx.inject(['tools', 'agents', 'assistantGoals', 'assistantPolicy', 'assistantDelivery'], runtime => {
|
|
233
297
|
runtime.tools.register(defineTool({ name: 'skill_save', description: 'Save the exact successful tool trace of this owner session’s independently achieved Goal as a private versioned skill. Requires the current human request. Historical acceptance is provenance, never permission or acceptance for a future run.',
|
|
234
298
|
parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, name: { type: 'string', required: true }, description: { type: 'string', required: true }, bindings_json: { type: 'string', description: 'JSON array of {name,stepId,path}; path is a scalar argument JSON pointer. Empty array keeps the original arguments.' }, expected_version: { type: 'integer' } }, output,
|
|
235
299
|
execute: async (args, exec) => ({ context: JSON.stringify(this.save(exec.agent, args.goal_id, { name: args.name, description: args.description, bindings: parse(args.bindings_json ?? '[]', true) }, args.expected_version ?? 0)) }) }));
|
|
236
|
-
|
|
300
|
+
const delegatedRun = defineTool({ name: 'skill_run', description: 'Replay a saved skill’s fixed tool steps in a fresh native Goal. If the current owner Goal is active but has no admitted native round, this returns awaiting-native-round with no steps or durable run, ends the turn, and the next native round must repeat the same invocation_id. Every nested call retains native permissions, approvals, cancellation and budgets. Tool success still requires fresh independent Goal acceptance.',
|
|
237
301
|
parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, name: { type: 'string', required: true }, version: { type: 'integer', required: true }, inputs_json: { type: 'string', description: 'JSON object with only declared typed input parameters.' }, invocation_id: { type: 'string', required: true } }, output,
|
|
238
|
-
execute: async (args, exec) => ({ context: JSON.stringify(await this.run(exec, args.goal_id, args.name, args.version, parse(args.inputs_json ?? '{}'), args.invocation_id)) }) })
|
|
239
|
-
runtime.tools.register(
|
|
240
|
-
|
|
302
|
+
execute: async (args, exec) => ({ context: JSON.stringify(await this.run(exec, args.goal_id, args.name, args.version, parse(args.inputs_json ?? '{}'), args.invocation_id)) }) });
|
|
303
|
+
runtime.tools.register(delegatedRun);
|
|
304
|
+
if (runtime.assistantPolicy.delegatedSkillToolPreauthorizationVersion?.() === 1)
|
|
305
|
+
runtime.assistantPolicy.registerPreauthorizedTool(runtime, delegatedRun, execution => this.#preauthorizeBenchmarkTool(execution));
|
|
306
|
+
const delegatedStatus = defineTool({ name: 'skill_status', description: 'Read this owner’s active saved skill definitions, typed inputs and source acceptance, or inspect a specific durable invocation. Success means steps executed, not Goal achievement.', parameters: { run_id: { type: 'string' } }, output,
|
|
307
|
+
execute: async (args, exec) => ({ context: JSON.stringify(this.inspect(exec.agent, args.run_id)) }) });
|
|
308
|
+
runtime.tools.register(delegatedStatus);
|
|
309
|
+
if (runtime.assistantPolicy.delegatedSkillToolPreauthorizationVersion?.() === 1)
|
|
310
|
+
runtime.assistantPolicy.registerPreauthorizedTool(runtime, delegatedStatus, execution => this.#preauthorizeBenchmarkTool(execution));
|
|
241
311
|
runtime.tools.register(defineTool({ name: 'skill_retire', description: 'Retire the current saved skill version following the current authenticated owner request. Pending steps recheck retirement; past effects remain and require explicit repair.', parameters: { name: { type: 'string', required: true }, expected_version: { type: 'integer', required: true } }, output,
|
|
242
312
|
execute: async (args, exec) => ({ context: JSON.stringify(this.retire(exec.agent, args.name, args.expected_version)) }) }));
|
|
243
313
|
});
|
|
@@ -515,9 +585,39 @@ export class AssistantSkillsService extends Service {
|
|
|
515
585
|
#capturedDefinition(source, options, scope) {
|
|
516
586
|
return createDefinition(source, options, this.#allowed, captureRunExpansions(source, scope, this.#store));
|
|
517
587
|
}
|
|
588
|
+
/** Only a live delegated, isolation-only workflow may cross Isolation's tool gate. */
|
|
589
|
+
#preauthorizeBenchmarkTool(execution) {
|
|
590
|
+
try {
|
|
591
|
+
if (execution.signal.aborted || !['skill_run', 'skill_status'].includes(execution.name))
|
|
592
|
+
return false;
|
|
593
|
+
const scope = this.#scope(execution.agent, execution.name === 'skill_run' ? 'run' : 'inspect');
|
|
594
|
+
const mounted = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
|
|
595
|
+
if (!mounted || mounted.skill.steps.some(step => step.toolName !== 'isolation_run'))
|
|
596
|
+
return false;
|
|
597
|
+
mounted.assertCurrent();
|
|
598
|
+
const args = plainArguments(execution.arguments);
|
|
599
|
+
if (!args)
|
|
600
|
+
return false;
|
|
601
|
+
if (execution.name === 'skill_status')
|
|
602
|
+
return Object.keys(args).every(key => key === 'run_id') && (args.run_id === undefined
|
|
603
|
+
|| typeof args.run_id === 'string' && this.#store.getRun(scope, args.run_id)?.delegationDigest === mounted.bindingDigest);
|
|
604
|
+
return Object.keys(args).length === 5 && typeof args.goal_id === 'string' && typeof args.inputs_json === 'string' && typeof args.invocation_id === 'string'
|
|
605
|
+
&& args.name === mounted.skill.name && args.version === mounted.skill.version;
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
518
611
|
inspect(agent, runId) {
|
|
519
612
|
const scope = this.#scope(agent, 'inspect');
|
|
520
|
-
|
|
613
|
+
if (runId)
|
|
614
|
+
return this.#store.getRun(scope, runId);
|
|
615
|
+
const mounted = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
|
|
616
|
+
if (mounted) {
|
|
617
|
+
mounted.assertCurrent();
|
|
618
|
+
return [{ ...structuredClone(mounted.skill), delegation: structuredClone(mounted.binding), delegationDigest: mounted.bindingDigest }];
|
|
619
|
+
}
|
|
620
|
+
return this.#store.list(scope);
|
|
521
621
|
}
|
|
522
622
|
retire(agent, name, expectedVersion) {
|
|
523
623
|
const scope = this.#scope(agent, 'retire');
|
|
@@ -938,6 +1038,107 @@ export class AssistantSkillsService extends Service {
|
|
|
938
1038
|
stageOwnerAuthorizedFailureCandidate = (exec, input, authority) => {
|
|
939
1039
|
return this.stageFailureCandidate(exec, input, authority);
|
|
940
1040
|
};
|
|
1041
|
+
/**
|
|
1042
|
+
* Host-only autonomous SUCCESS deposit for the growth driver. Unlike the
|
|
1043
|
+
* repair/failure path it is deliberately NOT anchored to a durable repair
|
|
1044
|
+
* continuation: a successful goal creates none. Instead the caller supplies
|
|
1045
|
+
* a narrow, short-lived OwnerSuccessCandidateAuthority minted from a frozen
|
|
1046
|
+
* configured owner scope, and this method:
|
|
1047
|
+
* 1. validates the authority (live background agent, exact scope, unexpired,
|
|
1048
|
+
* caller assertCurrent lease) and re-anchors the authenticated owner route
|
|
1049
|
+
* through delivery.validateOwnerRoute — never trusting a model-supplied id;
|
|
1050
|
+
* 2. independently re-reads EVERY model-suggested locator through
|
|
1051
|
+
* goals.inspectOwnerVerifiedWorkflowSource (owner-root, succeeded,
|
|
1052
|
+
* quiescent, double-read), counting DISTINCT (sessionId,goalId);
|
|
1053
|
+
* 3. stages one PENDING candidate only when the distinct verified-success
|
|
1054
|
+
* count meets minimumOccurrences. It never writes a current version, so
|
|
1055
|
+
* skill_run cannot reach it; activation stays behind the existing owner
|
|
1056
|
+
* gates. A retired same-name skill or any version conflict fails closed.
|
|
1057
|
+
*/
|
|
1058
|
+
async stageOwnerVerifiedSuccessCandidate(exec, rawInput, authority) {
|
|
1059
|
+
// --- authority + exact owner scope (no repair continuation is consulted) ---
|
|
1060
|
+
exec.signal.throwIfAborted();
|
|
1061
|
+
this.#lifecycle.signal.throwIfAborted();
|
|
1062
|
+
if (!this.#active || !exec.agent || this.ctx.get('agents')?.get(exec.agent.id) !== exec.agent
|
|
1063
|
+
|| !Number.isSafeInteger(authority.expiresAt) || authority.expiresAt <= Date.now()
|
|
1064
|
+
|| exec.agent.session.header.cwd !== authority.scope.workspace
|
|
1065
|
+
|| exec.agent.session.header.agentPreset !== authority.scope.preset) {
|
|
1066
|
+
throw new Error('assistant-skills: growth success authority unavailable');
|
|
1067
|
+
}
|
|
1068
|
+
authority.assertCurrent();
|
|
1069
|
+
const scope = { principalId: authority.scope.principalId, principalRecordId: authority.scope.principalRecordId,
|
|
1070
|
+
principalVersion: authority.scope.principalVersion, workspace: authority.scope.workspace, preset: authority.scope.preset };
|
|
1071
|
+
const route = this.#watchRoute(scope, authority.ownerRouteId);
|
|
1072
|
+
if (rawInput === null || typeof rawInput !== 'object' || rawInput.ownerRouteId !== authority.ownerRouteId) {
|
|
1073
|
+
throw new Error('assistant-skills: growth success input does not match authority');
|
|
1074
|
+
}
|
|
1075
|
+
const window = successWindow(rawInput);
|
|
1076
|
+
const goalsApi = this.#goals();
|
|
1077
|
+
if (typeof goalsApi.inspectOwnerVerifiedWorkflowSource !== 'function') {
|
|
1078
|
+
throw new Error('assistant-skills: upgrade assistant-goals to use growth success deposit');
|
|
1079
|
+
}
|
|
1080
|
+
const signal = AbortSignal.any([exec.signal, this.#lifecycle.signal]);
|
|
1081
|
+
// --- independent Host re-read of every model-suggested locator ---
|
|
1082
|
+
const sources = await Promise.all(window.successes.map(async (locator) => {
|
|
1083
|
+
const source = await goalsApi.inspectOwnerVerifiedWorkflowSource({ ownerRouteId: authority.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset,
|
|
1084
|
+
sessionId: locator.sessionId, goalId: locator.goalId }, signal);
|
|
1085
|
+
// The attestation must be for THIS exact owner scope, not a sibling route.
|
|
1086
|
+
if (acceptanceDigest(source.scope) !== acceptanceDigest(scope)) {
|
|
1087
|
+
throw new Error('assistant-skills: verified success source owner mismatch');
|
|
1088
|
+
}
|
|
1089
|
+
return { locator, source };
|
|
1090
|
+
}));
|
|
1091
|
+
// Distinct owner-root successes: different native Session AND business goal.
|
|
1092
|
+
// The embedded source carries no cross-goal signature, so the repeat gate is
|
|
1093
|
+
// a count of independently verified locators, never a model claim.
|
|
1094
|
+
const distinct = new Map();
|
|
1095
|
+
for (const item of sources) {
|
|
1096
|
+
const sessionId = item.source.goal.sessionId;
|
|
1097
|
+
if (sessionId !== item.locator.sessionId || item.source.goal.id !== item.locator.goalId) {
|
|
1098
|
+
throw new Error('assistant-skills: verified success locator mismatch');
|
|
1099
|
+
}
|
|
1100
|
+
distinct.set(`${sessionId}${item.locator.goalId}`, { sessionId, goalId: item.locator.goalId, source: item.source });
|
|
1101
|
+
}
|
|
1102
|
+
if (distinct.size < window.minimumOccurrences) {
|
|
1103
|
+
throw new Error(`assistant-skills: only ${distinct.size} distinct verified successes; ${window.minimumOccurrences} required`);
|
|
1104
|
+
}
|
|
1105
|
+
// --- resolve the current parent; a retired name blocks any new deposit ---
|
|
1106
|
+
const options = { name: rawInput.name, description: rawInput.description, ...(rawInput.bindings === undefined ? {} : { bindings: rawInput.bindings }) };
|
|
1107
|
+
const activeParent = this.#store.get(scope, options.name);
|
|
1108
|
+
const parentVersion = activeParent?.version ?? 0;
|
|
1109
|
+
// Build the definition from ONE representative verified source; provenance
|
|
1110
|
+
// below binds the full distinct-success set so the repeat evidence is durable.
|
|
1111
|
+
const representative = [...distinct.values()].sort((left, right) => left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1
|
|
1112
|
+
: left.goalId < right.goalId ? -1 : left.goalId > right.goalId ? 1 : 0)[0];
|
|
1113
|
+
const definition = this.#capturedDefinition(representative.source, options, scope);
|
|
1114
|
+
// --- re-validate authority/route right before the write ---
|
|
1115
|
+
authority.assertCurrent();
|
|
1116
|
+
this.#watchRoute(scope, authority.ownerRouteId, route);
|
|
1117
|
+
const provenance = Object.freeze({
|
|
1118
|
+
protocol: 'assistant-skills/growth-success-provenance/v1',
|
|
1119
|
+
ownerRouteId: authority.ownerRouteId,
|
|
1120
|
+
distinctSuccessCount: distinct.size,
|
|
1121
|
+
requiredCount: window.minimumOccurrences,
|
|
1122
|
+
successes: [...distinct.values()]
|
|
1123
|
+
.map(value => ({ sessionId: value.sessionId, goalId: value.goalId,
|
|
1124
|
+
sourceDigest: acceptanceDigest(value.source), goalDefinitionDigest: value.source.goal.definition.digest,
|
|
1125
|
+
runId: value.source.runId, acceptanceContractId: value.source.acceptance.contractId }))
|
|
1126
|
+
.sort((left, right) => left.sourceDigest < right.sourceDigest ? -1 : left.sourceDigest > right.sourceDigest ? 1 : 0),
|
|
1127
|
+
});
|
|
1128
|
+
this.#authorize(exec.agent, 'draft', [scope, definition, parentVersion, acceptanceDigest(provenance)]);
|
|
1129
|
+
// stageCandidate enforces retired/version conflict and pending idempotency.
|
|
1130
|
+
// The distinct-success set is not stored in the failure-provenance slot
|
|
1131
|
+
// (that shape is repair-specific); its digest is folded into trigger, which
|
|
1132
|
+
// is part of the durable candidate identity formula, making the repeat
|
|
1133
|
+
// evidence tamper-evident without a schema migration.
|
|
1134
|
+
const provenanceDigest = acceptanceDigest(provenance);
|
|
1135
|
+
const reason = `Host-verified autonomous growth deposit after ${distinct.size} independently owner-verified repeated successes.`;
|
|
1136
|
+
const trigger = `growth-success:${distinct.size}:${provenanceDigest}`;
|
|
1137
|
+
const candidate = this.#store.stageCandidate(scope, definition, {
|
|
1138
|
+
expectedVersion: parentVersion, reason, trigger, expiresAt: Date.now() + this.#candidateTtl,
|
|
1139
|
+
});
|
|
1140
|
+
return this.#preview(scope, candidate);
|
|
1141
|
+
}
|
|
941
1142
|
#preview(scope, candidate) {
|
|
942
1143
|
const parent = candidate.parentVersion ? this.#store.get(scope, candidate.definition.name, candidate.parentVersion) : undefined;
|
|
943
1144
|
return publicCandidate(candidate, parent);
|
|
@@ -949,6 +1150,142 @@ export class AssistantSkillsService extends Service {
|
|
|
949
1150
|
const candidate = this.#store.getCandidate(scope, id);
|
|
950
1151
|
return candidate ? this.#preview(scope, candidate) : null;
|
|
951
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Host-only read of the active current skills of a delivery-anchored owner
|
|
1155
|
+
* scope. The caller MUST first establish the scope through
|
|
1156
|
+
* delivery.validateOwnerRoute (exact owner route); this enumeration consumes
|
|
1157
|
+
* no live owner turn and exists for bounded background growth agents. It is
|
|
1158
|
+
* discovery-only and cannot write.
|
|
1159
|
+
*/
|
|
1160
|
+
inspectOwnerActiveSkills(scope) {
|
|
1161
|
+
if (!this.#active)
|
|
1162
|
+
throw new Error('assistant-skills: inactive');
|
|
1163
|
+
return this.#store.list(scope).map(publicDefinition);
|
|
1164
|
+
}
|
|
1165
|
+
/** Host-only read of every skill candidate of a delivery-anchored owner scope. */
|
|
1166
|
+
inspectOwnerSkillCandidates(scope) {
|
|
1167
|
+
if (!this.#active)
|
|
1168
|
+
throw new Error('assistant-skills: inactive');
|
|
1169
|
+
return this.#store.listCandidates(scope).map(candidate => this.#preview(scope, candidate));
|
|
1170
|
+
}
|
|
1171
|
+
#benchmarkSource(selection) {
|
|
1172
|
+
if (!this.#active)
|
|
1173
|
+
throw new Error('assistant-skills: benchmark source inactive');
|
|
1174
|
+
this.#watchRoute(selection.scope, selection.ownerRouteId);
|
|
1175
|
+
const policy = this.ctx.get('assistantPolicy', false);
|
|
1176
|
+
if (policy?.evaluate({ ...this.#watchPolicy(selection.scope, 'watch'), action: 'compare' }).effect !== 'allow')
|
|
1177
|
+
throw new Error('assistant-skills: benchmark comparison policy denied');
|
|
1178
|
+
const candidate = selection.candidateId === undefined ? undefined : this.#pending(selection.scope, selection.candidateId);
|
|
1179
|
+
const selected = candidate?.definition ?? this.#store.get(selection.scope, selection.skillName);
|
|
1180
|
+
const version = candidate ? candidate.parentVersion + 1 : selected?.version;
|
|
1181
|
+
if (!selected || selected.name !== selection.skillName || version !== selection.version
|
|
1182
|
+
|| acceptanceDigest(selected.source.scope) !== acceptanceDigest(selection.scope))
|
|
1183
|
+
throw new Error('assistant-skills: exact benchmark source required');
|
|
1184
|
+
const definition = benchmarkArmDefinition(selected);
|
|
1185
|
+
const expiresAt = Math.min(definition.source.acceptance.validUntil, candidate?.expiresAt ?? Number.MAX_SAFE_INTEGER);
|
|
1186
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())
|
|
1187
|
+
throw new Error('assistant-skills: benchmark source expired');
|
|
1188
|
+
return { definition, definitionDigest: acceptanceDigest(definition), sourceDigest: acceptanceDigest(definition.source), version, expiresAt };
|
|
1189
|
+
}
|
|
1190
|
+
/** Re-read original Goals provenance before freezing an arm. This grants no execution authority. */
|
|
1191
|
+
inspectOwnerBenchmarkArm = async (selection, signal) => {
|
|
1192
|
+
selection = structuredClone(selection);
|
|
1193
|
+
const combined = AbortSignal.any([this.#lifecycle.signal, AbortSignal.timeout(10000), ...(signal ? [signal] : [])]);
|
|
1194
|
+
combined.throwIfAborted();
|
|
1195
|
+
const before = this.#benchmarkSource(selection), goals = this.#goals(), generation = this.#benchmarkGeneration;
|
|
1196
|
+
const route = this.#watchRoute(selection.scope, selection.ownerRouteId);
|
|
1197
|
+
const source = await awaitBenchmarkSignal(goals.inspectOwnerVerifiedWorkflowSource({ ownerRouteId: selection.ownerRouteId,
|
|
1198
|
+
principalId: selection.scope.principalId, workspace: selection.scope.workspace, preset: selection.scope.preset,
|
|
1199
|
+
sessionId: before.definition.source.goal.sessionId, goalId: before.definition.source.goal.id }, combined), combined);
|
|
1200
|
+
combined.throwIfAborted();
|
|
1201
|
+
this.#watchRoute(selection.scope, selection.ownerRouteId, route);
|
|
1202
|
+
if (this.#benchmarkGeneration !== generation || !sameBenchmarkArm(before, this.#benchmarkSource(selection))
|
|
1203
|
+
|| acceptanceDigest(source) !== before.sourceDigest)
|
|
1204
|
+
throw new Error('assistant-skills: benchmark provenance changed');
|
|
1205
|
+
return structuredClone(before);
|
|
1206
|
+
};
|
|
1207
|
+
/** Host-only, single-use delegation to one exact live recipient service and frozen cell. */
|
|
1208
|
+
mintBenchmarkArm = async (input, signal) => {
|
|
1209
|
+
const recipient = AssistantSkillsService.#benchmarkRecipients.get(input.recipient.benchmarkRecipientIdentity());
|
|
1210
|
+
if (!recipient)
|
|
1211
|
+
throw new Error('assistant-skills: unrecognized benchmark recipient');
|
|
1212
|
+
const selection = structuredClone(input.selection), scope = structuredClone(input.recipientScope), binding = structuredClone(input.binding);
|
|
1213
|
+
const recipientOwnerRouteId = input.recipientOwnerRouteId;
|
|
1214
|
+
const snapshot = await this.inspectOwnerBenchmarkArm(selection, signal);
|
|
1215
|
+
const route = this.#watchRoute(selection.scope, selection.ownerRouteId);
|
|
1216
|
+
const recipientRoute = recipient.#watchRoute(scope, recipientOwnerRouteId);
|
|
1217
|
+
if (recipient.#benchmarkIdentity === this.#benchmarkIdentity || !recipient.#active || binding.protocol !== 'assistant-skills/delegated-arm/v1'
|
|
1218
|
+
|| binding.definitionDigest !== snapshot.definitionDigest || binding.sourceDigest !== snapshot.sourceDigest
|
|
1219
|
+
|| binding.recipientScopeDigest !== acceptanceDigest(scope) || binding.skillName !== snapshot.definition.name || binding.version !== snapshot.version
|
|
1220
|
+
|| !Number.isSafeInteger(binding.expiresAt) || binding.expiresAt <= Date.now() || binding.expiresAt > snapshot.expiresAt
|
|
1221
|
+
|| binding.expiresAt - Date.now() > 300000)
|
|
1222
|
+
throw new Error('assistant-skills: invalid benchmark delegation');
|
|
1223
|
+
if (!/^[a-f0-9]{64}$/u.test(binding.planDigest) || !binding.cellId || !binding.variantId)
|
|
1224
|
+
throw new Error('assistant-skills: invalid benchmark cell');
|
|
1225
|
+
const cellKey = acceptanceDigest([binding.planDigest, binding.cellId]);
|
|
1226
|
+
if (this.#issuedBenchmarkCells.has(cellKey))
|
|
1227
|
+
throw new Error('assistant-skills: benchmark cell already delegated');
|
|
1228
|
+
const authorization = { idempotencyKey: `skill-benchmark-${acceptanceDigest(binding)}` };
|
|
1229
|
+
if (this.ctx.get('assistantPolicy', false)?.authorize({ ...this.#watchPolicy(selection.scope, 'watch'), action: 'compare' }, authorization).effect !== 'allow'
|
|
1230
|
+
|| recipient.ctx.get('assistantPolicy', false)?.authorize({ ...recipient.#watchPolicy(scope, 'watch'), action: 'compare' }, authorization).effect !== 'allow')
|
|
1231
|
+
throw new Error('assistant-skills: benchmark comparison authorization denied');
|
|
1232
|
+
const controller = new AbortController();
|
|
1233
|
+
const combined = AbortSignal.any([controller.signal, this.#lifecycle.signal, recipient.#lifecycle.signal, ...(signal ? [signal] : [])]);
|
|
1234
|
+
const assertCurrent = () => {
|
|
1235
|
+
combined.throwIfAborted();
|
|
1236
|
+
if (!recipient.#active || Date.now() >= binding.expiresAt || !sameBenchmarkArm(snapshot, this.#benchmarkSource(selection)))
|
|
1237
|
+
throw new Error('assistant-skills: benchmark delegation unavailable');
|
|
1238
|
+
this.#watchRoute(selection.scope, selection.ownerRouteId, route);
|
|
1239
|
+
recipient.#watchRoute(scope, recipientOwnerRouteId, recipientRoute);
|
|
1240
|
+
if (recipient.ctx.get('assistantPolicy', false)?.evaluate({ ...recipient.#watchPolicy(scope, 'watch'), action: 'compare' }).effect !== 'allow')
|
|
1241
|
+
throw new Error('assistant-skills: recipient comparison policy denied');
|
|
1242
|
+
};
|
|
1243
|
+
assertCurrent();
|
|
1244
|
+
const capability = Object.freeze({ protocol: 'assistant-skills/benchmark-capability/v1' });
|
|
1245
|
+
AssistantSkillsService.#benchmarkCapabilities.set(capability, { recipient: recipient.#benchmarkIdentity, scope, binding, snapshot, controller, signal: combined, assertCurrent,
|
|
1246
|
+
refresh: async (runSignal) => {
|
|
1247
|
+
assertCurrent();
|
|
1248
|
+
const current = await this.inspectOwnerBenchmarkArm(selection, AbortSignal.any([combined, runSignal]));
|
|
1249
|
+
assertCurrent();
|
|
1250
|
+
if (!sameBenchmarkArm(snapshot, current))
|
|
1251
|
+
throw new Error('assistant-skills: benchmark source changed');
|
|
1252
|
+
}, consumed: false });
|
|
1253
|
+
this.#issuedBenchmarkCells.add(cellKey);
|
|
1254
|
+
this.#issuedBenchmarkArms.add(controller);
|
|
1255
|
+
this.ctx.effect(() => {
|
|
1256
|
+
const timer = setTimeout(() => controller.abort(), Math.max(1, binding.expiresAt - Date.now()));
|
|
1257
|
+
timer.unref?.();
|
|
1258
|
+
const stop = () => { clearTimeout(timer); this.#issuedBenchmarkArms.delete(controller); };
|
|
1259
|
+
combined.addEventListener('abort', stop, { once: true });
|
|
1260
|
+
return () => { combined.removeEventListener('abort', stop); stop(); controller.abort(); };
|
|
1261
|
+
}, 'assistant-skills.benchmark-grant');
|
|
1262
|
+
combined.addEventListener('abort', () => this.#issuedBenchmarkArms.delete(controller), { once: true });
|
|
1263
|
+
return capability;
|
|
1264
|
+
};
|
|
1265
|
+
/** No definition is imported into the active/candidate tables; a restart cannot restore the authority. */
|
|
1266
|
+
mountBenchmarkArm = async (capability, expected) => {
|
|
1267
|
+
const grant = AssistantSkillsService.#benchmarkCapabilities.get(capability);
|
|
1268
|
+
if (!grant || grant.consumed || grant.recipient !== this.#benchmarkIdentity || acceptanceDigest(expected) !== acceptanceDigest(grant.binding))
|
|
1269
|
+
throw new Error('assistant-skills: unrecognized benchmark capability');
|
|
1270
|
+
grant.consumed = true;
|
|
1271
|
+
await grant.refresh(this.#lifecycle.signal);
|
|
1272
|
+
grant.assertCurrent();
|
|
1273
|
+
const scopeKey = acceptanceDigest(grant.scope);
|
|
1274
|
+
if (this.#mountedBenchmarkArms.has(scopeKey))
|
|
1275
|
+
throw new Error('assistant-skills: benchmark recipient already mounted');
|
|
1276
|
+
this.#store.reserveDelegatedArm(grant.scope, grant.binding);
|
|
1277
|
+
const bindingDigest = acceptanceDigest(grant.binding), now = Date.now();
|
|
1278
|
+
const skill = { ...structuredClone(grant.snapshot.definition), version: grant.snapshot.version, parentVersion: null, retired: false, createdAt: now, updatedAt: now };
|
|
1279
|
+
const arm = { binding: grant.binding, bindingDigest, skill, scope: grant.scope, signal: grant.signal, revoke: () => grant.controller.abort(), assertCurrent: grant.assertCurrent, refresh: grant.refresh };
|
|
1280
|
+
this.#mountedBenchmarkArms.set(scopeKey, arm);
|
|
1281
|
+
const dispose = this.ctx.effect(() => {
|
|
1282
|
+
const timer = setTimeout(() => grant.controller.abort(), Math.max(1, grant.binding.expiresAt - Date.now()));
|
|
1283
|
+
timer.unref?.();
|
|
1284
|
+
return () => { clearTimeout(timer); grant.controller.abort(); if (this.#mountedBenchmarkArms.get(scopeKey) === arm)
|
|
1285
|
+
this.#mountedBenchmarkArms.delete(scopeKey); };
|
|
1286
|
+
}, 'assistant-skills.benchmark-arm');
|
|
1287
|
+
return { binding: structuredClone(grant.binding), bindingDigest, skill: { name: skill.name, version: skill.version, inputs: structuredClone(skill.inputs) }, dispose };
|
|
1288
|
+
};
|
|
952
1289
|
#pending(scope, id) {
|
|
953
1290
|
const candidate = this.#store.getCandidate(scope, id);
|
|
954
1291
|
if (!candidate || candidate.state !== 'pending' || candidate.expiresAt <= Date.now())
|
|
@@ -1034,6 +1371,15 @@ export class AssistantSkillsService extends Service {
|
|
|
1034
1371
|
return false;
|
|
1035
1372
|
}
|
|
1036
1373
|
};
|
|
1374
|
+
ownsOwnerAuthorizedRepairResume = (input, callback) => {
|
|
1375
|
+
if (!this.ownsOwnerAuthorizedRepair(input, callback))
|
|
1376
|
+
return false;
|
|
1377
|
+
const record = this.#store.getRepairContinuation(input.scope, input.authorizationId);
|
|
1378
|
+
const repair = record?.checkpoint.repair;
|
|
1379
|
+
return !!record && ['repairing', 'repair-achieved', 'capturing', 'candidate-staged', 'comparing', 'watching'].includes(record.state)
|
|
1380
|
+
&& repair?.sessionId === input.repair.sessionId && repair.goalId === input.repair.goalId
|
|
1381
|
+
&& this.#repairAgents?.ownsResume(input, callback) === true;
|
|
1382
|
+
};
|
|
1037
1383
|
#repairProfileAt(record, iteration) {
|
|
1038
1384
|
const entry = record.authorization.profileSequence?.[iteration - 1]
|
|
1039
1385
|
?? (iteration === 1 ? { id: record.authorization.profileId, digest: record.authorization.profileDigest } : undefined);
|
|
@@ -1044,6 +1390,20 @@ export class AssistantSkillsService extends Service {
|
|
|
1044
1390
|
return profile;
|
|
1045
1391
|
}
|
|
1046
1392
|
#repairProfile(record) { return this.#repairProfileAt(record, record.iteration); }
|
|
1393
|
+
/**
|
|
1394
|
+
* New authorizations persist this exact route. Legacy records predate the
|
|
1395
|
+
* field, so they may run only when their still-digest-bound profile supplied
|
|
1396
|
+
* an explicit pair; inherited routes must never be selected again on resume.
|
|
1397
|
+
*/
|
|
1398
|
+
#repairModelSelection(record) {
|
|
1399
|
+
const frozen = record.authorization.modelSelection;
|
|
1400
|
+
if (frozen)
|
|
1401
|
+
return frozen;
|
|
1402
|
+
const primary = this.#repairProfileAt(record, 1);
|
|
1403
|
+
if (primary.provider !== undefined && primary.model !== undefined)
|
|
1404
|
+
return { provider: primary.provider, model: primary.model };
|
|
1405
|
+
throw new Error('assistant-skills: legacy inherited repair model selection unavailable');
|
|
1406
|
+
}
|
|
1047
1407
|
#repairSource(record) {
|
|
1048
1408
|
if (record.iteration === 1)
|
|
1049
1409
|
return record.authorization.source;
|
|
@@ -1144,6 +1504,14 @@ export class AssistantSkillsService extends Service {
|
|
|
1144
1504
|
if (!this.#repairRuntime || !profile || !holdout?.profile.canaryAdmissionTemplate || !Number.isSafeInteger(input.expiresAt)
|
|
1145
1505
|
|| input.expiresAt <= Date.now() || input.expiresAt > Math.min(holdout.profile.execution.expiresAt, Date.now() + 604800000))
|
|
1146
1506
|
throw new Error('assistant-skills: finite current repair profile required');
|
|
1507
|
+
const selected = profile.provider === undefined
|
|
1508
|
+
? this.ctx.get('assistantDelivery', false)?.inspectOwnerModelSelection({ authorityId: input.ownerRouteId, principalId: scope.principalId,
|
|
1509
|
+
workspace: scope.workspace, agentPreset: scope.preset, sourceAgent: agent })
|
|
1510
|
+
: { provider: profile.provider, model: profile.model };
|
|
1511
|
+
if (!repairModelRoute(selected))
|
|
1512
|
+
throw new Error('assistant-skills: exact current repair model selection required');
|
|
1513
|
+
const modelSelection = { provider: selected.provider, model: selected.model,
|
|
1514
|
+
...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }) };
|
|
1147
1515
|
const parent = this.#store.get(scope, profile.skillName);
|
|
1148
1516
|
const sessionId = input.sessionId ?? String(agent.session.id);
|
|
1149
1517
|
const snapshot = this.#goals().inspectOwnerGoalExecution({ ownerRouteId: input.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset, sessionId, goalId: input.goalId });
|
|
@@ -1155,7 +1523,8 @@ export class AssistantSkillsService extends Service {
|
|
|
1155
1523
|
const profileSequence = sequenceIds.map(id => {
|
|
1156
1524
|
const next = this.#repairProfiles.find(value => value.id === id && acceptanceDigest(value.scope) === acceptanceDigest(scope));
|
|
1157
1525
|
const nextHoldout = next && this.inspectRepairProfile(next.externalHoldoutProfileId, scope);
|
|
1158
|
-
if (!next || !nextHoldout || next.skillName !== profile.skillName
|
|
1526
|
+
if (!next || !nextHoldout || next.skillName !== profile.skillName
|
|
1527
|
+
|| next.provider !== undefined && (next.provider !== modelSelection.provider || next.model !== modelSelection.model)
|
|
1159
1528
|
|| next.allowedTools.some(tool => !profile.allowedTools.includes(tool)) || next.maxOutputTokens > profile.maxOutputTokens || next.maxGoalRounds > profile.maxGoalRounds || next.maxDurationMs > profile.maxDurationMs
|
|
1160
1529
|
|| input.expiresAt > nextHoldout.profile.execution.expiresAt)
|
|
1161
1530
|
throw new Error('assistant-skills: followup profile exceeds the frozen repair authority');
|
|
@@ -1176,7 +1545,7 @@ export class AssistantSkillsService extends Service {
|
|
|
1176
1545
|
const authorization = { invocationId: input.invocationId, ownerRouteId: input.ownerRouteId,
|
|
1177
1546
|
source: { goalId: input.goalId, sessionId, nativeGoalId: snapshot.storedGoal.nativeAtLastObservation.goalId, definitionDigest: snapshot.storedGoal.definition.digest },
|
|
1178
1547
|
profileId: profile.id, profileDigest: acceptanceDigest([profile, holdout.digest]), skillName: profile.skillName,
|
|
1179
|
-
parentVersion: parent.version, parentDigest: acceptanceDigest(parent), maxIterations: profile.maxIterations ?? 1, expiresAt: input.expiresAt,
|
|
1548
|
+
parentVersion: parent.version, parentDigest: acceptanceDigest(parent), maxIterations: profile.maxIterations ?? 1, expiresAt: input.expiresAt, modelSelection,
|
|
1180
1549
|
...(profileSequence.length > 1 ? { profileSequence } : {}), ...(feedbackAuthority ? { feedbackAuthority } : {}) };
|
|
1181
1550
|
for (const action of ['draft', 'compare', 'canary', 'watch']) {
|
|
1182
1551
|
this.#scope(agent, action);
|
|
@@ -1268,9 +1637,21 @@ export class AssistantSkillsService extends Service {
|
|
|
1268
1637
|
}
|
|
1269
1638
|
}).catch(() => undefined).finally(() => { this.#repairTask = undefined; });
|
|
1270
1639
|
}
|
|
1640
|
+
#repairAgentInput(record, trigger) {
|
|
1641
|
+
const profile = this.#repairProfile(record), primary = this.#repairProfileAt(record, 1);
|
|
1642
|
+
const modelSelection = this.#repairModelSelection(record);
|
|
1643
|
+
const usage = this.#store.repairUsage(record.scope, record.id);
|
|
1644
|
+
const callback = () => this.#assertRepair(record);
|
|
1645
|
+
this.#repairAssertions.set(record.id, { callback, triggerDigest: acceptanceDigest(trigger) });
|
|
1646
|
+
return { ...profile, ...modelSelection, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
|
|
1647
|
+
initialModelCalls: usage.modelCalls, initialToolCalls: usage.toolCalls,
|
|
1648
|
+
recordUsage: kind => { this.#store.chargeRepairUsage(record.scope, record.id, kind, kind === 'model' ? primary.maxModelCalls : primary.maxToolCalls); },
|
|
1649
|
+
authorizationDigest: record.authorizationDigest, ownerRouteId: record.authorization.ownerRouteId,
|
|
1650
|
+
trigger, objective: trigger.taskFamily.objective, expiresAt: record.authorization.expiresAt, assertCurrent: callback };
|
|
1651
|
+
}
|
|
1271
1652
|
#installRepairRuntime(ctx) {
|
|
1272
1653
|
ctx.inject(['tools', 'agents', 'sessions', 'llm', 'systemPrompt', 'assistantGoals', 'assistantPolicy', 'assistantDelivery', 'assistantVerifier'], runtime => {
|
|
1273
|
-
const agents = new OwnerRepairAgentRuntime(runtime);
|
|
1654
|
+
const agents = new OwnerRepairAgentRuntime(runtime, this.#store);
|
|
1274
1655
|
const driver = new RepairContinuationRuntime(this.#store, {
|
|
1275
1656
|
assertCurrent: record => this.#assertRepair(record),
|
|
1276
1657
|
inspectTrigger: async (record, signal) => {
|
|
@@ -1281,14 +1662,18 @@ export class AssistantSkillsService extends Service {
|
|
|
1281
1662
|
return this.#goals().inspectOwnerFailureTrigger({ ownerRouteId: locator.ownerRouteId, principalId: locator.principalId, workspace: locator.workspace, preset: locator.preset, taskFamilyId: profile.taskFamilyId, failures: [{ goalId: locator.goalId, sessionId: locator.sessionId }], minimumOccurrences: 1 }, signal);
|
|
1282
1663
|
},
|
|
1283
1664
|
createRepair: async (record, evidence, signal) => {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
this.#
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1665
|
+
return agents.create(this.#repairAgentInput(record, evidence), signal);
|
|
1666
|
+
},
|
|
1667
|
+
ensureRepair: async (record, signal) => {
|
|
1668
|
+
const locator = this.#repairLocator(record, true);
|
|
1669
|
+
if (agents.get(locator.sessionId))
|
|
1670
|
+
return;
|
|
1671
|
+
const snapshot = this.#goals().inspectOwnerGoalExecution(locator);
|
|
1672
|
+
const stored = snapshot.storedGoal;
|
|
1673
|
+
await agents.resume(this.#repairAgentInput(record, record.checkpoint.trigger), {
|
|
1674
|
+
sessionId: locator.sessionId, goalId: locator.goalId, nativeGoalId: stored.nativeAtLastObservation.goalId,
|
|
1675
|
+
definitionDigest: stored.definition.digest,
|
|
1676
|
+
}, signal);
|
|
1292
1677
|
},
|
|
1293
1678
|
inspectRepair: async (record, signal) => {
|
|
1294
1679
|
const locator = this.#repairLocator(record, true), snapshot = this.#goals().inspectOwnerGoalExecution(locator);
|
|
@@ -1361,14 +1746,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1361
1746
|
this.#repairAgents = agents;
|
|
1362
1747
|
this.#repairRuntime = driver;
|
|
1363
1748
|
for (const record of this.#store.listRepairContinuations()) {
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
if (['repairing', 'repair-achieved', 'candidate-staged'].includes(record.state)) {
|
|
1368
|
-
this.#store.transitionRepairContinuation(record.scope, record.id, record.revision, 'unknown', { ...record.checkpoint, failure: 'repair-agent-interrupted' });
|
|
1369
|
-
}
|
|
1370
|
-
else
|
|
1371
|
-
driver.recover(record.scope, record.id);
|
|
1749
|
+
// Dispatches without a durable result are not replayable. Existing
|
|
1750
|
+
// repair Sessions are reattached by ensureRepair under their fence.
|
|
1751
|
+
driver.recover(record.scope, record.id);
|
|
1372
1752
|
}
|
|
1373
1753
|
runtime.tools.register(defineTool({ name: 'skill_repair_arm', description: 'Authorize a bounded autonomous repair sequence starting from an exact owner Goal using configured profiles. After independent failure evidence, a separate native Goal synthesizes a repair; only independent acceptance and prospective comparison can deploy a finite canary. Requires the current human request. Followups require the frozen profile sequence and a new independently failed task after promotion; budgets and expiry cannot be renewed.',
|
|
1374
1754
|
parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, source_session_id: { type: 'string' }, profile_id: { type: 'string', required: true }, owner_route_id: { type: 'string', required: true }, invocation_id: { type: 'string', required: true }, expires_at: { type: 'integer', required: true }, notify: { type: 'boolean', description: 'Send finite repair results to this exact current owner route Session.' } }, output,
|
|
@@ -1721,14 +2101,41 @@ export class AssistantSkillsService extends Service {
|
|
|
1721
2101
|
}
|
|
1722
2102
|
async run(exec, goalId, name, version, inputs, invocationId) {
|
|
1723
2103
|
const scope = this.#scope(exec.agent, 'run');
|
|
2104
|
+
const delegated = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
|
|
2105
|
+
if (delegated) {
|
|
2106
|
+
delegated.assertCurrent();
|
|
2107
|
+
if (delegated.skill.name !== name || delegated.skill.version !== version)
|
|
2108
|
+
throw new Error('assistant-skills: exact delegated arm required');
|
|
2109
|
+
const task = this.#run(exec, goalId, delegated.skill, inputs, invocationId, undefined, delegated);
|
|
2110
|
+
this.#benchmarkRuns.add(task);
|
|
2111
|
+
try {
|
|
2112
|
+
return await task;
|
|
2113
|
+
}
|
|
2114
|
+
finally {
|
|
2115
|
+
this.#benchmarkRuns.delete(task);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1724
2118
|
const skill = this.#store.get(scope, name);
|
|
1725
2119
|
if (!skill || skill.retired || skill.version !== version)
|
|
1726
2120
|
throw new Error('assistant-skills: active skill version required');
|
|
1727
2121
|
return this.#run(exec, goalId, skill, inputs, invocationId);
|
|
1728
2122
|
}
|
|
1729
|
-
async #run(exec, goalId, skill, inputs, invocationId, candidateId) {
|
|
2123
|
+
async #run(exec, goalId, skill, inputs, invocationId, candidateId, delegated) {
|
|
1730
2124
|
const action = candidateId ? 'trial' : 'run';
|
|
1731
2125
|
const scope = this.#scope(exec.agent, action), { name, version } = skill;
|
|
2126
|
+
const assertDelegated = () => {
|
|
2127
|
+
if (!delegated)
|
|
2128
|
+
return;
|
|
2129
|
+
delegated.assertCurrent();
|
|
2130
|
+
if (this.#mountedBenchmarkArms.get(acceptanceDigest(scope)) !== delegated
|
|
2131
|
+
|| acceptanceDigest(benchmarkArmDefinition(skill)) !== delegated.binding.definitionDigest)
|
|
2132
|
+
throw new Error('assistant-skills: delegated arm changed');
|
|
2133
|
+
};
|
|
2134
|
+
assertDelegated();
|
|
2135
|
+
if (delegated) {
|
|
2136
|
+
await delegated.refresh(exec.signal);
|
|
2137
|
+
assertDelegated();
|
|
2138
|
+
}
|
|
1732
2139
|
const definition = instantiate(skill, inputs);
|
|
1733
2140
|
const steps = definition.steps;
|
|
1734
2141
|
// These declarations are part of the immutable saved definition, rather
|
|
@@ -1766,8 +2173,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1766
2173
|
const tool = candidateId ? 'skill_trial' : 'skill_run';
|
|
1767
2174
|
return { state: 'awaiting-native-round', performed: false, context: 'No skill step or durable invocation has been claimed or executed; this is not a queued background task.', next: `After the Host starts this Goal's native round, call ${tool} again with this same goal, skill or candidate, inputs, and invocation_id.`, goalId, ...(candidateId ? { candidateId } : {}), skillName: name, version, invocationId, inputs };
|
|
1768
2175
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
2176
|
+
// Consume the typed precondition family rather than an ad-hoc condition so
|
|
2177
|
+
// an unknown/tampered declaration fails closed before a run is claimed.
|
|
2178
|
+
evaluatePreconditions(skill.preconditions, { ownerScopeDigest: acceptanceDigest(scope), currentScopeDigest: acceptanceDigest(current.scope), goalId, sourceGoalId: skill.source.goal.id });
|
|
1771
2179
|
const identity = acceptanceDigest(current);
|
|
1772
2180
|
const goalContext = current;
|
|
1773
2181
|
// claim() reconciles a deployed version while holding its transaction. Do
|
|
@@ -1793,7 +2201,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1793
2201
|
throw new Error('assistant-skills: deployment authority ended');
|
|
1794
2202
|
}
|
|
1795
2203
|
}
|
|
1796
|
-
|
|
2204
|
+
assertDelegated();
|
|
2205
|
+
const claimInput = { invocationId, goalId, sessionId: current.sessionId, skillName: name, version, inputs, goalExecutionRunId: current.goalExecutionRunId, goalDefinitionDigest: current.definition.digest, ...(typeof goalContext.nativeGoalId === 'string' ? { nativeGoalId: goalContext.nativeGoalId } : {}), ...(candidateId ? { candidateId } : {}) };
|
|
2206
|
+
const claim = delegated ? this.#store.claimDelegated(scope, claimInput, delegated.bindingDigest) : this.#store.claim(scope, claimInput);
|
|
1797
2207
|
if (!claim.claimed) {
|
|
1798
2208
|
if (claim.run.state !== 'succeeded')
|
|
1799
2209
|
throw new Error(`assistant-skills: invocation ${claim.run.id} is ${claim.run.state}; inspect skill_status, do not replay`);
|
|
@@ -1819,18 +2229,25 @@ export class AssistantSkillsService extends Service {
|
|
|
1819
2229
|
const timeout = new AbortController();
|
|
1820
2230
|
const timer = setTimeout(() => timeout.abort(), this.#duration);
|
|
1821
2231
|
timer.unref?.();
|
|
1822
|
-
const signal = AbortSignal.any([exec.signal, timeout.signal, this.#lifecycle.signal, ...(deploymentRevision ? [deploymentRevision.signal] : [])]);
|
|
2232
|
+
const signal = AbortSignal.any([exec.signal, timeout.signal, this.#lifecycle.signal, ...(delegated ? [delegated.signal] : []), ...(deploymentRevision ? [deploymentRevision.signal] : [])]);
|
|
2233
|
+
const settle = (work) => delegated ? awaitBenchmarkSignal(work, signal) : work;
|
|
1823
2234
|
const completed = [];
|
|
1824
2235
|
let state = 'unknown';
|
|
1825
2236
|
let dispatched = false;
|
|
2237
|
+
// Resolve the compensation family up front; an unsupported/tampered value
|
|
2238
|
+
// fails closed before any step is dispatched.
|
|
2239
|
+
const compensation = compensationDirective(definition.compensation);
|
|
1826
2240
|
const revalidate = () => {
|
|
1827
2241
|
signal.throwIfAborted();
|
|
2242
|
+
assertDelegated();
|
|
1828
2243
|
if (acceptanceDigest(this.#scope(exec.agent, action)) !== acceptanceDigest(scope)
|
|
1829
2244
|
|| acceptanceDigest(this.#goals().inspectWorkflowRunContext(exec.agent, goalId)) !== identity)
|
|
1830
2245
|
throw new Error('assistant-skills: current authority changed');
|
|
1831
2246
|
if (this.#store.getRun(scope, claim.run.id)?.state !== 'running')
|
|
1832
2247
|
throw new Error('assistant-skills: invocation no longer owns dispatch');
|
|
1833
|
-
if (
|
|
2248
|
+
if (delegated)
|
|
2249
|
+
assertDelegated();
|
|
2250
|
+
else if (candidateId)
|
|
1834
2251
|
this.#pending(scope, candidateId);
|
|
1835
2252
|
else {
|
|
1836
2253
|
const live = this.#store.get(scope, name);
|
|
@@ -1849,11 +2266,13 @@ export class AssistantSkillsService extends Service {
|
|
|
1849
2266
|
for (const observation of observations) {
|
|
1850
2267
|
if (observation.beforeStepId !== step.id)
|
|
1851
2268
|
continue;
|
|
2269
|
+
if (delegated)
|
|
2270
|
+
await delegated.refresh(signal);
|
|
1852
2271
|
revalidate();
|
|
1853
2272
|
if (!this.#allowed.includes('read'))
|
|
1854
2273
|
throw new Error('assistant-skills: current tool allowlist denied required file observation');
|
|
1855
2274
|
dispatched = true;
|
|
1856
|
-
const observed = await exec.agent.ctx.get('tools').execute({
|
|
2275
|
+
const observed = await settle(exec.agent.ctx.get('tools').execute({
|
|
1857
2276
|
callId: ToolCallId(`${exec.callId}:skill-observation:${index + 1}`),
|
|
1858
2277
|
rootCallId: exec.rootCallId,
|
|
1859
2278
|
parent: exec.token,
|
|
@@ -1861,7 +2280,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1861
2280
|
name: 'read',
|
|
1862
2281
|
arguments: { file_path: observation.filePath, limit: 1 },
|
|
1863
2282
|
signal,
|
|
1864
|
-
});
|
|
2283
|
+
}));
|
|
2284
|
+
if (delegated)
|
|
2285
|
+
await delegated.refresh(signal);
|
|
1865
2286
|
revalidate();
|
|
1866
2287
|
for (const context of observed.additionalContexts ?? [])
|
|
1867
2288
|
exec.deferContext(context);
|
|
@@ -1883,14 +2304,29 @@ export class AssistantSkillsService extends Service {
|
|
|
1883
2304
|
state = 'failed';
|
|
1884
2305
|
break stepsLoop;
|
|
1885
2306
|
}
|
|
2307
|
+
if (delegated)
|
|
2308
|
+
await delegated.refresh(signal);
|
|
1886
2309
|
revalidate();
|
|
1887
2310
|
dispatched = false;
|
|
1888
2311
|
}
|
|
2312
|
+
if (delegated)
|
|
2313
|
+
await delegated.refresh(signal);
|
|
1889
2314
|
revalidate();
|
|
2315
|
+
// Dependency DAG gate: every declared predecessor must have completed
|
|
2316
|
+
// successfully. Topological storage means a missing predecessor can only
|
|
2317
|
+
// result from an earlier failure, at which point the resolved compensation
|
|
2318
|
+
// family stops the remaining steps (the v1 family always does).
|
|
2319
|
+
const succeededById = new Set(completed.filter(entry => entry.state === 'succeeded').map(entry => entry.id));
|
|
2320
|
+
if (compensation.stopRemainingSteps && step.dependsOn.some(dependency => !succeededById.has(dependency))) {
|
|
2321
|
+
state = 'failed';
|
|
2322
|
+
break stepsLoop;
|
|
2323
|
+
}
|
|
1890
2324
|
if (!this.#allowed.includes(step.toolName))
|
|
1891
2325
|
throw new Error('assistant-skills: current tool allowlist denied');
|
|
1892
2326
|
dispatched = true;
|
|
1893
|
-
const result = await exec.agent.ctx.get('tools').execute({ callId: ToolCallId(`${exec.callId}:skill:${index + 1}`), rootCallId: exec.rootCallId, parent: exec.token, agent: exec.agent, name: step.toolName, arguments: step.arguments, signal });
|
|
2327
|
+
const result = await settle(exec.agent.ctx.get('tools').execute({ callId: ToolCallId(`${exec.callId}:skill:${index + 1}`), rootCallId: exec.rootCallId, parent: exec.token, agent: exec.agent, name: step.toolName, arguments: step.arguments, signal }));
|
|
2328
|
+
if (delegated)
|
|
2329
|
+
await delegated.refresh(signal);
|
|
1894
2330
|
revalidate();
|
|
1895
2331
|
for (const context of result.additionalContexts ?? [])
|
|
1896
2332
|
exec.deferContext(context);
|
|
@@ -1903,6 +2339,8 @@ export class AssistantSkillsService extends Service {
|
|
|
1903
2339
|
}
|
|
1904
2340
|
if (result.concludesTurn)
|
|
1905
2341
|
exec.concludeTurn();
|
|
2342
|
+
if (delegated)
|
|
2343
|
+
await delegated.refresh(signal);
|
|
1906
2344
|
revalidate();
|
|
1907
2345
|
dispatched = false;
|
|
1908
2346
|
if (index === steps.length - 1)
|