@sema-agent/client-core 0.84.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/README.md +27 -13
- package/dist/abortableSleep.d.ts +10 -0
- package/dist/abortableSleep.js +37 -0
- package/dist/adapt/arms.js +1 -1
- package/dist/adapt.d.ts +1 -1
- package/dist/adapt.js +1 -1
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +1 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +19 -10
- package/dist/adapter/runStream.js +6 -2
- package/dist/agentSession/engineSessionBackgroundStop.d.ts +50 -0
- package/dist/agentSession/engineSessionBackgroundStop.js +291 -0
- package/dist/agentsWireCaps.d.ts +2 -1
- package/dist/agentsWireCaps.js +8 -0
- package/dist/cloudConfigWireCaps.js +2 -1
- package/dist/corruptSession.d.ts +3 -0
- package/dist/corruptSession.js +29 -0
- package/dist/corruptSessionCopy.d.ts +1 -0
- package/dist/corruptSessionCopy.js +4 -0
- package/dist/decideFailureNote.d.ts +2 -1
- package/dist/decideFailureNote.js +8 -3
- package/dist/decideReceipt.d.ts +6 -0
- package/dist/decideReceipt.js +30 -1
- package/dist/detachWire.d.ts +1 -0
- package/dist/detachWire.js +13 -5
- package/dist/displayUntrusted.js +114 -18
- package/dist/effortWire.d.ts +2 -1
- package/dist/effortWire.js +2 -4
- package/dist/engineErrorCodes.d.ts +11 -0
- package/dist/engineErrorCodes.js +32 -0
- package/dist/engineNoticeCodes.d.ts +39 -4
- package/dist/engineNoticeCodes.js +117 -161
- package/dist/engineWireFor.d.ts +43 -0
- package/dist/engineWireFor.js +399 -0
- package/dist/engineWireSdk.d.ts +6 -0
- package/dist/fileHistoryCaptureCapability.d.ts +4 -1
- package/dist/fileHistoryCaptureCapability.js +43 -9
- package/dist/gateVocabulary.d.ts +1 -2
- package/dist/gateVocabulary.js +1 -22
- package/dist/generated/engineNoticeTables.d.ts +3 -0
- package/dist/generated/engineNoticeTables.js +156 -0
- package/dist/generated/toolNameTables.d.ts +2 -0
- package/dist/generated/toolNameTables.js +43 -0
- package/dist/headlessPermissionModeWire.js +2 -4
- package/dist/hitl/askGateWire.d.ts +1 -1
- package/dist/hitl/askGateWire.js +1 -1
- package/dist/hitl/frameRouter.js +4 -2
- package/dist/hitl/hitlHostSurface.js +1 -1
- package/dist/hitl/parkResolver.d.ts +1 -0
- package/dist/hitl/parkResolver.js +11 -3
- package/dist/hitl/planReviewWire.d.ts +41 -3
- package/dist/hitl/planReviewWire.js +264 -49
- package/dist/hitl/rosterMountedNames.d.ts +2 -0
- package/dist/hitl/rosterMountedNames.js +38 -0
- package/dist/hitl/sessionPolicyDeliverable.d.ts +8 -2
- package/dist/hitl/sessionPolicyDeliverable.js +39 -48
- package/dist/hitl/sessionPolicyWire.d.ts +40 -0
- package/dist/hitl/sessionPolicyWire.js +173 -0
- package/dist/hitl/toolApprovalWire.d.ts +4 -2
- package/dist/hitl/toolApprovalWire.js +6 -4
- package/dist/index.d.ts +10 -3
- package/dist/index.js +7 -2
- package/dist/liveInitToolFace.js +28 -10
- package/dist/permissionWireCaps.d.ts +2 -1
- package/dist/permissionWireCaps.js +2 -7
- package/dist/request/taskRequest.d.ts +2 -0
- package/dist/request/taskRequest.js +18 -1
- package/dist/resumeRefusalCopy.d.ts +11 -0
- package/dist/resumeRefusalCopy.js +42 -2
- package/dist/runTerminal.js +4 -0
- package/dist/sandboxWire.js +1 -1
- package/dist/sealedKeyCapability.d.ts +17 -0
- package/dist/sealedKeyCapability.js +42 -0
- package/dist/seatContract.d.ts +2 -1
- package/dist/seatContract.js +5 -21
- package/dist/subagent/engineCompactWire.d.ts +13 -2
- package/dist/subagent/engineCompactWire.js +48 -37
- package/dist/subagent/engineDelegatedPrompt.d.ts +9 -1
- package/dist/subagent/engineDelegatedPrompt.js +16 -16
- package/dist/subagent/engineRowStopGate.d.ts +2 -2
- package/dist/subagent/engineRowStopGate.js +4 -4
- package/dist/subagent/engineSubagentOutput.d.ts +8 -0
- package/dist/subagent/engineSubagentOutput.js +37 -18
- package/dist/subagent/engineSubagentResume.d.ts +11 -2
- package/dist/subagent/engineSubagentResume.js +28 -14
- package/dist/subagent/engineSubagentSteer.d.ts +10 -1
- package/dist/subagent/engineSubagentSteer.js +16 -11
- package/dist/subagent/engineSubagentTail.d.ts +10 -1
- package/dist/subagent/engineSubagentTail.js +35 -17
- package/dist/subagent/engineTaskHandleWire.d.ts +13 -0
- package/dist/subagent/engineTaskHandleWire.js +41 -20
- package/dist/systemReminderTag.d.ts +5 -0
- package/dist/systemReminderTag.js +20 -9
- package/dist/taskRequestWords.d.ts +5 -0
- package/dist/taskRequestWords.js +4 -0
- package/dist/wireErrorTriage.d.ts +1 -0
- package/dist/wireErrorTriage.js +3 -1
- package/dist/wireRefusalCopy.d.ts +10 -0
- package/dist/wireRefusalCopy.js +62 -2
- package/docs/INTEGRATION-CLIENTS.md +1128 -22
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionResponder, unregisterLocalQuestionResponder, hasLocalQuestionResponder, hasQuestionOverlayFor, } from '../liveQuestionStore.js';
|
|
2
2
|
import { hostLog } from '../host.js';
|
|
3
|
-
import { abortableSleep } from '../abortableSleep.js';
|
|
4
|
-
import {
|
|
3
|
+
import { abortableSleep, settleWithinDeadline } from '../abortableSleep.js';
|
|
4
|
+
import { clientHasVerbs, engineWireFor, noAnswerKindOf, sendOnce } from '../engineWireFor.js';
|
|
5
5
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
6
6
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
7
7
|
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
8
8
|
import { decideFailureNote } from '../decideFailureNote.js';
|
|
9
|
-
import { isPlanReviewModeAfter } from './hitlBridge.js';
|
|
9
|
+
import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
|
|
10
10
|
import { engineCapString } from '../engineCapsCache.js';
|
|
11
11
|
import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
|
|
12
12
|
import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
|
|
@@ -102,6 +102,18 @@ export function isPlanReviewPark(result) {
|
|
|
102
102
|
const taskId = result?.taskId;
|
|
103
103
|
return typeof taskId === 'string' && taskId.length > 0;
|
|
104
104
|
}
|
|
105
|
+
function pausedCheckpointIdOf(carrier) {
|
|
106
|
+
try {
|
|
107
|
+
const read = readRunTerminal(carrier);
|
|
108
|
+
if (read === null || read.kind !== 'paused')
|
|
109
|
+
return undefined;
|
|
110
|
+
const id = read.checkpointId;
|
|
111
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
105
117
|
const armedPlanReviewTaskIds = new Set();
|
|
106
118
|
const offerModeAfterByTask = new Map();
|
|
107
119
|
export function _resetArmedPlanReviewsForTest() {
|
|
@@ -225,9 +237,21 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
225
237
|
'此处拒绝而不是回落默认槽 —— 回落会把这条会话的决断带着别的槽的 baseUrl/token/principal 发出去。');
|
|
226
238
|
return false;
|
|
227
239
|
}
|
|
228
|
-
|
|
240
|
+
const wire = opts?.wire;
|
|
241
|
+
if (wire !== undefined) {
|
|
242
|
+
if (!usablePlanReviewWireClient(wire.client)) {
|
|
243
|
+
hostLog('error', 'planReviewWire: plan_review card NOT armed — the engine client supplied by the host is not usable (needs assistant.planReview and runs.get)');
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (!engineWireTarget()) {
|
|
229
248
|
return false;
|
|
249
|
+
}
|
|
250
|
+
const delivery = wire !== undefined || opts?.onOutcome !== undefined
|
|
251
|
+
? { ...(wire !== undefined ? { wire } : {}), ...(opts?.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
|
|
252
|
+
: undefined;
|
|
230
253
|
const { taskId } = result;
|
|
254
|
+
const gateBefore = pausedCheckpointIdOf(result);
|
|
231
255
|
retireActiveReopen(DEFAULT_SESSION_KEY, planReviewQuestionId(taskId));
|
|
232
256
|
if (armedPlanReviewTaskIds.has(taskId)) {
|
|
233
257
|
if (hasLocalQuestionResponder(planReviewQuestionId(taskId))) {
|
|
@@ -240,8 +264,9 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
240
264
|
}
|
|
241
265
|
armedPlanReviewTaskIds.add(taskId);
|
|
242
266
|
armedTaskId = taskId;
|
|
267
|
+
const capsBaseUrl = wire !== undefined ? wire.capsBaseUrl : engineWireTarget()?.baseUrl;
|
|
243
268
|
const offerModeAfter = opts?.submittedInPlanMode === true &&
|
|
244
|
-
versionSupportsPlanReviewModeAfter(engineCapString(
|
|
269
|
+
versionSupportsPlanReviewModeAfter(engineCapString(capsBaseUrl, 'version')) === 'supported';
|
|
245
270
|
if (offerModeAfter)
|
|
246
271
|
offerModeAfterByTask.set(taskId, true);
|
|
247
272
|
else
|
|
@@ -262,7 +287,7 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
262
287
|
offerModeAfterByTask.delete(taskId);
|
|
263
288
|
notePlanReviewAnswered(questionId);
|
|
264
289
|
hostLog('debug', `planReviewWire: arm responder delivering ${decided} for task ${taskId} (path=arm)`);
|
|
265
|
-
void
|
|
290
|
+
void decidePlanReviewFor(taskId, decided, modeAfter, delivery, gateBefore);
|
|
266
291
|
return { ok: true };
|
|
267
292
|
});
|
|
268
293
|
publishPlanReviewCard(taskId);
|
|
@@ -296,73 +321,211 @@ export function planReviewOutcomeTail(taskId, decision, effect) {
|
|
|
296
321
|
return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; TaskOutput("${taskId}") shows where the task stands.`;
|
|
297
322
|
}
|
|
298
323
|
}
|
|
299
|
-
|
|
324
|
+
function planReviewDecisionTimedOutOutcome(decision) {
|
|
325
|
+
return (`The plan_review ${decision} was sent, but no answer came back from the engine within the time limit this host allows for it — ` +
|
|
326
|
+
`it may have taken effect (${decision === 'approve' ? 'an approval keeps the engine busy until the resumed work finishes' : 'the engine may still be applying it'}).`);
|
|
327
|
+
}
|
|
328
|
+
function planReviewDecisionConnectionLostOutcome(decision) {
|
|
329
|
+
return (`The plan_review ${decision} got no answer from the engine: the connection failed before an answer came back, and the request may already have reached the engine — ` +
|
|
330
|
+
`it may have taken effect (${decision === 'approve' ? 'an approval keeps the engine busy until the resumed work finishes' : 'the engine may still be applying it'}).`);
|
|
331
|
+
}
|
|
332
|
+
function planReviewAdvancedOutcome(decision, postStatus) {
|
|
333
|
+
return (`The plan was ${decision === 'approve' ? 'approved' : 'rejected'} and the task advanced to a NEW approval gate ` +
|
|
334
|
+
`(post-decide status: ${postStatus}) — the next approval card will surface it; this is not a completion yet.`);
|
|
335
|
+
}
|
|
336
|
+
function usablePlanReviewWireClient(client) {
|
|
337
|
+
if (client === null || typeof client !== 'object')
|
|
338
|
+
return false;
|
|
339
|
+
return clientHasVerbs(client, [['assistant', 'planReview'], ['runs', 'get']]);
|
|
340
|
+
}
|
|
341
|
+
const PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS = 15_000;
|
|
342
|
+
const planReviewDecisionOps = [];
|
|
343
|
+
const MAX_PLAN_REVIEW_DECISION_OPS = 32;
|
|
344
|
+
let planReviewLedgerClock = 0;
|
|
300
345
|
let decideDispatchSeq = 0;
|
|
346
|
+
function planReviewOpsOf(taskId) {
|
|
347
|
+
return planReviewDecisionOps.filter((op) => op.taskId === taskId);
|
|
348
|
+
}
|
|
349
|
+
function settlePlanReviewDecisionOp(op) {
|
|
350
|
+
if (op.settledAt === null)
|
|
351
|
+
op.settledAt = ++planReviewLedgerClock;
|
|
352
|
+
}
|
|
353
|
+
function pushPlanReviewDecisionOp(op) {
|
|
354
|
+
planReviewDecisionOps.push(op);
|
|
355
|
+
while (planReviewDecisionOps.length > MAX_PLAN_REVIEW_DECISION_OPS) {
|
|
356
|
+
let victim = planReviewDecisionOps.findIndex((o) => o.settledAt !== null && !o.ownCallOpen);
|
|
357
|
+
if (victim < 0)
|
|
358
|
+
victim = planReviewDecisionOps.findIndex((o) => o !== op && o.phase === 'handed');
|
|
359
|
+
if (victim < 0)
|
|
360
|
+
break;
|
|
361
|
+
planReviewDecisionOps.splice(victim, 1);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function dropPlanReviewDecisionOp(op) {
|
|
365
|
+
const i = planReviewDecisionOps.indexOf(op);
|
|
366
|
+
if (i >= 0)
|
|
367
|
+
planReviewDecisionOps.splice(i, 1);
|
|
368
|
+
}
|
|
369
|
+
export function notePlanReviewDecisionHandedOver(taskId) {
|
|
370
|
+
if (typeof taskId !== 'string' || taskId === '')
|
|
371
|
+
return null;
|
|
372
|
+
if (planReviewDecisionOps.some((op) => op.taskId === taskId && op.settledAt === null))
|
|
373
|
+
return null;
|
|
374
|
+
const op = { taskId, handedAt: ++planReviewLedgerClock, settledAt: null, phase: 'handed', notApplied: false, ownCallOpen: false };
|
|
375
|
+
pushPlanReviewDecisionOp(op);
|
|
376
|
+
return {
|
|
377
|
+
settle: () => {
|
|
378
|
+
if (op.phase !== 'handed')
|
|
379
|
+
return;
|
|
380
|
+
settlePlanReviewDecisionOp(op);
|
|
381
|
+
},
|
|
382
|
+
revoke: () => {
|
|
383
|
+
if (op.phase !== 'handed' || op.settledAt !== null)
|
|
384
|
+
return;
|
|
385
|
+
dropPlanReviewDecisionOp(op);
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
export function planReviewPendingReadMark() {
|
|
390
|
+
return ++planReviewLedgerClock;
|
|
391
|
+
}
|
|
392
|
+
export function planReviewDecisionInFlight(taskId, pendingReadStartedAt) {
|
|
393
|
+
if (typeof taskId !== 'string' || taskId === '')
|
|
394
|
+
return false;
|
|
395
|
+
const mark = typeof pendingReadStartedAt === 'number' && Number.isFinite(pendingReadStartedAt) ? pendingReadStartedAt : undefined;
|
|
396
|
+
return planReviewDecisionOps.some((op) => op.taskId === taskId && (op.settledAt === null || (mark !== undefined && op.settledAt > mark && !op.notApplied)));
|
|
397
|
+
}
|
|
398
|
+
export function notePlanReviewDecisionNotApplied(taskId) {
|
|
399
|
+
if (typeof taskId !== 'string' || taskId === '')
|
|
400
|
+
return;
|
|
401
|
+
const ops = planReviewOpsOf(taskId);
|
|
402
|
+
if (ops.some((op) => op.settledAt === null))
|
|
403
|
+
return;
|
|
404
|
+
let latest;
|
|
405
|
+
for (const op of ops)
|
|
406
|
+
if (latest === undefined || op.handedAt > latest.handedAt)
|
|
407
|
+
latest = op;
|
|
408
|
+
if (latest !== undefined)
|
|
409
|
+
latest.notApplied = true;
|
|
410
|
+
}
|
|
411
|
+
export function notePlanReviewGateLeft(taskId) {
|
|
412
|
+
if (typeof taskId !== 'string' || taskId === '')
|
|
413
|
+
return;
|
|
414
|
+
for (const op of planReviewOpsOf(taskId))
|
|
415
|
+
settlePlanReviewDecisionOp(op);
|
|
416
|
+
}
|
|
417
|
+
export function latestPlanReviewDecisionMark(taskId) {
|
|
418
|
+
if (typeof taskId !== 'string' || taskId === '')
|
|
419
|
+
return null;
|
|
420
|
+
let mark = null;
|
|
421
|
+
for (const op of planReviewOpsOf(taskId))
|
|
422
|
+
if (mark === null || op.handedAt > mark)
|
|
423
|
+
mark = op.handedAt;
|
|
424
|
+
return mark;
|
|
425
|
+
}
|
|
301
426
|
export function __resetPlanReviewDecisionLatchForTests() {
|
|
302
|
-
|
|
427
|
+
planReviewDecisionOps.length = 0;
|
|
303
428
|
}
|
|
304
|
-
export async function decidePlanReview(taskId, decision, permissionModeAfter) {
|
|
305
|
-
|
|
429
|
+
export async function decidePlanReview(taskId, decision, permissionModeAfter, opts) {
|
|
430
|
+
return decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, undefined);
|
|
431
|
+
}
|
|
432
|
+
async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, gateBefore) {
|
|
433
|
+
if (typeof taskId === 'string' && planReviewDecisionOps.some((op) => op.taskId === taskId && op.phase === 'posting')) {
|
|
306
434
|
hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
|
|
307
435
|
return;
|
|
308
436
|
}
|
|
309
|
-
|
|
310
|
-
|
|
437
|
+
let op = planReviewDecisionOps.find((o) => o.taskId === taskId && o.phase === 'handed' && o.settledAt === null);
|
|
438
|
+
if (op === undefined) {
|
|
439
|
+
op = { taskId: typeof taskId === 'string' ? taskId : String(taskId), handedAt: ++planReviewLedgerClock, settledAt: null, phase: 'posting', notApplied: false, ownCallOpen: true };
|
|
440
|
+
pushPlanReviewDecisionOp(op);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
op.phase = 'posting';
|
|
444
|
+
op.ownCallOpen = true;
|
|
445
|
+
}
|
|
446
|
+
const mine = op;
|
|
311
447
|
const releaseLatch = () => {
|
|
312
|
-
if (
|
|
313
|
-
|
|
448
|
+
if (mine.phase === 'posting')
|
|
449
|
+
mine.phase = 'landed';
|
|
450
|
+
};
|
|
451
|
+
const settle = (notApplied) => {
|
|
452
|
+
if (notApplied)
|
|
453
|
+
mine.notApplied = true;
|
|
454
|
+
settlePlanReviewDecisionOp(mine);
|
|
314
455
|
};
|
|
315
456
|
const dispatchNo = ++decideDispatchSeq;
|
|
316
457
|
hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
|
|
317
458
|
try {
|
|
318
|
-
await decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch);
|
|
459
|
+
await decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore);
|
|
319
460
|
}
|
|
320
461
|
finally {
|
|
321
462
|
releaseLatch();
|
|
463
|
+
settlePlanReviewDecisionOp(mine);
|
|
464
|
+
mine.ownCallOpen = false;
|
|
322
465
|
}
|
|
323
466
|
}
|
|
324
|
-
|
|
467
|
+
function planReviewClientsFor(resolved) {
|
|
468
|
+
if (resolved.kind === 'injected') {
|
|
469
|
+
const client = resolved.client;
|
|
470
|
+
return { decide: client, repull: () => client };
|
|
471
|
+
}
|
|
472
|
+
const decide = resolved.make({ timeoutMs: PLAN_REVIEW_RESUME_TIMEOUT_MS });
|
|
473
|
+
if (!decide)
|
|
474
|
+
return null;
|
|
475
|
+
return {
|
|
476
|
+
decide,
|
|
477
|
+
repull: () => resolved.make({ timeoutMs: 15_000 }),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore) {
|
|
325
481
|
let outcome;
|
|
326
482
|
let effect = 'unconfirmed';
|
|
327
|
-
|
|
483
|
+
let stillParkedProven = false;
|
|
484
|
+
let statusSeen;
|
|
485
|
+
const wire = opts?.wire;
|
|
486
|
+
const resolved = engineWireFor(wire, DEFAULT_SESSION_KEY, usablePlanReviewWireClient);
|
|
328
487
|
const badMode = permissionModeAfter !== undefined && (decision !== 'approve' || !isPlanReviewModeAfter(permissionModeAfter));
|
|
329
488
|
if (badMode) {
|
|
330
489
|
hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter is only valid with approve and must be "default" or "acceptEdits"`);
|
|
331
490
|
effect = 'not_sent';
|
|
332
491
|
outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid for this decision (it only applies to an approval, and must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
|
|
333
492
|
}
|
|
334
|
-
else if (
|
|
493
|
+
else if (resolved.kind === 'no-target') {
|
|
335
494
|
hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
|
|
336
495
|
effect = 'not_sent';
|
|
337
496
|
outcome = `The plan_review ${decision} could NOT be sent: no engine connection is configured on this host. Tell the user plainly that the decision did not go through.`;
|
|
338
497
|
}
|
|
498
|
+
else if (resolved.kind === 'injected-unusable') {
|
|
499
|
+
hostLog('error', `planReviewWire: ${decision} NOT sent — the engine client supplied by the host is not usable (needs assistant.planReview and runs.get)`);
|
|
500
|
+
effect = 'not_sent';
|
|
501
|
+
outcome = `The plan_review ${decision} could NOT be sent: the engine client supplied by the host cannot send it (it lacks the plan-review verb). Tell the user plainly that the decision did not go through.`;
|
|
502
|
+
}
|
|
339
503
|
else {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
...(cfg.token ? { token: cfg.token } : {}),
|
|
343
|
-
principal: cfg.principal,
|
|
344
|
-
timeoutMs: PLAN_REVIEW_RESUME_TIMEOUT_MS,
|
|
345
|
-
});
|
|
346
|
-
if (!client) {
|
|
504
|
+
const clients = planReviewClientsFor(resolved);
|
|
505
|
+
if (!clients) {
|
|
347
506
|
hostLog('error', `planReviewWire: ${decision} NOT sent — makeEngineWireClient() returned null`);
|
|
348
|
-
effect = 'not_sent';
|
|
349
507
|
outcome = `The plan_review ${decision} could NOT be sent: the engine client could not be constructed. Tell the user plainly that the decision did not go through.`;
|
|
508
|
+
effect = 'not_sent';
|
|
350
509
|
}
|
|
351
510
|
else {
|
|
511
|
+
const client = clients.decide;
|
|
352
512
|
try {
|
|
353
513
|
const mode = isPlanReviewModeAfter(permissionModeAfter) ? permissionModeAfter : undefined;
|
|
514
|
+
const reason = denyReasonForWire(opts?.reason, `plan-review ${taskId}`);
|
|
354
515
|
const reqBody = { decision };
|
|
516
|
+
if (reason !== undefined)
|
|
517
|
+
reqBody.reason = reason;
|
|
355
518
|
if (mode !== undefined)
|
|
356
519
|
reqBody.permissionModeAfter = mode;
|
|
357
520
|
let body;
|
|
358
521
|
try {
|
|
359
|
-
body = await client.assistant.planReview(taskId, reqBody);
|
|
522
|
+
body = await sendOnce(undefined, () => client.assistant.planReview(taskId, reqBody));
|
|
360
523
|
}
|
|
361
524
|
catch (e1) {
|
|
362
525
|
const conflict = e1;
|
|
363
526
|
if (mode === 'default' && conflict?.status === 400 && conflict?.errorCode === 'request.field_conflict') {
|
|
364
527
|
hostLog('debug', 'planReviewWire: permissionModeAfter "default" refused as not applicable — re-sending the approval without the key (same meaning)');
|
|
365
|
-
body = await client.assistant.planReview(taskId, { decision });
|
|
528
|
+
body = await sendOnce(undefined, () => client.assistant.planReview(taskId, reason !== undefined ? { decision, reason } : { decision }));
|
|
366
529
|
}
|
|
367
530
|
else {
|
|
368
531
|
throw e1;
|
|
@@ -373,22 +536,39 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
373
536
|
}
|
|
374
537
|
hostLog('debug', `planReviewWire: ${decision} → ok (dispatch #${dispatchNo}) ${JSON.stringify(body).slice(0, 200)}`);
|
|
375
538
|
let postStatus;
|
|
539
|
+
let gateAfter;
|
|
376
540
|
try {
|
|
377
|
-
const probe =
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
541
|
+
const probe = clients.repull();
|
|
542
|
+
if (probe) {
|
|
543
|
+
let post;
|
|
544
|
+
if (wire !== undefined) {
|
|
545
|
+
const got = await settleWithinDeadline(PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS, (signal) => probe.runs.get(taskId, { signal }));
|
|
546
|
+
if (got.kind === 'error')
|
|
547
|
+
throw new Error(got.detail);
|
|
548
|
+
if (got.kind === 'deadline')
|
|
549
|
+
throw new Error(`post-decide status re-pull passed its ${PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS} ms deadline`);
|
|
550
|
+
post = got.value;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
post = (await probe.runs.get(taskId));
|
|
554
|
+
}
|
|
555
|
+
postStatus = typeof post?.status === 'string' ? post.status : undefined;
|
|
556
|
+
gateAfter = pausedCheckpointIdOf(post?.result);
|
|
557
|
+
}
|
|
385
558
|
}
|
|
386
559
|
catch (e) {
|
|
387
560
|
hostLog('debug', `planReviewWire: post-decide status re-pull failed (fail-soft): ${String(e)}`);
|
|
388
561
|
}
|
|
389
562
|
const effective = postStatus ?? (typeof body?.status === 'string' ? body.status : undefined);
|
|
390
|
-
|
|
563
|
+
statusSeen = effective;
|
|
564
|
+
const gateMoved = postStatus === 'needs_review' && gateBefore !== undefined && gateAfter !== undefined && gateAfter !== gateBefore;
|
|
565
|
+
if (effective === 'needs_review' && gateMoved) {
|
|
566
|
+
effect = 'advanced';
|
|
567
|
+
outcome = planReviewAdvancedOutcome(decision, 'needs_review');
|
|
568
|
+
}
|
|
569
|
+
else if (effective === 'needs_review') {
|
|
391
570
|
effect = 'still_parked';
|
|
571
|
+
stillParkedProven = postStatus === 'needs_review' && gateBefore !== undefined && gateAfter !== undefined && gateAfter === gateBefore;
|
|
392
572
|
outcome =
|
|
393
573
|
`The plan_review ${decision} returned HTTP 200 but the session is STILL locked on the same review gate ` +
|
|
394
574
|
`(post-decide status: needs_review). The decision did NOT take effect (known engine issue, RB-471 family). ` +
|
|
@@ -396,9 +576,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
396
576
|
}
|
|
397
577
|
else if (effective === 'suspended') {
|
|
398
578
|
effect = 'advanced';
|
|
399
|
-
outcome =
|
|
400
|
-
`The plan was ${decision === 'approve' ? 'approved' : 'rejected'} and the task advanced to a NEW approval gate ` +
|
|
401
|
-
`(post-decide status: suspended) — the next approval card will surface it; this is not a completion yet.`;
|
|
579
|
+
outcome = planReviewAdvancedOutcome(decision, 'suspended');
|
|
402
580
|
}
|
|
403
581
|
else if (effective !== undefined) {
|
|
404
582
|
effect = 'took_effect';
|
|
@@ -411,9 +589,11 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
411
589
|
catch (e) {
|
|
412
590
|
const status = e?.status;
|
|
413
591
|
const refusedCode = e?.errorCode;
|
|
592
|
+
const noAnswer = typeof status === 'number' || typeof e !== 'object' ? null : noAnswerKindOf(e);
|
|
414
593
|
if (status === 400 && refusedCode === 'request.field_conflict' && permissionModeAfter === 'acceptEdits') {
|
|
415
594
|
hostLog('debug', 'planReviewWire: approve + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode');
|
|
416
595
|
effect = 'still_parked';
|
|
596
|
+
stillParkedProven = true;
|
|
417
597
|
outcome =
|
|
418
598
|
'The plan approval was NOT applied: the engine refused "auto-accept edits" for this task (it was not started read-only in plan mode, so that option does not apply). ' +
|
|
419
599
|
'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly, and that approving with "manually approve edits" will go through.';
|
|
@@ -437,21 +617,46 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
437
617
|
effect = status >= 400 && status < 500 ? 'not_applied' : 'unconfirmed';
|
|
438
618
|
outcome = `${`The plan_review decision failed: HTTP ${status} ${msg}`.trim()}${decideFailureNote(e)}`;
|
|
439
619
|
}
|
|
620
|
+
else if (noAnswer === 'time-limit' || noAnswer === 'deadline') {
|
|
621
|
+
hostLog('debug', `planReviewWire: ${decision} sent but no answer within the request time limit (task ${taskId})`);
|
|
622
|
+
outcome = planReviewDecisionTimedOutOutcome(decision);
|
|
623
|
+
}
|
|
624
|
+
else if (noAnswer === 'connection-lost') {
|
|
625
|
+
hostLog('debug', `planReviewWire: ${decision} got no answer — the connection failed and it cannot be told whether the request reached the engine (task ${taskId})`);
|
|
626
|
+
outcome = planReviewDecisionConnectionLostOutcome(decision);
|
|
627
|
+
}
|
|
440
628
|
else {
|
|
441
629
|
outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
|
|
442
630
|
}
|
|
443
631
|
}
|
|
444
632
|
}
|
|
445
633
|
}
|
|
634
|
+
const tail = planReviewOutcomeTail(taskId, decision, effect);
|
|
635
|
+
const prompt = `<plan-review-outcome>\n${outcome}\n${tail}\n</plan-review-outcome>`;
|
|
636
|
+
const onOutcome = opts?.onOutcome;
|
|
637
|
+
settle(effect === 'not_sent' || (effect === 'still_parked' && stillParkedProven));
|
|
446
638
|
try {
|
|
447
|
-
const delivered = enqueuePlanReviewOutcome(
|
|
639
|
+
const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision, effect });
|
|
448
640
|
if (!delivered) {
|
|
449
|
-
|
|
641
|
+
if (onOutcome !== undefined) {
|
|
642
|
+
hostLog('debug', `planReviewWire: outcome queue port not installed for task ${taskId} — outcome handed to the host's onOutcome callback only`);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
|
|
646
|
+
}
|
|
450
647
|
}
|
|
451
648
|
}
|
|
452
649
|
catch (e) {
|
|
453
650
|
hostLog('debug', `planReviewWire: outcome enqueue threw: ${String(e)}`);
|
|
454
651
|
}
|
|
652
|
+
if (onOutcome !== undefined) {
|
|
653
|
+
try {
|
|
654
|
+
onOutcome({ taskId, dispatchNo, decision, effect, detail: outcome, tail, prompt, ...(statusSeen !== undefined ? { status: statusSeen } : {}) });
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
hostLog('debug', `planReviewWire: onOutcome callback threw for task ${taskId} (ignored): ${String(e)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
455
660
|
}
|
|
456
661
|
function reopenIdSuffix() {
|
|
457
662
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -545,6 +750,9 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
545
750
|
if (opts?.deliverDecision !== undefined) {
|
|
546
751
|
hostLog('debug', `planReviewWire: reopen canonical-reuse branch for task ${taskId} ignores opts.deliverDecision (answers route through the existing arm responder — documented exception)`);
|
|
547
752
|
}
|
|
753
|
+
if (opts?.wire !== undefined || opts?.onOutcome !== undefined) {
|
|
754
|
+
hostLog('debug', `planReviewWire: reopen canonical-reuse branch for task ${taskId} ignores opts.wire / opts.onOutcome (answers route through the existing arm responder and the wire it was armed with — documented exception)`);
|
|
755
|
+
}
|
|
548
756
|
retireActiveReopen(sessionKey, canonicalId);
|
|
549
757
|
if (receiptMs !== undefined) {
|
|
550
758
|
const receipt = waitForGateArmedFor(sessionKey, [canonicalId], receiptMs);
|
|
@@ -571,10 +779,13 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
571
779
|
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: canonicalId });
|
|
572
780
|
const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
|
|
573
781
|
hostLog('debug', `planReviewWire: reopen for task ${taskId} minting a fresh card ${questionId} (path=reopen-fresh)`);
|
|
782
|
+
const delivery = opts?.wire !== undefined || opts?.onOutcome !== undefined
|
|
783
|
+
? { ...(opts.wire !== undefined ? { wire: opts.wire } : {}), ...(opts.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
|
|
784
|
+
: undefined;
|
|
574
785
|
const deliver = opts?.deliverDecision ??
|
|
575
786
|
((tid, decision) => {
|
|
576
787
|
hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
|
|
577
|
-
void decidePlanReview(tid, decision);
|
|
788
|
+
void (delivery === undefined ? decidePlanReview(tid, decision) : decidePlanReview(tid, decision, undefined, delivery));
|
|
578
789
|
});
|
|
579
790
|
const activeKey = `${sessionKey}\u0000${canonicalId}`;
|
|
580
791
|
const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
@@ -616,7 +827,7 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
616
827
|
revokePlanReviewUserDismissal(sessionKey, taskId, markBefore);
|
|
617
828
|
return { reopened: true, firstSight };
|
|
618
829
|
}
|
|
619
|
-
async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark) {
|
|
830
|
+
async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart) {
|
|
620
831
|
const refusedByUser = { reopened: false, dismissedByUser: true };
|
|
621
832
|
try {
|
|
622
833
|
let fresh = false;
|
|
@@ -633,7 +844,8 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
|
|
|
633
844
|
}
|
|
634
845
|
}
|
|
635
846
|
let handedOver = false;
|
|
636
|
-
|
|
847
|
+
const ledgerSaysInFlight = planReviewDecisionInFlight(taskId, recheckReadStart);
|
|
848
|
+
if (!ledgerSaysInFlight) {
|
|
637
849
|
try {
|
|
638
850
|
handedOver = opts.decisionHandedOver?.(taskId) === true;
|
|
639
851
|
}
|
|
@@ -642,7 +854,7 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
|
|
|
642
854
|
return { reopened: false };
|
|
643
855
|
}
|
|
644
856
|
}
|
|
645
|
-
if (
|
|
857
|
+
if (ledgerSaysInFlight || handedOver) {
|
|
646
858
|
hostLog('debug', `planReviewWire: reopen for task ${taskId} refused after the dismissal check — a decision for this task went out while the check was reading`);
|
|
647
859
|
return { reopened: false, _sema_decisionInFlight: true };
|
|
648
860
|
}
|
|
@@ -715,16 +927,19 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
715
927
|
if (typeof taskId !== 'string' || taskId.length === 0) {
|
|
716
928
|
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
717
929
|
}
|
|
718
|
-
|
|
930
|
+
const readStart = typeof opts?.pendingReadStartedAt === 'number' && Number.isFinite(opts.pendingReadStartedAt) ? opts.pendingReadStartedAt : undefined;
|
|
931
|
+
if (planReviewDecisionInFlight(taskId, readStart)) {
|
|
719
932
|
hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — a decision for this task is in flight (its answer is still being delivered)`);
|
|
720
933
|
const refused = { reopened: false, _sema_decisionInFlight: true };
|
|
721
934
|
return receiptMs === undefined ? refused : Promise.resolve(refused);
|
|
722
935
|
}
|
|
936
|
+
const guardStartedAt = planReviewPendingReadMark();
|
|
937
|
+
const recheckReadStart = readStart === undefined ? guardStartedAt : Math.min(readStart, guardStartedAt);
|
|
723
938
|
const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
724
939
|
if (!hasQuestionOverlayFor(sessionKey)) {
|
|
725
940
|
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
726
941
|
}
|
|
727
|
-
if (sessionKey !== DEFAULT_SESSION_KEY && opts?.deliverDecision === undefined) {
|
|
942
|
+
if (sessionKey !== DEFAULT_SESSION_KEY && opts?.deliverDecision === undefined && opts?.wire === undefined) {
|
|
728
943
|
hostLog('error', `planReviewWire: plan_review 卡未重开 —— sessionKey=${JSON.stringify(sessionKey)} 是非默认槽,` +
|
|
729
944
|
'而缺省决断投递口(decidePlanReview)今天固定打默认槽的 baseUrl/token/principal。' +
|
|
730
945
|
'整条链转 keyed 之前,非默认槽要重开请自带 deliverDecision(按本槽投递);' +
|
|
@@ -739,7 +954,7 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
739
954
|
hostLog('debug', `planReviewWire: automatic reopen for task ${taskId} refused — the user closed this gate's card`);
|
|
740
955
|
return { reopened: false, dismissedByUser: true };
|
|
741
956
|
}
|
|
742
|
-
return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark);
|
|
957
|
+
return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart);
|
|
743
958
|
}
|
|
744
959
|
}
|
|
745
960
|
return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function rosterMountedNames(roster) {
|
|
2
|
+
if (roster === undefined || roster === null)
|
|
3
|
+
return 'absent';
|
|
4
|
+
try {
|
|
5
|
+
if (typeof roster !== 'object' || Array.isArray(roster))
|
|
6
|
+
return 'unreadable';
|
|
7
|
+
const entries = roster.entries;
|
|
8
|
+
if (!Array.isArray(entries))
|
|
9
|
+
return 'unreadable';
|
|
10
|
+
const names = new Set();
|
|
11
|
+
const n = entries.length;
|
|
12
|
+
for (let i = 0; i < n; i++) {
|
|
13
|
+
if (!Object.hasOwn(entries, i))
|
|
14
|
+
return 'unreadable';
|
|
15
|
+
const row = entries[i];
|
|
16
|
+
if (typeof row !== 'object' || row === null)
|
|
17
|
+
return 'unreadable';
|
|
18
|
+
const name = row.name;
|
|
19
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
20
|
+
return 'unreadable';
|
|
21
|
+
names.add(name);
|
|
22
|
+
const aliases = row.aliases;
|
|
23
|
+
if (aliases === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
if (!Array.isArray(aliases))
|
|
26
|
+
return 'unreadable';
|
|
27
|
+
for (const a of aliases) {
|
|
28
|
+
if (typeof a !== 'string')
|
|
29
|
+
return 'unreadable';
|
|
30
|
+
names.add(a);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return names;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return 'unreadable';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionPermissionRules } from '@sema-agent/sdk';
|
|
2
|
-
|
|
2
|
+
import type { ToolRosterView } from '../toolRoster.js';
|
|
3
|
+
export type SessionPolicyWithheldWhy = 'ask_no_bucket' | 'qualified_deny' | 'peer_wide' | 'wildcard' | 'not_a_tool_name' | 'legacy_tool_name' | 'not_in_roster';
|
|
3
4
|
export declare const SESSION_POLICY_WITHHELD_WHY: readonly SessionPolicyWithheldWhy[];
|
|
4
5
|
export interface SessionPolicyWithheldRule {
|
|
5
6
|
readonly rule: string;
|
|
@@ -11,5 +12,10 @@ export interface SessionPolicyDeliverability {
|
|
|
11
12
|
readonly withheld: readonly SessionPolicyWithheldRule[];
|
|
12
13
|
readonly unreadable?: true;
|
|
13
14
|
}
|
|
14
|
-
export
|
|
15
|
+
export interface SessionPolicyDeliverableOptions {
|
|
16
|
+
readonly roster?: ToolRosterView;
|
|
17
|
+
}
|
|
18
|
+
export declare function isEngineRefusedToolName(name: string): boolean;
|
|
19
|
+
export declare function sessionPolicyDeliverable(behavior: 'deny' | 'ask', rules: readonly string[], opts?: SessionPolicyDeliverableOptions): SessionPolicyDeliverability;
|
|
15
20
|
export declare function sessionPolicyWithheldNotice(why: SessionPolicyWithheldWhy): string;
|
|
21
|
+
export declare function legacyToolNameFailureNoteOf(code: string | undefined): string | undefined;
|