@dsh-enhanced/assistant-evaluation 0.1.66 → 0.1.67

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/lib/service.js CHANGED
@@ -4,7 +4,7 @@ import { acceptanceDigest, goalDefinitionSituation, validateTaskAcceptanceContra
4
4
  import { EvaluationStore, EvaluationStoreError, canonicalEvaluationScope, evaluationLearningProjectionDigest, } from './store.js';
5
5
  import { registerEvaluationTools } from './tools.js';
6
6
  import { normalizeAttentionCohort, } from './attention-tuning.js';
7
- import { ATTENTION_OWNER_QUESTION_SOURCE_ID, attentionOwnerQuestionIdempotencyKey, attentionOwnerQuestionText, } from './attention-owner-questions.js';
7
+ import { ATTENTION_OWNER_QUESTION_SOURCE_ID, attentionOwnerQuestionIdempotencyKey, attentionOwnerQuestionText, parseAttentionOwnerQuestionChoice, ATTENTION_OWNER_QUESTION_REPLY_PROTOCOL, } from './attention-owner-questions.js';
8
8
  import { validateWorkItemOwnerFeedbackProof } from './work-item-owner-proof.js';
9
9
  import { computeIntakeAttentionAgreement, INTAKE_ATTENTION_AGREEMENT_SCAN_LIMIT, isIntakeProvenanceSource } from './intake-attention-agreement.js';
10
10
  import { executionStatuses, objectiveStatuses, outcomeSourceKinds, TRUSTED_EVALUATION_PRODUCER_PROTOCOL, } from './types.js';
@@ -15,6 +15,10 @@ function isOwnerQuestionDeliveryPort(value) {
15
15
  && typeof port.validateOwnerRoute === 'function' && typeof port.resolveOwnerRoute === 'function'
16
16
  && typeof port.inspectOwnerNotification === 'function' && typeof port.registerOwnerNotificationWithdrawal === 'function';
17
17
  }
18
+ function isOwnerQuestionReplyPort(value) {
19
+ const port = value;
20
+ return typeof port === 'object' && port !== null && typeof port.registerOwnerNotificationReply === 'function';
21
+ }
18
22
  export const ASSISTANT_EVALUATION_SKILL = `# Personal assistant self-evaluation
19
23
 
20
24
  Use this workflow only to assess whether an already-finished task met its objective. A self-assessment is diagnostic evidence, never ground truth and never permission to change production behavior.
@@ -250,6 +254,8 @@ export class AssistantEvaluationService extends Service {
250
254
  lastNotificationSyncAt = Number.NEGATIVE_INFINITY;
251
255
  ownerQuestionWithdrawal;
252
256
  activeOwnerQuestionWithdrawals = new WeakSet();
257
+ ownerQuestionReply;
258
+ activeOwnerQuestionReplies = new WeakSet();
253
259
  attentionCohorts;
254
260
  active = true;
255
261
  constructor(ctx, input, options = {}) {
@@ -401,6 +407,8 @@ export class AssistantEvaluationService extends Service {
401
407
  if (this.config.attentionOwnerQuestionDeliveryEnabled) {
402
408
  this.bindOwnerQuestionWithdrawal(currentDelivery);
403
409
  ctx.inject(['assistantDelivery'], deliveryCtx => this.bindOwnerQuestionWithdrawal(deliveryCtx.get('assistantDelivery')));
410
+ this.bindOwnerQuestionReply(currentDelivery);
411
+ ctx.inject(['assistantDelivery'], deliveryCtx => this.bindOwnerQuestionReply(deliveryCtx.get('assistantDelivery')));
404
412
  }
405
413
  const currentVerifier = ctx.get('assistantVerifier');
406
414
  if (isTrustedVerifierEvaluationProducer(currentVerifier))
@@ -449,6 +457,7 @@ export class AssistantEvaluationService extends Service {
449
457
  this.projectionTimer = undefined;
450
458
  this.notificationFacts = undefined;
451
459
  this.ownerQuestionWithdrawal?.dispose();
460
+ this.ownerQuestionReply?.dispose();
452
461
  disposeCurrentProjector?.();
453
462
  this.automationBinding?.dispose();
454
463
  this.deliveryBinding?.dispose();
@@ -942,7 +951,14 @@ export class AssistantEvaluationService extends Service {
942
951
  });
943
952
  }
944
953
  #looseningOptions() {
945
- return this.config.attentionLooseningEnabled ? Object.freeze({ replayWindowMs: this.config.attentionLooseningReplayWindowMs }) : undefined;
954
+ if (!this.config.attentionLooseningEnabled)
955
+ return undefined;
956
+ return Object.freeze({
957
+ replayWindowMs: this.config.attentionLooseningReplayWindowMs,
958
+ ...(this.config.attentionOwnerQuestionDeliveryEnabled
959
+ ? { ownerQuestionReplyLoop: Object.freeze({ noticeTtlMs: this.config.attentionOwnerQuestionNoticeTtlMs }) }
960
+ : {}),
961
+ });
946
962
  }
947
963
  #attentionCohort(input) {
948
964
  let cohort;
@@ -1095,6 +1111,95 @@ export class AssistantEvaluationService extends Service {
1095
1111
  this.ownerQuestionWithdrawal = binding;
1096
1112
  return binding.dispose;
1097
1113
  }
1114
+ /**
1115
+ * Register (once per Delivery instance) the slice-5 reply consumer. A
1116
+ * Delivery without the reply port is treated as absent: slice-4 notice
1117
+ * delivery keeps working, only replies are not read. A registration error
1118
+ * only disables replies; it never blocks notice delivery.
1119
+ */
1120
+ bindOwnerQuestionReply(source) {
1121
+ if (!this.active || !isOwnerQuestionReplyPort(source))
1122
+ return undefined;
1123
+ if (this.ownerQuestionReply?.source === source)
1124
+ return undefined;
1125
+ const registration = Object.freeze({
1126
+ protocol: ATTENTION_OWNER_QUESTION_REPLY_PROTOCOL,
1127
+ authority: 'assistantEvaluation',
1128
+ sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID,
1129
+ idempotencyKeyPrefix: attentionOwnerQuestionIdempotencyKey(''),
1130
+ consume: (input) => this.#consumeOwnerQuestionReply(input),
1131
+ });
1132
+ this.activeOwnerQuestionReplies.add(registration);
1133
+ let unregister;
1134
+ try {
1135
+ unregister = source.registerOwnerNotificationReply(registration)?.unregister;
1136
+ }
1137
+ catch (error) {
1138
+ this.activeOwnerQuestionReplies.delete(registration);
1139
+ this.ctx.logger.warn(`assistant-evaluation: owner-question reply registration failed; replies are not read: ${String(error)}`);
1140
+ return undefined;
1141
+ }
1142
+ if (typeof unregister !== 'function') {
1143
+ this.activeOwnerQuestionReplies.delete(registration);
1144
+ this.ctx.logger.warn('assistant-evaluation: owner-question reply registration returned no unregister; replies are not read');
1145
+ return undefined;
1146
+ }
1147
+ const release = unregister;
1148
+ this.ownerQuestionReply?.dispose();
1149
+ let live = true;
1150
+ const binding = Object.freeze({ source, registration, dispose: () => {
1151
+ if (!live)
1152
+ return;
1153
+ live = false;
1154
+ this.activeOwnerQuestionReplies.delete(registration);
1155
+ try {
1156
+ release();
1157
+ }
1158
+ catch { /* Delivery already gone */ }
1159
+ if (this.ownerQuestionReply === binding)
1160
+ this.ownerQuestionReply = undefined;
1161
+ } });
1162
+ this.ownerQuestionReply = binding;
1163
+ return binding.dispose;
1164
+ }
1165
+ /**
1166
+ * Synchronous Delivery callback for one authenticated reply to an exact
1167
+ * owner-question notice. Only exact whole-phrase choices are accepted; a
1168
+ * recognized choice is consumed even when its question is no longer open,
1169
+ * so a stale "authorize-once" never reaches the Agent. Storage errors are
1170
+ * `transient` and leave Delivery's Inbox pending for a bounded retry.
1171
+ */
1172
+ #consumeOwnerQuestionReply(input) {
1173
+ if (!this.active || !this.config.attentionOwnerQuestionDeliveryEnabled)
1174
+ return 'ignored';
1175
+ if (typeof input?.idempotencyKey !== 'string' || typeof input.text !== 'string')
1176
+ return 'ignored';
1177
+ const prefix = attentionOwnerQuestionIdempotencyKey('');
1178
+ if (!input.idempotencyKey.startsWith(prefix))
1179
+ return 'ignored';
1180
+ const questionId = input.idempotencyKey.slice(prefix.length);
1181
+ const choice = parseAttentionOwnerQuestionChoice(input.text);
1182
+ if (choice === undefined || questionId.length === 0)
1183
+ return 'ignored';
1184
+ try {
1185
+ for (const cohort of this.attentionCohorts) {
1186
+ for (const scope of this.store.attention.scopes(cohort)) {
1187
+ const latest = this.store.attention.latestOwnerQuestion(scope, cohort);
1188
+ if (latest === undefined || latest === 'unverifiable' || latest.questionId !== questionId)
1189
+ continue;
1190
+ this.applyAttentionOwnerQuestionReply({ scope, cohort, questionId, choice });
1191
+ return 'applied';
1192
+ }
1193
+ }
1194
+ // The notice was genuinely issued under this source and key, but the
1195
+ // local ledger no longer holds that exact question: consume the
1196
+ // recognized choice instead of sending it to the Agent.
1197
+ return 'applied';
1198
+ }
1199
+ catch {
1200
+ return 'transient';
1201
+ }
1202
+ }
1098
1203
  /**
1099
1204
  * Send-time answer for one owner-question notice. It stays sendable only
1100
1205
  * while its question is still the cohort's latest, open, unanswered, inside
@@ -1171,6 +1276,9 @@ export class AssistantEvaluationService extends Service {
1171
1276
  continue;
1172
1277
  }
1173
1278
  this.bindOwnerQuestionWithdrawal(delivery);
1279
+ // A replaced Delivery instance needs the reply consumer bound as well;
1280
+ // the tick is the existing retry point, so no extra loop is added.
1281
+ this.bindOwnerQuestionReply(delivery);
1174
1282
  // Only queue (or adopt) while this service's own withdrawal check is
1175
1283
  // bound to this exact Delivery instance; otherwise a closed question
1176
1284
  // could be sent unchecked.
@@ -1232,7 +1340,7 @@ export class AssistantEvaluationService extends Service {
1232
1340
  scope: { principalId: owner.principalId, ...principal, workspace: scope.workspace, preset: scope.preset },
1233
1341
  sessionId,
1234
1342
  idempotencyKey,
1235
- text: attentionOwnerQuestionText(question, scope),
1343
+ text: attentionOwnerQuestionText(question, scope, { repliesRead: true }),
1236
1344
  expiresAt,
1237
1345
  attention: { purpose: 'result' },
1238
1346
  withdrawable: true,
@@ -1255,8 +1363,10 @@ export class AssistantEvaluationService extends Service {
1255
1363
  }
1256
1364
  /**
1257
1365
  * Host-internal record-only owner reply to the cohort's latest question.
1258
- * It never records a grant or changes a tuning version; an owner who
1259
- * authorizes still records a grant through `grantAttentionLoosening`.
1366
+ * It never records a grant or changes a tuning version, and it is not the
1367
+ * slice-5 apply path; an owner who authorizes uses
1368
+ * `applyAttentionOwnerQuestionReply` (the only path the Delivery reply
1369
+ * consumer calls) or records a host grant through `grantAttentionLoosening`.
1260
1370
  */
1261
1371
  recordAttentionOwnerQuestionReply(input) {
1262
1372
  this.assertActive();
@@ -1272,6 +1382,23 @@ export class AssistantEvaluationService extends Service {
1272
1382
  || this.now() >= latest.question.createdAt + this.config.attentionOwnerQuestionNoticeTtlMs;
1273
1383
  return this.store.attentionQuestions.recordReply(input.scope, cohort, latest.questionId, input.choice, latest.open, expired);
1274
1384
  }
1385
+ /**
1386
+ * Slice 5 evidence-backed apply path for one exact owner reply; the
1387
+ * Delivery consumer and Host callers use this instead of the record-only
1388
+ * seam. `authorize-once` records the reply and creates one bounded one-time
1389
+ * grant for exactly the questioned proposal (parent/candidate bounds, the
1390
+ * proposal's step size, one adoption); the next tuning tick still re-runs
1391
+ * the independent holdout replay before adopting. `decline` closes the
1392
+ * proposal. A proposal that already had an immutable decision returns
1393
+ * `proposal-terminal` and nothing adopts.
1394
+ */
1395
+ applyAttentionOwnerQuestionReply(input) {
1396
+ this.assertActive();
1397
+ const cohort = this.#attentionCohort(input.cohort);
1398
+ if (!this.config.attentionOwnerQuestionDeliveryEnabled)
1399
+ return Object.freeze({ status: 'rejected', reason: 'delivery-disabled' });
1400
+ return this.store.attention.applyOwnerQuestionReply(input.scope, cohort, input.questionId, input.choice, { noticeTtlMs: this.config.attentionOwnerQuestionNoticeTtlMs });
1401
+ }
1275
1402
  /** Per-question delivery decisions and recorded replies for one cohort. */
1276
1403
  attentionOwnerQuestionStatus(input) {
1277
1404
  this.assertActive();
@@ -1303,6 +1430,14 @@ export class AssistantEvaluationService extends Service {
1303
1430
  return this.active && typeof registration === 'object' && registration !== null
1304
1431
  && this.activeOwnerQuestionWithdrawals.has(registration);
1305
1432
  }
1433
+ /**
1434
+ * Delivery's slot-anchored proof that the slice-5 reply consumer under the
1435
+ * `assistantEvaluation` authority is this live service's own registration.
1436
+ */
1437
+ ownsOwnerNotificationReply(registration) {
1438
+ return this.active && typeof registration === 'object' && registration !== null
1439
+ && this.activeOwnerQuestionReplies.has(registration);
1440
+ }
1306
1441
  /** Delivery uses the same ownership boundary as Automations. */
1307
1442
  ownsTrustedDeliveryEvaluationRegistration(registration) {
1308
1443
  return this.active && typeof registration === 'object' && registration !== null