@sema-agent/client-core 0.82.1 → 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 +11 -0
- package/README.md +4 -3
- package/dist/cloudConfigWireCaps.d.ts +77 -2
- package/dist/cloudConfigWireCaps.js +405 -8
- 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/parkResolver.js +6 -2
- package/dist/hitl/planReviewWire.d.ts +6 -0
- package/dist/hitl/planReviewWire.js +1 -1
- package/dist/hitl/toolApprovalWire.d.ts +2 -1
- package/dist/hitl/toolApprovalWire.js +104 -97
- package/dist/notifications.d.ts +3 -1
- package/dist/notifications.js +18 -2
- package/docs/INTEGRATION-CLIENTS.md +50 -6
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
2
|
-
import type { HitlClientLike } from './hitlBridge.js';
|
|
2
|
+
import type { DecideOperationObserver, HitlClientLike } from './hitlBridge.js';
|
|
3
3
|
import { type RespondToolApprovalFn, type ToolApprovalFrame, type ToolApprovalFrameLaneOpts, type ToolApprovalFrameOutcome } from './toolApprovalWire.js';
|
|
4
4
|
import type { GateLedger } from './gateLedger.js';
|
|
5
5
|
export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
@@ -22,6 +22,7 @@ export interface AskGateWireDeps {
|
|
|
22
22
|
onParkReattach?: (e: ParkReattachNotice) => void;
|
|
23
23
|
approvalLane?: ToolApprovalFrameLaneOpts;
|
|
24
24
|
onToolApprovalOutcome?: (frame: ToolApprovalFrame, outcome: ToolApprovalFrameOutcome) => void | Promise<void>;
|
|
25
|
+
onDecideOperation?: DecideOperationObserver;
|
|
25
26
|
}
|
|
26
27
|
export type GateKind = 'ask' | 'fs';
|
|
27
28
|
export interface GatePark {
|
|
@@ -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;
|
|
@@ -116,7 +116,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
116
116
|
onPresented?.();
|
|
117
117
|
publishQuestionFrame({ type: 'question', questionId, questions });
|
|
118
118
|
});
|
|
119
|
-
const bridge = new HitlBridge(deps.client, taskId);
|
|
119
|
+
const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
|
|
120
120
|
if (answer === null) {
|
|
121
121
|
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
|
|
122
122
|
return { kind: 'aborted', gatedCallId };
|
|
@@ -170,7 +170,11 @@ function isFetchStepNoPending(outcome) {
|
|
|
170
170
|
async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
|
|
171
171
|
const { deps, led, taskId } = ctx;
|
|
172
172
|
if (park.gate === 'fs') {
|
|
173
|
-
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);
|
|
174
178
|
const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
|
|
175
179
|
if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
|
|
176
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>;
|
|
@@ -347,7 +347,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
try {
|
|
350
|
-
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 });
|
|
351
351
|
if (!delivered) {
|
|
352
352
|
hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
|
|
353
353
|
}
|
|
@@ -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;
|
|
@@ -133,7 +133,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
133
133
|
if (typeof args !== 'object' || args === null) {
|
|
134
134
|
return { kind: 'failed', stage: 'input', gatedCallId, reason: 'gate has no tool input payload' };
|
|
135
135
|
}
|
|
136
|
-
const bridge = new HitlBridge(deps.client, taskId);
|
|
136
|
+
const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
|
|
137
137
|
const callKey = approvalCallKey(gatedCallId, taskId);
|
|
138
138
|
const ruleOffersReadOnly = readRuleOfferSupply(pending.ruleOffers, pending.ruleSuggestions);
|
|
139
139
|
const durableProbeCause = pending.riskDescriptor?.probeCause;
|
|
@@ -163,110 +163,117 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
163
163
|
? { toolCallId: pending.toolCallId }
|
|
164
164
|
: {}),
|
|
165
165
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
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 } : {}) };
|
|
190
197
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
case 'deny': {
|
|
211
|
-
const denySettledByForWire = readApprovalDenySettledBy(card);
|
|
212
|
-
const denyReasonSnapshot = typeof card.reason === 'string' ? card.reason : undefined;
|
|
213
|
-
let lastSendCarriedAttribution = false;
|
|
214
|
-
try {
|
|
215
|
-
const denyBody = {
|
|
216
|
-
decision: 'deny',
|
|
217
|
-
reason: denyReasonForWire(denyReasonSnapshot, `task ${taskId}`) ?? DEFAULT_DENY_REASON,
|
|
218
|
-
...(denySettledByForWire === 'policy' ? { settledBy: 'policy' } : {}),
|
|
219
|
-
};
|
|
220
|
-
let attributionDropped = false;
|
|
221
|
-
let raw;
|
|
222
|
-
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;
|
|
223
217
|
try {
|
|
224
|
-
|
|
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
|
+
};
|
|
225
253
|
}
|
|
226
|
-
catch (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
};
|
|
237
270
|
}
|
|
238
|
-
const receipt = readDecideReceipt(raw);
|
|
239
|
-
const denySettledBy = denySettledByForWire;
|
|
240
|
-
const denyReason = denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '' ? denyReasonSnapshot : undefined;
|
|
241
|
-
return {
|
|
242
|
-
kind: 'decided',
|
|
243
|
-
gatedCallId,
|
|
244
|
-
denied: true,
|
|
245
|
-
...(denySettledBy !== undefined ? { denySettledBy } : {}),
|
|
246
|
-
...(denyReason !== undefined ? { denyReason } : {}),
|
|
247
|
-
...(attributionDropped ? { denyAttributionDropped: true } : {}),
|
|
248
|
-
...(receipt !== undefined ? { receipt } : {}),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
catch (e) {
|
|
252
|
-
const currentPending = readDecideCurrentPending(e);
|
|
253
|
-
const wireCode = readWireErrorCode(e);
|
|
254
|
-
const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
|
|
255
|
-
const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
|
|
256
|
-
return {
|
|
257
|
-
kind: 'failed',
|
|
258
|
-
stage: 'decide',
|
|
259
|
-
gatedCallId,
|
|
260
|
-
reason: `decide(deny) failed: ${String(e)}${decideFailureNote(e)}`,
|
|
261
|
-
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
262
|
-
...(currentPending !== undefined ? { currentPending } : {}),
|
|
263
|
-
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
|
264
|
-
...(safetyCode !== undefined ? { safetyCode } : {}),
|
|
265
|
-
...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
|
|
266
|
-
};
|
|
267
271
|
}
|
|
268
272
|
}
|
|
269
273
|
}
|
|
274
|
+
finally {
|
|
275
|
+
decideSpan.close();
|
|
276
|
+
}
|
|
270
277
|
}
|
|
271
278
|
function readReadRootCandidate(v) {
|
|
272
279
|
if (typeof v !== 'object' || v === null)
|
package/dist/notifications.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PlanReviewOutcomeMeta } from './hitl/planReviewWire.js';
|
|
1
2
|
export interface TaskNotificationFields {
|
|
2
3
|
taskId: string;
|
|
3
4
|
status: string;
|
|
@@ -48,12 +49,13 @@ export interface NotificationQueuePort {
|
|
|
48
49
|
priority: 'later';
|
|
49
50
|
isMeta: true;
|
|
50
51
|
workload: string;
|
|
52
|
+
_sema_planReviewOutcome?: PlanReviewOutcomeMeta;
|
|
51
53
|
}): void;
|
|
52
54
|
}
|
|
53
55
|
export declare function installNotificationQueuePort(port: NotificationQueuePort | null): void;
|
|
54
56
|
export declare function notificationQueuePortMisses(): number;
|
|
55
57
|
export declare function _resetNotificationQueuePortForTest(): void;
|
|
56
|
-
export declare function enqueuePlanReviewOutcome(value: string): boolean;
|
|
58
|
+
export declare function enqueuePlanReviewOutcome(value: string, meta?: PlanReviewOutcomeMeta): boolean;
|
|
57
59
|
export declare function dropQueuedNotificationsForRun(taskId: string): number;
|
|
58
60
|
export declare function markEngineWorkflowNotified(runId: string): void;
|
|
59
61
|
export declare function isEngineWorkflowNotified(runId: string): boolean;
|
package/dist/notifications.js
CHANGED
|
@@ -188,7 +188,7 @@ function notifiedBgCycleAtLeast(taskId, cycle) {
|
|
|
188
188
|
const high = notifiedBgCycleHigh.get(taskId);
|
|
189
189
|
return high !== undefined && high.cycle >= cycle ? high : undefined;
|
|
190
190
|
}
|
|
191
|
-
export function enqueuePlanReviewOutcome(value) {
|
|
191
|
+
export function enqueuePlanReviewOutcome(value, meta) {
|
|
192
192
|
const p = port();
|
|
193
193
|
if (!p)
|
|
194
194
|
return false;
|
|
@@ -196,7 +196,23 @@ export function enqueuePlanReviewOutcome(value) {
|
|
|
196
196
|
queuePortMisses++;
|
|
197
197
|
return false;
|
|
198
198
|
}
|
|
199
|
-
p.enqueueMetaPrompt({
|
|
199
|
+
p.enqueueMetaPrompt({
|
|
200
|
+
value,
|
|
201
|
+
mode: 'prompt',
|
|
202
|
+
priority: 'later',
|
|
203
|
+
isMeta: true,
|
|
204
|
+
workload: 'plan-review',
|
|
205
|
+
...(meta !== undefined
|
|
206
|
+
? {
|
|
207
|
+
_sema_planReviewOutcome: Object.freeze({
|
|
208
|
+
taskId: meta.taskId,
|
|
209
|
+
dispatchNo: meta.dispatchNo,
|
|
210
|
+
decision: meta.decision,
|
|
211
|
+
effect: meta.effect,
|
|
212
|
+
}),
|
|
213
|
+
}
|
|
214
|
+
: {}),
|
|
215
|
+
});
|
|
200
216
|
return true;
|
|
201
217
|
}
|
|
202
218
|
export function dropQueuedNotificationsForRun(taskId) {
|