@sema-agent/client-core 0.78.2 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +22 -18
- package/dist/adapt/arms.js +7 -0
- package/dist/adapt/textStream.d.ts +1 -0
- package/dist/adapt/textStream.js +10 -0
- package/dist/adapt/toolCards.d.ts +1 -0
- package/dist/adapt/toolCards.js +2 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +4 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +9 -1
- package/dist/adapter/downstream/terminalToSdkResult.js +29 -3
- package/dist/adapter/runStream.js +60 -3
- package/dist/agentSession/contract.d.ts +2 -1
- package/dist/engineNoticeCodes.js +2 -0
- package/dist/gateVocabulary.d.ts +6 -0
- package/dist/gateVocabulary.js +21 -0
- package/dist/hitl/askGateWire.d.ts +3 -0
- package/dist/hitl/askGateWire.js +2 -0
- package/dist/hitl/frameRouter.d.ts +1 -0
- package/dist/hitl/frameRouter.js +39 -8
- package/dist/hitl/gateLedger.d.ts +9 -4
- package/dist/hitl/gateLedger.js +29 -5
- package/dist/hitl/parkResolver.js +6 -1
- package/dist/hitl/persistedRulesWire.d.ts +12 -1
- package/dist/hitl/persistedRulesWire.js +20 -5
- package/dist/hitl/toolApprovalWire.d.ts +15 -1
- package/dist/hitl/toolApprovalWire.js +45 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcpLiveness.js +3 -0
- package/dist/mcpProbeCapability.d.ts +18 -0
- package/dist/mcpProbeCapability.js +55 -0
- package/dist/mcpProbeWire.d.ts +51 -0
- package/dist/mcpProbeWire.js +217 -0
- package/dist/printToolResultFrame.d.ts +1 -0
- package/dist/printToolResultFrame.js +2 -0
- package/dist/request/taskRequest.d.ts +20 -1
- package/dist/request/taskRequest.js +198 -52
- package/dist/sdkWireTransit.d.ts +2 -1
- package/dist/sdkWireTransit.js +1 -0
- package/dist/toolResult.d.ts +15 -1
- package/dist/toolResult.js +61 -4
- package/dist/wireFailureShape.d.ts +1 -0
- package/dist/wireFailureShape.js +44 -10
- package/docs/INTEGRATION-CLIENTS.md +148 -14
- package/package.json +4 -4
package/dist/adapt/arms.js
CHANGED
|
@@ -509,6 +509,7 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
|
|
|
509
509
|
const closeParentRaw = m.parentToolCallId;
|
|
510
510
|
const closeEventId = typeof closeEventIdRaw === 'string' ? closeEventIdRaw : undefined;
|
|
511
511
|
const closeParent = typeof closeParentRaw === 'string' ? closeParentRaw : undefined;
|
|
512
|
+
const closeDenialKindRaw = m._sema_denial_kind;
|
|
512
513
|
yield* cards.close(p, {
|
|
513
514
|
output: m.output,
|
|
514
515
|
structured,
|
|
@@ -516,6 +517,7 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
|
|
|
516
517
|
isError: m.isError === true,
|
|
517
518
|
eventId: closeEventId,
|
|
518
519
|
parentToolCallId: closeParent,
|
|
520
|
+
...(typeof closeDenialKindRaw === 'string' ? { denialKind: closeDenialKindRaw } : {}),
|
|
519
521
|
});
|
|
520
522
|
const s = structured !== undefined && structured !== null && typeof structured === 'object'
|
|
521
523
|
? structured
|
|
@@ -656,6 +658,11 @@ const streamEventArm = function* (m, { text }) {
|
|
|
656
658
|
const turnUsageArm = function* (m, { text, flags }) {
|
|
657
659
|
const ot = typeof m.outputTokens === 'number' ? m.outputTokens : undefined;
|
|
658
660
|
yield* text.drainLive();
|
|
661
|
+
if (!isSubFlowSegmentEnd(m)) {
|
|
662
|
+
yield* text.takeAnswerSegment();
|
|
663
|
+
text.beginAssistantMessage();
|
|
664
|
+
text.closeSegmentAuthorityWindowOnNextText();
|
|
665
|
+
}
|
|
659
666
|
if (ot !== undefined) {
|
|
660
667
|
yield* flags.emitTurnUsageEnd(ot);
|
|
661
668
|
}
|
|
@@ -32,6 +32,7 @@ export interface TextStream {
|
|
|
32
32
|
readonly committedAnswerText: string;
|
|
33
33
|
readonly lastCommittedAnswerText: string;
|
|
34
34
|
beginAssistantMessage(): void;
|
|
35
|
+
closeSegmentAuthorityWindowOnNextText(): void;
|
|
35
36
|
replaceThinkingSegment(content: string, anchor?: Frame): ThinkingSegmentReplacement;
|
|
36
37
|
segmentId(): string;
|
|
37
38
|
rotateSegmentIdentity(anchor: Frame): void;
|
package/dist/adapt/textStream.js
CHANGED
|
@@ -37,6 +37,7 @@ export function createTextStream(ctx, idOf) {
|
|
|
37
37
|
let idleFlushUsedInSegment = false;
|
|
38
38
|
let segmentCommitted = '';
|
|
39
39
|
let previousSegmentCommitted = '';
|
|
40
|
+
let segmentWindowCloseArmed = false;
|
|
40
41
|
let segmentSealed = '';
|
|
41
42
|
const flushEveryMs = typeof ctx.coalesceIntervalMs === 'number' && ctx.coalesceIntervalMs >= 0
|
|
42
43
|
? ctx.coalesceIntervalMs
|
|
@@ -145,6 +146,12 @@ export function createTextStream(ctx, idOf) {
|
|
|
145
146
|
}
|
|
146
147
|
return {
|
|
147
148
|
*feedText(delta, frame) {
|
|
149
|
+
if (segmentWindowCloseArmed) {
|
|
150
|
+
segmentWindowCloseArmed = false;
|
|
151
|
+
previousSegmentCommitted = '';
|
|
152
|
+
segmentCommitted = '';
|
|
153
|
+
committedTextRows = [];
|
|
154
|
+
}
|
|
148
155
|
if (thinking.length > 0 || thinkingPending.length > 0) {
|
|
149
156
|
yield* drainLive();
|
|
150
157
|
yield* takeThinking();
|
|
@@ -372,6 +379,9 @@ export function createTextStream(ctx, idOf) {
|
|
|
372
379
|
previousSegmentCommitted += segmentCommitted;
|
|
373
380
|
segmentCommitted = '';
|
|
374
381
|
},
|
|
382
|
+
closeSegmentAuthorityWindowOnNextText: () => {
|
|
383
|
+
segmentWindowCloseArmed = true;
|
|
384
|
+
},
|
|
375
385
|
get committedAnswerText() {
|
|
376
386
|
return committedText;
|
|
377
387
|
},
|
package/dist/adapt/toolCards.js
CHANGED
|
@@ -2,6 +2,7 @@ import { deriveTranscriptId } from '../seam.js';
|
|
|
2
2
|
import { degradedToolResultBody, detectEngineBgShellReceipt, reportFindingsToolUseResult, structuredToToolUseResult, todoWriteToolUseResult, wireOutputToBody, } from '../toolResult.js';
|
|
3
3
|
import { chrome, mainLane, transcript } from './ids.js';
|
|
4
4
|
import { flattenWireOutput } from './wireShapes.js';
|
|
5
|
+
import { isCcToolDenialKind } from '../gateVocabulary.js';
|
|
5
6
|
export function createToolCardLedger(ctx, idOf) {
|
|
6
7
|
const pending = new Map();
|
|
7
8
|
let assistantResponseId = null;
|
|
@@ -50,6 +51,7 @@ export function createToolCardLedger(ctx, idOf) {
|
|
|
50
51
|
...(parentToolCallId !== undefined ? { parent_tool_use_id: parentToolCallId } : {}),
|
|
51
52
|
toolUseResult: rich !== null ? rich.toolUseResult : (body.toolUseResult ?? body.content),
|
|
52
53
|
...(body.degraded !== undefined ? { _sema_degraded: body.degraded } : {}),
|
|
54
|
+
...(isCcToolDenialKind(end?.denialKind) ? { _sema_denial_kind: end.denialKind } : {}),
|
|
53
55
|
}, ctx.now());
|
|
54
56
|
}
|
|
55
57
|
function* closeCard(p, end) {
|
|
@@ -2,6 +2,7 @@ import { wiringManifestViewOf } from './wiringManifestView.js';
|
|
|
2
2
|
import { stamp, snapshotSegmentIdentity, } from '../types.js';
|
|
3
3
|
import { turnUsageToModelUsage } from './turnUsageToModelUsage.js';
|
|
4
4
|
import { gateOutcomeOf } from '../../gateOutcome.js';
|
|
5
|
+
import { isCcToolDenialKind } from '../../gateVocabulary.js';
|
|
5
6
|
import { projectToolRoster, projectToolRosterDelta } from '../../toolRoster.js';
|
|
6
7
|
import { readMcpLiveness } from '../../mcpLiveness.js';
|
|
7
8
|
import { SEGMENT_END_IDENTITY_KEYS } from '../types.js';
|
|
@@ -170,6 +171,8 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
170
171
|
const toolEndDelivered = ev.delivered;
|
|
171
172
|
const toolEndGatedCallId = ev.gatedCallId;
|
|
172
173
|
const collateralAbort = ev._sema_collateral_abort;
|
|
174
|
+
const denialKindRaw = ev._sema_denial_kind;
|
|
175
|
+
const denialKind = isCcToolDenialKind(denialKindRaw) ? denialKindRaw : undefined;
|
|
173
176
|
return projected(stamp(ctx, armBody({
|
|
174
177
|
type: 'tool_end_result',
|
|
175
178
|
toolCallId: ev.toolCallId,
|
|
@@ -190,6 +193,7 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
190
193
|
? { gatedCallId: toolEndGatedCallId }
|
|
191
194
|
: {}),
|
|
192
195
|
...(collateralAbort === true ? { _sema_collateral_abort: true } : {}),
|
|
196
|
+
...(denialKind !== undefined ? { _sema_denial_kind: denialKind } : {}),
|
|
193
197
|
...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
|
|
194
198
|
...(ev.parentToolCallId !== undefined
|
|
195
199
|
? { parentToolCallId: ev.parentToolCallId }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentEvent, TaskStats } from '@sema-agent/sdk';
|
|
1
|
+
import type { AgentEvent, DeniedBy, TaskStats } from '@sema-agent/sdk';
|
|
2
|
+
import { type CcToolDenialKind } from '../../gateVocabulary.js';
|
|
2
3
|
import { type SDKMessage, type EmitContext } from '../types.js';
|
|
3
4
|
import { type SemaModelUsage } from './turnUsageToModelUsage.js';
|
|
4
5
|
export interface SemaFlatUsage {
|
|
@@ -15,6 +16,10 @@ export interface TerminalStreamObserved {
|
|
|
15
16
|
readonly usageMissingObserved?: boolean;
|
|
16
17
|
readonly nestedUsageByTask?: ReadonlyMap<string, MutableSubagentUsageRow>;
|
|
17
18
|
readonly toolInputByCallId?: ReadonlyMap<string, unknown>;
|
|
19
|
+
readonly gateDeniedByByCallId?: ReadonlyMap<string, string>;
|
|
20
|
+
readonly denialKindByCallId?: ReadonlyMap<string, string>;
|
|
21
|
+
readonly successfulToolEndObserved?: boolean;
|
|
22
|
+
readonly streamSawRunOpen?: boolean;
|
|
18
23
|
}
|
|
19
24
|
export declare const TOOL_INPUT_JOIN_MAX_CALLS = 4096;
|
|
20
25
|
export interface SemaPermissionDenial {
|
|
@@ -23,6 +28,9 @@ export interface SemaPermissionDenial {
|
|
|
23
28
|
readonly tool_input?: Record<string, unknown>;
|
|
24
29
|
readonly _sema_tool_input_source?: 'tool_start';
|
|
25
30
|
readonly _sema_tool_arg?: string;
|
|
31
|
+
readonly deniedBy?: DeniedBy;
|
|
32
|
+
readonly _sema_denied_by_source?: 'tool_end';
|
|
33
|
+
readonly toolDenialKind?: CcToolDenialKind;
|
|
26
34
|
}
|
|
27
35
|
export interface SemaCostBreakdown {
|
|
28
36
|
readonly llmRootMicroUsd?: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCcToolDenialKind, isGateDeniedByWord } from '../../gateVocabulary.js';
|
|
1
2
|
import { stamp } from '../types.js';
|
|
2
3
|
import { putOwnKey } from '../../ownKey.js';
|
|
3
4
|
import { readEffectiveReasoning, readEffectiveMemoryScopes } from '../../effectiveFacts.js';
|
|
@@ -59,7 +60,10 @@ function joinableToolInput(args) {
|
|
|
59
60
|
}
|
|
60
61
|
const GATE_DENIED_DECISIONS = new Set(['deny']);
|
|
61
62
|
const GATE_NON_DENIAL_DECISIONS = new Set(['allow', 'approve', 'edit', 'reject']);
|
|
62
|
-
function permissionDenialParts(stats,
|
|
63
|
+
function permissionDenialParts(stats, observed) {
|
|
64
|
+
const toolInputByCallId = observed?.toolInputByCallId;
|
|
65
|
+
const gateDeniedByByCallId = observed?.gateDeniedByByCallId;
|
|
66
|
+
const denialKindByCallId = observed?.denialKindByCallId;
|
|
63
67
|
const hr = stats?.humanReview;
|
|
64
68
|
const gates = hr !== null && typeof hr === 'object' ? hr.gates : undefined;
|
|
65
69
|
if (!Array.isArray(gates))
|
|
@@ -93,20 +97,42 @@ function permissionDenialParts(stats, toolInputByCallId) {
|
|
|
93
97
|
? joinableToolInput(toolInputByCallId.get(toolUseId))
|
|
94
98
|
: undefined;
|
|
95
99
|
const toolInput = ledgerInput ?? joinedInput;
|
|
100
|
+
const ledgerDeniedByRaw = row.deniedBy;
|
|
101
|
+
const ledgerDeniedBy = typeof ledgerDeniedByRaw === 'string' && isGateDeniedByWord(ledgerDeniedByRaw)
|
|
102
|
+
? ledgerDeniedByRaw
|
|
103
|
+
: undefined;
|
|
104
|
+
const joinedDeniedByRaw = ledgerDeniedBy === undefined && toolUseId !== undefined && gateDeniedByByCallId?.has(toolUseId) === true
|
|
105
|
+
? gateDeniedByByCallId.get(toolUseId)
|
|
106
|
+
: undefined;
|
|
107
|
+
const joinedDeniedBy = typeof joinedDeniedByRaw === 'string' && isGateDeniedByWord(joinedDeniedByRaw)
|
|
108
|
+
? joinedDeniedByRaw
|
|
109
|
+
: undefined;
|
|
110
|
+
const deniedBy = ledgerDeniedBy ?? joinedDeniedBy;
|
|
111
|
+
const joinedKindRaw = toolUseId !== undefined && denialKindByCallId?.has(toolUseId) === true
|
|
112
|
+
? denialKindByCallId.get(toolUseId)
|
|
113
|
+
: undefined;
|
|
114
|
+
const toolDenialKind = isCcToolDenialKind(joinedKindRaw) ? joinedKindRaw : undefined;
|
|
96
115
|
denials.push({
|
|
97
116
|
...(toolName !== undefined ? { tool_name: toolName } : {}),
|
|
98
117
|
...(toolUseId !== undefined ? { tool_use_id: toolUseId } : {}),
|
|
99
118
|
...(toolInput !== undefined ? { tool_input: toolInput } : {}),
|
|
100
119
|
...(joinedInput !== undefined ? { _sema_tool_input_source: 'tool_start' } : {}),
|
|
120
|
+
...(deniedBy !== undefined ? { deniedBy } : {}),
|
|
121
|
+
...(joinedDeniedBy !== undefined && ledgerDeniedBy === undefined
|
|
122
|
+
? { _sema_denied_by_source: 'tool_end' }
|
|
123
|
+
: {}),
|
|
124
|
+
...(toolDenialKind !== undefined ? { toolDenialKind } : {}),
|
|
101
125
|
...(toolArg !== undefined ? { _sema_tool_arg: toolArg } : {}),
|
|
102
126
|
});
|
|
103
127
|
}
|
|
104
128
|
const ccList = denials.filter((d) => d.tool_name !== undefined && d.tool_use_id !== undefined && d.tool_input !== undefined);
|
|
105
129
|
const claimable = complete && ccList.length === denials.length;
|
|
130
|
+
const zeroOutputDenied = denials.length > 0 && observed?.streamSawRunOpen === true && observed.successfulToolEndObserved === false;
|
|
106
131
|
return {
|
|
107
132
|
permission_denials: ccList,
|
|
108
133
|
...(denials.length > 0 ? { _sema_permission_denials: denials } : {}),
|
|
109
134
|
...(claimable ? {} : { _sema_permission_denials_absent: true }),
|
|
135
|
+
...(zeroOutputDenied ? { _sema_zero_output_denied: true } : {}),
|
|
110
136
|
};
|
|
111
137
|
}
|
|
112
138
|
const putOwn = putOwnKey;
|
|
@@ -366,7 +392,7 @@ function errorResult(ctx, parts) {
|
|
|
366
392
|
total_cost_usd: costOrNull(parts.stats),
|
|
367
393
|
usage: flattenUsage(parts.stats),
|
|
368
394
|
modelUsage: modelUsageFor(parts.stats, parts.model),
|
|
369
|
-
...permissionDenialParts(parts.stats, parts.observed
|
|
395
|
+
...permissionDenialParts(parts.stats, parts.observed),
|
|
370
396
|
...costFactParts(parts.stats, parts.observed),
|
|
371
397
|
...(parts.facts ?? {}),
|
|
372
398
|
...nestedUsageByTaskParts(parts.stats, parts.observed?.nestedUsageByTask),
|
|
@@ -486,7 +512,7 @@ export function doneToSdkResult(ev, ctx, observed) {
|
|
|
486
512
|
total_cost_usd: costOrNull(stats),
|
|
487
513
|
usage: flattenUsage(stats),
|
|
488
514
|
modelUsage: modelUsageFor(stats, r.model),
|
|
489
|
-
...permissionDenialParts(stats, observed
|
|
515
|
+
...permissionDenialParts(stats, observed),
|
|
490
516
|
...costFactParts(stats, observed),
|
|
491
517
|
...effectiveFactParts(rec),
|
|
492
518
|
...nestedUsageByTaskParts(stats, observed?.nestedUsageByTask),
|
|
@@ -5,6 +5,8 @@ import { readRunCostFacts, terminalToSdkResult, TOOL_INPUT_JOIN_MAX_CALLS } from
|
|
|
5
5
|
import { coerceOutput, publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
6
6
|
import { ACTIVE_RUN_BUSY_ERROR_CODE, OUTPUT_INVALID, isLimitsExceededCode } from '../engineErrorCodes.js';
|
|
7
7
|
import { isReviewPark, readRunTerminal, runTerminalCode } from '../runTerminal.js';
|
|
8
|
+
import { gateDeniedBy, gateOutcomeOf } from '../gateOutcome.js';
|
|
9
|
+
import { isCcToolDenialKind, isGateDeniedByWord } from '../gateVocabulary.js';
|
|
8
10
|
const mainLane = () => ({ lane: 'main' });
|
|
9
11
|
function emitChromeFireAndForget(ctx, event) {
|
|
10
12
|
try {
|
|
@@ -178,6 +180,12 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
178
180
|
const toolInputByCallId = new Map();
|
|
179
181
|
const toolInputAmbiguous = new Set();
|
|
180
182
|
let toolInputOverflowed = false;
|
|
183
|
+
const gateDeniedByByCallId = new Map();
|
|
184
|
+
const denialKindByCallId = new Map();
|
|
185
|
+
const denialAmbiguous = new Set();
|
|
186
|
+
let denialJoinOverflowed = false;
|
|
187
|
+
let successfulToolEndObserved = false;
|
|
188
|
+
let streamSawRunOpen = false;
|
|
181
189
|
const sameArgs = (a, b) => {
|
|
182
190
|
if (a === b)
|
|
183
191
|
return true;
|
|
@@ -189,6 +197,8 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
189
197
|
}
|
|
190
198
|
};
|
|
191
199
|
for await (const ev of events) {
|
|
200
|
+
if (ev.type === 'tool_end' && ev.isError !== true)
|
|
201
|
+
successfulToolEndObserved = true;
|
|
192
202
|
const seq = eventSeq(ev);
|
|
193
203
|
if (seq !== undefined) {
|
|
194
204
|
if (seen.has(seq)) {
|
|
@@ -213,6 +223,43 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
213
223
|
}
|
|
214
224
|
}
|
|
215
225
|
}
|
|
226
|
+
if (ev.type === 'meta')
|
|
227
|
+
streamSawRunOpen = true;
|
|
228
|
+
if (ev.type === 'tool_end') {
|
|
229
|
+
const endCallId = ev.toolCallId;
|
|
230
|
+
if (typeof endCallId === 'string' && endCallId.length > 0) {
|
|
231
|
+
const deniedByRaw = gateDeniedBy(gateOutcomeOf(ev));
|
|
232
|
+
const deniedBy = isGateDeniedByWord(deniedByRaw) ? deniedByRaw : undefined;
|
|
233
|
+
const kindRaw = ev._sema_denial_kind;
|
|
234
|
+
const kind = isCcToolDenialKind(kindRaw) ? kindRaw : undefined;
|
|
235
|
+
if (deniedBy !== undefined || kind !== undefined) {
|
|
236
|
+
const isNew = (deniedBy !== undefined && !gateDeniedByByCallId.has(endCallId)) ||
|
|
237
|
+
(kind !== undefined && !denialKindByCallId.has(endCallId));
|
|
238
|
+
if (isNew &&
|
|
239
|
+
(gateDeniedByByCallId.size >= TOOL_INPUT_JOIN_MAX_CALLS ||
|
|
240
|
+
denialKindByCallId.size >= TOOL_INPUT_JOIN_MAX_CALLS)) {
|
|
241
|
+
denialJoinOverflowed = true;
|
|
242
|
+
}
|
|
243
|
+
if (!denialJoinOverflowed && !denialAmbiguous.has(endCallId)) {
|
|
244
|
+
const seenDeniedBy = gateDeniedByByCallId.get(endCallId);
|
|
245
|
+
const seenKind = denialKindByCallId.get(endCallId);
|
|
246
|
+
const conflict = (deniedBy !== undefined && seenDeniedBy !== undefined && seenDeniedBy !== deniedBy) ||
|
|
247
|
+
(kind !== undefined && seenKind !== undefined && seenKind !== kind);
|
|
248
|
+
if (conflict) {
|
|
249
|
+
gateDeniedByByCallId.delete(endCallId);
|
|
250
|
+
denialKindByCallId.delete(endCallId);
|
|
251
|
+
denialAmbiguous.add(endCallId);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (deniedBy !== undefined && seenDeniedBy === undefined)
|
|
255
|
+
gateDeniedByByCallId.set(endCallId, deniedBy);
|
|
256
|
+
if (kind !== undefined && seenKind === undefined)
|
|
257
|
+
denialKindByCallId.set(endCallId, kind);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
216
263
|
if ((ev.type === 'text_delta' ||
|
|
217
264
|
ev.type === 'reasoning_delta' ||
|
|
218
265
|
ev.type === 'text' ||
|
|
@@ -254,8 +301,10 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
254
301
|
reportDroppedFrame('turn_end_usage_absent', ev.type, ctx);
|
|
255
302
|
continue;
|
|
256
303
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
304
|
+
const identityEnvelope = ev;
|
|
305
|
+
const isSubFlow = identityEnvelope.parentToolCallId !== undefined ||
|
|
306
|
+
identityEnvelope.sourceTaskId !== undefined ||
|
|
307
|
+
identityEnvelope.bgAgentId !== undefined;
|
|
259
308
|
{
|
|
260
309
|
if (measured !== undefined) {
|
|
261
310
|
handle.latestUsage = usage;
|
|
@@ -379,7 +428,15 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
379
428
|
emitChromeFireAndForget(ctx, { kind: 'run_cost_reconciled', laneProof: mainLane(), ...costFacts.reconcile });
|
|
380
429
|
}
|
|
381
430
|
}
|
|
382
|
-
const resultFrame = terminalToSdkResult(ev, ctx, {
|
|
431
|
+
const resultFrame = terminalToSdkResult(ev, ctx, {
|
|
432
|
+
usageMissingObserved,
|
|
433
|
+
nestedUsageByTask,
|
|
434
|
+
toolInputByCallId,
|
|
435
|
+
gateDeniedByByCallId,
|
|
436
|
+
denialKindByCallId,
|
|
437
|
+
successfulToolEndObserved,
|
|
438
|
+
streamSawRunOpen,
|
|
439
|
+
});
|
|
383
440
|
const outcome = resultFrame.
|
|
384
441
|
_sema_outcome;
|
|
385
442
|
if (outcome === 'unknown') {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentClient, AgentClientConfig } from '@sema-agent/sdk';
|
|
2
|
+
import type { RequestLane } from '../request/taskRequest.js';
|
|
2
3
|
export interface ProvidedClientWire {
|
|
3
4
|
readonly client: AgentClient;
|
|
4
5
|
readonly baseUrl: string;
|
|
@@ -24,7 +25,7 @@ export interface AgentSessionIdentity {
|
|
|
24
25
|
}
|
|
25
26
|
export interface AgentSessionConfig {
|
|
26
27
|
readonly wire: AgentSessionWire;
|
|
27
|
-
readonly lane:
|
|
28
|
+
readonly lane: Exclude<RequestLane, 'utility'>;
|
|
28
29
|
readonly hostSurface: AgentSessionHostSurface;
|
|
29
30
|
readonly sessionKey?: string;
|
|
30
31
|
}
|
|
@@ -51,6 +51,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
|
|
|
51
51
|
'memory.consolidation_refused',
|
|
52
52
|
'memory.consolidation_withheld',
|
|
53
53
|
'memory.restore_withheld',
|
|
54
|
+
'memory.project_scope_absent',
|
|
54
55
|
'route.fallback_to_primary',
|
|
55
56
|
'route.classifier_unroutable',
|
|
56
57
|
'route.base_url_changed_key_unchanged',
|
|
@@ -124,6 +125,7 @@ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
|
|
|
124
125
|
'memory.consolidation_refused': 'operator',
|
|
125
126
|
'memory.consolidation_withheld': 'operator',
|
|
126
127
|
'memory.restore_withheld': 'operator',
|
|
128
|
+
'memory.project_scope_absent': 'operator',
|
|
127
129
|
'route.fallback_to_primary': 'operator',
|
|
128
130
|
'route.classifier_unroutable': 'operator',
|
|
129
131
|
'route.base_url_changed_key_unchanged': 'operator',
|
package/dist/gateVocabulary.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { DeniedBy } from '@sema-agent/sdk';
|
|
2
|
+
export declare const CC_TOOL_DENIAL_KINDS: readonly ["user-rejected", "permission-rule", "automode-blocked", "automode-unavailable", "automode-parsing-error", "interrupted", "cancelled"];
|
|
3
|
+
export type CcToolDenialKind = (typeof CC_TOOL_DENIAL_KINDS)[number];
|
|
4
|
+
export declare function isCcToolDenialKind(v: unknown): v is CcToolDenialKind;
|
|
5
|
+
export declare function isCcToolDenialKindADenial(v: unknown): boolean;
|
|
2
6
|
export declare const GATE_DENIED_BY_WORDS: readonly DeniedBy[];
|
|
7
|
+
export declare function isGateDeniedByWord(v: unknown): v is DeniedBy;
|
|
3
8
|
export declare function gateDeniedByDetail(deniedBy: unknown): string;
|
|
4
9
|
export declare const ASK_ORIGIN_WORDS: readonly string[];
|
|
5
10
|
export declare function askOriginDetail(origin: unknown, message?: unknown): string;
|
|
@@ -7,3 +12,4 @@ export declare const RULE_STORE_UNREADABLE_KINDS: readonly ["store", "call"];
|
|
|
7
12
|
export type RuleStoreUnreadableKind = (typeof RULE_STORE_UNREADABLE_KINDS)[number];
|
|
8
13
|
export declare function isRuleStoreUnreadableKind(v: unknown): v is RuleStoreUnreadableKind;
|
|
9
14
|
export declare function ruleStoreUnreadableDetail(kind: RuleStoreUnreadableKind): string;
|
|
15
|
+
export declare function mandatedApprovalDetail(): string;
|
package/dist/gateVocabulary.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export const CC_TOOL_DENIAL_KINDS = Object.freeze([
|
|
2
|
+
'user-rejected',
|
|
3
|
+
'permission-rule',
|
|
4
|
+
'automode-blocked',
|
|
5
|
+
'automode-unavailable',
|
|
6
|
+
'automode-parsing-error',
|
|
7
|
+
'interrupted',
|
|
8
|
+
'cancelled',
|
|
9
|
+
]);
|
|
10
|
+
export function isCcToolDenialKind(v) {
|
|
11
|
+
return typeof v === 'string' && CC_TOOL_DENIAL_KINDS.includes(v);
|
|
12
|
+
}
|
|
13
|
+
export function isCcToolDenialKindADenial(v) {
|
|
14
|
+
return isCcToolDenialKind(v) && v !== 'interrupted' && v !== 'cancelled';
|
|
15
|
+
}
|
|
1
16
|
export const GATE_DENIED_BY_WORDS = Object.freeze([
|
|
2
17
|
'policy',
|
|
3
18
|
'hook',
|
|
@@ -22,6 +37,9 @@ const DENIED_BY_SENTENCES = Object.freeze({
|
|
|
22
37
|
ask_resolution: 'denied when the approval was resolved',
|
|
23
38
|
read_boundary: 'denied by the read boundary',
|
|
24
39
|
});
|
|
40
|
+
export function isGateDeniedByWord(v) {
|
|
41
|
+
return typeof v === 'string' && GATE_DENIED_BY_WORDS.includes(v);
|
|
42
|
+
}
|
|
25
43
|
export function gateDeniedByDetail(deniedBy) {
|
|
26
44
|
if (typeof deniedBy === 'string' && Object.hasOwn(DENIED_BY_SENTENCES, deniedBy)) {
|
|
27
45
|
return DENIED_BY_SENTENCES[deniedBy];
|
|
@@ -90,3 +108,6 @@ export function ruleStoreUnreadableDetail(kind) {
|
|
|
90
108
|
const word = typeof kind === 'string' && kind.length > 0 ? kind : '(none)';
|
|
91
109
|
return `your standing deny/ask rules could not be applied to this call; the reported reason ${word} is not one of the mechanisms this client's closed set knows, so answer this ask as usual`;
|
|
92
110
|
}
|
|
111
|
+
export function mandatedApprovalDetail() {
|
|
112
|
+
return 'no saved allow rule and no standing posture can retire this question, so it is asked every time — answering it here settles this one call';
|
|
113
|
+
}
|
|
@@ -2,9 +2,12 @@ import type { AgentEvent } from '@sema-agent/sdk';
|
|
|
2
2
|
import { type AskGateWireDeps } from './frameRouter.js';
|
|
3
3
|
export { HITL_REJECT_MESSAGE, ENGINE_ABORT_TOOL_RESULT } from './frameRouter.js';
|
|
4
4
|
export { HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE } from './frameRouter.js';
|
|
5
|
+
export { HITL_POLICY_DENY_MESSAGE } from './frameRouter.js';
|
|
5
6
|
export type { AskGateWireDeps, ParkReattachNotice } from './frameRouter.js';
|
|
6
7
|
export type { AskAnsweredOutput } from './gateLedger.js';
|
|
7
8
|
export { SEMA_COLLATERAL_ABORT_KEY } from './gateLedger.js';
|
|
9
|
+
export { SEMA_DENIAL_KIND_KEY } from './gateLedger.js';
|
|
10
|
+
export type { GateDenyAttribution } from './gateLedger.js';
|
|
8
11
|
export { toAnsweredOutput, isAlreadyResolvedGateReason, GATE_FAILURE_CODES, nextHopBudget, MAX_GATE_HOPS, MAX_TOTAL_PARKS } from './parkResolver.js';
|
|
9
12
|
export type { HopBudget, HopRound } from './parkResolver.js';
|
|
10
13
|
export type { GateOutcome, GateFailureCode } from './parkResolver.js';
|
package/dist/hitl/askGateWire.js
CHANGED
|
@@ -3,7 +3,9 @@ import { flushHeldWithInterruptRewrite, isHostProgressFrame, routeFrame, } from
|
|
|
3
3
|
import { resolvePark, nextHopBudget, stalledTerminal, MAX_GATE_HOPS } from './parkResolver.js';
|
|
4
4
|
export { HITL_REJECT_MESSAGE, ENGINE_ABORT_TOOL_RESULT } from './frameRouter.js';
|
|
5
5
|
export { HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE } from './frameRouter.js';
|
|
6
|
+
export { HITL_POLICY_DENY_MESSAGE } from './frameRouter.js';
|
|
6
7
|
export { SEMA_COLLATERAL_ABORT_KEY } from './gateLedger.js';
|
|
8
|
+
export { SEMA_DENIAL_KIND_KEY } from './gateLedger.js';
|
|
7
9
|
export { toAnsweredOutput, isAlreadyResolvedGateReason, GATE_FAILURE_CODES, nextHopBudget, MAX_GATE_HOPS, MAX_TOTAL_PARKS } from './parkResolver.js';
|
|
8
10
|
export async function* bridgeAskUserQuestionGates(source, deps, opts) {
|
|
9
11
|
const led = createGateLedger();
|
|
@@ -3,6 +3,7 @@ import type { 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.";
|
|
6
|
+
export declare const HITL_POLICY_DENY_MESSAGE: string;
|
|
6
7
|
export declare const HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
|
|
7
8
|
export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
8
9
|
export declare function toolEndOutputText(output: unknown): string | undefined;
|
package/dist/hitl/frameRouter.js
CHANGED
|
@@ -4,9 +4,34 @@ import { classifierDenyFromToolEnd } from '../classifierVerdictWire.js';
|
|
|
4
4
|
import { GATE_PARKED_ERROR_CODE } from '../engineErrorCodes.js';
|
|
5
5
|
import { readRunTerminal, runTerminalCode, runTerminalGateKind, runTerminalGateToolName } from '../runTerminal.js';
|
|
6
6
|
import { surfaceForCurrentSession } from './hitlHostSurface.js';
|
|
7
|
+
import { SEMA_DENIAL_KIND_KEY } from './gateLedger.js';
|
|
7
8
|
export 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.";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
export const HITL_POLICY_DENY_MESSAGE = 'Permission for this tool use was denied: it requires interactive approval, and permission prompts are ' +
|
|
10
|
+
'not available in this session. The action was NOT performed. Do not claim it succeeded, and do not retry ' +
|
|
11
|
+
'it in this session \u2014 report the limitation to the user, or suggest an alternative.';
|
|
12
|
+
function denyOutputForRender(attribution) {
|
|
13
|
+
if (attribution?.settledBy !== 'policy')
|
|
14
|
+
return HITL_REJECT_MESSAGE;
|
|
15
|
+
const reason = attribution.reason;
|
|
16
|
+
return reason !== undefined && reason.trim() !== ''
|
|
17
|
+
? `${HITL_POLICY_DENY_MESSAGE}\n${reason}`
|
|
18
|
+
: HITL_POLICY_DENY_MESSAGE;
|
|
19
|
+
}
|
|
20
|
+
function denialKindForTranscript(attribution) {
|
|
21
|
+
if (attribution?.settledBy === 'human')
|
|
22
|
+
return 'user-rejected';
|
|
23
|
+
if (attribution?.settledBy === 'policy')
|
|
24
|
+
return 'permission-rule';
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function denyStampedEnd(ev, attribution) {
|
|
28
|
+
const kind = denialKindForTranscript(attribution);
|
|
29
|
+
return {
|
|
30
|
+
...ev,
|
|
31
|
+
output: denyOutputForRender(attribution),
|
|
32
|
+
structured: undefined,
|
|
33
|
+
...(kind !== undefined ? { [SEMA_DENIAL_KIND_KEY]: kind } : {}),
|
|
34
|
+
};
|
|
10
35
|
}
|
|
11
36
|
export const HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE = '[Request interrupted by user for tool use]';
|
|
12
37
|
const RUN_CANCELLED_ERROR_CODE = 'cancelled';
|
|
@@ -146,11 +171,12 @@ const TOOL_END_ARMS = [
|
|
|
146
171
|
run(ev, callId, led) {
|
|
147
172
|
if (ev.isError !== true || !isDenyStampCandidate(ev) || led.isDenied(callId))
|
|
148
173
|
return undefined;
|
|
149
|
-
|
|
174
|
+
const stamp = led.takeDenyStamp();
|
|
175
|
+
if (stamp === undefined)
|
|
150
176
|
return undefined;
|
|
151
177
|
led.markEnded(callId);
|
|
152
178
|
led.dropHeld(callId);
|
|
153
|
-
return { kind: 'yield', events: [
|
|
179
|
+
return { kind: 'yield', events: [denyStampedEnd(ev, stamp)] };
|
|
154
180
|
},
|
|
155
181
|
},
|
|
156
182
|
{
|
|
@@ -158,11 +184,12 @@ const TOOL_END_ARMS = [
|
|
|
158
184
|
run(ev, callId, led) {
|
|
159
185
|
if (ev.isError !== true || !isGatedToolEnd(ev))
|
|
160
186
|
return undefined;
|
|
161
|
-
|
|
187
|
+
const denial = led.takeDenied(callId);
|
|
188
|
+
if (denial === undefined)
|
|
162
189
|
return undefined;
|
|
163
190
|
led.markEnded(callId);
|
|
164
191
|
led.dropHeld(callId);
|
|
165
|
-
return { kind: 'yield', events: [
|
|
192
|
+
return { kind: 'yield', events: [denyStampedEnd(ev, denial)] };
|
|
166
193
|
},
|
|
167
194
|
},
|
|
168
195
|
{
|
|
@@ -238,10 +265,14 @@ async function routeToolApprovalFrame(ev, ctx) {
|
|
|
238
265
|
}
|
|
239
266
|
}
|
|
240
267
|
if (decision === 'deny' && !fromSubagent) {
|
|
268
|
+
const attribution = {
|
|
269
|
+
...(outcome.denySettledBy !== undefined ? { settledBy: outcome.denySettledBy } : {}),
|
|
270
|
+
...(outcome.denyReason !== undefined ? { reason: outcome.denyReason } : {}),
|
|
271
|
+
};
|
|
241
272
|
if (gatedCallId !== undefined)
|
|
242
|
-
led.markDenied(gatedCallId);
|
|
273
|
+
led.markDenied(gatedCallId, attribution);
|
|
243
274
|
else
|
|
244
|
-
led.armDenyStamp();
|
|
275
|
+
led.armDenyStamp(attribution);
|
|
245
276
|
}
|
|
246
277
|
return { kind: 'skip' };
|
|
247
278
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
2
2
|
export declare const SEMA_COLLATERAL_ABORT_KEY = "_sema_collateral_abort";
|
|
3
|
+
export declare const SEMA_DENIAL_KIND_KEY = "_sema_denial_kind";
|
|
4
|
+
export interface GateDenyAttribution {
|
|
5
|
+
settledBy?: 'human' | 'policy';
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
3
8
|
export interface AskAnsweredOutput {
|
|
4
9
|
type: 'ask-user-question';
|
|
5
10
|
questions: unknown[];
|
|
@@ -25,11 +30,11 @@ export interface GateLedger {
|
|
|
25
30
|
dropHeldForDecidedPark(candidateCallId: string | undefined, trustedCallId?: string): void;
|
|
26
31
|
heldCount(): number;
|
|
27
32
|
flushHeld(): Generator<AgentEvent>;
|
|
28
|
-
markDenied(callId: string): void;
|
|
33
|
+
markDenied(callId: string, attribution?: GateDenyAttribution): void;
|
|
29
34
|
isDenied(callId: string): boolean;
|
|
30
|
-
takeDenied(callId: string):
|
|
31
|
-
armDenyStamp(): void;
|
|
32
|
-
takeDenyStamp():
|
|
35
|
+
takeDenied(callId: string): GateDenyAttribution | undefined;
|
|
36
|
+
armDenyStamp(attribution?: GateDenyAttribution): void;
|
|
37
|
+
takeDenyStamp(): GateDenyAttribution | undefined;
|
|
33
38
|
pushFsCall(callId: string): void;
|
|
34
39
|
dropFsCall(callId: string): void;
|
|
35
40
|
lastPendingFsCall(): string | undefined;
|
package/dist/hitl/gateLedger.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export const SEMA_COLLATERAL_ABORT_KEY = '_sema_collateral_abort';
|
|
2
|
+
export const SEMA_DENIAL_KIND_KEY = '_sema_denial_kind';
|
|
2
3
|
export const START_ARGS_MAX_ENTRIES = 256;
|
|
4
|
+
function normalizeDenyAttribution(a) {
|
|
5
|
+
if (a === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
const settledBy = a.settledBy === 'human' || a.settledBy === 'policy' ? a.settledBy : undefined;
|
|
8
|
+
const reason = typeof a.reason === 'string' && a.reason.trim() !== '' ? a.reason : undefined;
|
|
9
|
+
if (settledBy === undefined && reason === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return { ...(settledBy !== undefined ? { settledBy } : {}), ...(reason !== undefined ? { reason } : {}) };
|
|
12
|
+
}
|
|
3
13
|
export function createGateLedger() {
|
|
4
14
|
const startedCalls = new Set();
|
|
5
15
|
const endedCalls = new Set();
|
|
@@ -7,6 +17,7 @@ export function createGateLedger() {
|
|
|
7
17
|
const gatedStartArgsByCall = new Map();
|
|
8
18
|
const startArgsByCall = new Map();
|
|
9
19
|
const deniedCalls = new Set();
|
|
20
|
+
const denyAttributionByCall = new Map();
|
|
10
21
|
const pendingFsCalls = [];
|
|
11
22
|
const resolvedAnswers = new Map();
|
|
12
23
|
const decidedGates = new Set();
|
|
@@ -61,6 +72,7 @@ export function createGateLedger() {
|
|
|
61
72
|
heldSinceLastPark = false;
|
|
62
73
|
};
|
|
63
74
|
let denyStampNextFsEnd = false;
|
|
75
|
+
let denyStampAttribution;
|
|
64
76
|
let seq;
|
|
65
77
|
return {
|
|
66
78
|
noteSeq(ev) {
|
|
@@ -146,23 +158,35 @@ export function createGateLedger() {
|
|
|
146
158
|
heldAskEnds.clear();
|
|
147
159
|
retireBatchState();
|
|
148
160
|
},
|
|
149
|
-
markDenied(callId) {
|
|
161
|
+
markDenied(callId, attribution) {
|
|
150
162
|
deniedCalls.add(callId);
|
|
163
|
+
const a = normalizeDenyAttribution(attribution);
|
|
164
|
+
if (a !== undefined)
|
|
165
|
+
denyAttributionByCall.set(callId, a);
|
|
166
|
+
else
|
|
167
|
+
denyAttributionByCall.delete(callId);
|
|
151
168
|
},
|
|
152
169
|
isDenied(callId) {
|
|
153
170
|
return deniedCalls.has(callId);
|
|
154
171
|
},
|
|
155
172
|
takeDenied(callId) {
|
|
156
|
-
|
|
173
|
+
if (!deniedCalls.delete(callId))
|
|
174
|
+
return undefined;
|
|
175
|
+
const a = denyAttributionByCall.get(callId);
|
|
176
|
+
denyAttributionByCall.delete(callId);
|
|
177
|
+
return a ?? {};
|
|
157
178
|
},
|
|
158
|
-
armDenyStamp() {
|
|
179
|
+
armDenyStamp(attribution) {
|
|
159
180
|
denyStampNextFsEnd = true;
|
|
181
|
+
denyStampAttribution = normalizeDenyAttribution(attribution);
|
|
160
182
|
},
|
|
161
183
|
takeDenyStamp() {
|
|
162
184
|
if (!denyStampNextFsEnd)
|
|
163
|
-
return
|
|
185
|
+
return undefined;
|
|
164
186
|
denyStampNextFsEnd = false;
|
|
165
|
-
|
|
187
|
+
const a = denyStampAttribution;
|
|
188
|
+
denyStampAttribution = undefined;
|
|
189
|
+
return a ?? {};
|
|
166
190
|
},
|
|
167
191
|
pushFsCall(callId) {
|
|
168
192
|
pendingFsCalls.push(callId);
|
|
@@ -164,7 +164,12 @@ async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
|
|
|
164
164
|
const outcome = await surfaceFsApprovalAndDecide({ client: deps.client, ...(onPresented ? { onPresented } : {}) }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
165
165
|
const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
|
|
166
166
|
if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
|
|
167
|
-
|
|
167
|
+
const denySettledBy = 'denySettledBy' in outcome ? outcome.denySettledBy : undefined;
|
|
168
|
+
const denyReason = 'denyReason' in outcome ? outcome.denyReason : undefined;
|
|
169
|
+
led.markDenied(outcome.gatedCallId, {
|
|
170
|
+
...(denySettledBy !== undefined ? { settledBy: denySettledBy } : {}),
|
|
171
|
+
...(denyReason !== undefined ? { reason: denyReason } : {}),
|
|
172
|
+
});
|
|
168
173
|
}
|
|
169
174
|
return outcome;
|
|
170
175
|
}
|