@sema-agent/client-core 0.82.0 → 0.82.2
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 +33 -0
- package/README.md +10 -7
- package/dist/adapt/toolCards.d.ts +15 -0
- package/dist/adapt/toolCards.js +34 -19
- package/dist/adapter/activeRunSelfHeal.d.ts +3 -0
- package/dist/adapter/activeRunSelfHeal.js +37 -7
- package/dist/adapter/downstream/eventToSdkMessage.js +7 -4
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +44 -5
- package/dist/adapter/runStream.js +33 -4
- package/dist/adapter/types.d.ts +2 -0
- package/dist/agentsWireCaps.d.ts +1 -0
- package/dist/agentsWireCaps.js +110 -29
- package/dist/cloudConfigWireCaps.d.ts +77 -2
- package/dist/cloudConfigWireCaps.js +405 -8
- package/dist/decideFailureNote.d.ts +1 -0
- package/dist/decideFailureNote.js +8 -0
- package/dist/engineNoticeCodes.js +2 -0
- package/dist/executionLaneCapability.js +3 -0
- package/dist/fileHistoryCaptureCapability.d.ts +18 -0
- package/dist/fileHistoryCaptureCapability.js +58 -0
- package/dist/gateOutcome.js +2 -0
- package/dist/hitl/askGateWire.d.ts +1 -1
- package/dist/hitl/askGateWire.js +1 -1
- package/dist/hitl/frameRouter.d.ts +2 -1
- package/dist/hitl/hitlBridge.d.ts +29 -1
- package/dist/hitl/hitlBridge.js +135 -3
- package/dist/hitl/hitlHostSurface.js +2 -3
- package/dist/hitl/parkResolver.d.ts +1 -0
- package/dist/hitl/parkResolver.js +17 -9
- package/dist/hitl/planReviewWire.d.ts +6 -0
- package/dist/hitl/planReviewWire.js +8 -2
- package/dist/hitl/toolApprovalWire.d.ts +2 -1
- package/dist/hitl/toolApprovalWire.js +105 -102
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/modelIdentityResolvability.d.ts +31 -0
- package/dist/modelIdentityResolvability.js +145 -0
- package/dist/notifications.d.ts +3 -1
- package/dist/notifications.js +18 -2
- package/dist/printToolResultFrame.js +15 -1
- package/dist/resumeRefusalCopy.d.ts +12 -0
- package/dist/resumeRefusalCopy.js +45 -1
- package/dist/seam.d.ts +2 -0
- package/dist/toolResult.d.ts +2 -2
- package/dist/toolResult.js +7 -6
- package/docs/INTEGRATION-CLIENTS.md +168 -11
- package/package.json +3 -3
|
@@ -64,6 +64,29 @@ export declare class DecideTransportRetryExhaustedError extends Error {
|
|
|
64
64
|
readonly attempts: number;
|
|
65
65
|
constructor(attempts: number, lastFailureText: string);
|
|
66
66
|
}
|
|
67
|
+
export interface DecideOperationIdentity {
|
|
68
|
+
readonly operation: object;
|
|
69
|
+
readonly taskId: string;
|
|
70
|
+
readonly sessionId: string;
|
|
71
|
+
readonly boundCallId?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface DecideOperationStart extends DecideOperationIdentity {
|
|
74
|
+
readonly phase: 'start';
|
|
75
|
+
}
|
|
76
|
+
export interface DecideOperationEnd extends DecideOperationIdentity {
|
|
77
|
+
readonly phase: 'end';
|
|
78
|
+
readonly ok: boolean;
|
|
79
|
+
readonly attempts: number;
|
|
80
|
+
readonly retryExhausted: boolean;
|
|
81
|
+
}
|
|
82
|
+
export type DecideOperationEvent = DecideOperationStart | DecideOperationEnd;
|
|
83
|
+
export type DecideOperationObserver = (ev: DecideOperationEvent) => void | Promise<void>;
|
|
84
|
+
export interface HitlBridgeOptions {
|
|
85
|
+
onDecideOperation?: DecideOperationObserver;
|
|
86
|
+
}
|
|
87
|
+
export interface DecideOperationSpan {
|
|
88
|
+
close(): void;
|
|
89
|
+
}
|
|
67
90
|
export interface ActiveGate {
|
|
68
91
|
gate: CheckpointGate;
|
|
69
92
|
seq?: string | undefined;
|
|
@@ -85,7 +108,8 @@ export declare class HitlBridge {
|
|
|
85
108
|
private readonly client;
|
|
86
109
|
private readonly taskId;
|
|
87
110
|
private active;
|
|
88
|
-
|
|
111
|
+
private readonly decideObserver;
|
|
112
|
+
constructor(client: HitlClientLike, taskId: string, opts?: HitlBridgeOptions);
|
|
89
113
|
observe(ev: AgentEvent): void;
|
|
90
114
|
currentGate(): CheckpointGate | null;
|
|
91
115
|
currentGateReopen(): SuspendedReopenReading | null;
|
|
@@ -97,6 +121,7 @@ export declare class HitlBridge {
|
|
|
97
121
|
remember?: 'session' | undefined;
|
|
98
122
|
} | HitlDecideDenyOutcome, toolUseID?: string, opts?: {
|
|
99
123
|
signal?: AbortSignal;
|
|
124
|
+
operation?: DecideOperationSpan;
|
|
100
125
|
}, preResolvedPending?: PendingCheckpoint): Promise<unknown>;
|
|
101
126
|
answerQuestion(answers: AskAnswer[], toolUseID?: string, opts?: {
|
|
102
127
|
signal?: AbortSignal;
|
|
@@ -108,6 +133,9 @@ export declare class HitlBridge {
|
|
|
108
133
|
signal?: AbortSignal;
|
|
109
134
|
}): Promise<AssistantTaskStatus>;
|
|
110
135
|
private decideRaw;
|
|
136
|
+
private decideIdentityOf;
|
|
137
|
+
openDecideOperation(): DecideOperationSpan;
|
|
138
|
+
private decideRawAttempts;
|
|
111
139
|
}
|
|
112
140
|
export type GatePrompt<D extends HitlPermissionDecisionLike = HitlPermissionDecisionLike> = (ctx: {
|
|
113
141
|
toolName: string;
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -106,6 +106,94 @@ export class DecideTransportRetryExhaustedError extends Error {
|
|
|
106
106
|
this.name = 'DecideTransportRetryExhaustedError';
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
class DecideSpan {
|
|
110
|
+
owner;
|
|
111
|
+
observer;
|
|
112
|
+
identity;
|
|
113
|
+
attempts = 0;
|
|
114
|
+
ok = false;
|
|
115
|
+
retryExhausted = false;
|
|
116
|
+
closedFlag = false;
|
|
117
|
+
inFlight = 0;
|
|
118
|
+
ended = false;
|
|
119
|
+
constructor(owner, observer) {
|
|
120
|
+
this.owner = owner;
|
|
121
|
+
this.observer = observer;
|
|
122
|
+
}
|
|
123
|
+
admits(bridge, identity) {
|
|
124
|
+
if (bridge !== this.owner || this.closedFlag)
|
|
125
|
+
return false;
|
|
126
|
+
const mine = this.identity;
|
|
127
|
+
return mine === undefined || (mine.sessionId === identity.sessionId && mine.boundCallId === identity.boundCallId);
|
|
128
|
+
}
|
|
129
|
+
async run(identity, send) {
|
|
130
|
+
if (this.observer !== undefined && this.identity === undefined) {
|
|
131
|
+
this.identity = identity;
|
|
132
|
+
notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'start' }));
|
|
133
|
+
}
|
|
134
|
+
this.inFlight++;
|
|
135
|
+
const tally = { attempts: 0 };
|
|
136
|
+
try {
|
|
137
|
+
const r = await send(tally);
|
|
138
|
+
this.ok = true;
|
|
139
|
+
this.retryExhausted = false;
|
|
140
|
+
return r;
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
this.ok = false;
|
|
144
|
+
this.retryExhausted = e instanceof DecideTransportRetryExhaustedError;
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
this.attempts += tally.attempts;
|
|
149
|
+
this.inFlight--;
|
|
150
|
+
if (this.closedFlag && this.inFlight === 0)
|
|
151
|
+
this.emitEnd();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
close() {
|
|
155
|
+
if (this.closedFlag)
|
|
156
|
+
return;
|
|
157
|
+
this.closedFlag = true;
|
|
158
|
+
if (this.inFlight === 0)
|
|
159
|
+
this.emitEnd();
|
|
160
|
+
}
|
|
161
|
+
emitEnd() {
|
|
162
|
+
if (this.ended)
|
|
163
|
+
return;
|
|
164
|
+
this.ended = true;
|
|
165
|
+
if (this.observer === undefined || this.identity === undefined)
|
|
166
|
+
return;
|
|
167
|
+
notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'end', ok: this.ok, attempts: this.attempts, retryExhausted: this.retryExhausted }));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function reportObserverFault(phase, what, e) {
|
|
171
|
+
try {
|
|
172
|
+
hostLog('debug', `hitlBridge: onDecideOperation(${phase}) ${what}: ${String(e).slice(0, 160)}`);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function wireOptsOf(opts) {
|
|
180
|
+
if (opts === undefined || opts.operation === undefined)
|
|
181
|
+
return opts;
|
|
182
|
+
return opts.signal !== undefined ? { signal: opts.signal } : undefined;
|
|
183
|
+
}
|
|
184
|
+
function notifyDecideObserver(observer, ev) {
|
|
185
|
+
try {
|
|
186
|
+
const out = observer(ev);
|
|
187
|
+
if (out && typeof out.then === 'function') {
|
|
188
|
+
void Promise.resolve(out).then(undefined, (e) => {
|
|
189
|
+
reportObserverFault(ev.phase, 'rejected', e);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
reportObserverFault(ev.phase, 'threw', e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
109
197
|
const ANY_TOOL_FAMILY = () => true;
|
|
110
198
|
export async function findPendingForTask(client, taskId, matches, opts, callId, identityAccepts = (r) => matches(typeof r.toolName === 'string' ? r.toolName : undefined)) {
|
|
111
199
|
let rows;
|
|
@@ -132,9 +220,11 @@ export class HitlBridge {
|
|
|
132
220
|
client;
|
|
133
221
|
taskId;
|
|
134
222
|
active = null;
|
|
135
|
-
|
|
223
|
+
decideObserver;
|
|
224
|
+
constructor(client, taskId, opts) {
|
|
136
225
|
this.client = client;
|
|
137
226
|
this.taskId = taskId;
|
|
227
|
+
this.decideObserver = typeof opts?.onDecideOperation === 'function' ? opts.onDecideOperation : undefined;
|
|
138
228
|
}
|
|
139
229
|
observe(ev) {
|
|
140
230
|
switch (ev.type) {
|
|
@@ -180,7 +270,7 @@ export class HitlBridge {
|
|
|
180
270
|
return { boundCallId: p.boundCallId, boundInputHash: p.boundInputHash };
|
|
181
271
|
}
|
|
182
272
|
async decideTool(outcome, toolUseID, opts, preResolvedPending) {
|
|
183
|
-
const pending = preResolvedPending ?? (await this.pendingForDecide(toolUseID, opts));
|
|
273
|
+
const pending = preResolvedPending ?? (await this.pendingForDecide(toolUseID, wireOptsOf(opts)));
|
|
184
274
|
if (!pending) {
|
|
185
275
|
throw new HitlSafetyError('no pending checkpoint to decide (resolved/expired under the human) — refetch + re-present', 'no_pending');
|
|
186
276
|
}
|
|
@@ -253,7 +343,48 @@ export class HitlBridge {
|
|
|
253
343
|
}
|
|
254
344
|
return this.client.assistant.resume(this.taskId, opts);
|
|
255
345
|
}
|
|
256
|
-
async decideRaw(sessionId, decision,
|
|
346
|
+
async decideRaw(sessionId, decision, optsIn) {
|
|
347
|
+
const opts = wireOptsOf(optsIn);
|
|
348
|
+
const op = optsIn?.operation;
|
|
349
|
+
if (op instanceof DecideSpan) {
|
|
350
|
+
const identity = this.decideIdentityOf(sessionId, decision);
|
|
351
|
+
if (op.admits(this, identity))
|
|
352
|
+
return op.run(identity, (tally) => this.decideRawAttempts(sessionId, decision, opts, tally));
|
|
353
|
+
}
|
|
354
|
+
const observer = this.decideObserver;
|
|
355
|
+
const tally = { attempts: 0 };
|
|
356
|
+
if (observer === undefined)
|
|
357
|
+
return this.decideRawAttempts(sessionId, decision, opts, tally);
|
|
358
|
+
const identity = this.decideIdentityOf(sessionId, decision);
|
|
359
|
+
notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'start' }));
|
|
360
|
+
let ok = false;
|
|
361
|
+
let retryExhausted = false;
|
|
362
|
+
try {
|
|
363
|
+
const r = await this.decideRawAttempts(sessionId, decision, opts, tally);
|
|
364
|
+
ok = true;
|
|
365
|
+
return r;
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
retryExhausted = e instanceof DecideTransportRetryExhaustedError;
|
|
369
|
+
throw e;
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'end', ok, attempts: tally.attempts, retryExhausted }));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
decideIdentityOf(sessionId, decision) {
|
|
376
|
+
const bound = decision.boundCallId;
|
|
377
|
+
return {
|
|
378
|
+
operation: Object.freeze({}),
|
|
379
|
+
taskId: this.taskId,
|
|
380
|
+
sessionId,
|
|
381
|
+
...(typeof bound === 'string' && bound.length > 0 ? { boundCallId: bound } : {}),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
openDecideOperation() {
|
|
385
|
+
return new DecideSpan(this, this.decideObserver);
|
|
386
|
+
}
|
|
387
|
+
async decideRawAttempts(sessionId, decision, opts, tally) {
|
|
257
388
|
const callerAborted = () => opts?.signal?.aborted === true;
|
|
258
389
|
const startedAt = Date.now();
|
|
259
390
|
let attempts = 0;
|
|
@@ -261,6 +392,7 @@ export class HitlBridge {
|
|
|
261
392
|
let backoff = DECIDE_TRANSPORT_RETRY_BACKOFF_MS;
|
|
262
393
|
for (;;) {
|
|
263
394
|
attempts++;
|
|
395
|
+
tally.attempts = attempts;
|
|
264
396
|
try {
|
|
265
397
|
const r = await this.client.approvals.decide(sessionId, decision, opts);
|
|
266
398
|
return r;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
2
2
|
import { hostLog } from '../host.js';
|
|
3
3
|
import { unrefTimer } from '../unrefTimer.js';
|
|
4
|
-
import {
|
|
4
|
+
import { decideFailureNote } from '../decideFailureNote.js';
|
|
5
5
|
import { HitlSafetyError } from './hitlBridge.js';
|
|
6
6
|
const hostSurfaceByKey = createSessionSlot();
|
|
7
7
|
const hostSurfaceMissesByKey = new Map();
|
|
@@ -113,8 +113,7 @@ export function observeCancelByDeny(settle, taskId) {
|
|
|
113
113
|
hostLog('debug', `[hitl-cancel-deny] cancel-by-deny no_pending for run ${taskId} (benign — already resolved)`);
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
|
-
const
|
|
117
|
-
const rowNote = where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`;
|
|
116
|
+
const rowNote = decideFailureNote(e);
|
|
118
117
|
hostLog('error', `[hitl-cancel-deny] cancel-by-deny FAILED for run ${taskId}: ${String(e)}${rowNote}`);
|
|
119
118
|
surfaceCancelDenyWarn(`${CANCEL_DENY_WARN_TEXT}${rowNote}`);
|
|
120
119
|
})
|
|
@@ -33,6 +33,7 @@ export type GateOutcome = {
|
|
|
33
33
|
currentPending?: GateCurrentPending | undefined;
|
|
34
34
|
};
|
|
35
35
|
export declare function isAlreadyResolvedGateReason(reason: unknown): boolean;
|
|
36
|
+
export declare function isGateStandingErrorCode(code: string | null | undefined): boolean;
|
|
36
37
|
export declare function toAnsweredOutput(questions: unknown[], answer: QuestionAnswer): AskAnsweredOutput;
|
|
37
38
|
export type ParkResolution = {
|
|
38
39
|
kind: 'reattach';
|
|
@@ -1,5 +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
4
|
import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
|
|
4
5
|
import { hostLog } from '../host.js';
|
|
5
6
|
import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
|
|
@@ -7,7 +8,7 @@ import { observeCancelByDeny } from './hitlHostSurface.js';
|
|
|
7
8
|
import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
|
|
8
9
|
import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
|
|
9
10
|
import { readDecideReceipt } from '../decideReceipt.js';
|
|
10
|
-
import {
|
|
11
|
+
import { decideFailureNote } from '../decideFailureNote.js';
|
|
11
12
|
export const MAX_GATE_HOPS = 2;
|
|
12
13
|
export function nextHopBudget(prev, round) {
|
|
13
14
|
const total = (prev?.total ?? 0) + 1;
|
|
@@ -41,7 +42,14 @@ function alreadyResolvedReasonToken(reason) {
|
|
|
41
42
|
}
|
|
42
43
|
function isAlreadyResolvedFailure(outcome) {
|
|
43
44
|
const code = 'code' in outcome && isGateFailureCode(outcome.code) ? outcome.code : undefined;
|
|
44
|
-
|
|
45
|
+
if (code !== undefined)
|
|
46
|
+
return code === 'no_pending';
|
|
47
|
+
if ('errorCode' in outcome && isGateStandingErrorCode(outcome.errorCode))
|
|
48
|
+
return false;
|
|
49
|
+
return isAlreadyResolvedGateReason(outcome.reason);
|
|
50
|
+
}
|
|
51
|
+
export function isGateStandingErrorCode(code) {
|
|
52
|
+
return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt';
|
|
45
53
|
}
|
|
46
54
|
export function toAnsweredOutput(questions, answer) {
|
|
47
55
|
const answers = {};
|
|
@@ -108,7 +116,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
108
116
|
onPresented?.();
|
|
109
117
|
publishQuestionFrame({ type: 'question', questionId, questions });
|
|
110
118
|
});
|
|
111
|
-
const bridge = new HitlBridge(deps.client, taskId);
|
|
119
|
+
const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
|
|
112
120
|
if (answer === null) {
|
|
113
121
|
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
|
|
114
122
|
return { kind: 'aborted', gatedCallId };
|
|
@@ -141,7 +149,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
141
149
|
kind: 'failed',
|
|
142
150
|
stage: 'decide',
|
|
143
151
|
gatedCallId,
|
|
144
|
-
reason:
|
|
152
|
+
reason: `decide failed: ${String(e)}${decideFailureNote(e)}`,
|
|
145
153
|
...(code !== undefined ? { code } : {}),
|
|
146
154
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
147
155
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
@@ -149,10 +157,6 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
149
157
|
};
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
|
-
function withCorruptStoredRow(reason, err) {
|
|
153
|
-
const where = corruptStoredRowWhere(err);
|
|
154
|
-
return where === undefined ? reason : `${reason} — ${corruptStoredRowContent(where)}`;
|
|
155
|
-
}
|
|
156
160
|
function argsByCallOf(led) {
|
|
157
161
|
return new Map(led.gatedStartArgs());
|
|
158
162
|
}
|
|
@@ -166,7 +170,11 @@ function isFetchStepNoPending(outcome) {
|
|
|
166
170
|
async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
|
|
167
171
|
const { deps, led, taskId } = ctx;
|
|
168
172
|
if (park.gate === 'fs') {
|
|
169
|
-
const outcome = await surfaceFsApprovalAndDecide({
|
|
173
|
+
const outcome = await surfaceFsApprovalAndDecide({
|
|
174
|
+
client: deps.client,
|
|
175
|
+
...(onPresented ? { onPresented } : {}),
|
|
176
|
+
...(deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : {}),
|
|
177
|
+
}, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
170
178
|
const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
|
|
171
179
|
if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
|
|
172
180
|
const denySettledBy = 'denySettledBy' in outcome ? outcome.denySettledBy : undefined;
|
|
@@ -29,6 +29,12 @@ export declare function armPlanReviewApproval(result: unknown, sessionKey?: stri
|
|
|
29
29
|
submittedInPlanMode?: boolean;
|
|
30
30
|
}): boolean;
|
|
31
31
|
export type PlanReviewDecisionEffect = 'took_effect' | 'advanced' | 'still_parked' | 'not_sent' | 'not_applied' | 'unconfirmed';
|
|
32
|
+
export interface PlanReviewOutcomeMeta {
|
|
33
|
+
taskId: string;
|
|
34
|
+
dispatchNo: number;
|
|
35
|
+
decision: 'approve' | 'reject';
|
|
36
|
+
effect: PlanReviewDecisionEffect;
|
|
37
|
+
}
|
|
32
38
|
export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect): string;
|
|
33
39
|
export declare function __resetPlanReviewDecisionLatchForTests(): void;
|
|
34
40
|
export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject', permissionModeAfter?: string): Promise<void>;
|
|
@@ -4,6 +4,7 @@ import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
|
4
4
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
5
5
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
6
6
|
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
7
|
+
import { decideFailureNote } from '../decideFailureNote.js';
|
|
7
8
|
import { isPlanReviewModeAfter } from './hitlBridge.js';
|
|
8
9
|
import { engineCapString } from '../engineCapsCache.js';
|
|
9
10
|
import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
@@ -337,7 +338,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
337
338
|
const msg = e instanceof Error ? e.message : String(e);
|
|
338
339
|
hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
|
|
339
340
|
effect = status >= 400 && status < 500 ? 'not_applied' : 'unconfirmed';
|
|
340
|
-
outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim()
|
|
341
|
+
outcome = `${`The plan_review decision failed: HTTP ${status} ${msg}`.trim()}${decideFailureNote(e)}`;
|
|
341
342
|
}
|
|
342
343
|
else {
|
|
343
344
|
outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
|
|
@@ -346,7 +347,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
try {
|
|
349
|
-
const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}\n${planReviewOutcomeTail(taskId, decision, effect)}\n</plan-review-outcome
|
|
350
|
+
const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}\n${planReviewOutcomeTail(taskId, decision, effect)}\n</plan-review-outcome>`, { taskId, dispatchNo, decision, effect });
|
|
350
351
|
if (!delivered) {
|
|
351
352
|
hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
|
|
352
353
|
}
|
|
@@ -382,6 +383,11 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
382
383
|
if (typeof taskId !== 'string' || taskId.length === 0) {
|
|
383
384
|
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
384
385
|
}
|
|
386
|
+
if (decideInFlight.has(taskId)) {
|
|
387
|
+
hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — a decision for this task is in flight (its answer is still being delivered)`);
|
|
388
|
+
const refused = { reopened: false, _sema_decisionInFlight: true };
|
|
389
|
+
return receiptMs === undefined ? refused : Promise.resolve(refused);
|
|
390
|
+
}
|
|
385
391
|
const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
386
392
|
if (!hasQuestionOverlayFor(sessionKey)) {
|
|
387
393
|
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike } from './hitlBridge.js';
|
|
1
|
+
import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike, type DecideOperationObserver } from './hitlBridge.js';
|
|
2
2
|
import { type RuleStoreUnreadableKind } from '../gateVocabulary.js';
|
|
3
3
|
import { type DenyAttributionRefusalDetail } from '../wireRefusalCopy.js';
|
|
4
4
|
import { type DecideReceiptView } from '../decideReceipt.js';
|
|
@@ -43,6 +43,7 @@ export type FsApprovalOutcome = {
|
|
|
43
43
|
export interface FsApprovalWireDeps {
|
|
44
44
|
client: HitlClientLike;
|
|
45
45
|
onPresented?: () => void;
|
|
46
|
+
onDecideOperation?: DecideOperationObserver;
|
|
46
47
|
}
|
|
47
48
|
export declare function structurallyEqual(a: unknown, b: unknown): boolean;
|
|
48
49
|
export type RuleOfferMatch = SdkRuleOfferMatch;
|
|
@@ -7,7 +7,7 @@ import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded
|
|
|
7
7
|
import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
|
|
8
8
|
import { isRuleStoreUnreadableKind } from '../gateVocabulary.js';
|
|
9
9
|
import { denyAttributionRefusalFromError } from '../wireRefusalCopy.js';
|
|
10
|
-
import {
|
|
10
|
+
import { decideFailureNote } from '../decideFailureNote.js';
|
|
11
11
|
import { readDecideReceipt } from '../decideReceipt.js';
|
|
12
12
|
import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS, } from '@sema-agent/sdk';
|
|
13
13
|
export function isFsApprovalGate(gate) {
|
|
@@ -29,10 +29,6 @@ export function isToolApprovalGate(gate) {
|
|
|
29
29
|
return true;
|
|
30
30
|
return typeof gate?.toolName === 'string' && toolNameIsShellExec(gate.toolName);
|
|
31
31
|
}
|
|
32
|
-
function withCorruptStoredRow(reason, err) {
|
|
33
|
-
const where = corruptStoredRowWhere(err);
|
|
34
|
-
return where === undefined ? reason : `${reason} — ${corruptStoredRowContent(where)}`;
|
|
35
|
-
}
|
|
36
32
|
export function structurallyEqual(a, b) {
|
|
37
33
|
if (a === b)
|
|
38
34
|
return true;
|
|
@@ -137,7 +133,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
137
133
|
if (typeof args !== 'object' || args === null) {
|
|
138
134
|
return { kind: 'failed', stage: 'input', gatedCallId, reason: 'gate has no tool input payload' };
|
|
139
135
|
}
|
|
140
|
-
const bridge = new HitlBridge(deps.client, taskId);
|
|
136
|
+
const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
|
|
141
137
|
const callKey = approvalCallKey(gatedCallId, taskId);
|
|
142
138
|
const ruleOffersReadOnly = readRuleOfferSupply(pending.ruleOffers, pending.ruleSuggestions);
|
|
143
139
|
const durableProbeCause = pending.riskDescriptor?.probeCause;
|
|
@@ -167,110 +163,117 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
167
163
|
? { toolCallId: pending.toolCallId }
|
|
168
164
|
: {}),
|
|
169
165
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
166
|
+
const decideSpan = bridge.openDecideOperation();
|
|
167
|
+
const decideOpts = { ...(signal ? { signal } : {}), operation: decideSpan };
|
|
168
|
+
try {
|
|
169
|
+
switch (card.kind) {
|
|
170
|
+
case 'failed':
|
|
171
|
+
return { kind: 'failed', stage: 'card', gatedCallId, reason: card.reason };
|
|
172
|
+
case 'retracted':
|
|
173
|
+
hostLog('debug', `liveHitlAskWire: approval card retracted without a decision for ${gatedCallId ?? '(no call)'} — nothing sent`);
|
|
174
|
+
return { kind: 'retracted', gatedCallId };
|
|
175
|
+
case 'aborted':
|
|
176
|
+
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
|
|
177
|
+
return { kind: 'aborted', gatedCallId };
|
|
178
|
+
case 'allow':
|
|
179
|
+
try {
|
|
180
|
+
if (card.allowSession) {
|
|
181
|
+
try {
|
|
182
|
+
const raw = await bridge.decideTool({ decision: 'approve', remember: 'session', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, decideOpts, pending);
|
|
183
|
+
const receipt = readDecideReceipt(raw);
|
|
184
|
+
return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
if (e instanceof HitlSafetyError || e instanceof DecideTransportRetryExhaustedError)
|
|
188
|
+
throw e;
|
|
189
|
+
if (readDecideCurrentPending(e) !== undefined)
|
|
190
|
+
throw e;
|
|
191
|
+
hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
|
|
192
|
+
}
|
|
193
193
|
}
|
|
194
|
+
const raw = await bridge.decideTool({ decision: 'approve', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, decideOpts, pending);
|
|
195
|
+
const receipt = readDecideReceipt(raw);
|
|
196
|
+
return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
|
|
194
197
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
case 'deny': {
|
|
215
|
-
const denySettledByForWire = readApprovalDenySettledBy(card);
|
|
216
|
-
const denyReasonSnapshot = typeof card.reason === 'string' ? card.reason : undefined;
|
|
217
|
-
let lastSendCarriedAttribution = false;
|
|
218
|
-
try {
|
|
219
|
-
const denyBody = {
|
|
220
|
-
decision: 'deny',
|
|
221
|
-
reason: denyReasonForWire(denyReasonSnapshot, `task ${taskId}`) ?? DEFAULT_DENY_REASON,
|
|
222
|
-
...(denySettledByForWire === 'policy' ? { settledBy: 'policy' } : {}),
|
|
223
|
-
};
|
|
224
|
-
let attributionDropped = false;
|
|
225
|
-
let raw;
|
|
226
|
-
lastSendCarriedAttribution = denySettledByForWire === 'policy';
|
|
198
|
+
catch (e) {
|
|
199
|
+
const currentPending = readDecideCurrentPending(e);
|
|
200
|
+
const wireCode = readWireErrorCode(e);
|
|
201
|
+
const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
|
|
202
|
+
return {
|
|
203
|
+
kind: 'failed',
|
|
204
|
+
stage: 'decide',
|
|
205
|
+
gatedCallId,
|
|
206
|
+
reason: `decide(approve) failed: ${String(e)}${decideFailureNote(e)}`,
|
|
207
|
+
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
208
|
+
...(currentPending !== undefined ? { currentPending } : {}),
|
|
209
|
+
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
|
210
|
+
...(safetyCode !== undefined ? { safetyCode } : {}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case 'deny': {
|
|
214
|
+
const denySettledByForWire = readApprovalDenySettledBy(card);
|
|
215
|
+
const denyReasonSnapshot = typeof card.reason === 'string' ? card.reason : undefined;
|
|
216
|
+
let lastSendCarriedAttribution = false;
|
|
227
217
|
try {
|
|
228
|
-
|
|
218
|
+
const denyBody = {
|
|
219
|
+
decision: 'deny',
|
|
220
|
+
reason: denyReasonForWire(denyReasonSnapshot, `task ${taskId}`) ?? DEFAULT_DENY_REASON,
|
|
221
|
+
...(denySettledByForWire === 'policy' ? { settledBy: 'policy' } : {}),
|
|
222
|
+
};
|
|
223
|
+
let attributionDropped = false;
|
|
224
|
+
let raw;
|
|
225
|
+
lastSendCarriedAttribution = denySettledByForWire === 'policy';
|
|
226
|
+
try {
|
|
227
|
+
raw = await bridge.decideTool(denyBody, gatedCallId, decideOpts, pending);
|
|
228
|
+
}
|
|
229
|
+
catch (first) {
|
|
230
|
+
if (!shouldResendWithoutAttribution(first, lastSendCarriedAttribution, signal))
|
|
231
|
+
throw first;
|
|
232
|
+
const { settledBy: _droppedKey, ...withoutAttribution } = denyBody;
|
|
233
|
+
void _droppedKey;
|
|
234
|
+
lastSendCarriedAttribution = false;
|
|
235
|
+
hostLog('debug', `liveToolApprovalWire: decide(deny) for task ${taskId} was refused because the settlement note is not part of ` +
|
|
236
|
+
'what this deployment signs — re-sending the SAME decision exactly once with that one key removed ' +
|
|
237
|
+
'(the refusal happened before the approval was judged, so nothing was consumed and this is a re-delivery, not a second act)');
|
|
238
|
+
raw = await bridge.decideTool(withoutAttribution, gatedCallId, decideOpts, pending);
|
|
239
|
+
attributionDropped = true;
|
|
240
|
+
}
|
|
241
|
+
const receipt = readDecideReceipt(raw);
|
|
242
|
+
const denySettledBy = denySettledByForWire;
|
|
243
|
+
const denyReason = denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '' ? denyReasonSnapshot : undefined;
|
|
244
|
+
return {
|
|
245
|
+
kind: 'decided',
|
|
246
|
+
gatedCallId,
|
|
247
|
+
denied: true,
|
|
248
|
+
...(denySettledBy !== undefined ? { denySettledBy } : {}),
|
|
249
|
+
...(denyReason !== undefined ? { denyReason } : {}),
|
|
250
|
+
...(attributionDropped ? { denyAttributionDropped: true } : {}),
|
|
251
|
+
...(receipt !== undefined ? { receipt } : {}),
|
|
252
|
+
};
|
|
229
253
|
}
|
|
230
|
-
catch (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
254
|
+
catch (e) {
|
|
255
|
+
const currentPending = readDecideCurrentPending(e);
|
|
256
|
+
const wireCode = readWireErrorCode(e);
|
|
257
|
+
const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
|
|
258
|
+
const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
|
|
259
|
+
return {
|
|
260
|
+
kind: 'failed',
|
|
261
|
+
stage: 'decide',
|
|
262
|
+
gatedCallId,
|
|
263
|
+
reason: `decide(deny) failed: ${String(e)}${decideFailureNote(e)}`,
|
|
264
|
+
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
265
|
+
...(currentPending !== undefined ? { currentPending } : {}),
|
|
266
|
+
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
|
267
|
+
...(safetyCode !== undefined ? { safetyCode } : {}),
|
|
268
|
+
...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
|
|
269
|
+
};
|
|
241
270
|
}
|
|
242
|
-
const receipt = readDecideReceipt(raw);
|
|
243
|
-
const denySettledBy = denySettledByForWire;
|
|
244
|
-
const denyReason = denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '' ? denyReasonSnapshot : undefined;
|
|
245
|
-
return {
|
|
246
|
-
kind: 'decided',
|
|
247
|
-
gatedCallId,
|
|
248
|
-
denied: true,
|
|
249
|
-
...(denySettledBy !== undefined ? { denySettledBy } : {}),
|
|
250
|
-
...(denyReason !== undefined ? { denyReason } : {}),
|
|
251
|
-
...(attributionDropped ? { denyAttributionDropped: true } : {}),
|
|
252
|
-
...(receipt !== undefined ? { receipt } : {}),
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
catch (e) {
|
|
256
|
-
const currentPending = readDecideCurrentPending(e);
|
|
257
|
-
const wireCode = readWireErrorCode(e);
|
|
258
|
-
const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
|
|
259
|
-
const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
|
|
260
|
-
return {
|
|
261
|
-
kind: 'failed',
|
|
262
|
-
stage: 'decide',
|
|
263
|
-
gatedCallId,
|
|
264
|
-
reason: withCorruptStoredRow(`decide(deny) failed: ${String(e)}`, e),
|
|
265
|
-
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
266
|
-
...(currentPending !== undefined ? { currentPending } : {}),
|
|
267
|
-
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
|
268
|
-
...(safetyCode !== undefined ? { safetyCode } : {}),
|
|
269
|
-
...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
|
|
270
|
-
};
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
+
finally {
|
|
275
|
+
decideSpan.close();
|
|
276
|
+
}
|
|
274
277
|
}
|
|
275
278
|
function readReadRootCandidate(v) {
|
|
276
279
|
if (typeof v !== 'object' || v === null)
|