@sema-agent/client-core 0.84.0 → 0.84.1
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 +98 -0
- package/README.md +12 -5
- package/dist/abortableSleep.d.ts +10 -0
- package/dist/abortableSleep.js +37 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +1 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +10 -5
- package/dist/agentsWireCaps.js +8 -0
- package/dist/decideFailureNote.d.ts +2 -1
- package/dist/decideFailureNote.js +5 -3
- package/dist/detachWire.d.ts +1 -0
- package/dist/detachWire.js +13 -5
- package/dist/displayUntrusted.js +114 -18
- package/dist/engineErrorCodes.d.ts +2 -0
- package/dist/engineErrorCodes.js +3 -0
- package/dist/engineNoticeCodes.d.ts +30 -4
- package/dist/engineNoticeCodes.js +73 -5
- package/dist/engineWireSdk.d.ts +2 -0
- package/dist/hitl/askGateWire.d.ts +1 -1
- package/dist/hitl/askGateWire.js +1 -1
- package/dist/hitl/hitlHostSurface.js +1 -1
- package/dist/hitl/parkResolver.d.ts +1 -0
- package/dist/hitl/parkResolver.js +9 -3
- package/dist/hitl/planReviewWire.d.ts +30 -3
- package/dist/hitl/planReviewWire.js +139 -32
- package/dist/hitl/sessionPolicyDeliverable.d.ts +2 -0
- package/dist/hitl/sessionPolicyDeliverable.js +14 -3
- package/dist/hitl/sessionPolicyWire.d.ts +40 -0
- package/dist/hitl/sessionPolicyWire.js +210 -0
- package/dist/hitl/toolApprovalWire.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/liveInitToolFace.js +28 -10
- package/dist/resumeRefusalCopy.d.ts +11 -0
- package/dist/resumeRefusalCopy.js +39 -1
- package/dist/systemReminderTag.d.ts +5 -0
- package/dist/systemReminderTag.js +20 -9
- package/docs/INTEGRATION-CLIENTS.md +461 -16
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { putOwnKey } from '../ownKey.js';
|
|
2
2
|
import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
|
|
3
|
-
import { RESUME_POLICY_FOLD_CODE } from '../engineErrorCodes.js';
|
|
3
|
+
import { PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE } from '../engineErrorCodes.js';
|
|
4
4
|
import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
|
|
5
5
|
import { hostLog } from '../host.js';
|
|
6
6
|
import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
|
|
@@ -44,13 +44,16 @@ function isAlreadyResolvedFailure(outcome) {
|
|
|
44
44
|
const code = 'code' in outcome && isGateFailureCode(outcome.code) ? outcome.code : undefined;
|
|
45
45
|
if (code !== undefined)
|
|
46
46
|
return code === 'no_pending';
|
|
47
|
-
if ('errorCode' in outcome &&
|
|
47
|
+
if ('errorCode' in outcome && isCodeClassifiedGateFailure(outcome.errorCode))
|
|
48
48
|
return false;
|
|
49
49
|
return isAlreadyResolvedGateReason(outcome.reason);
|
|
50
50
|
}
|
|
51
51
|
export function isGateStandingErrorCode(code) {
|
|
52
52
|
return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt';
|
|
53
53
|
}
|
|
54
|
+
export function isCodeClassifiedGateFailure(code) {
|
|
55
|
+
return isGateStandingErrorCode(code) || code === PARKED_RESUME_STARTUP_FAILED;
|
|
56
|
+
}
|
|
54
57
|
export function toAnsweredOutput(questions, answer) {
|
|
55
58
|
const answers = {};
|
|
56
59
|
const annotations = {};
|
|
@@ -122,10 +125,12 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
122
125
|
return { kind: 'aborted', gatedCallId };
|
|
123
126
|
}
|
|
124
127
|
let declinedReceipt;
|
|
128
|
+
let attemptedDecision;
|
|
125
129
|
try {
|
|
126
130
|
const entries = answer.answers ?? [];
|
|
127
131
|
const hasContent = entries.some(a => (a.selected?.length ?? 0) > 0 || (a.note?.length ?? 0) > 0);
|
|
128
132
|
if (hasContent) {
|
|
133
|
+
attemptedDecision = 'approve';
|
|
129
134
|
const raw = await bridge.answerQuestion(entries, gatedCallId, signal ? { signal } : undefined, pending);
|
|
130
135
|
const receipt = readDecideReceipt(raw);
|
|
131
136
|
return {
|
|
@@ -136,6 +141,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
136
141
|
};
|
|
137
142
|
}
|
|
138
143
|
else {
|
|
144
|
+
attemptedDecision = 'deny';
|
|
139
145
|
const raw = await bridge.decideTool({ decision: 'deny', reason: 'User declined to answer' }, gatedCallId, signal ? { signal } : undefined, pending);
|
|
140
146
|
declinedReceipt = readDecideReceipt(raw);
|
|
141
147
|
}
|
|
@@ -149,7 +155,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
149
155
|
kind: 'failed',
|
|
150
156
|
stage: 'decide',
|
|
151
157
|
gatedCallId,
|
|
152
|
-
reason: `decide failed: ${String(e)}${decideFailureNote(e)}`,
|
|
158
|
+
reason: `decide failed: ${String(e)}${decideFailureNote(e, attemptedDecision)}`,
|
|
153
159
|
...(code !== undefined ? { code } : {}),
|
|
154
160
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
155
161
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type QuestionAnswer } from '../liveQuestionStore.js';
|
|
2
2
|
import { type PlanReviewModeAfter } from './hitlBridge.js';
|
|
3
|
+
import type { AgentClient } from '@sema-agent/sdk';
|
|
3
4
|
import type { ReopenCardVerdict } from '../adapter/activeRunSelfHeal.js';
|
|
4
5
|
export declare const PLAN_REVIEW_APPROVE_LABEL = "Yes, approve and run the plan";
|
|
5
6
|
export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
|
|
@@ -28,10 +29,13 @@ export declare function _resetArmedPlanReviewsForTest(): void;
|
|
|
28
29
|
export type PlanReviewReopenTrigger = 'user' | 'automatic';
|
|
29
30
|
export type PlanReviewGateInstanceReader = (taskId: string) => Promise<ReadonlySet<string> | null>;
|
|
30
31
|
export declare function clearPlanReviewUserDismissals(sessionKey?: string): void;
|
|
31
|
-
export
|
|
32
|
+
export interface ArmPlanReviewOptions {
|
|
32
33
|
submittedInPlanMode?: boolean;
|
|
33
34
|
readGateInstanceKeys?: PlanReviewGateInstanceReader;
|
|
34
|
-
|
|
35
|
+
wire?: PlanReviewWire;
|
|
36
|
+
onOutcome?: PlanReviewDeliveryOptions['onOutcome'];
|
|
37
|
+
}
|
|
38
|
+
export declare function armPlanReviewApproval(result: unknown, sessionKey?: string, opts?: ArmPlanReviewOptions): boolean;
|
|
35
39
|
export type PlanReviewDecisionEffect = 'took_effect' | 'advanced' | 'still_parked' | 'not_sent' | 'not_applied' | 'unconfirmed';
|
|
36
40
|
export interface PlanReviewOutcomeMeta {
|
|
37
41
|
taskId: string;
|
|
@@ -40,12 +44,35 @@ export interface PlanReviewOutcomeMeta {
|
|
|
40
44
|
effect: PlanReviewDecisionEffect;
|
|
41
45
|
}
|
|
42
46
|
export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect): string;
|
|
47
|
+
export type PlanReviewWireClient = {
|
|
48
|
+
assistant: Pick<AgentClient['assistant'], 'planReview'>;
|
|
49
|
+
runs: Pick<AgentClient['runs'], 'get'>;
|
|
50
|
+
};
|
|
51
|
+
export interface PlanReviewWire {
|
|
52
|
+
client: PlanReviewWireClient;
|
|
53
|
+
capsBaseUrl?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PlanReviewDecisionOutcome extends PlanReviewOutcomeMeta {
|
|
56
|
+
detail: string;
|
|
57
|
+
tail: string;
|
|
58
|
+
prompt: string;
|
|
59
|
+
status?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface PlanReviewDeliveryOptions {
|
|
62
|
+
wire?: PlanReviewWire;
|
|
63
|
+
onOutcome?: (outcome: PlanReviewDecisionOutcome) => void;
|
|
64
|
+
}
|
|
65
|
+
export interface DecidePlanReviewOptions extends PlanReviewDeliveryOptions {
|
|
66
|
+
reason?: string;
|
|
67
|
+
}
|
|
43
68
|
export declare function __resetPlanReviewDecisionLatchForTests(): void;
|
|
44
|
-
export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject', permissionModeAfter?: string): Promise<void>;
|
|
69
|
+
export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject', permissionModeAfter?: string, opts?: DecidePlanReviewOptions): Promise<void>;
|
|
45
70
|
export interface ReopenPlanReviewOpts {
|
|
46
71
|
mintFreshQuestionId?: boolean;
|
|
47
72
|
deliverDecision?: (taskId: string, decision: 'approve' | 'reject') => void | Promise<void>;
|
|
48
73
|
sessionKey?: string;
|
|
74
|
+
wire?: PlanReviewWire;
|
|
75
|
+
onOutcome?: PlanReviewDeliveryOptions['onOutcome'];
|
|
49
76
|
presentationReceiptMs?: number;
|
|
50
77
|
trigger?: PlanReviewReopenTrigger;
|
|
51
78
|
readGateInstanceKeys?: PlanReviewGateInstanceReader;
|
|
@@ -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';
|
|
3
|
+
import { abortableSleep, settleWithinDeadline } from '../abortableSleep.js';
|
|
4
4
|
import { makeEngineWireClient } from '../engineWireSdk.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';
|
|
@@ -225,8 +225,19 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
225
225
|
'此处拒绝而不是回落默认槽 —— 回落会把这条会话的决断带着别的槽的 baseUrl/token/principal 发出去。');
|
|
226
226
|
return false;
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
const wire = opts?.wire;
|
|
229
|
+
if (wire !== undefined) {
|
|
230
|
+
if (!usablePlanReviewWireClient(wire.client)) {
|
|
231
|
+
hostLog('error', 'planReviewWire: plan_review card NOT armed — the engine client supplied by the host is not usable (needs assistant.planReview and runs.get)');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (!engineWireTarget()) {
|
|
229
236
|
return false;
|
|
237
|
+
}
|
|
238
|
+
const delivery = wire !== undefined || opts?.onOutcome !== undefined
|
|
239
|
+
? { ...(wire !== undefined ? { wire } : {}), ...(opts?.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
|
|
240
|
+
: undefined;
|
|
230
241
|
const { taskId } = result;
|
|
231
242
|
retireActiveReopen(DEFAULT_SESSION_KEY, planReviewQuestionId(taskId));
|
|
232
243
|
if (armedPlanReviewTaskIds.has(taskId)) {
|
|
@@ -240,8 +251,9 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
240
251
|
}
|
|
241
252
|
armedPlanReviewTaskIds.add(taskId);
|
|
242
253
|
armedTaskId = taskId;
|
|
254
|
+
const capsBaseUrl = wire !== undefined ? wire.capsBaseUrl : engineWireTarget()?.baseUrl;
|
|
243
255
|
const offerModeAfter = opts?.submittedInPlanMode === true &&
|
|
244
|
-
versionSupportsPlanReviewModeAfter(engineCapString(
|
|
256
|
+
versionSupportsPlanReviewModeAfter(engineCapString(capsBaseUrl, 'version')) === 'supported';
|
|
245
257
|
if (offerModeAfter)
|
|
246
258
|
offerModeAfterByTask.set(taskId, true);
|
|
247
259
|
else
|
|
@@ -262,7 +274,7 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
262
274
|
offerModeAfterByTask.delete(taskId);
|
|
263
275
|
notePlanReviewAnswered(questionId);
|
|
264
276
|
hostLog('debug', `planReviewWire: arm responder delivering ${decided} for task ${taskId} (path=arm)`);
|
|
265
|
-
void decidePlanReview(taskId, decided, modeAfter);
|
|
277
|
+
void (delivery === undefined ? decidePlanReview(taskId, decided, modeAfter) : decidePlanReview(taskId, decided, modeAfter, delivery));
|
|
266
278
|
return { ok: true };
|
|
267
279
|
});
|
|
268
280
|
publishPlanReviewCard(taskId);
|
|
@@ -296,12 +308,31 @@ export function planReviewOutcomeTail(taskId, decision, effect) {
|
|
|
296
308
|
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
309
|
}
|
|
298
310
|
}
|
|
311
|
+
function planReviewDecisionTimedOutOutcome(decision) {
|
|
312
|
+
return (`The plan_review ${decision} was sent, but no answer came back from the engine within the time limit this host allows for it — ` +
|
|
313
|
+
`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'}).`);
|
|
314
|
+
}
|
|
315
|
+
function isRequestTimeout(e) {
|
|
316
|
+
try {
|
|
317
|
+
return typeof e === 'object' && e !== null && e.name === 'TimeoutError';
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function usablePlanReviewWireClient(client) {
|
|
324
|
+
if (client === null || typeof client !== 'object')
|
|
325
|
+
return false;
|
|
326
|
+
const c = client;
|
|
327
|
+
return typeof c.assistant?.planReview === 'function' && typeof c.runs?.get === 'function';
|
|
328
|
+
}
|
|
329
|
+
const PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS = 15_000;
|
|
299
330
|
const decideInFlight = new Map();
|
|
300
331
|
let decideDispatchSeq = 0;
|
|
301
332
|
export function __resetPlanReviewDecisionLatchForTests() {
|
|
302
333
|
decideInFlight.clear();
|
|
303
334
|
}
|
|
304
|
-
export async function decidePlanReview(taskId, decision, permissionModeAfter) {
|
|
335
|
+
export async function decidePlanReview(taskId, decision, permissionModeAfter, opts) {
|
|
305
336
|
if (decideInFlight.has(taskId)) {
|
|
306
337
|
hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
|
|
307
338
|
return;
|
|
@@ -315,43 +346,84 @@ export async function decidePlanReview(taskId, decision, permissionModeAfter) {
|
|
|
315
346
|
const dispatchNo = ++decideDispatchSeq;
|
|
316
347
|
hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
|
|
317
348
|
try {
|
|
318
|
-
await decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch);
|
|
349
|
+
await decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, opts);
|
|
319
350
|
}
|
|
320
351
|
finally {
|
|
321
352
|
releaseLatch();
|
|
322
353
|
}
|
|
323
354
|
}
|
|
324
|
-
|
|
355
|
+
function planReviewClientsFor(wire, cfg) {
|
|
356
|
+
if (wire !== undefined) {
|
|
357
|
+
let injected;
|
|
358
|
+
try {
|
|
359
|
+
injected = typeof wire === 'object' && wire !== null ? wire.client : undefined;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
injected = undefined;
|
|
363
|
+
}
|
|
364
|
+
if (!usablePlanReviewWireClient(injected))
|
|
365
|
+
return null;
|
|
366
|
+
const client = injected;
|
|
367
|
+
return { decide: client, repull: () => client };
|
|
368
|
+
}
|
|
369
|
+
if (!cfg)
|
|
370
|
+
return null;
|
|
371
|
+
const decide = makeEngineWireClient({
|
|
372
|
+
baseUrl: cfg.baseUrl,
|
|
373
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
374
|
+
principal: cfg.principal,
|
|
375
|
+
timeoutMs: PLAN_REVIEW_RESUME_TIMEOUT_MS,
|
|
376
|
+
});
|
|
377
|
+
if (!decide)
|
|
378
|
+
return null;
|
|
379
|
+
return {
|
|
380
|
+
decide,
|
|
381
|
+
repull: () => makeEngineWireClient({
|
|
382
|
+
baseUrl: cfg.baseUrl,
|
|
383
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
384
|
+
principal: cfg.principal,
|
|
385
|
+
timeoutMs: 15_000,
|
|
386
|
+
}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, opts) {
|
|
325
390
|
let outcome;
|
|
326
391
|
let effect = 'unconfirmed';
|
|
327
|
-
|
|
392
|
+
let statusSeen;
|
|
393
|
+
const wire = opts?.wire;
|
|
394
|
+
const cfg = wire === undefined ? engineWireTarget() : null;
|
|
328
395
|
const badMode = permissionModeAfter !== undefined && (decision !== 'approve' || !isPlanReviewModeAfter(permissionModeAfter));
|
|
329
396
|
if (badMode) {
|
|
330
397
|
hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter is only valid with approve and must be "default" or "acceptEdits"`);
|
|
331
398
|
effect = 'not_sent';
|
|
332
399
|
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
400
|
}
|
|
334
|
-
else if (!cfg) {
|
|
401
|
+
else if (wire === undefined && !cfg) {
|
|
335
402
|
hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
|
|
336
403
|
effect = 'not_sent';
|
|
337
404
|
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
405
|
}
|
|
339
406
|
else {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
407
|
+
const clients = planReviewClientsFor(wire, cfg);
|
|
408
|
+
if (!clients) {
|
|
409
|
+
if (wire !== undefined) {
|
|
410
|
+
hostLog('error', `planReviewWire: ${decision} NOT sent — the engine client supplied by the host is not usable (needs assistant.planReview and runs.get)`);
|
|
411
|
+
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.`;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
hostLog('error', `planReviewWire: ${decision} NOT sent — makeEngineWireClient() returned null`);
|
|
415
|
+
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.`;
|
|
416
|
+
}
|
|
348
417
|
effect = 'not_sent';
|
|
349
|
-
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.`;
|
|
350
418
|
}
|
|
351
419
|
else {
|
|
420
|
+
const client = clients.decide;
|
|
352
421
|
try {
|
|
353
422
|
const mode = isPlanReviewModeAfter(permissionModeAfter) ? permissionModeAfter : undefined;
|
|
423
|
+
const reason = denyReasonForWire(opts?.reason, `plan-review ${taskId}`);
|
|
354
424
|
const reqBody = { decision };
|
|
425
|
+
if (reason !== undefined)
|
|
426
|
+
reqBody.reason = reason;
|
|
355
427
|
if (mode !== undefined)
|
|
356
428
|
reqBody.permissionModeAfter = mode;
|
|
357
429
|
let body;
|
|
@@ -362,7 +434,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
362
434
|
const conflict = e1;
|
|
363
435
|
if (mode === 'default' && conflict?.status === 400 && conflict?.errorCode === 'request.field_conflict') {
|
|
364
436
|
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 });
|
|
437
|
+
body = await client.assistant.planReview(taskId, reason !== undefined ? { decision, reason } : { decision });
|
|
366
438
|
}
|
|
367
439
|
else {
|
|
368
440
|
throw e1;
|
|
@@ -374,19 +446,28 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
374
446
|
hostLog('debug', `planReviewWire: ${decision} → ok (dispatch #${dispatchNo}) ${JSON.stringify(body).slice(0, 200)}`);
|
|
375
447
|
let postStatus;
|
|
376
448
|
try {
|
|
377
|
-
const probe =
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
449
|
+
const probe = clients.repull();
|
|
450
|
+
if (probe) {
|
|
451
|
+
let post;
|
|
452
|
+
if (wire !== undefined) {
|
|
453
|
+
const got = await settleWithinDeadline(PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS, (signal) => probe.runs.get(taskId, { signal }));
|
|
454
|
+
if (got.kind === 'error')
|
|
455
|
+
throw new Error(got.detail);
|
|
456
|
+
if (got.kind === 'deadline')
|
|
457
|
+
throw new Error(`post-decide status re-pull passed its ${PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS} ms deadline`);
|
|
458
|
+
post = got.value;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
post = (await probe.runs.get(taskId));
|
|
462
|
+
}
|
|
463
|
+
postStatus = typeof post?.status === 'string' ? post.status : undefined;
|
|
464
|
+
}
|
|
385
465
|
}
|
|
386
466
|
catch (e) {
|
|
387
467
|
hostLog('debug', `planReviewWire: post-decide status re-pull failed (fail-soft): ${String(e)}`);
|
|
388
468
|
}
|
|
389
469
|
const effective = postStatus ?? (typeof body?.status === 'string' ? body.status : undefined);
|
|
470
|
+
statusSeen = effective;
|
|
390
471
|
if (effective === 'needs_review') {
|
|
391
472
|
effect = 'still_parked';
|
|
392
473
|
outcome =
|
|
@@ -437,21 +518,41 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
437
518
|
effect = status >= 400 && status < 500 ? 'not_applied' : 'unconfirmed';
|
|
438
519
|
outcome = `${`The plan_review decision failed: HTTP ${status} ${msg}`.trim()}${decideFailureNote(e)}`;
|
|
439
520
|
}
|
|
521
|
+
else if (isRequestTimeout(e)) {
|
|
522
|
+
hostLog('debug', `planReviewWire: ${decision} sent but no answer within the request time limit (task ${taskId})`);
|
|
523
|
+
outcome = planReviewDecisionTimedOutOutcome(decision);
|
|
524
|
+
}
|
|
440
525
|
else {
|
|
441
526
|
outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
|
|
442
527
|
}
|
|
443
528
|
}
|
|
444
529
|
}
|
|
445
530
|
}
|
|
531
|
+
const tail = planReviewOutcomeTail(taskId, decision, effect);
|
|
532
|
+
const prompt = `<plan-review-outcome>\n${outcome}\n${tail}\n</plan-review-outcome>`;
|
|
533
|
+
const onOutcome = opts?.onOutcome;
|
|
446
534
|
try {
|
|
447
|
-
const delivered = enqueuePlanReviewOutcome(
|
|
535
|
+
const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision, effect });
|
|
448
536
|
if (!delivered) {
|
|
449
|
-
|
|
537
|
+
if (onOutcome !== undefined) {
|
|
538
|
+
hostLog('debug', `planReviewWire: outcome queue port not installed for task ${taskId} — outcome handed to the host's onOutcome callback only`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
|
|
542
|
+
}
|
|
450
543
|
}
|
|
451
544
|
}
|
|
452
545
|
catch (e) {
|
|
453
546
|
hostLog('debug', `planReviewWire: outcome enqueue threw: ${String(e)}`);
|
|
454
547
|
}
|
|
548
|
+
if (onOutcome !== undefined) {
|
|
549
|
+
try {
|
|
550
|
+
onOutcome({ taskId, dispatchNo, decision, effect, detail: outcome, tail, prompt, ...(statusSeen !== undefined ? { status: statusSeen } : {}) });
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
hostLog('debug', `planReviewWire: onOutcome callback threw for task ${taskId} (ignored): ${String(e)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
455
556
|
}
|
|
456
557
|
function reopenIdSuffix() {
|
|
457
558
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -545,6 +646,9 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
545
646
|
if (opts?.deliverDecision !== undefined) {
|
|
546
647
|
hostLog('debug', `planReviewWire: reopen canonical-reuse branch for task ${taskId} ignores opts.deliverDecision (answers route through the existing arm responder — documented exception)`);
|
|
547
648
|
}
|
|
649
|
+
if (opts?.wire !== undefined || opts?.onOutcome !== undefined) {
|
|
650
|
+
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)`);
|
|
651
|
+
}
|
|
548
652
|
retireActiveReopen(sessionKey, canonicalId);
|
|
549
653
|
if (receiptMs !== undefined) {
|
|
550
654
|
const receipt = waitForGateArmedFor(sessionKey, [canonicalId], receiptMs);
|
|
@@ -571,10 +675,13 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
571
675
|
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: canonicalId });
|
|
572
676
|
const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
|
|
573
677
|
hostLog('debug', `planReviewWire: reopen for task ${taskId} minting a fresh card ${questionId} (path=reopen-fresh)`);
|
|
678
|
+
const delivery = opts?.wire !== undefined || opts?.onOutcome !== undefined
|
|
679
|
+
? { ...(opts.wire !== undefined ? { wire: opts.wire } : {}), ...(opts.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
|
|
680
|
+
: undefined;
|
|
574
681
|
const deliver = opts?.deliverDecision ??
|
|
575
682
|
((tid, decision) => {
|
|
576
683
|
hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
|
|
577
|
-
void decidePlanReview(tid, decision);
|
|
684
|
+
void (delivery === undefined ? decidePlanReview(tid, decision) : decidePlanReview(tid, decision, undefined, delivery));
|
|
578
685
|
});
|
|
579
686
|
const activeKey = `${sessionKey}\u0000${canonicalId}`;
|
|
580
687
|
const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
@@ -724,7 +831,7 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
724
831
|
if (!hasQuestionOverlayFor(sessionKey)) {
|
|
725
832
|
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
726
833
|
}
|
|
727
|
-
if (sessionKey !== DEFAULT_SESSION_KEY && opts?.deliverDecision === undefined) {
|
|
834
|
+
if (sessionKey !== DEFAULT_SESSION_KEY && opts?.deliverDecision === undefined && opts?.wire === undefined) {
|
|
728
835
|
hostLog('error', `planReviewWire: plan_review 卡未重开 —— sessionKey=${JSON.stringify(sessionKey)} 是非默认槽,` +
|
|
729
836
|
'而缺省决断投递口(decidePlanReview)今天固定打默认槽的 baseUrl/token/principal。' +
|
|
730
837
|
'整条链转 keyed 之前,非默认槽要重开请自带 deliverDecision(按本槽投递);' +
|
|
@@ -11,5 +11,7 @@ export interface SessionPolicyDeliverability {
|
|
|
11
11
|
readonly withheld: readonly SessionPolicyWithheldRule[];
|
|
12
12
|
readonly unreadable?: true;
|
|
13
13
|
}
|
|
14
|
+
export declare function isEngineRefusedToolName(name: string): boolean;
|
|
14
15
|
export declare function sessionPolicyDeliverable(behavior: 'deny' | 'ask', rules: readonly string[]): SessionPolicyDeliverability;
|
|
15
16
|
export declare function sessionPolicyWithheldNotice(why: SessionPolicyWithheldWhy): string;
|
|
17
|
+
export declare function legacyToolNameFailureNoteOf(code: string | undefined): string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONFIG_LEGACY_TOOL_NAME } from '../engineErrorCodes.js';
|
|
1
2
|
const COVERING_NAMESPACE_PREFIXES = ['mcp__', 'a2a__'];
|
|
2
3
|
const ENGINE_RETIRED_TOOL_NAMES = new Map([
|
|
3
4
|
['read_file', 'Read'],
|
|
@@ -71,12 +72,17 @@ function withheldWhyOf(rule) {
|
|
|
71
72
|
}
|
|
72
73
|
if (rule.trim() !== rule)
|
|
73
74
|
return 'not_a_tool_name';
|
|
74
|
-
if (
|
|
75
|
-
return 'legacy_tool_name';
|
|
76
|
-
if (rule.includes('__') && !COVERING_NAMESPACE_PREFIXES.some((prefix) => rule.startsWith(prefix)))
|
|
75
|
+
if (isEngineRefusedToolName(rule))
|
|
77
76
|
return 'legacy_tool_name';
|
|
78
77
|
return undefined;
|
|
79
78
|
}
|
|
79
|
+
export function isEngineRefusedToolName(name) {
|
|
80
|
+
if (typeof name !== 'string')
|
|
81
|
+
return false;
|
|
82
|
+
if (ENGINE_RETIRED_TOOL_NAMES.has(name))
|
|
83
|
+
return true;
|
|
84
|
+
return name.includes('__') && !COVERING_NAMESPACE_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
85
|
+
}
|
|
80
86
|
export function sessionPolicyDeliverable(behavior, rules) {
|
|
81
87
|
const entries = [];
|
|
82
88
|
try {
|
|
@@ -128,3 +134,8 @@ export function sessionPolicyWithheldNotice(why) {
|
|
|
128
134
|
? WITHHELD_NOTICE[why]
|
|
129
135
|
: 'Not written to this session’s rule record.';
|
|
130
136
|
}
|
|
137
|
+
const LEGACY_TOOL_NAME_FAILURE_NOTE = `This run did not start: a tool rule applied to it names a tool the engine refuses — a retired tool name, or a name that contains "__" but does not start with ${PREFIXES_IN_PROSE} (the engine's own error text names the entry). ` +
|
|
138
|
+
'Every later run fails the same way until that entry is removed from wherever it is set: this session’s rule record, the settings sent with the run, a parent session’s rule record, or the deployment’s own tool rules.';
|
|
139
|
+
export function legacyToolNameFailureNoteOf(code) {
|
|
140
|
+
return code === CONFIG_LEGACY_TOOL_NAME ? LEGACY_TOOL_NAME_FAILURE_NOTE : undefined;
|
|
141
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionPermissionRules, StoredSessionRules } from '@sema-agent/sdk';
|
|
2
2
|
import type { SessionPolicyReading } from '../sessionPolicyCapability.js';
|
|
3
|
+
import type { ToolRosterView } from '../toolRoster.js';
|
|
3
4
|
export interface SessionPolicyFacade {
|
|
4
5
|
getPolicy(sessionId: string, opts?: {
|
|
5
6
|
signal?: AbortSignal;
|
|
@@ -93,3 +94,42 @@ export declare function tightenSessionPolicy(facade: SessionPolicyFacade, req: S
|
|
|
93
94
|
signal?: AbortSignal;
|
|
94
95
|
}): Promise<SessionPolicyTightenOutcome>;
|
|
95
96
|
export declare function sessionPolicyTightenNotice(outcome: SessionPolicyTightenOutcome): string;
|
|
97
|
+
export interface SessionPolicyRemovedEntry {
|
|
98
|
+
readonly field: 'toolDeny' | 'toolAllow';
|
|
99
|
+
readonly rule: string;
|
|
100
|
+
}
|
|
101
|
+
export type SessionPolicyRemovalOutcome = {
|
|
102
|
+
kind: 'roster_required';
|
|
103
|
+
why: SessionPolicyRosterRequiredWhy;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'removed';
|
|
106
|
+
rev: number;
|
|
107
|
+
removed: readonly SessionPolicyRemovedEntry[];
|
|
108
|
+
} | {
|
|
109
|
+
kind: 'nothing_to_remove';
|
|
110
|
+
rev: number;
|
|
111
|
+
} | {
|
|
112
|
+
kind: 'loosen_forbidden';
|
|
113
|
+
message: string;
|
|
114
|
+
refused: readonly SessionPolicyRemovedEntry[];
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'conflict';
|
|
117
|
+
rev: number;
|
|
118
|
+
} | {
|
|
119
|
+
kind: 'read_failed';
|
|
120
|
+
why: Extract<SessionPolicyTightenUnknownWhy, 'face_unavailable' | 'session_not_found' | 'session_ownerless' | 'unauthorized' | 'request_rejected' | 'store_corrupt' | 'read_unreadable' | 'read_failed'>;
|
|
121
|
+
message?: string;
|
|
122
|
+
} | {
|
|
123
|
+
kind: 'write_refused';
|
|
124
|
+
why: Extract<SessionPolicyTightenUnknownWhy, 'face_unavailable' | 'session_not_found' | 'session_ownerless' | 'unauthorized' | 'request_rejected' | 'store_corrupt' | 'write_refused'>;
|
|
125
|
+
message?: string;
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'write_unconfirmed';
|
|
128
|
+
why: Extract<SessionPolicyTightenUnknownWhy, 'write_indeterminate' | 'write_receipt_unreadable' | 'write_receipt_mismatch'>;
|
|
129
|
+
message?: string;
|
|
130
|
+
};
|
|
131
|
+
export type SessionPolicyRosterRequiredWhy = 'absent' | 'unreadable';
|
|
132
|
+
export declare function removeRefusedSessionRules(facade: SessionPolicyFacade, sessionId: string, roster: ToolRosterView | undefined, opts?: {
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
}): Promise<SessionPolicyRemovalOutcome>;
|
|
135
|
+
export declare function sessionPolicyRemovalNotice(outcome: SessionPolicyRemovalOutcome): string;
|