@sema-agent/client-core 0.81.1 → 0.82.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -8
  3. package/dist/adapt/toolCards.d.ts +15 -0
  4. package/dist/adapt/toolCards.js +34 -19
  5. package/dist/adapter/activeRunSelfHeal.d.ts +3 -0
  6. package/dist/adapter/activeRunSelfHeal.js +42 -9
  7. package/dist/adapter/downstream/eventToSdkMessage.js +7 -4
  8. package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -0
  9. package/dist/adapter/downstream/terminalToSdkResult.js +44 -5
  10. package/dist/adapter/runStream.js +33 -4
  11. package/dist/adapter/types.d.ts +2 -0
  12. package/dist/agentsWireCaps.d.ts +1 -0
  13. package/dist/agentsWireCaps.js +110 -29
  14. package/dist/decideFailureNote.d.ts +1 -0
  15. package/dist/decideFailureNote.js +8 -0
  16. package/dist/engineErrorCodes.d.ts +1 -0
  17. package/dist/engineErrorCodes.js +1 -0
  18. package/dist/engineNoticeCodes.js +2 -0
  19. package/dist/executionLaneCapability.js +3 -0
  20. package/dist/fileHistoryCaptureCapability.d.ts +18 -0
  21. package/dist/fileHistoryCaptureCapability.js +58 -0
  22. package/dist/gateOutcome.js +22 -2
  23. package/dist/hitl/askGateWire.d.ts +1 -1
  24. package/dist/hitl/askGateWire.js +1 -1
  25. package/dist/hitl/hitlHostSurface.js +6 -4
  26. package/dist/hitl/parkResolver.d.ts +1 -0
  27. package/dist/hitl/parkResolver.js +11 -2
  28. package/dist/hitl/planReviewWire.js +14 -1
  29. package/dist/hitl/toolApprovalWire.js +3 -2
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +4 -0
  32. package/dist/modelIdentityResolvability.d.ts +31 -0
  33. package/dist/modelIdentityResolvability.js +145 -0
  34. package/dist/printToolResultFrame.js +15 -1
  35. package/dist/registryQuotaUsage.d.ts +44 -0
  36. package/dist/registryQuotaUsage.js +111 -0
  37. package/dist/resumeRefusalCopy.d.ts +12 -0
  38. package/dist/resumeRefusalCopy.js +45 -1
  39. package/dist/rewindArchiveCapability.d.ts +24 -0
  40. package/dist/rewindArchiveCapability.js +125 -0
  41. package/dist/seam.d.ts +2 -0
  42. package/dist/toolResult.d.ts +2 -2
  43. package/dist/toolResult.js +7 -6
  44. package/dist/toolRoster.d.ts +12 -0
  45. package/dist/toolRoster.js +35 -0
  46. package/dist/wireErrorTriage.d.ts +4 -0
  47. package/dist/wireErrorTriage.js +59 -5
  48. package/dist/wireFailureShape.js +27 -22
  49. package/docs/INTEGRATION-CLIENTS.md +192 -13
  50. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import { abortableSleep } from '../abortableSleep.js';
2
+ import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
2
3
  export const PLAN_REVIEW_GATE_KIND = 'plan_review';
3
4
  export const PLAN_REVIEW_GATE_KINDS = [PLAN_REVIEW_GATE_KIND, 'dry_run_review'];
4
5
  export const ASK_PARK_GATE_KINDS = ['human', 'irreversible_ask', 'policy_ask', 'tool_approval'];
@@ -45,12 +46,14 @@ function sessionOpts(deps) {
45
46
  return typeof sessionId === 'string' && sessionId.length > 0 ? { session: sessionId } : {};
46
47
  }
