@dsh-enhanced/assistant-goals 0.1.67 → 0.1.70
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 +38 -1
- package/lib/budget.d.ts +6 -1
- package/lib/budget.d.ts.map +1 -1
- package/lib/budget.js +24 -1
- package/lib/budget.js.map +1 -1
- package/lib/execution.d.ts +7 -1
- package/lib/execution.d.ts.map +1 -1
- package/lib/execution.js +51 -6
- package/lib/execution.js.map +1 -1
- package/lib/outcome.d.ts +9 -1
- package/lib/outcome.d.ts.map +1 -1
- package/lib/outcome.js +101 -2
- package/lib/outcome.js.map +1 -1
- package/lib/owner-acceptance-store.d.ts +19 -0
- package/lib/owner-acceptance-store.d.ts.map +1 -0
- package/lib/owner-acceptance-store.js +114 -0
- package/lib/owner-acceptance-store.js.map +1 -0
- package/lib/owner-acceptance.d.ts +82 -0
- package/lib/owner-acceptance.d.ts.map +1 -0
- package/lib/owner-acceptance.js +158 -0
- package/lib/owner-acceptance.js.map +1 -0
- package/lib/owner-intent-store.d.ts +36 -0
- package/lib/owner-intent-store.d.ts.map +1 -0
- package/lib/owner-intent-store.js +136 -0
- package/lib/owner-intent-store.js.map +1 -0
- package/lib/owner-intent.d.ts +100 -0
- package/lib/owner-intent.d.ts.map +1 -0
- package/lib/owner-intent.js +236 -0
- package/lib/owner-intent.js.map +1 -0
- package/lib/service.d.ts +38 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +427 -8
- package/lib/service.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +29 -3
- package/lib/tools.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +7 -7
package/lib/service.js
CHANGED
|
@@ -17,11 +17,17 @@ import { buildGoalStrategyHistory } from './strategy-feedback.js';
|
|
|
17
17
|
import { failureSummaryEvidenceDigest, OwnerVerifiedWorkflowSourceError, ownerGoalRunProof, verifiedWorkflowSource, verifiedWorkflowSourceChain } from './verified-workflow.js';
|
|
18
18
|
import { buildOwnerAcceptedStepArtifacts, buildOwnerVerifiedArtifacts, validateOwnerVerifiedArtifactsInput } from './verified-artifact.js';
|
|
19
19
|
import { assertGoalDependenciesAchieved, goalDependencies } from './dependency.js';
|
|
20
|
+
import { buildGoalOwnerIntent, ownerIntentObjectiveSpan } from './owner-intent.js';
|
|
21
|
+
import { GoalOwnerIntentStore, goalOwnerIntentSupplier } from './owner-intent-store.js';
|
|
22
|
+
import { validateOwnerAcceptanceSeal, validateOwnerPreparedAcceptanceInput, resolveOwnerPreparedAcceptanceInput, preparedRegistration } from './owner-acceptance.js';
|
|
23
|
+
import { GoalOwnerAcceptanceStore } from './owner-acceptance-store.js';
|
|
20
24
|
export const Config = Schema.object({
|
|
21
25
|
databasePath: Schema.string().default(join(homedir(), '.dsh', 'assistant-goals.sqlite')),
|
|
22
26
|
preauthorizedCreateMaxRounds: Schema.number().step(1).min(0).max(32).default(0),
|
|
23
27
|
preauthorizedSchedule: Schema.boolean().default(false),
|
|
24
28
|
eventWaits: Schema.boolean().default(false),
|
|
29
|
+
ownerIntent: Schema.boolean().default(false),
|
|
30
|
+
ownerPreparedAcceptance: Schema.boolean().default(false),
|
|
25
31
|
maxContextChars: Schema.number().step(1).min(1024).max(65536).default(12000),
|
|
26
32
|
verifyNativeRounds: Schema.boolean().default(false),
|
|
27
33
|
verifyGoalOutcome: Schema.boolean().default(false),
|
|
@@ -280,6 +286,7 @@ export class AssistantGoalsService extends Service {
|
|
|
280
286
|
#active = true;
|
|
281
287
|
#maxChars;
|
|
282
288
|
#createMaxRounds;
|
|
289
|
+
#stepMaxDurationMs = 60_000;
|
|
283
290
|
#preauthorizedSchedule = false;
|
|
284
291
|
preauthorizedCreateEnabled;
|
|
285
292
|
preauthorizedScheduleEnabled;
|
|
@@ -291,8 +298,17 @@ export class AssistantGoalsService extends Service {
|
|
|
291
298
|
#wake;
|
|
292
299
|
#outcome;
|
|
293
300
|
#eventWait;
|
|
301
|
+
/** Set only when owner intent freezing is enabled; every Goal still needs its own frozen proof. */
|
|
302
|
+
#ownerIntent;
|
|
303
|
+
/** Set only when Host-prepared owner acceptance is enabled; every goal needs its own seal. */
|
|
304
|
+
#ownerAcceptance;
|
|
305
|
+
/** The current Host producer registration; seal requests are rejected without it. */
|
|
306
|
+
#acceptanceRegistration;
|
|
307
|
+
/** Native create emits goal/changed synchronously; this marker defers legacy binding. */
|
|
308
|
+
#preparingPreparedCreation = false;
|
|
294
309
|
#ownerRepairBindings = new Map();
|
|
295
310
|
eventWaitsEnabled;
|
|
311
|
+
ownerAcceptanceEnabled;
|
|
296
312
|
#ownerGoalOutcomeFeedbackTargets = new WeakMap();
|
|
297
313
|
constructor(ctx, input = {}) {
|
|
298
314
|
super(ctx, 'assistantGoals');
|
|
@@ -308,6 +324,7 @@ export class AssistantGoalsService extends Service {
|
|
|
308
324
|
const duration = input.stepMaxDurationMs ?? 60000;
|
|
309
325
|
if (!Number.isSafeInteger(duration) || duration < 1 || duration > 300000 || (input.verifyNativeRounds !== undefined && typeof input.verifyNativeRounds !== 'boolean'))
|
|
310
326
|
throw new Error('assistant-goals: invalid execution limits');
|
|
327
|
+
this.#stepMaxDurationMs = duration;
|
|
311
328
|
const budget = input.executionBudget === undefined ? undefined : validateGoalBudgetConfig(input.executionBudget);
|
|
312
329
|
if (budget !== undefined && input.verifyNativeRounds !== true)
|
|
313
330
|
throw new Error('assistant-goals: execution budget requires verified native rounds');
|
|
@@ -334,9 +351,49 @@ export class AssistantGoalsService extends Service {
|
|
|
334
351
|
Object.defineProperty(this, 'eventWaitsEnabled', { value: input.eventWaits === true, enumerable: true, writable: false, configurable: false });
|
|
335
352
|
if (this.eventWaitsEnabled && (wake === undefined || budget === undefined || path === ':memory:' || input.verifyGoalOutcome !== true))
|
|
336
353
|
throw new Error('assistant-goals: event waits require durable wake, budgets and verified outcomes');
|
|
354
|
+
if (input.ownerIntent !== undefined && typeof input.ownerIntent !== 'boolean')
|
|
355
|
+
throw new Error('assistant-goals: ownerIntent must be boolean');
|
|
356
|
+
const ownerIntent = input.ownerIntent === true;
|
|
357
|
+
// The side ledger must be a durable private file: an in-memory goal database
|
|
358
|
+
// cannot prove its intent ledger survived the exact owner request.
|
|
359
|
+
if (ownerIntent && path === ':memory:')
|
|
360
|
+
throw new Error('assistant-goals: owner intent requires a durable private database file');
|
|
361
|
+
if (input.ownerPreparedAcceptance !== undefined && typeof input.ownerPreparedAcceptance !== 'boolean')
|
|
362
|
+
throw new Error('assistant-goals: ownerPreparedAcceptance must be boolean');
|
|
363
|
+
const ownerPreparedAcceptance = input.ownerPreparedAcceptance === true;
|
|
364
|
+
if (ownerPreparedAcceptance && path === ':memory:')
|
|
365
|
+
throw new Error('assistant-goals: owner prepared acceptance requires a durable private database file');
|
|
366
|
+
Object.defineProperty(this, 'ownerAcceptanceEnabled', { value: ownerPreparedAcceptance, enumerable: true, writable: false, configurable: false });
|
|
337
367
|
this.#store = new GoalStore(path);
|
|
338
|
-
|
|
339
|
-
|
|
368
|
+
if (ownerIntent) {
|
|
369
|
+
try {
|
|
370
|
+
this.#ownerIntent = { store: new GoalOwnerIntentStore(`${path}.owner-intents`) };
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
this.#store.close();
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (ownerPreparedAcceptance) {
|
|
378
|
+
try {
|
|
379
|
+
this.#ownerAcceptance = { store: new GoalOwnerAcceptanceStore(`${path}.owner-acceptance`) };
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
this.#ownerIntent?.store.close();
|
|
383
|
+
this.#store.close();
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
ctx.effect(() => () => {
|
|
388
|
+
this.#active = false;
|
|
389
|
+
this.#acceptanceRegistration = undefined;
|
|
390
|
+
for (const binding of this.#ownerRepairBindings.values())
|
|
391
|
+
binding.dispose();
|
|
392
|
+
this.#ownerRepairBindings.clear();
|
|
393
|
+
this.#store.close();
|
|
394
|
+
this.#ownerIntent?.store.close();
|
|
395
|
+
this.#ownerAcceptance?.store.close();
|
|
396
|
+
}, 'assistant-goals.store');
|
|
340
397
|
this.#execution = new GoalExecutionRuntime(ctx, input.verifyNativeRounds === true ? (path === ':memory:' ? path : `${path}.executions`) : undefined, duration, (agent, consume = true) => {
|
|
341
398
|
const scope = this.#scope(agent, 'execute', consume);
|
|
342
399
|
const record = this.#observe(agent, false, consume);
|
|
@@ -346,7 +403,7 @@ export class AssistantGoalsService extends Service {
|
|
|
346
403
|
}, input.verifyGoalOutcome === true ? {
|
|
347
404
|
prepare: (agent, run) => this.#outcome.prepare(agent, run),
|
|
348
405
|
settled: (agent, run, assertCurrent) => this.#outcome.settled(agent, run, assertCurrent),
|
|
349
|
-
} : undefined, record => this.#assertDependencies(record));
|
|
406
|
+
} : undefined, record => this.#assertDependencies(record), () => this.#ownerAcceptance !== undefined, record => this.#ownerAcceptance === undefined ? undefined : this.#sealFor(record));
|
|
350
407
|
if (input.verifyGoalOutcome === true)
|
|
351
408
|
this.#outcome = new GoalOutcomeRuntime(ctx, `${path}.outcomes`, agent => {
|
|
352
409
|
this.#scope(agent, 'execute');
|
|
@@ -355,7 +412,7 @@ export class AssistantGoalsService extends Service {
|
|
|
355
412
|
throw new Error('assistant-goals: current whole-goal definition required');
|
|
356
413
|
return record;
|
|
357
414
|
}, this.#execution.list, duration, record => this.#assertDependencies(record), (record, run, agent) => this.#eventWait?.acceptsPausedOutcomeSettlement(record, run) === true
|
|
358
|
-
&& this.#execution.acceptsPausedEventWaitSettlement(record, run, agent), (record, wake, run) => this.#eventWait?.acceptsHistoricalPausedOutcomeCompletion(record, wake, run) === true);
|
|
415
|
+
&& this.#execution.acceptsPausedEventWaitSettlement(record, run, agent), (record, wake, run) => this.#eventWait?.acceptsHistoricalPausedOutcomeCompletion(record, wake, run) === true, () => this.#ownerAcceptance !== undefined, record => this.#ownerAcceptance === undefined ? undefined : this.#sealFor(record));
|
|
359
416
|
if (this.#outcome !== undefined)
|
|
360
417
|
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
361
418
|
signal.throwIfAborted();
|
|
@@ -366,7 +423,7 @@ export class AssistantGoalsService extends Service {
|
|
|
366
423
|
return await next();
|
|
367
424
|
});
|
|
368
425
|
if (budget !== undefined)
|
|
369
|
-
this.#budget = new GoalBudgetRuntime(ctx, path === ':memory:' ? path : `${path}.budgets`, budget, this.#execution.budgetState);
|
|
426
|
+
this.#budget = new GoalBudgetRuntime(ctx, path === ':memory:' ? path : `${path}.budgets`, budget, this.#execution.budgetState, record => goalOwnerIntentSupplier(this.#ownerIntent?.store, record));
|
|
370
427
|
if (strategy)
|
|
371
428
|
ctx.inject(['subagents'], runtime => {
|
|
372
429
|
const instance = new GoalStrategyRuntime(runtime, `${path}.strategies`, strategy, {
|
|
@@ -438,7 +495,8 @@ export class AssistantGoalsService extends Service {
|
|
|
438
495
|
this.#clear(agent, change.ref.id, change.ref.revision);
|
|
439
496
|
else {
|
|
440
497
|
const record = this.#observe(agent, change.operation === 'create');
|
|
441
|
-
if (record !== undefined &&
|
|
498
|
+
if (record !== undefined && !this.#preparingPreparedCreation
|
|
499
|
+
&& (change.operation === 'create' || change.operation === 'edit'))
|
|
442
500
|
this.#bindOutcome(agent, record);
|
|
443
501
|
}
|
|
444
502
|
this.#eventWait?.reconcile();
|
|
@@ -515,6 +573,15 @@ export class AssistantGoalsService extends Service {
|
|
|
515
573
|
#bindOutcome(agent, record) {
|
|
516
574
|
if (this.#outcome === undefined)
|
|
517
575
|
return;
|
|
576
|
+
if (this.#ownerAcceptance !== undefined) {
|
|
577
|
+
// Prepared acceptance is sealed by the Host after the business goal
|
|
578
|
+
// exists; never silently fall back to the legacy profile binding here.
|
|
579
|
+
const seal = this.#sealFor(record);
|
|
580
|
+
if (seal === undefined)
|
|
581
|
+
return;
|
|
582
|
+
this.#outcome.bindPrepared(record, seal);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
518
585
|
this.#requireOwnerTurn(agent, record.scope);
|
|
519
586
|
this.#outcome.bind(record);
|
|
520
587
|
}
|
|
@@ -694,6 +761,10 @@ export class AssistantGoalsService extends Service {
|
|
|
694
761
|
const scope = this.#scope(execution.agent, 'create', false);
|
|
695
762
|
this.#scope(execution.agent, 'observe', false);
|
|
696
763
|
this.#requireOwnerTurn(execution.agent, scope);
|
|
764
|
+
// Preauthorization is a read-only prediction of the same frozen proof the
|
|
765
|
+
// real execution will require; it never writes or consumes a budget.
|
|
766
|
+
if (this.#ownerIntent !== undefined)
|
|
767
|
+
this.#freezeOwnerIntent(execution.agent, execution, objective, scope);
|
|
697
768
|
this.#outcome.preflight(scope, objective);
|
|
698
769
|
const verifier = this.ctx.get('assistantVerifier', false);
|
|
699
770
|
return ['goal-step', 'goal-outcome'].every(taskKind => {
|
|
@@ -728,7 +799,15 @@ export class AssistantGoalsService extends Service {
|
|
|
728
799
|
this.#outcome?.preflight(scope, objective.trim());
|
|
729
800
|
// Delivery retains its real source kind. Its current owner-turn proof is the
|
|
730
801
|
// Host authority for this bridge; never forge a native direct-user event.
|
|
731
|
-
|
|
802
|
+
// Native create emits goal/changed synchronously; prepared mode must not
|
|
803
|
+
// attempt its legacy whole-goal binding before the Host seals acceptance.
|
|
804
|
+
this.#preparingPreparedCreation = this.#ownerAcceptance !== undefined;
|
|
805
|
+
try {
|
|
806
|
+
native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
this.#preparingPreparedCreation = false;
|
|
810
|
+
}
|
|
732
811
|
try {
|
|
733
812
|
const record = this.#observe(agent, true);
|
|
734
813
|
if (record !== undefined) {
|
|
@@ -741,6 +820,334 @@ export class AssistantGoalsService extends Service {
|
|
|
741
820
|
? 'assistant-goals: native goal created but context could not be indexed; inspect the current native goal before retrying'
|
|
742
821
|
: 'assistant-goals: native goal created but context or whole-goal acceptance is unavailable; inspect the current native goal and exact acceptance profile before retrying');
|
|
743
822
|
};
|
|
823
|
+
/**
|
|
824
|
+
* Create one goal from the live authenticated owner tool call after freezing
|
|
825
|
+
* the real owner text, supplier and call identity. Disabled (undefined
|
|
826
|
+
* ledger) keeps the historical create path; enabled means every native goal
|
|
827
|
+
* creation requires a fresh Delivery proof and fails closed otherwise.
|
|
828
|
+
*/
|
|
829
|
+
createFromOwnerIntent = (execution, objective, maxGoalRounds, acceptance) => {
|
|
830
|
+
if (this.#ownerAcceptance !== undefined)
|
|
831
|
+
return this.#createFromOwnerIntentPrepared(execution, objective, maxGoalRounds, acceptance);
|
|
832
|
+
if (acceptance !== undefined)
|
|
833
|
+
throw new Error('assistant-goals: owner-prepared acceptance is not enabled; omit acceptance or enable ownerPreparedAcceptance');
|
|
834
|
+
if (this.#ownerIntent === undefined)
|
|
835
|
+
return this.create(execution.agent, objective, maxGoalRounds);
|
|
836
|
+
const agent = execution.agent;
|
|
837
|
+
if (agent === undefined)
|
|
838
|
+
throw new Error('assistant-goals: owner intent requires the live owner tool call');
|
|
839
|
+
if (this.#createMaxRounds > 0) {
|
|
840
|
+
maxGoalRounds ??= this.#createMaxRounds;
|
|
841
|
+
if (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1 || maxGoalRounds > this.#createMaxRounds)
|
|
842
|
+
throw new Error('assistant-goals: configured creation round limit exceeded');
|
|
843
|
+
}
|
|
844
|
+
const scope = this.#scope(agent, 'create');
|
|
845
|
+
this.#scope(agent, 'observe');
|
|
846
|
+
this.#requireOwnerTurn(agent, scope);
|
|
847
|
+
if (typeof objective !== 'string' || objective.trim().length === 0 || objective.length > 16384)
|
|
848
|
+
throw new Error('assistant-goals: invalid objective');
|
|
849
|
+
if (maxGoalRounds !== undefined && (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1))
|
|
850
|
+
throw new Error('assistant-goals: invalid round limit');
|
|
851
|
+
const intent = this.#freezeOwnerIntent(agent, execution, objective, scope);
|
|
852
|
+
const native = this.ctx.get('goals');
|
|
853
|
+
if (native === undefined)
|
|
854
|
+
throw new Error('assistant-goals: native goal service unavailable');
|
|
855
|
+
// Match the native domain's normalization before looking up exact profiles.
|
|
856
|
+
this.#outcome?.preflight(scope, objective.trim());
|
|
857
|
+
this.#preparingPreparedCreation = this.#ownerAcceptance !== undefined;
|
|
858
|
+
try {
|
|
859
|
+
native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
this.#preparingPreparedCreation = false;
|
|
863
|
+
}
|
|
864
|
+
let record;
|
|
865
|
+
try {
|
|
866
|
+
record = this.#observe(agent, true);
|
|
867
|
+
if (record !== undefined)
|
|
868
|
+
this.#bindOutcome(agent, record);
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
record = undefined;
|
|
872
|
+
}
|
|
873
|
+
if (record === undefined) {
|
|
874
|
+
// Never attach the frozen intent to a goal we could not read back.
|
|
875
|
+
throw new Error(this.#outcome === undefined
|
|
876
|
+
? 'assistant-goals: native goal created but context could not be indexed; inspect the current native goal before retrying'
|
|
877
|
+
: 'assistant-goals: native goal created but context or whole-goal acceptance is unavailable; inspect the current native goal and exact acceptance profile before retrying');
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
this.#ownerIntent.store.record(intent, record.id, record.definition.digest);
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
// The native goal must never survive without its frozen proof. Roll it
|
|
884
|
+
// back and report which outcome the caller must inspect.
|
|
885
|
+
const rolledBack = this.#rollbackCreatedGoal(agent, record);
|
|
886
|
+
throw new Error(rolledBack
|
|
887
|
+
? 'assistant-goals: native goal created but the owner intent could not be recorded; the native goal was rolled back'
|
|
888
|
+
: 'assistant-goals: native goal created without a frozen owner intent; inspect and clear it before use', { cause: error });
|
|
889
|
+
}
|
|
890
|
+
return record;
|
|
891
|
+
};
|
|
892
|
+
/** Owner-turn creation that also seals the protected acceptance before returning. */
|
|
893
|
+
#createFromOwnerIntentPrepared(execution, objective, maxGoalRounds, acceptance) {
|
|
894
|
+
const agent = execution.agent;
|
|
895
|
+
if (agent === undefined)
|
|
896
|
+
throw new Error('assistant-goals: owner intent requires the live owner tool call');
|
|
897
|
+
if (acceptance === undefined)
|
|
898
|
+
throw new Error('assistant-goals: owner-prepared acceptance requires explicit criteria');
|
|
899
|
+
const input = resolveOwnerPreparedAcceptanceInput(acceptance, this.#stepMaxDurationMs);
|
|
900
|
+
// Fail before native creation when the verifier side cannot seal at all.
|
|
901
|
+
this.#sealCapability();
|
|
902
|
+
if (this.#createMaxRounds > 0) {
|
|
903
|
+
maxGoalRounds ??= this.#createMaxRounds;
|
|
904
|
+
if (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1 || maxGoalRounds > this.#createMaxRounds)
|
|
905
|
+
throw new Error('assistant-goals: configured creation round limit exceeded');
|
|
906
|
+
}
|
|
907
|
+
const scope = this.#scope(agent, 'create');
|
|
908
|
+
this.#scope(agent, 'observe');
|
|
909
|
+
this.#requireOwnerTurn(agent, scope);
|
|
910
|
+
if (typeof objective !== 'string' || objective.trim().length === 0 || objective.length > 16384)
|
|
911
|
+
throw new Error('assistant-goals: invalid objective');
|
|
912
|
+
if (maxGoalRounds !== undefined && (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1))
|
|
913
|
+
throw new Error('assistant-goals: invalid round limit');
|
|
914
|
+
const intent = this.#ownerIntent === undefined ? undefined : this.#freezeOwnerIntent(agent, execution, objective, scope);
|
|
915
|
+
const native = this.ctx.get('goals');
|
|
916
|
+
if (native === undefined)
|
|
917
|
+
throw new Error('assistant-goals: native goal service unavailable');
|
|
918
|
+
// Match the native domain's normalization before looking up exact profiles.
|
|
919
|
+
this.#outcome?.preflight(scope, objective.trim());
|
|
920
|
+
this.#preparingPreparedCreation = true;
|
|
921
|
+
try {
|
|
922
|
+
native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
this.#preparingPreparedCreation = false;
|
|
926
|
+
}
|
|
927
|
+
let record;
|
|
928
|
+
try {
|
|
929
|
+
record = this.#observe(agent, true);
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
record = undefined;
|
|
933
|
+
}
|
|
934
|
+
if (record === undefined) {
|
|
935
|
+
const rolledBack = this.#rollbackCurrentGoal(agent);
|
|
936
|
+
throw new Error(rolledBack
|
|
937
|
+
? 'assistant-goals: native goal created but context could not be indexed; the native goal was rolled back'
|
|
938
|
+
: 'assistant-goals: native goal created but context could not be indexed; inspect and clear the current native goal before retrying');
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
this.#sealOwnerAcceptanceRecord(record, input);
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
const rolledBack = this.#rollbackCreatedGoal(agent, record);
|
|
945
|
+
throw new Error(rolledBack
|
|
946
|
+
? 'assistant-goals: native goal created but the owner acceptance could not be sealed; the native goal was rolled back'
|
|
947
|
+
: 'assistant-goals: native goal created without a sealed owner acceptance; inspect and clear it before use', { cause: error });
|
|
948
|
+
}
|
|
949
|
+
if (intent !== undefined) {
|
|
950
|
+
try {
|
|
951
|
+
this.#ownerIntent.store.record(intent, record.id, record.definition.digest);
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
const rolledBack = this.#rollbackCreatedGoal(agent, record);
|
|
955
|
+
throw new Error(rolledBack
|
|
956
|
+
? 'assistant-goals: native goal created but the owner intent could not be recorded; the native goal was rolled back'
|
|
957
|
+
: 'assistant-goals: native goal created without a frozen owner intent; inspect and clear it before use', { cause: error });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return record;
|
|
961
|
+
}
|
|
962
|
+
/** Clear a just-created native goal when the business record could not be observed. */
|
|
963
|
+
#rollbackCurrentGoal(agent) {
|
|
964
|
+
try {
|
|
965
|
+
const native = this.ctx.get('goals');
|
|
966
|
+
if (native === undefined)
|
|
967
|
+
return false;
|
|
968
|
+
const current = native.get(agent);
|
|
969
|
+
if (current === undefined)
|
|
970
|
+
return true;
|
|
971
|
+
native.clear(agent, { id: current.id, revision: current.revision });
|
|
972
|
+
return native.get(agent) === undefined;
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
#freezeOwnerIntent(agent, execution, objective, scope) {
|
|
979
|
+
const delivery = this.ctx.get('assistantDelivery');
|
|
980
|
+
if (delivery === undefined || typeof delivery.inspectOwnerForegroundToolCallForAgent !== 'function'
|
|
981
|
+
|| typeof delivery.readOwnerForegroundToolInputForAgent !== 'function')
|
|
982
|
+
throw new Error('assistant-goals: owner intent proof is unavailable');
|
|
983
|
+
const intent = buildGoalOwnerIntent({ delivery, agent, execution, scope });
|
|
984
|
+
// The model selects only words already present verbatim in the owner input;
|
|
985
|
+
// the frozen span must be exactly the span of the live objective argument.
|
|
986
|
+
const span = ownerIntentObjectiveSpan(intent.text, objective);
|
|
987
|
+
if (objective !== intent.objective.text || span.start !== intent.objective.start || span.end !== intent.objective.end) {
|
|
988
|
+
throw new Error("assistant-goals: objective must quote the owner's original words verbatim");
|
|
989
|
+
}
|
|
990
|
+
return intent;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Read-only catalog of installed acceptance authorities for the caller's
|
|
994
|
+
* owner scope. The verifier keeps the authority list; Goals only proxies it
|
|
995
|
+
* inside a model tool that is registered in owner-prepared mode.
|
|
996
|
+
*/
|
|
997
|
+
inspectAcceptanceAuthorities = (agent) => {
|
|
998
|
+
const scope = this.#scope(agent, 'inspect');
|
|
999
|
+
const verifier = this.ctx.get('assistantVerifier', false);
|
|
1000
|
+
if (verifier === undefined || typeof verifier.inspectAcceptanceAuthorities !== 'function') {
|
|
1001
|
+
throw new Error('assistant-goals: acceptance authority catalog is unavailable; the verifier peer does not expose it');
|
|
1002
|
+
}
|
|
1003
|
+
return verifier.inspectAcceptanceAuthorities({ scope: { workspace: scope.workspace, preset: scope.preset },
|
|
1004
|
+
owner: { principalRecordId: scope.principalRecordId, principalVersion: scope.principalVersion } });
|
|
1005
|
+
};
|
|
1006
|
+
/**
|
|
1007
|
+
* Host-only read of the frozen owner intent for one owner-scoped business
|
|
1008
|
+
* goal. A stored row whose definition digest no longer matches the current
|
|
1009
|
+
* goal definition is a stale binding and returns undefined instead of
|
|
1010
|
+
* silently authorizing a different objective.
|
|
1011
|
+
*/
|
|
1012
|
+
ownerIntent = (agent, goalId) => {
|
|
1013
|
+
const store = this.#ownerIntent?.store;
|
|
1014
|
+
if (store === undefined)
|
|
1015
|
+
return undefined;
|
|
1016
|
+
const scope = this.#scope(agent, 'inspect');
|
|
1017
|
+
const record = this.#store.get(scope, goalId);
|
|
1018
|
+
if (record === undefined)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
const stored = store.inspect(goalId);
|
|
1021
|
+
if (stored === undefined || stored.definitionDigest !== record.definition.digest)
|
|
1022
|
+
return undefined;
|
|
1023
|
+
return stored.intent;
|
|
1024
|
+
};
|
|
1025
|
+
/** Best-effort clear of a just-created native goal whose proof could not be frozen. */
|
|
1026
|
+
#rollbackCreatedGoal(agent, record) {
|
|
1027
|
+
try {
|
|
1028
|
+
const native = this.ctx.get('goals');
|
|
1029
|
+
if (native === undefined)
|
|
1030
|
+
return false;
|
|
1031
|
+
const current = native.get(agent);
|
|
1032
|
+
if (current === undefined)
|
|
1033
|
+
return true;
|
|
1034
|
+
if (String(current.id) !== record.native.goalId)
|
|
1035
|
+
return false;
|
|
1036
|
+
native.clear(agent, { id: current.id, revision: current.revision });
|
|
1037
|
+
return native.get(agent) === undefined;
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/** Current seal for one business goal; a stale definition digest fails closed. */
|
|
1044
|
+
#sealFor(record) {
|
|
1045
|
+
const store = this.#ownerAcceptance?.store;
|
|
1046
|
+
if (store === undefined)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
const seal = store.get(record.id);
|
|
1049
|
+
if (seal === undefined)
|
|
1050
|
+
return undefined;
|
|
1051
|
+
if (seal.definition.version !== record.definition.version || seal.definition.digest !== record.definition.digest) {
|
|
1052
|
+
throw new Error('assistant-goals: sealed owner acceptance is stale for the current goal definition; create a new goal');
|
|
1053
|
+
}
|
|
1054
|
+
return seal;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Host-only: seal the protected acceptance for one owner-scoped business goal
|
|
1058
|
+
* before its first native round. Criteria are Host-prepared and never reachable
|
|
1059
|
+
* from model tool arguments; a goal in prepared mode cannot run unsealed.
|
|
1060
|
+
*/
|
|
1061
|
+
sealOwnerAcceptance = (agent, goalId, value) => {
|
|
1062
|
+
const scope = this.#scope(agent, 'create');
|
|
1063
|
+
const record = this.#store.get(scope, goalId);
|
|
1064
|
+
if (record === undefined)
|
|
1065
|
+
throw new Error('assistant-goals: goal not found');
|
|
1066
|
+
if (record.native.phase !== 'active' && record.native.phase !== 'paused')
|
|
1067
|
+
throw new Error('assistant-goals: only an active or paused goal can seal owner acceptance');
|
|
1068
|
+
const input = validateOwnerPreparedAcceptanceInput(value, this.#stepMaxDurationMs);
|
|
1069
|
+
const capability = this.#sealCapability();
|
|
1070
|
+
const stepCriteria = input.stepCriteria ?? input.criteria;
|
|
1071
|
+
const existing = capability.store.get(record.id);
|
|
1072
|
+
if (existing !== undefined) {
|
|
1073
|
+
const same = existing.definition.version === record.definition.version && existing.definition.digest === record.definition.digest
|
|
1074
|
+
&& acceptanceDigest(existing.criteria) === acceptanceDigest(input.criteria)
|
|
1075
|
+
&& acceptanceDigest(existing.stepCriteria) === acceptanceDigest(stepCriteria)
|
|
1076
|
+
&& acceptanceDigest(existing.bounds) === acceptanceDigest(input.bounds)
|
|
1077
|
+
&& existing.validityMs === input.validityMs && acceptanceDigest(existing.preparer) === acceptanceDigest(input.preparer);
|
|
1078
|
+
if (!same)
|
|
1079
|
+
throw new Error('assistant-goals: owner acceptance is already sealed for this goal with different criteria; create a new goal to change it');
|
|
1080
|
+
this.#outcome.bindPrepared(record, existing);
|
|
1081
|
+
return this.#sealView(existing);
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
return this.#sealView(this.#sealOwnerAcceptanceRecord(record, input));
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
// An unsealed goal cannot run in prepared mode; never leave it silently usable.
|
|
1088
|
+
const rolledBack = this.#rollbackCreatedGoal(agent, record);
|
|
1089
|
+
throw new Error(rolledBack
|
|
1090
|
+
? 'assistant-goals: owner acceptance could not be sealed; the native goal was rolled back'
|
|
1091
|
+
: 'assistant-goals: owner acceptance could not be sealed; the native goal remains without a sealed owner acceptance and must be inspected and cleared before use', { cause: error });
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
#sealView(seal) {
|
|
1095
|
+
return Object.freeze({ goalId: seal.goalId, definitionDigest: seal.definition.digest,
|
|
1096
|
+
outcome: Object.freeze({ contractId: seal.outcome.contractId, contractDigest: seal.outcome.contractDigest }),
|
|
1097
|
+
expiresAt: seal.outcomeExpiresAt });
|
|
1098
|
+
}
|
|
1099
|
+
/** Resolve the live verifier capability required to seal prepared acceptance. */
|
|
1100
|
+
#sealCapability() {
|
|
1101
|
+
const store = this.#ownerAcceptance?.store;
|
|
1102
|
+
if (store === undefined)
|
|
1103
|
+
throw new Error('assistant-goals: owner prepared acceptance is not enabled');
|
|
1104
|
+
const registration = this.#acceptanceRegistration;
|
|
1105
|
+
if (registration === undefined)
|
|
1106
|
+
throw new Error('assistant-goals: owner prepared acceptance requires the goal verifier registration');
|
|
1107
|
+
if (this.#outcome === undefined)
|
|
1108
|
+
throw new Error('assistant-goals: owner prepared acceptance requires whole-goal verification');
|
|
1109
|
+
const verifier = this.ctx.get('assistantVerifier', false);
|
|
1110
|
+
const prepared = preparedRegistration(registration);
|
|
1111
|
+
if (prepared === undefined)
|
|
1112
|
+
throw new Error('assistant-goals: the goal verifier does not support owner-prepared acceptance');
|
|
1113
|
+
return { store, prepared, verifier };
|
|
1114
|
+
}
|
|
1115
|
+
/** Seal one prepared outcome contract and persist it before the first round. */
|
|
1116
|
+
#sealOwnerAcceptanceRecord(record, input) {
|
|
1117
|
+
const capability = this.#sealCapability();
|
|
1118
|
+
const stepCriteria = input.stepCriteria ?? input.criteria;
|
|
1119
|
+
const assessmentId = `goal-assessment-${acceptanceDigest([record.scope, record.id, record.definition, 'initial'])}`;
|
|
1120
|
+
const task = { kind: 'goal-outcome', ref: assessmentId, goal: {
|
|
1121
|
+
id: record.id, definitionVersion: record.definition.version, definitionDigest: record.definition.digest,
|
|
1122
|
+
assessmentId, sessionId: record.native.sessionId, nativeGoalId: record.native.goalId
|
|
1123
|
+
} };
|
|
1124
|
+
const request = Object.freeze({
|
|
1125
|
+
scope: Object.freeze({ workspace: record.scope.workspace, preset: record.scope.preset }),
|
|
1126
|
+
owner: Object.freeze({ principalRecordId: record.scope.principalRecordId, principalVersion: record.scope.principalVersion }),
|
|
1127
|
+
task, objective: record.definition.objective, criteria: input.criteria, bounds: input.bounds,
|
|
1128
|
+
validityMs: input.validityMs, preparer: input.preparer,
|
|
1129
|
+
});
|
|
1130
|
+
const handle = capability.prepared.prepareOwnerContract(request);
|
|
1131
|
+
const contract = capability.verifier?.inspectAcceptedTask?.(handle.contractId)?.contract;
|
|
1132
|
+
if (contract === undefined || contract.id !== handle.contractId || contract.digest !== handle.contractDigest
|
|
1133
|
+
|| contract.task.kind !== 'goal-outcome' || acceptanceDigest(contract.task) !== acceptanceDigest(task)
|
|
1134
|
+
|| contract.objective !== record.definition.objective
|
|
1135
|
+
|| acceptanceDigest(contract.criteria) !== acceptanceDigest(input.criteria)
|
|
1136
|
+
|| acceptanceDigest(contract.bounds) !== acceptanceDigest(input.bounds)
|
|
1137
|
+
|| contract.profile.id !== 'owner-prepared' || contract.profile.version !== 1
|
|
1138
|
+
|| contract.expiresAt <= Date.now())
|
|
1139
|
+
throw new Error('assistant-goals: sealed owner acceptance readback failed');
|
|
1140
|
+
const seal = validateOwnerAcceptanceSeal({
|
|
1141
|
+
goalId: record.id, definition: record.definition, objective: record.definition.objective,
|
|
1142
|
+
criteria: input.criteria, stepCriteria, bounds: input.bounds, validityMs: input.validityMs,
|
|
1143
|
+
preparer: input.preparer, outcome: handle, preparedProfileDigest: contract.profile.digest,
|
|
1144
|
+
outcomeExpiresAt: contract.expiresAt, at: Date.now(),
|
|
1145
|
+
});
|
|
1146
|
+
// Bind the frozen definition before persisting the seal: either both land or
|
|
1147
|
+
// the caller rolls the native goal back, never a half-sealed goal.
|
|
1148
|
+
this.#outcome.bindPrepared(record, seal);
|
|
1149
|
+
return capability.store.record(seal);
|
|
1150
|
+
}
|
|
744
1151
|
control = (agent, value) => {
|
|
745
1152
|
const input = this.#controlInput(value);
|
|
746
1153
|
const scope = this.#scope(agent, input.operation);
|
|
@@ -749,6 +1156,12 @@ export class AssistantGoalsService extends Service {
|
|
|
749
1156
|
const record = this.#store.get(scope, input.goalId);
|
|
750
1157
|
if (record === undefined)
|
|
751
1158
|
throw new Error('assistant-goals: goal not found');
|
|
1159
|
+
// A frozen owner intent binds the objective to the authenticated owner's
|
|
1160
|
+
// words. Editing it would silently break that proof and orphan the ledger
|
|
1161
|
+
// row, so the caller must create a new goal instead.
|
|
1162
|
+
if (input.operation === 'edit' && this.#ownerIntent?.store.inspect(input.goalId) !== undefined) {
|
|
1163
|
+
throw new Error('assistant-goals: the objective is bound to the authenticated owner request; create a new goal to change it');
|
|
1164
|
+
}
|
|
752
1165
|
const native = this.ctx.get('goals');
|
|
753
1166
|
if (native === undefined)
|
|
754
1167
|
throw new Error('assistant-goals: native goal service unavailable');
|
|
@@ -2024,7 +2437,13 @@ export class AssistantGoalsService extends Service {
|
|
|
2024
2437
|
execution();
|
|
2025
2438
|
throw error;
|
|
2026
2439
|
}
|
|
2027
|
-
|
|
2440
|
+
this.#acceptanceRegistration = registration;
|
|
2441
|
+
return () => {
|
|
2442
|
+
if (this.#acceptanceRegistration === registration)
|
|
2443
|
+
this.#acceptanceRegistration = undefined;
|
|
2444
|
+
outcome?.();
|
|
2445
|
+
execution();
|
|
2446
|
+
};
|
|
2028
2447
|
};
|
|
2029
2448
|
currentArtifactAdmission = (agent) => this.#execution.currentArtifactAdmission(agent);
|
|
2030
2449
|
inspectAcceptedArtifactSource = (contract) => contract.task.kind === 'goal-outcome'
|