@sema-agent/client-core 0.78.1 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +23 -18
  3. package/dist/adapt/arms.js +7 -0
  4. package/dist/adapt/textStream.d.ts +1 -0
  5. package/dist/adapt/textStream.js +10 -0
  6. package/dist/adapt/toolCards.d.ts +1 -0
  7. package/dist/adapt/toolCards.js +2 -0
  8. package/dist/adapter/downstream/eventToSdkMessage.js +4 -0
  9. package/dist/adapter/downstream/terminalToSdkResult.d.ts +9 -1
  10. package/dist/adapter/downstream/terminalToSdkResult.js +29 -3
  11. package/dist/adapter/runStream.js +60 -3
  12. package/dist/agentSession/contract.d.ts +2 -1
  13. package/dist/engineNoticeCodes.js +2 -0
  14. package/dist/gateVocabulary.d.ts +6 -0
  15. package/dist/gateVocabulary.js +21 -0
  16. package/dist/hitl/askGateWire.d.ts +3 -0
  17. package/dist/hitl/askGateWire.js +2 -0
  18. package/dist/hitl/frameRouter.d.ts +1 -0
  19. package/dist/hitl/frameRouter.js +39 -8
  20. package/dist/hitl/gateLedger.d.ts +9 -4
  21. package/dist/hitl/gateLedger.js +29 -5
  22. package/dist/hitl/parkResolver.js +6 -1
  23. package/dist/hitl/persistedRulesWire.d.ts +12 -1
  24. package/dist/hitl/persistedRulesWire.js +20 -5
  25. package/dist/hitl/toolApprovalWire.d.ts +15 -1
  26. package/dist/hitl/toolApprovalWire.js +45 -2
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +2 -0
  29. package/dist/mcpLiveness.js +3 -0
  30. package/dist/mcpProbeCapability.d.ts +18 -0
  31. package/dist/mcpProbeCapability.js +55 -0
  32. package/dist/mcpProbeWire.d.ts +51 -0
  33. package/dist/mcpProbeWire.js +217 -0
  34. package/dist/printToolResultFrame.d.ts +1 -0
  35. package/dist/printToolResultFrame.js +2 -0
  36. package/dist/request/taskRequest.d.ts +20 -1
  37. package/dist/request/taskRequest.js +198 -52
  38. package/dist/sdkWireTransit.d.ts +2 -1
  39. package/dist/sdkWireTransit.js +1 -0
  40. package/dist/toolResult.d.ts +15 -1
  41. package/dist/toolResult.js +61 -4
  42. package/dist/wireFailureShape.d.ts +1 -0
  43. package/dist/wireFailureShape.js +44 -10
  44. package/docs/INTEGRATION-CLIENTS.md +178 -14
  45. package/package.json +4 -4
@@ -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;
@@ -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
  },
@@ -14,6 +14,7 @@ export interface ToolEndPayload {
14
14
  isError?: boolean;
15
15
  eventId?: string | undefined;
16
16
  parentToolCallId?: string | undefined;
17
+ denialKind?: string | undefined;
17
18
  }
18
19
  export interface OpenCardView {
19
20
  has(id: string): boolean;
@@ -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, toolInputByCallId) {
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?.toolInputByCallId),
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?.toolInputByCallId),
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 isSubFlow = ev.parentToolCallId !== undefined ||
258
- ev.sourceTaskId !== undefined;
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, { usageMissingObserved, nestedUsageByTask, toolInputByCallId });
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: 'interactive' | 'print';
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',
@@ -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;
@@ -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';
@@ -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;
@@ -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
- function rejectMessageForRender() {
9
- return HITL_REJECT_MESSAGE;
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
- if (!led.takeDenyStamp())
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: [{ ...ev, output: rejectMessageForRender(), structured: undefined }] };
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
- if (!led.takeDenied(callId))
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: [{ ...ev, output: rejectMessageForRender(), structured: undefined }] };
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): boolean;
31
- armDenyStamp(): void;
32
- takeDenyStamp(): boolean;
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;
@@ -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
- return deniedCalls.delete(callId);
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 false;
185
+ return undefined;
164
186
  denyStampNextFsEnd = false;
165
- return true;
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
- led.markDenied(outcome.gatedCallId);
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
  }