47
48
  function describeFailure(e) {
49
+ const where = corruptStoredRowWhere(e);
50
+ const rowNote = where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`;
48
51
  if (typeof e === 'object' && e !== null && 'message' in e) {
49
52
  const m = e.message;
50
53
  if (typeof m === 'string' && m.length > 0)
51
- return m.length > 200 ? `${m.slice(0, 200)}…` : m;
54
+ return `${m.length > 200 ? `${m.slice(0, 200)}…` : m}${rowNote}`;
52
55
  }
53
- return 'the engine gave no detail';
56
+ return `the engine gave no detail${rowNote}`;
54
57
  }
55
58
  function readStatus(record) {
56
59
  const st = record?.status;
@@ -76,6 +79,9 @@ export function readCancelRequested(body) {
76
79
  const v = body.cancelRequested;
77
80
  return typeof v === 'boolean' ? v : undefined;
78
81
  }
82
+ function reopenRefusedForDecisionInFlight(verdict) {
83
+ return verdict !== null && verdict.reopened === false && verdict._sema_decisionInFlight === true;
84
+ }
79
85
  function reopenDelivered(verdict) {
80
86
  return verdict.reopened === true && verdict.presented !== false;
81
87
  }
@@ -297,9 +303,14 @@ async function askVerdict(taskId, deps) {
297
303
  }
298
304
  async function planReviewArm(taskId, signal, deps) {
299
305
  const verdict = await planVerdict(taskId, deps);
300
- return reopenDelivered(verdict)
301
- ? { kind: 'plan-review-reopened', taskId, firstSight: verdict.firstSight === true }
302
- : { kind: 'plan-review-reopen-failed', taskId, decidePath: signal.pendingGate?.decidePath ?? null };
306
+ if (reopenDelivered(verdict))
307
+ return { kind: 'plan-review-reopened', taskId, firstSight: verdict.firstSight === true };
308
+ return {
309
+ kind: 'plan-review-reopen-failed',
310
+ taskId,
311
+ decidePath: signal.pendingGate?.decidePath ?? null,
312
+ ...(reopenRefusedForDecisionInFlight(verdict) ? { decisionInFlight: true } : {}),
313
+ };
303
314
  }
304
315
  async function askParkArm(taskId, signal, runs, deps) {
305
316
  const verdict = await askVerdict(taskId, deps);
@@ -323,10 +334,16 @@ async function askParkArm(taskId, signal, runs, deps) {
323
334
  }
324
335
  return { kind: 'ask-decided-release-unknown', taskId, lastStatus: release.lastStatus };
325
336
  }
326
- if (verdict.reopened === false && verdict.decidedWithoutCard !== true && verdict.pendingRowGone === true) {
337
+ const inFlight = reopenRefusedForDecisionInFlight(verdict);
338
+ if (verdict.reopened === false && verdict.decidedWithoutCard !== true && verdict.pendingRowGone === true && !inFlight) {
327
339
  return staleParkArm(taskId, signal, runs, deps);
328
340
  }
329
- return { kind: 'ask-reopen-failed', taskId, decidePath: signal.pendingGate?.decidePath ?? null };
341
+ return {
342
+ kind: 'ask-reopen-failed',
343
+ taskId,
344
+ decidePath: signal.pendingGate?.decidePath ?? null,
345
+ ...(inFlight ? { decisionInFlight: true } : {}),
346
+ };
330
347
  }
331
348
  async function probeRunStatusOnce(taskId, durable, durableGet, deps) {
332
349
  let status = null;
@@ -608,6 +625,10 @@ function injectedSubmissionRow(outcome, following = false) {
608
625
  : `A system notification was NOT delivered: the engine refused it for the run that is already working${handle} ` +
609
626
  `(${outcome.detail}); sema did not retry. The model was not told about it.`;
610
627
  }
628
+ if ((outcome.kind === 'ask-reopen-failed' || outcome.kind === 'plan-review-reopen-failed') && outcome.decisionInFlight === true) {
629
+ return (`A system notification was NOT delivered: this session is still held by an earlier run${handle} whose decision ` +
630
+ `is still on its way to the engine, so sema did not retry. The model was not told about it.`);
631
+ }
611
632
  switch (selfHealSubmissionDisposition(outcome)) {
612
633
  case 'held-for-decision':
613
634
  return (`A system notification could not be delivered while an earlier turn${handle} is waiting on a ` +
@@ -625,6 +646,11 @@ function injectedSubmissionRow(outcome, following = false) {
625
646
  `sema could not put a decision for it on screen, so it did not retry. The model was not told about it.`);
626
647
  }
627
648
  }
649
+ function decisionInFlightRow(parkedOn) {
650
+ return (`The previous turn is parked ${parkedOn} and your answer to that card is still on its way to the engine, so ` +
651
+ `sema did not show the card again — answering it a second time would send a second decision. It did NOT ` +
652
+ `cancel the run. Your message was NOT sent; send it again in a moment, once the engine has applied that decision.`);
653
+ }
628
654
  function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false) {
629
655
  switch (outcome.kind) {
630
656
  case 'decision-pending':
@@ -662,6 +688,8 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false) {
662
688
  (outcome.lastStatus ? ` (last engine status: ${outcome.lastStatus})` : '') +
663
689
  `. sema could not yet confirm it finished, so your message was not sent — wait a moment and send it again.`);
664
690
  case 'ask-reopen-failed': {
691
+ if (outcome.decisionInFlight === true)
692
+ return decisionInFlightRow(`waiting for your decision (run ${outcome.taskId})`);
665
693
  const viaEngine = outcome.decidePath
666
694
  ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
667
695
  : '';
@@ -670,6 +698,8 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false) {
670
698
  `Your message was NOT sent; ${wayOut} if you no longer want it.${viaEngine}`);
671
699
  }
672
700
  case 'plan-review-reopen-failed': {
701
+ if (outcome.decisionInFlight === true)
702
+ return decisionInFlightRow(`on a plan review (run ${outcome.taskId})`);
673
703
  const viaEngine = outcome.decidePath
674
704
  ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
675
705
  : '';
@@ -692,8 +722,11 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false) {
692
722
  const parked = outcome.status ? `parked (status ${outcome.status})` : 'parked';
693
723
  const next = outcome.reopened !== null && reopenDelivered(outcome.reopened)
694
724
  ? `sema has surfaced that decision card — answering it is what lets that run resume and pick your message up.`
695
- : `sema could not surface that decision card here, so nothing will resume that run until that decision is made; ` +
696
- `${wayOut} if you no longer want it.`;
725
+ : reopenRefusedForDecisionInFlight(outcome.reopened)
726
+ ? `Your answer to that decision is still on its way to the engine, so sema did not show the card again; ` +
727
+ `that run moves on once the engine applies it.`
728
+ : `sema could not surface that decision card here, so nothing will resume that run until that decision is made; ` +
729
+ `${wayOut} if you no longer want it.`;
697
730
  return (`${handle} is not actually working right now: the engine reports it ${parked} on a decision it needs ` +
698
731
  `from you, so it queued your message on that park instead of running it. ${next} ${textOnly} ` +
699
732
  `Nothing was cancelled, and your message did NOT start a new turn.`);
@@ -36,9 +36,10 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
36
36
  'approval_revoke',
37
37
  'message_committed',
38
38
  ]);
39
- function assistantArm(ctx, content, eventId) {
39
+ function assistantArm(ctx, content, eventId, parentToolCallId) {
40
40
  const id = typeof eventId === 'string' && eventId.length > 0 ? eventId : undefined;
41
- const model = typeof ctx.model === 'string' && ctx.model.length > 0 ? ctx.model : undefined;
41
+ const parent = typeof parentToolCallId === 'string' && parentToolCallId.length > 0 ? parentToolCallId : undefined;
42
+ const model = parent === undefined && typeof ctx.model === 'string' && ctx.model.length > 0 ? ctx.model : undefined;
42
43
  return stamp(ctx, armBody({
43
44
  type: 'assistant',
44
45
  message: {
@@ -47,7 +48,7 @@ function assistantArm(ctx, content, eventId) {
47
48
  ...(id !== undefined ? { id } : {}),
48
49
  ...(model !== undefined ? { model } : {}),
49
50
  },
50
- parent_tool_use_id: null,
51
+ parent_tool_use_id: parent ?? null,
51
52
  }));
52
53
  }
53
54
  function streamEventArm(ctx, event) {
@@ -136,7 +137,7 @@ export function eventToSdkMessage(ev, ctx) {
136
137
  ? { parentToolCallId: ev.parentToolCallId }
137
138
  : {}),
138
139
  },
139
- ], ev.eventId));
140
+ ], ev.eventId, ev.parentToolCallId));
140
141
  case 'compacted': {
141
142
  const raw = ev;
142
143
  const freedTokens = typeof raw.freedTokens === 'number' && Number.isFinite(raw.freedTokens) && raw.freedTokens >= 0 ? raw.freedTokens : undefined;
@@ -171,6 +172,7 @@ export function eventToSdkMessage(ev, ctx) {
171
172
  const toolEndGatedCallId = ev.gatedCallId;
172
173
  const collateralAbort = ev._sema_collateral_abort;
173
174
  const denialKind = ccToolDenialKindForToolEnd(ev);
175
+ const denialUnclassified = denialKind === undefined && toolEndGate?.disposition.kind === 'denied';
174
176
  return projected(stamp(ctx, armBody({
175
177
  type: 'tool_end_result',
176
178
  toolCallId: ev.toolCallId,
@@ -192,6 +194,7 @@ export function eventToSdkMessage(ev, ctx) {
192
194
  : {}),
193
195
  ...(collateralAbort === true ? { _sema_collateral_abort: true } : {}),
194
196
  ...(denialKind !== undefined ? { _sema_denial_kind: denialKind } : {}),
197
+ ...(denialUnclassified ? { _sema_denial_unclassified: true } : {}),
195
198
  ...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
196
199
  ...(ev.parentToolCallId !== undefined
197
200
  ? { parentToolCallId: ev.parentToolCallId }
@@ -20,6 +20,8 @@ export interface TerminalStreamObserved {
20
20
  readonly denialKindByCallId?: ReadonlyMap<string, string>;
21
21
  readonly successfulToolEndObserved?: boolean;
22
22
  readonly streamSawRunOpen?: boolean;
23
+ readonly deniedToolNameByCallId?: ReadonlyMap<string, string | undefined>;
24
+ readonly deniedCallsOverflowed?: boolean;
23
25
  }
24
26
  export declare const TOOL_INPUT_JOIN_MAX_CALLS = 4096;
25
27
  export interface SemaPermissionDenial {
@@ -1,4 +1,4 @@
1
- import { isCcToolDenialKind, isGateDeniedByWord } from '../../gateVocabulary.js';
1
+ import { isCcToolDenialKind, isCcToolDenialKindADenial, isGateDeniedByWord } from '../../gateVocabulary.js';
2
2
  import { stamp } from '../types.js';
3
3
  import { putOwnKey } from '../../ownKey.js';
4
4
  import { readEffectiveReasoning, readEffectiveMemoryScopes } from '../../effectiveFacts.js';
@@ -65,11 +65,10 @@ function permissionDenialParts(stats, observed) {
65
65
  const gateDeniedByByCallId = observed?.gateDeniedByByCallId;
66
66
  const denialKindByCallId = observed?.denialKindByCallId;
67
67
  const hr = stats?.humanReview;
68
- const gates = hr !== null && typeof hr === 'object' ? hr.gates : undefined;
69
- if (!Array.isArray(gates))
70
- return { permission_denials: [], _sema_permission_denials_absent: true };
68
+ const gatesRaw = hr !== null && typeof hr === 'object' ? hr.gates : undefined;
71
69
  const denials = [];
72
- let complete = true;
70
+ const gates = Array.isArray(gatesRaw) ? gatesRaw : [];
71
+ let complete = Array.isArray(gatesRaw);
73
72
  for (const g of gates) {
74
73
  if (g === null || typeof g !== 'object') {
75
74
  complete = false;
@@ -125,6 +124,43 @@ function permissionDenialParts(stats, observed) {
125
124
  ...(toolArg !== undefined ? { _sema_tool_arg: toolArg } : {}),
126
125
  });
127
126
  }
127
+ const deniedToolNameByCallId = observed?.deniedToolNameByCallId;
128
+ if (deniedToolNameByCallId !== undefined) {
129
+ const ledgerDeniedIds = new Set();
130
+ let ledgerHasIdlessDenial = false;
131
+ for (const d of denials) {
132
+ if (d.tool_use_id !== undefined)
133
+ ledgerDeniedIds.add(d.tool_use_id);
134
+ else
135
+ ledgerHasIdlessDenial = true;
136
+ }
137
+ for (const [callId, toolNameRaw] of deniedToolNameByCallId) {
138
+ if (typeof callId !== 'string' || callId.length === 0 || ledgerDeniedIds.has(callId))
139
+ continue;
140
+ const mergedKind = denialKindByCallId?.has(callId) === true ? denialKindByCallId.get(callId) : undefined;
141
+ if (isCcToolDenialKind(mergedKind) && !isCcToolDenialKindADenial(mergedKind))
142
+ continue;
143
+ if (ledgerHasIdlessDenial) {
144
+ complete = false;
145
+ continue;
146
+ }
147
+ const toolName = nonEmptyStr(toolNameRaw);
148
+ const joinedInput = toolInputByCallId?.has(callId) === true ? joinableToolInput(toolInputByCallId.get(callId)) : undefined;
149
+ const deniedByRaw = gateDeniedByByCallId?.has(callId) === true ? gateDeniedByByCallId.get(callId) : undefined;
150
+ const deniedBy = typeof deniedByRaw === 'string' && isGateDeniedByWord(deniedByRaw) ? deniedByRaw : undefined;
151
+ const toolDenialKind = isCcToolDenialKind(mergedKind) ? mergedKind : undefined;
152
+ denials.push({
153
+ ...(toolName !== undefined ? { tool_name: toolName } : {}),
154
+ tool_use_id: callId,
155
+ ...(joinedInput !== undefined ? { tool_input: joinedInput, _sema_tool_input_source: 'tool_start' } : {}),
156
+ ...(deniedBy !== undefined ? { deniedBy, _sema_denied_by_source: 'tool_end' } : {}),
157
+ ...(toolDenialKind !== undefined ? { toolDenialKind } : {}),
158
+ });
159
+ ledgerDeniedIds.add(callId);
160
+ }
161
+ if (observed?.deniedCallsOverflowed === true)
162
+ complete = false;
163
+ }
128
164
  const ccList = denials.filter((d) => d.tool_name !== undefined && d.tool_use_id !== undefined && d.tool_input !== undefined);
129
165
  const claimable = complete && ccList.length === denials.length;
130
166
  const zeroOutputDenied = denials.length > 0 && observed?.streamSawRunOpen === true && observed.successfulToolEndObserved === false;
@@ -200,15 +236,18 @@ function structuredOutputParts(r) {
200
236
  function effectiveFactParts(rec) {
201
237
  const reasoning = readEffectiveReasoning(rec.effectiveReasoning);
202
238
  const scopes = readEffectiveMemoryScopes(rec.effectiveMemoryScopes);
239
+ const responseModel = rec.responseModel;
203
240
  return {
204
241
  ...(reasoning !== undefined ? { _sema_effective_reasoning: reasoning } : {}),
205
242
  ...(scopes !== undefined ? { _sema_effective_memory_scopes: scopes } : {}),
243
+ ...(typeof responseModel === 'string' && responseModel.length > 0 ? { _sema_response_model: responseModel } : {}),
206
244
  };
207
245
  }
208
246
  function reemitEffectiveFacts(f) {
209
247
  const out = {
210
248
  ...(f?._sema_effective_reasoning !== undefined ? { _sema_effective_reasoning: f._sema_effective_reasoning } : {}),
211
249
  ...(f?._sema_effective_memory_scopes !== undefined ? { _sema_effective_memory_scopes: f._sema_effective_memory_scopes } : {}),
250
+ ...(f?._sema_response_model !== undefined ? { _sema_response_model: f._sema_response_model } : {}),
212
251
  };
213
252
  const _keySetPin = true;
214
253
  void _keySetPin;
@@ -6,7 +6,7 @@ import { coerceOutput, publishSubagentContentEvent } from '../subagentContentSto
6
6
  import { ACTIVE_RUN_BUSY_ERROR_CODE, OUTPUT_INVALID, isLimitsExceededCode } from '../engineErrorCodes.js';
7
7
  import { isReviewPark, readRunTerminal, runTerminalCode } from '../runTerminal.js';
8
8
  import { ccToolDenialKindForToolEnd, gateDeniedBy, gateOutcomeOf } from '../gateOutcome.js';
9
- import { isCcToolDenialKind, isGateDeniedByWord } from '../gateVocabulary.js';
9
+ import { isCcToolDenialKind, isCcToolDenialKindADenial, isGateDeniedByWord } from '../gateVocabulary.js';
10
10
  const mainLane = () => ({ lane: 'main' });
11
11
  function emitChromeFireAndForget(ctx, event) {
12
12
  try {
@@ -185,6 +185,8 @@ async function* runStreamInner(events, ctx, handle = {}) {
185
185
  const denialKindSourceByCallId = new Map();
186
186
  const denialAmbiguous = new Set();
187
187
  let denialJoinOverflowed = false;
188
+ const deniedToolNameByCallId = new Map();
189
+ let deniedCallsOverflowed = false;
188
190
  let successfulToolEndObserved = false;
189
191
  let streamSawRunOpen = false;
190
192
  const sameArgs = (a, b) => {
@@ -229,7 +231,8 @@ async function* runStreamInner(events, ctx, handle = {}) {
229
231
  if (ev.type === 'tool_end') {
230
232
  const endCallId = ev.toolCallId;
231
233
  if (typeof endCallId === 'string' && endCallId.length > 0) {
232
- const deniedByRaw = gateDeniedBy(gateOutcomeOf(ev));
234
+ const endGate = gateOutcomeOf(ev);
235
+ const deniedByRaw = gateDeniedBy(endGate);
233
236
  const deniedBy = isGateDeniedByWord(deniedByRaw) ? deniedByRaw : undefined;
234
237
  const kind = ccToolDenialKindForToolEnd(ev);
235
238
  const kindSource = kind === undefined
@@ -237,6 +240,23 @@ async function* runStreamInner(events, ctx, handle = {}) {
237
240
  : isCcToolDenialKind(ev._sema_denial_kind)
238
241
  ? 'local'
239
242
  : 'wire';
243
+ const deniedCall = endGate !== undefined
244
+ ? endGate.disposition.kind === 'denied' && (kind === undefined || isCcToolDenialKindADenial(kind))
245
+ : kind !== undefined && isCcToolDenialKindADenial(kind);
246
+ if (deniedCall) {
247
+ const endToolNameRaw = ev.toolName;
248
+ const endToolName = typeof endToolNameRaw === 'string' && endToolNameRaw.length > 0 ? endToolNameRaw : undefined;
249
+ if (deniedToolNameByCallId.has(endCallId)) {
250
+ if (deniedToolNameByCallId.get(endCallId) !== endToolName)
251
+ deniedToolNameByCallId.set(endCallId, undefined);
252
+ }
253
+ else if (deniedToolNameByCallId.size >= TOOL_INPUT_JOIN_MAX_CALLS) {
254
+ deniedCallsOverflowed = true;
255
+ }
256
+ else if (!deniedCallsOverflowed) {
257
+ deniedToolNameByCallId.set(endCallId, endToolName);
258
+ }
259
+ }
240
260
  if (deniedBy !== undefined || kind !== undefined) {
241
261
  const isNew = (deniedBy !== undefined && !gateDeniedByByCallId.has(endCallId)) ||
242
262
  (kind !== undefined && !denialKindByCallId.has(endCallId));
@@ -298,14 +318,18 @@ async function* runStreamInner(events, ctx, handle = {}) {
298
318
  output: coerceOutput(sub.output),
299
319
  isError: sub.isError,
300
320
  });
301
- continue;
321
+ if (!(ctx.lane === 'print' && (sub.type === 'tool_start' || sub.type === 'tool_end')))
322
+ continue;
302
323
  }
303
324
  if (ev.type === 'turn_end') {
304
325
  const usageMissing = ev.usageMissing === true;
305
326
  if (usageMissing)
306
327
  usageMissingObserved = true;
307
- const stopReasonRaw = ev.stopReason;
328
+ const turnEndLoose = ev;
329
+ const stopReasonRaw = turnEndLoose.stopReason;
308
330
  const stopWord = typeof stopReasonRaw === 'string' && stopReasonRaw.length > 0 ? stopReasonRaw : undefined;
331
+ const responseModelRaw = turnEndLoose.responseModel;
332
+ const responseModelWord = typeof responseModelRaw === 'string' && responseModelRaw.length > 0 ? responseModelRaw : undefined;
309
333
  const rawUsage = ev.usage;
310
334
  const engineUsage = typeof rawUsage === 'object' && rawUsage !== null && !Array.isArray(rawUsage)
311
335
  ? rawUsage
@@ -340,6 +364,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
340
364
  engineUsage,
341
365
  ...(usageMissing ? { usageMissing: true } : {}),
342
366
  ...(stopWord !== undefined ? { stopReason: stopWord } : {}),
367
+ ...(responseModelWord !== undefined ? { responseModel: responseModelWord } : {}),
343
368
  });
344
369
  }
345
370
  }
@@ -362,6 +387,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
362
387
  engineUsage,
363
388
  ...(usageMissing ? { usageMissing: true } : {}),
364
389
  ...(stopWord !== undefined ? { stopReason: stopWord } : {}),
390
+ ...(responseModelWord !== undefined ? { responseModel: responseModelWord } : {}),
365
391
  });
366
392
  }
367
393
  const rowKey = sourceTaskId !== undefined ? `s:${sourceTaskId}` : `p:${taskId}`;
@@ -389,6 +415,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
389
415
  ? { outputTokens }
390
416
  : {}),
391
417
  ...(stopWord !== undefined ? { _sema_stop_reason: stopWord } : {}),
418
+ ...(responseModelWord !== undefined ? { _sema_response_model: responseModelWord } : {}),
392
419
  };
393
420
  }
394
421
  continue;
@@ -453,6 +480,8 @@ async function* runStreamInner(events, ctx, handle = {}) {
453
480
  denialKindByCallId,
454
481
  successfulToolEndObserved,
455
482
  streamSawRunOpen,
483
+ deniedToolNameByCallId,
484
+ deniedCallsOverflowed,
456
485
  });
457
486
  const outcome = resultFrame.
458
487
  _sema_outcome;
@@ -1,6 +1,7 @@
1
1
  import type { AgentEvent } from '@sema-agent/sdk';
2
2
  import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
3
3
  import type { ChromeEvent } from '../seam.js';
4
+ import type { RequestLane } from '../request/taskRequest.js';
4
5
  export type { ModelUsage, SDKMessage };
5
6
  export type StampedAgentEvent = AgentEvent & {
6
7
  id?: string;
@@ -16,6 +17,7 @@ export interface EmitContext {
16
17
  sessionId: string;
17
18
  startedAtMs?: number;
18
19
  model?: string;
20
+ lane?: RequestLane;
19
21
  emitChrome?(event: ChromeEvent): void | Promise<void>;
20
22
  onDroppedFrame?(info: DroppedFrameInfo): void;
21
23
  }
@@ -69,6 +69,7 @@ export type WireConfig = {
69
69
  };
70
70
  export type ProjectorFn = () => Promise<ProjectAgentsResult>;
71
71
  export declare function prepareTaskAgentsWireWith(cfg: WireConfig, projector: ProjectorFn): Promise<void>;
72
+ export declare function invalidateTaskAgentsWire(): Promise<void>;
72
73
  export declare function awaitTaskAgentsWire(timeoutMs?: number): Promise<void>;
73
74
  export declare function taskAgentsField(): {
74
75
  agents?: TaskAgentWireItem[];
@@ -194,6 +194,7 @@ let preparedKey = null;
194
194
  let preparedPromise = null;
195
195
  let payload;
196
196
  let capsVerdict;
197
+ let settleGeneration = null;
197
198
  const emittedWarnings = new Set();
198
199
  function warnOnce(msg) {
199
200
  if (emittedWarnings.has(msg))
@@ -201,30 +202,57 @@ function warnOnce(msg) {
201
202
  emittedWarnings.add(msg);
202
203
  console.error(`[sema] ${msg}`);
203
204
  }
204
- async function runPrepare(cfg, projector) {
205
- try {
206
- const [caps, projected] = await Promise.all([
207
- engineSupportsTaskAgents(cfg.baseUrl, {
208
- authToken: cfg.authToken,
209
- principal: cfg.principal,
210
- }),
211
- projector(),
212
- ]);
213
- capsVerdict = caps;
214
- if (projected.agents && caps === false) {
215
- warnOnce(`agents wire: this engine does not support per-task custom agents (capabilities.taskAgents absent/false — server <1.205 or multi-tenant) — ${projected.agents.length} custom agent definition(s) will NOT be sent`);
216
- payload = undefined;
217
- return;
218
- }
205
+ function applyProjectionIfCurrent(generation, projected) {
206
+ if (generation === null || preparedPromise !== generation)
207
+ return false;
208
+ const accepted = capsVerdict !== false;
209
+ if (accepted) {
219
210
  for (const w of projected.warnings)
220
211
  warnOnce(w);
221
212
  payload = projected.agents;
213
+ }
214
+ settleGeneration?.();
215
+ return accepted;
216
+ }
217
+ function startCapsProbe(cfg) {
218
+ const land = (verdict) => {
219
+ if (capsVerdict !== probe)
220
+ return verdict;
221
+ capsVerdict = verdict;
222
+ if (verdict === false) {
223
+ payload = undefined;
224
+ settleGeneration?.();
225
+ }
226
+ return verdict;
227
+ };
228
+ const probe = engineSupportsTaskAgents(cfg.baseUrl, {
229
+ authToken: cfg.authToken,
230
+ principal: cfg.principal,
231
+ }).then(land, () => land(undefined));
232
+ capsVerdict = probe;
233
+ return probe;
234
+ }
235
+ function capsSettled() {
236
+ return capsVerdict instanceof Promise ? capsVerdict : Promise.resolve();
237
+ }
238
+ async function runPrepare(cfg, projector, probe, generation) {
239
+ try {
240
+ const [caps, projected] = await Promise.all([probe, projector()]);
241
+ if (caps === false) {
242
+ if (projected.agents && preparedKey === cfg.baseUrl) {
243
+ warnOnce(`agents wire: this engine does not support per-task custom agents (capabilities.taskAgents absent/false — server <1.205 or multi-tenant) — ${projected.agents.length} custom agent definition(s) will NOT be sent`);
244
+ }
245
+ return;
246
+ }
247
+ if (!applyProjectionIfCurrent(generation, projected))
248
+ return;
222
249
  if (payload && hostEnv().SEMA_DEBUG) {
223
250
  console.error(`[sema] agents wire: ${payload.length} custom agent(s) → TaskRequest.agents (${payload.map(a => a.name).join(', ')})`);
224
251
  }
225
252
  }
226
253
  catch (e) {
227
- payload = undefined;
254
+ if (preparedPromise === generation)
255
+ payload = undefined;
228
256
  if (hostEnv().SEMA_DEBUG) {
229
257
  console.error('[sema] agents wire prepare soft-failed (no agents stamped):', e);
230
258
  }
@@ -235,21 +263,66 @@ export function prepareTaskAgentsWireWith(cfg, projector) {
235
263
  if (preparedPromise && preparedKey === key)
236
264
  return preparedPromise;
237
265
  preparedKey = key;
238
- preparedPromise = runPrepare(cfg, projector);
239
266
  activeProjector = projector;
240
- return preparedPromise;
267
+ const probe = startCapsProbe(cfg);
268
+ return startGeneration(generation => runPrepare(cfg, projector, probe, generation));
269
+ }
270
+ function startGeneration(run) {
271
+ let settle;
272
+ const generation = new Promise(resolve => {
273
+ settle = resolve;
274
+ });
275
+ const superseded = settleGeneration;
276
+ preparedPromise = generation;
277
+ settleGeneration = settle;
278
+ superseded?.();
279
+ void run(generation).then(settle, settle);
280
+ return generation;
281
+ }
282
+ export function invalidateTaskAgentsWire() {
283
+ const projector = activeProjector;
284
+ if (!preparedPromise || !projector)
285
+ return Promise.resolve();
286
+ payload = undefined;
287
+ return startGeneration(generation => runReproject(projector, generation));
288
+ }
289
+ async function runReproject(projector, generation) {
290
+ try {
291
+ let projected;
292
+ try {
293
+ projected = await projector();
294
+ }
295
+ catch (e) {
296
+ if (hostEnv().SEMA_DEBUG) {
297
+ console.error('[sema] agents wire re-project soft-failed (no agents stamped until the next successful projection):', e);
298
+ }
299
+ return;
300
+ }
301
+ await capsSettled();
302
+ applyProjectionIfCurrent(generation, projected);
303
+ }
304
+ catch {
305
+ }
241
306
  }
242
307
  export async function awaitTaskAgentsWire(timeoutMs = 4000) {
243
308
  if (!preparedPromise)
244
309
  return;
310
+ if (capsVerdict === false)
311
+ return;
245
312
  let timeoutHandle;
313
+ let timedOut = false;
314
+ const deadline = new Promise(resolve => {
315
+ timeoutHandle = setTimeout(() => {
316
+ timedOut = true;
317
+ resolve();
318
+ }, timeoutMs);
319
+ });
246
320
  try {
247
- await Promise.race([
248
- preparedPromise.catch(() => { }),
249
- new Promise(resolve => {
250
- timeoutHandle = setTimeout(resolve, timeoutMs);
251
- }),
252
- ]);
321
+ let waited = null;
322
+ while (!timedOut && preparedPromise !== null && preparedPromise !== waited) {
323
+ waited = preparedPromise;
324
+ await Promise.race([waited.catch(() => { }), deadline]);
325
+ }
253
326
  }
254
327
  finally {
255
328
  if (timeoutHandle !== undefined)
@@ -259,11 +332,18 @@ export async function awaitTaskAgentsWire(timeoutMs = 4000) {
259
332
  export function taskAgentsField() {
260
333
  const current = payload;
261
334
  if (preparedPromise && capsVerdict !== false && activeProjector) {
262
- void activeProjector()
263
- .then(p => {
264
- for (const w of p.warnings)
265
- warnOnce(w);
266
- payload = p.agents;
335
+ const generation = preparedPromise;
336
+ let refreshing;
337
+ try {
338
+ refreshing = activeProjector();
339
+ }
340
+ catch {
341
+ return current ? { agents: current } : {};
342
+ }
343
+ void refreshing
344
+ .then(async (p) => {
345
+ await capsSettled();
346
+ applyProjectionIfCurrent(generation, p);
267
347
  })
268
348
  .catch(() => { });
269
349
  }
@@ -273,6 +353,7 @@ export function _resetAgentsWireForTest() {
273
353
  activeProjector = null;
274
354
  preparedKey = null;
275
355
  preparedPromise = null;
356
+ settleGeneration = null;
276
357
  payload = undefined;
277
358
  capsVerdict = undefined;
278
359
  emittedWarnings.clear();
@@ -0,0 +1 @@
1
+ export declare function decideFailureNote(err: unknown): string;
@@ -0,0 +1,8 @@
1
+ import { corruptStoredRowContent, corruptStoredRowWhere } from './wireErrorTriage.js';
2
+ import { resumePolicyBlockContent, resumePolicyBlockFromError } from './resumeRefusalCopy.js';
3
+ export function decideFailureNote(err) {
4
+ const where = corruptStoredRowWhere(err);
5
+ const fold = resumePolicyBlockFromError(err);
6
+ return ((where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`) +
7
+ (fold === null ? '' : ` — ${resumePolicyBlockContent(fold)}`));
8
+ }
@@ -34,6 +34,7 @@ export declare const STOP_NOT_LOCAL = "stop.not_local";
34
34
  export declare const STOP_PARKED = "stop.parked";
