@dsh-enhanced/assistant-skills 0.1.32 → 0.1.35
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 +28 -4
- 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 +1 -0
- package/lib/repair-agent.d.ts.map +1 -1
- package/lib/repair-agent.js +15 -4
- package/lib/repair-agent.js.map +1 -1
- 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-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 +269 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +438 -19
- package/lib/service.js.map +1 -1
- package/lib/store.d.ts +26 -0
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +80 -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())
|
|
@@ -1053,6 +1390,20 @@ export class AssistantSkillsService extends Service {
|
|
|
1053
1390
|
return profile;
|
|
1054
1391
|
}
|
|
1055
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
|
+
}
|
|
1056
1407
|
#repairSource(record) {
|
|
1057
1408
|
if (record.iteration === 1)
|
|
1058
1409
|
return record.authorization.source;
|
|
@@ -1153,6 +1504,14 @@ export class AssistantSkillsService extends Service {
|
|
|
1153
1504
|
if (!this.#repairRuntime || !profile || !holdout?.profile.canaryAdmissionTemplate || !Number.isSafeInteger(input.expiresAt)
|
|
1154
1505
|
|| input.expiresAt <= Date.now() || input.expiresAt > Math.min(holdout.profile.execution.expiresAt, Date.now() + 604800000))
|
|
1155
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 }) };
|
|
1156
1515
|
const parent = this.#store.get(scope, profile.skillName);
|
|
1157
1516
|
const sessionId = input.sessionId ?? String(agent.session.id);
|
|
1158
1517
|
const snapshot = this.#goals().inspectOwnerGoalExecution({ ownerRouteId: input.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset, sessionId, goalId: input.goalId });
|
|
@@ -1164,7 +1523,8 @@ export class AssistantSkillsService extends Service {
|
|
|
1164
1523
|
const profileSequence = sequenceIds.map(id => {
|
|
1165
1524
|
const next = this.#repairProfiles.find(value => value.id === id && acceptanceDigest(value.scope) === acceptanceDigest(scope));
|
|
1166
1525
|
const nextHoldout = next && this.inspectRepairProfile(next.externalHoldoutProfileId, scope);
|
|
1167
|
-
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)
|
|
1168
1528
|
|| next.allowedTools.some(tool => !profile.allowedTools.includes(tool)) || next.maxOutputTokens > profile.maxOutputTokens || next.maxGoalRounds > profile.maxGoalRounds || next.maxDurationMs > profile.maxDurationMs
|
|
1169
1529
|
|| input.expiresAt > nextHoldout.profile.execution.expiresAt)
|
|
1170
1530
|
throw new Error('assistant-skills: followup profile exceeds the frozen repair authority');
|
|
@@ -1185,7 +1545,7 @@ export class AssistantSkillsService extends Service {
|
|
|
1185
1545
|
const authorization = { invocationId: input.invocationId, ownerRouteId: input.ownerRouteId,
|
|
1186
1546
|
source: { goalId: input.goalId, sessionId, nativeGoalId: snapshot.storedGoal.nativeAtLastObservation.goalId, definitionDigest: snapshot.storedGoal.definition.digest },
|
|
1187
1547
|
profileId: profile.id, profileDigest: acceptanceDigest([profile, holdout.digest]), skillName: profile.skillName,
|
|
1188
|
-
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,
|
|
1189
1549
|
...(profileSequence.length > 1 ? { profileSequence } : {}), ...(feedbackAuthority ? { feedbackAuthority } : {}) };
|
|
1190
1550
|
for (const action of ['draft', 'compare', 'canary', 'watch']) {
|
|
1191
1551
|
this.#scope(agent, action);
|
|
@@ -1279,10 +1639,11 @@ export class AssistantSkillsService extends Service {
|
|
|
1279
1639
|
}
|
|
1280
1640
|
#repairAgentInput(record, trigger) {
|
|
1281
1641
|
const profile = this.#repairProfile(record), primary = this.#repairProfileAt(record, 1);
|
|
1642
|
+
const modelSelection = this.#repairModelSelection(record);
|
|
1282
1643
|
const usage = this.#store.repairUsage(record.scope, record.id);
|
|
1283
1644
|
const callback = () => this.#assertRepair(record);
|
|
1284
1645
|
this.#repairAssertions.set(record.id, { callback, triggerDigest: acceptanceDigest(trigger) });
|
|
1285
|
-
return { ...profile, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
|
|
1646
|
+
return { ...profile, ...modelSelection, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
|
|
1286
1647
|
initialModelCalls: usage.modelCalls, initialToolCalls: usage.toolCalls,
|
|
1287
1648
|
recordUsage: kind => { this.#store.chargeRepairUsage(record.scope, record.id, kind, kind === 'model' ? primary.maxModelCalls : primary.maxToolCalls); },
|
|
1288
1649
|
authorizationDigest: record.authorizationDigest, ownerRouteId: record.authorization.ownerRouteId,
|
|
@@ -1740,14 +2101,41 @@ export class AssistantSkillsService extends Service {
|
|
|
1740
2101
|
}
|
|
1741
2102
|
async run(exec, goalId, name, version, inputs, invocationId) {
|
|
1742
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
|
+
}
|
|
1743
2118
|
const skill = this.#store.get(scope, name);
|
|
1744
2119
|
if (!skill || skill.retired || skill.version !== version)
|
|
1745
2120
|
throw new Error('assistant-skills: active skill version required');
|
|
1746
2121
|
return this.#run(exec, goalId, skill, inputs, invocationId);
|
|
1747
2122
|
}
|
|
1748
|
-
async #run(exec, goalId, skill, inputs, invocationId, candidateId) {
|
|
2123
|
+
async #run(exec, goalId, skill, inputs, invocationId, candidateId, delegated) {
|
|
1749
2124
|
const action = candidateId ? 'trial' : 'run';
|
|
1750
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
|
+
}
|
|
1751
2139
|
const definition = instantiate(skill, inputs);
|
|
1752
2140
|
const steps = definition.steps;
|
|
1753
2141
|
// These declarations are part of the immutable saved definition, rather
|
|
@@ -1785,8 +2173,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1785
2173
|
const tool = candidateId ? 'skill_trial' : 'skill_run';
|
|
1786
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 };
|
|
1787
2175
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
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 });
|
|
1790
2179
|
const identity = acceptanceDigest(current);
|
|
1791
2180
|
const goalContext = current;
|
|
1792
2181
|
// claim() reconciles a deployed version while holding its transaction. Do
|
|
@@ -1812,7 +2201,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1812
2201
|
throw new Error('assistant-skills: deployment authority ended');
|
|
1813
2202
|
}
|
|
1814
2203
|
}
|
|
1815
|
-
|
|
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);
|
|
1816
2207
|
if (!claim.claimed) {
|
|
1817
2208
|
if (claim.run.state !== 'succeeded')
|
|
1818
2209
|
throw new Error(`assistant-skills: invocation ${claim.run.id} is ${claim.run.state}; inspect skill_status, do not replay`);
|
|
@@ -1838,18 +2229,25 @@ export class AssistantSkillsService extends Service {
|
|
|
1838
2229
|
const timeout = new AbortController();
|
|
1839
2230
|
const timer = setTimeout(() => timeout.abort(), this.#duration);
|
|
1840
2231
|
timer.unref?.();
|
|
1841
|
-
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;
|
|
1842
2234
|
const completed = [];
|
|
1843
2235
|
let state = 'unknown';
|
|
1844
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);
|
|
1845
2240
|
const revalidate = () => {
|
|
1846
2241
|
signal.throwIfAborted();
|
|
2242
|
+
assertDelegated();
|
|
1847
2243
|
if (acceptanceDigest(this.#scope(exec.agent, action)) !== acceptanceDigest(scope)
|
|
1848
2244
|
|| acceptanceDigest(this.#goals().inspectWorkflowRunContext(exec.agent, goalId)) !== identity)
|
|
1849
2245
|
throw new Error('assistant-skills: current authority changed');
|
|
1850
2246
|
if (this.#store.getRun(scope, claim.run.id)?.state !== 'running')
|
|
1851
2247
|
throw new Error('assistant-skills: invocation no longer owns dispatch');
|
|
1852
|
-
if (
|
|
2248
|
+
if (delegated)
|
|
2249
|
+
assertDelegated();
|
|
2250
|
+
else if (candidateId)
|
|
1853
2251
|
this.#pending(scope, candidateId);
|
|
1854
2252
|
else {
|
|
1855
2253
|
const live = this.#store.get(scope, name);
|
|
@@ -1868,11 +2266,13 @@ export class AssistantSkillsService extends Service {
|
|
|
1868
2266
|
for (const observation of observations) {
|
|
1869
2267
|
if (observation.beforeStepId !== step.id)
|
|
1870
2268
|
continue;
|
|
2269
|
+
if (delegated)
|
|
2270
|
+
await delegated.refresh(signal);
|
|
1871
2271
|
revalidate();
|
|
1872
2272
|
if (!this.#allowed.includes('read'))
|
|
1873
2273
|
throw new Error('assistant-skills: current tool allowlist denied required file observation');
|
|
1874
2274
|
dispatched = true;
|
|
1875
|
-
const observed = await exec.agent.ctx.get('tools').execute({
|
|
2275
|
+
const observed = await settle(exec.agent.ctx.get('tools').execute({
|
|
1876
2276
|
callId: ToolCallId(`${exec.callId}:skill-observation:${index + 1}`),
|
|
1877
2277
|
rootCallId: exec.rootCallId,
|
|
1878
2278
|
parent: exec.token,
|
|
@@ -1880,7 +2280,9 @@ export class AssistantSkillsService extends Service {
|
|
|
1880
2280
|
name: 'read',
|
|
1881
2281
|
arguments: { file_path: observation.filePath, limit: 1 },
|
|
1882
2282
|
signal,
|
|
1883
|
-
});
|
|
2283
|
+
}));
|
|
2284
|
+
if (delegated)
|
|
2285
|
+
await delegated.refresh(signal);
|
|
1884
2286
|
revalidate();
|
|
1885
2287
|
for (const context of observed.additionalContexts ?? [])
|
|
1886
2288
|
exec.deferContext(context);
|
|
@@ -1902,14 +2304,29 @@ export class AssistantSkillsService extends Service {
|
|
|
1902
2304
|
state = 'failed';
|
|
1903
2305
|
break stepsLoop;
|
|
1904
2306
|
}
|
|
2307
|
+
if (delegated)
|
|
2308
|
+
await delegated.refresh(signal);
|
|
1905
2309
|
revalidate();
|
|
1906
2310
|
dispatched = false;
|
|
1907
2311
|
}
|
|
2312
|
+
if (delegated)
|
|
2313
|
+
await delegated.refresh(signal);
|
|
1908
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
|
+
}
|
|
1909
2324
|
if (!this.#allowed.includes(step.toolName))
|
|
1910
2325
|
throw new Error('assistant-skills: current tool allowlist denied');
|
|
1911
2326
|
dispatched = true;
|
|
1912
|
-
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);
|
|
1913
2330
|
revalidate();
|
|
1914
2331
|
for (const context of result.additionalContexts ?? [])
|
|
1915
2332
|
exec.deferContext(context);
|
|
@@ -1922,6 +2339,8 @@ export class AssistantSkillsService extends Service {
|
|
|
1922
2339
|
}
|
|
1923
2340
|
if (result.concludesTurn)
|
|
1924
2341
|
exec.concludeTurn();
|
|
2342
|
+
if (delegated)
|
|
2343
|
+
await delegated.refresh(signal);
|
|
1925
2344
|
revalidate();
|
|
1926
2345
|
dispatched = false;
|
|
1927
2346
|
if (index === steps.length - 1)
|