35
35
  export declare const STOP_CONFLICT_CODES: readonly ["stop.park_arbiter_unreachable", "stop.park_resume_won", "stop.not_landed", "stop.not_local", "stop.parked"];
36
36
  export declare const OUTPUT_INVALID = "output.invalid";
37
+ export declare const RESUME_POLICY_FOLD_CODE = "resume_blocked_by_policy";
37
38
  export declare const RESUME_AT_ERROR_CODE_PREFIX = "resume_at.";
38
39
  export declare const REWIND_ERROR_CODE_PREFIXES: readonly ["resume_at.", "rewind_snapshot."];
39
40
  export declare function isRewindFamilyCode(code: string | undefined): boolean;
@@ -69,6 +69,7 @@ export const STOP_CONFLICT_CODES = [
69
69
  STOP_PARKED,
70
70
  ];
71
71
  export const OUTPUT_INVALID = 'output.invalid';
72
+ export const RESUME_POLICY_FOLD_CODE = 'resume_blocked_by_policy';
72
73
  export const RESUME_AT_ERROR_CODE_PREFIX = 'resume_at.';
73
74
  export const REWIND_ERROR_CODE_PREFIXES = [RESUME_AT_ERROR_CODE_PREFIX, 'rewind_snapshot.'];
74
75
  export function isRewindFamilyCode(code) {
@@ -55,6 +55,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
55
55
  'route.fallback_to_primary',
56
56
  'route.classifier_unroutable',
57
57
  'route.base_url_changed_key_unchanged',
58
+ 'route.response_model_mismatch',
58
59
  'task.user_steer_undrained',
59
60
  'task.user_followup_undrained',
60
61
  'steering.parked_input_blocked',
@@ -129,6 +130,7 @@ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
129
130
  'route.fallback_to_primary': 'operator',
130
131
  'route.classifier_unroutable': 'operator',
131
132
  'route.base_url_changed_key_unchanged': 'operator',
133
+ 'route.response_model_mismatch': 'operator',
132
134
  'task.user_steer_undrained': 'user',
133
135
  'task.user_followup_undrained': 'user',
134
136
  'steering.parked_input_blocked': 'user',
@@ -51,6 +51,9 @@ export function executionLaneDoctorDetail(reading) {
51
51
  if (reading.view.toolsOnThisHost) {
52
52
  return `execution lane ${lane} — tools run on the engine host (same filesystem), so skills that have a directory of their own can carry a baseDir`;
53
53
  }
54
+ if (reading.view.provider === 'in-process') {
55
+ return 'no execution lane on this deployment (in-process) — the engine has no machine of its own for running tools here, so its built-in file and shell tools are not mounted and skill baseDir is never sent; whether a given run had tools is answered by its tool roster';
56
+ }
54
57
  return `execution lane ${lane} — tools run on a different machine than the engine, so skill baseDir is never sent on this deployment`;
55
58
  }
56
59
  }