@sema-agent/client-core 0.89.0 → 0.91.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 (104) hide show
  1. package/CHANGELOG.md +297 -0
  2. package/README.md +35 -24
  3. package/dist/adapt/arms.js +9 -0
  4. package/dist/adapter/downstream/eventToSdkMessage.js +4 -2
  5. package/dist/adapter/downstream/wiringManifestView.d.ts +6 -0
  6. package/dist/adapter/downstream/wiringManifestView.js +16 -0
  7. package/dist/adapter/runStream.d.ts +2 -0
  8. package/dist/adapter/runStream.js +180 -38
  9. package/dist/askSurvivesStandingAnswer.d.ts +10 -0
  10. package/dist/askSurvivesStandingAnswer.js +40 -0
  11. package/dist/attachmentSeatCapability.d.ts +11 -0
  12. package/dist/attachmentSeatCapability.js +27 -0
  13. package/dist/attachmentsWireCaps.d.ts +22 -2
  14. package/dist/attachmentsWireCaps.js +95 -22
  15. package/dist/compensations.js +10 -14
  16. package/dist/controlRouter.d.ts +9 -2
  17. package/dist/controlRouter.js +66 -28
  18. package/dist/coreValuePorts.d.ts +1 -0
  19. package/dist/decideFailureNote.js +4 -2
  20. package/dist/disableSkillsWire.d.ts +1 -0
  21. package/dist/disableSkillsWire.js +8 -1
  22. package/dist/displayUntrusted.js +153 -4
  23. package/dist/engineCapReader.js +12 -1
  24. package/dist/engineCapsCache.d.ts +3 -1
  25. package/dist/engineCapsCache.js +56 -6
  26. package/dist/engineClosedSets.d.ts +2 -0
  27. package/dist/engineClosedSets.js +3 -0
  28. package/dist/engineErrorCodes.d.ts +3 -0
  29. package/dist/engineErrorCodes.js +10 -0
  30. package/dist/engineNoticeCodes.d.ts +2 -9
  31. package/dist/engineNoticeCodes.js +19 -27
  32. package/dist/fileHistoryLegNotice.d.ts +20 -0
  33. package/dist/fileHistoryLegNotice.js +122 -0
  34. package/dist/gateOutcome.d.ts +1 -1
  35. package/dist/gateOutcome.js +1 -0
  36. package/dist/generated/engineFactTables.d.ts +13 -0
  37. package/dist/generated/engineFactTables.js +50 -0
  38. package/dist/generated/engineNoticeTables.js +0 -3
  39. package/dist/hitl/askGateWire.d.ts +1 -1
  40. package/dist/hitl/askGateWire.js +1 -1
  41. package/dist/hitl/crashConverged.d.ts +7 -0
  42. package/dist/hitl/crashConverged.js +57 -0
  43. package/dist/hitl/crashConvergedNotice.d.ts +4 -0
  44. package/dist/hitl/crashConvergedNotice.js +57 -0
  45. package/dist/hitl/hitlBridge.d.ts +7 -4
  46. package/dist/hitl/parkResolver.d.ts +7 -2
  47. package/dist/hitl/parkResolver.js +19 -7
  48. package/dist/hitl/planReviewWire.d.ts +1 -1
  49. package/dist/hitl/planReviewWire.js +18 -4
  50. package/dist/hitl/sessionPolicyWire.d.ts +5 -0
  51. package/dist/hitl/sessionPolicyWire.js +63 -0
  52. package/dist/hitl/suspendedReopen.js +2 -2
  53. package/dist/hitl/toolApprovalWire.d.ts +4 -1
  54. package/dist/hitl/toolApprovalWire.js +20 -0
  55. package/dist/hookMatcherVerdict.d.ts +29 -0
  56. package/dist/hookMatcherVerdict.js +265 -0
  57. package/dist/hookToolAliases.d.ts +3 -0
  58. package/dist/hookToolAliases.js +10 -0
  59. package/dist/humanTurn.d.ts +91 -0
  60. package/dist/humanTurn.js +253 -0
  61. package/dist/humanTurnRead.d.ts +105 -0
  62. package/dist/humanTurnRead.js +331 -0
  63. package/dist/index.d.ts +30 -5
  64. package/dist/index.js +19 -4
  65. package/dist/inheritEnvWire.js +1 -1
  66. package/dist/liveModelCatalog.d.ts +2 -0
  67. package/dist/liveModelCatalog.js +51 -3
  68. package/dist/permissionRuleIssue.d.ts +1 -1
  69. package/dist/permissionRuleIssue.js +22 -3
  70. package/dist/permissionsAllowGrantCapability.d.ts +18 -0
  71. package/dist/permissionsAllowGrantCapability.js +84 -0
  72. package/dist/permissionsDeclaration.d.ts +48 -0
  73. package/dist/permissionsDeclaration.js +307 -0
  74. package/dist/promptDelivery.d.ts +16 -5
  75. package/dist/promptDelivery.js +172 -32
  76. package/dist/providerRejection.d.ts +3 -1
  77. package/dist/providerRejection.js +27 -1
  78. package/dist/readDenyTiers.d.ts +10 -0
  79. package/dist/readDenyTiers.js +29 -0
  80. package/dist/request/taskRequest.js +27 -1
  81. package/dist/resumeRefusalCopy.d.ts +11 -0
  82. package/dist/resumeRefusalCopy.js +61 -4
  83. package/dist/rewindArchiveCapability.js +4 -4
  84. package/dist/rewindWireCaps.d.ts +2 -1
  85. package/dist/rewindWireCaps.js +19 -2
  86. package/dist/sdkRegistryTransit.d.ts +1 -0
  87. package/dist/sdkRegistryTransit.js +1 -0
  88. package/dist/seam.d.ts +2 -0
  89. package/dist/supportFloor.d.ts +10 -0
  90. package/dist/supportFloor.js +42 -0
  91. package/dist/taskRequestWords.d.ts +7 -0
  92. package/dist/taskRequestWords.js +24 -0
  93. package/dist/toolHistoryBadTurn.d.ts +1 -0
  94. package/dist/toolHistoryBadTurn.js +89 -0
  95. package/dist/toolHistoryMismatch.d.ts +2 -1
  96. package/dist/toolHistoryMismatch.js +19 -6
  97. package/dist/userAttachmentEnvelope.d.ts +61 -0
  98. package/dist/userAttachmentEnvelope.js +204 -0
  99. package/dist/wireFailureShape.d.ts +1 -0
  100. package/dist/wireFailureShape.js +4 -0
  101. package/dist/wireRefusalCopy.d.ts +6 -0
  102. package/dist/wireRefusalCopy.js +26 -3
  103. package/docs/INTEGRATION-CLIENTS.md +1369 -18
  104. package/package.json +3 -3
@@ -114,3 +114,53 @@ export const CORE_TOOL_DECLARATION_CODES = Object.freeze([
114
114
  export const CORE_MODE_RELEASE_REASONS = Object.freeze([
115
115
  "rule_unreadable",
116
116
  ]);
117
+ export const CORE_HOOK_TOOL_ALIASES = Object.freeze(Object.assign(Object.create(null), Object.fromEntries([
118
+ ["ListPeers", "ListAgents"],
119
+ ])));
120
+ export const CORE_ASK_EVIDENCE_ABSENCE_VALUES = Object.freeze([
121
+ "not_wired",
122
+ "not_adjudicated",
123
+ "unavailable",
124
+ "no_match",
125
+ "not_reported",
126
+ ]);
127
+ export const CORE_SYSTEM_INJECTION_PRIORITIES = Object.freeze([
128
+ "now",
129
+ "next",
130
+ "later",
131
+ ]);
132
+ export const CORE_READ_DENY_BUILTIN_TIERS = Object.freeze([
133
+ "credentials",
134
+ "shell-history",
135
+ "browser",
136
+ "wallet",
137
+ "agent-config",
138
+ ]);
139
+ export const CORE_READ_DENY_DEFAULT_TIERS = Object.freeze([
140
+ "credentials",
141
+ "agent-config",
142
+ ]);
143
+ export const CORE_MODE_ARBITRATION = Object.freeze({
144
+ "content_question": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true }),
145
+ "hook": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
146
+ "ancestor": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
147
+ "unverifiable_delete": Object.freeze({ bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true }),
148
+ "transcript_write": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true }),
149
+ "org_rule": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
150
+ "governance_unavailable": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
151
+ "rule_unreadable": Object.freeze({ bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
152
+ "ask_rule": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true }),
153
+ "denial_limit_fallback": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
154
+ "operator_forced": Object.freeze({ bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false }),
155
+ "shell_floor": Object.freeze({ bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false }),
156
+ "write_protection_unresolved": Object.freeze({ bypassImmune: true, classifierEligible: false, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true }),
157
+ "probe": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
158
+ "write_protection": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true }),
159
+ "tool_marks_mcp": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false }),
160
+ "tool_marks_a2a": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false }),
161
+ "policy_rra": Object.freeze({ bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
162
+ "artifact_shared_read": Object.freeze({ bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true }),
163
+ "mode": Object.freeze({ bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false }),
164
+ "unstated": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false }),
165
+ "ordinary": Object.freeze({ bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false }),
166
+ });
@@ -28,7 +28,6 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
28
28
  "permission.ask_released_by_mode",
29
29
  "permission.read_deny_unapplied",
30
30
  "permission.rule_store_unavailable",
31
- "resume.legacy_mode_seed_migrated",
32
31
  "peer.inbound_disposition",
33
32
  "peer.held_settled",
34
33
  "peer.idle_subscription",
@@ -116,7 +115,6 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
116
115
  "permission.ask_released_by_mode": "user",
117
116
  "permission.read_deny_unapplied": "user",
118
117
  "permission.rule_store_unavailable": "user",
119
- "resume.legacy_mode_seed_migrated": "operator",
120
118
  "peer.inbound_disposition": "user",
121
119
  "peer.held_settled": "user",
122
120
  "peer.idle_subscription": "user",
@@ -210,7 +208,6 @@ export const CORE_NOTICE_MINTER = Object.freeze({
210
208
  "permission.ask_released_by_mode": "engine",
211
209
  "permission.read_deny_unapplied": "engine",
212
210
  "permission.rule_store_unavailable": "engine",
213
- "resume.legacy_mode_seed_migrated": "engine",
214
211
  "peer.inbound_disposition": "engine",
215
212
  "peer.held_settled": "engine",
216
213
  "peer.idle_subscription": "engine",
@@ -8,7 +8,7 @@ export type { AskAnsweredOutput } from './gateLedger.js';
8
8
  export { SEMA_COLLATERAL_ABORT_KEY } from './gateLedger.js';
9
9
  export { SEMA_DENIAL_KIND_KEY } from './gateLedger.js';
10
10
  export type { GateDenyAttribution } from './gateLedger.js';
11
- export { toAnsweredOutput, isAlreadyResolvedGateReason, isGateStandingErrorCode, isCodeClassifiedGateFailure, GATE_FAILURE_CODES, nextHopBudget, MAX_GATE_HOPS, MAX_TOTAL_PARKS } from './parkResolver.js';
11
+ export { toAnsweredOutput, isAlreadyResolvedFailure, isAlreadyResolvedGateReason, isGateStandingErrorCode, isCodeClassifiedGateFailure, GATE_FAILURE_CODES, nextHopBudget, MAX_GATE_HOPS, MAX_TOTAL_PARKS } from './parkResolver.js';
12
12
  export type { HopBudget, HopRound } from './parkResolver.js';
13
13
  export type { GateOutcome, GateFailureCode } from './parkResolver.js';
14
14
  export declare function bridgeAskUserQuestionGates(source: AsyncGenerator<AgentEvent>, deps: AskGateWireDeps, opts?: {
@@ -6,7 +6,7 @@ export { HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE } from './frameRouter.js';
6
6
  export { HITL_POLICY_DENY_MESSAGE } from './frameRouter.js';
7
7
  export { SEMA_COLLATERAL_ABORT_KEY } from './gateLedger.js';
8
8
  export { SEMA_DENIAL_KIND_KEY } from './gateLedger.js';
9
- export { toAnsweredOutput, isAlreadyResolvedGateReason, isGateStandingErrorCode, isCodeClassifiedGateFailure, GATE_FAILURE_CODES, nextHopBudget, MAX_GATE_HOPS, MAX_TOTAL_PARKS } from './parkResolver.js';
9
+ export { toAnsweredOutput, isAlreadyResolvedFailure, isAlreadyResolvedGateReason, isGateStandingErrorCode, isCodeClassifiedGateFailure, GATE_FAILURE_CODES, nextHopBudget, MAX_GATE_HOPS, MAX_TOTAL_PARKS } from './parkResolver.js';
10
10
  export async function* bridgeAskUserQuestionGates(source, deps, opts) {
11
11
  const led = createGateLedger();
12
12
  const taskId = { current: opts?.taskId };
@@ -28,3 +28,10 @@ export interface CrashConvergedProjection {
28
28
  readonly dropped: number;
29
29
  }
30
30
  export declare function projectCrashConverged(env: unknown): CrashConvergedProjection | undefined;
31
+ export interface CrashConvergedNoticeFacts {
32
+ readonly total: number;
33
+ readonly dropped: number;
34
+ readonly needsHumanReasons: readonly (string | undefined)[];
35
+ readonly causes: readonly CrashConvergedRow['cause'][] | undefined;
36
+ }
37
+ export declare function readCrashConvergedNoticeFacts(projection: CrashConvergedProjection | undefined): CrashConvergedNoticeFacts | undefined;
@@ -127,3 +127,60 @@ export function projectCrashConverged(env) {
127
127
  }
128
128
  return { total: resumeSafe.length + needsHuman.length, resumeSafe, needsHuman, dropped };
129
129
  }
130
+ const isCount = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
131
+ function bucketRowsOf(bucket) {
132
+ if (!Array.isArray(bucket))
133
+ return undefined;
134
+ const len = ownDataValue(bucket, 'length');
135
+ if (!isCount(len) || len > MAX_CRASH_CONVERGED_ROWS)
136
+ return undefined;
137
+ const rows = [];
138
+ for (let i = 0; i < len; i++)
139
+ rows.push(ownDataValue(bucket, String(i)));
140
+ return rows;
141
+ }
142
+ function reasonTextOf(row) {
143
+ try {
144
+ if (typeof row !== 'object' || row === null)
145
+ return undefined;
146
+ const v = ownDataValue(row, 'needsHumanReason');
147
+ return typeof v === 'string' ? v : undefined;
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ }
153
+ function causeWordsOf(rows) {
154
+ const seen = [];
155
+ try {
156
+ for (const row of rows) {
157
+ if (typeof row !== 'object' || row === null)
158
+ return undefined;
159
+ const c = ownDataValue(row, 'cause');
160
+ if (isCrashConvergedCause(c) && !seen.includes(c))
161
+ seen.push(c);
162
+ }
163
+ }
164
+ catch {
165
+ return undefined;
166
+ }
167
+ return seen;
168
+ }
169
+ export function readCrashConvergedNoticeFacts(projection) {
170
+ try {
171
+ if (typeof projection !== 'object' || projection === null)
172
+ return undefined;
173
+ const total = ownDataValue(projection, 'total');
174
+ const dropped = ownDataValue(projection, 'dropped');
175
+ if (!isCount(total) || !isCount(dropped) || total > MAX_CRASH_CONVERGED_ROWS)
176
+ return undefined;
177
+ const safe = bucketRowsOf(ownDataValue(projection, 'resumeSafe'));
178
+ const human = bucketRowsOf(ownDataValue(projection, 'needsHuman'));
179
+ if (safe === undefined || human === undefined || safe.length + human.length !== total)
180
+ return undefined;
181
+ return { total, dropped, needsHumanReasons: human.map(reasonTextOf), causes: causeWordsOf([...safe, ...human]) };
182
+ }
183
+ catch {
184
+ return undefined;
185
+ }
186
+ }
@@ -0,0 +1,4 @@
1
+ import type { CrashConvergedProjection } from './crashConverged.js';
2
+ export declare const NEEDS_HUMAN_REASON_ORDER: readonly ["approved_then_interrupted", "pending_unsafe", "contradictory", "unstable_row"];
3
+ export declare function crashConvergedNeedsHumanSentence(reason: string | undefined, n: number, total: number): string | undefined;
4
+ export declare function crashConvergedNotice(projection: CrashConvergedProjection | undefined): string | undefined;
@@ -0,0 +1,57 @@
1
+ import { readCrashConvergedNoticeFacts } from './crashConverged.js';
2
+ export const NEEDS_HUMAN_REASON_ORDER = Object.freeze(['approved_then_interrupted', 'pending_unsafe', 'contradictory', 'unstable_row']);
3
+ const _orderCoversReasons = true;
4
+ const _orderInsideReasons = true;
5
+ const _orderHasNoDuplicate = true;
6
+ void _orderCoversReasons;
7
+ void _orderInsideReasons;
8
+ void _orderHasNoDuplicate;
9
+ const RECORDED = 'based on what was recorded';
10
+ const NO_AUTO_RERUN = 'sema has not re-run anything';
11
+ const plural = (n, one, many) => (n === 1 ? one : many);
12
+ const SENTENCE = Object.freeze({
13
+ approved_then_interrupted: (n, total) => `${n} of ${total} had already been approved and may have been running when the previous engine run ended, so a side effect may have partly landed`,
14
+ pending_unsafe: (n, total) => `${n} of ${total} had not started, ${RECORDED}, but the engine did not mark ${plural(n, 'it', 'them')} as safe to re-run`,
15
+ contradictory: (n, total) => `${n} of ${total} ${plural(n, 'has a contradictory record', 'have contradictory records')}`,
16
+ unstable_row: (n, total) => `${n} of ${total} ${plural(n, 'has a record that read differently each time it was read', 'have records that read differently each time they were read')}`,
17
+ });
18
+ const fallbackSentence = (n, total) => `${n} of ${total} had already been approved, or cannot be accounted for, so a side effect may have partly landed`;
19
+ const isNeedsHumanReason = (v) => typeof v === 'string' && Object.hasOwn(SENTENCE, v);
20
+ export function crashConvergedNeedsHumanSentence(reason, n, total) {
21
+ if (!isNeedsHumanReason(reason))
22
+ return undefined;
23
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < 1)
24
+ return undefined;
25
+ if (typeof total !== 'number' || !Number.isSafeInteger(total) || total < n)
26
+ return undefined;
27
+ return SENTENCE[reason](n, total);
28
+ }
29
+ function needsHumanClauses(reasons, total) {
30
+ const clauses = [];
31
+ for (const reason of NEEDS_HUMAN_REASON_ORDER) {
32
+ const n = reasons.filter((r) => r === reason).length;
33
+ if (n > 0)
34
+ clauses.push(SENTENCE[reason](n, total));
35
+ }
36
+ const fallback = reasons.filter((r) => !isNeedsHumanReason(r)).length;
37
+ if (fallback > 0)
38
+ clauses.push(fallbackSentence(fallback, total));
39
+ return `${clauses.join('; ')} — check ${plural(reasons.length, 'it', 'them')} by hand before re-running`;
40
+ }
41
+ export function crashConvergedNotice(projection) {
42
+ const facts = readCrashConvergedNoticeFacts(projection);
43
+ if (facts === undefined)
44
+ return undefined;
45
+ const { total, dropped, needsHumanReasons, causes } = facts;
46
+ if (total === 0 && dropped === 0)
47
+ return undefined;
48
+ if (total === 0) {
49
+ return `the previous session left ${dropped} crash-converged approval ${plural(dropped, 'record', 'records')} that this version could not read. ${NO_AUTO_RERUN}.`;
50
+ }
51
+ const recordedAs = causes !== undefined && causes.length > 0 ? ` (recorded as ${causes.join(', ')})` : '';
52
+ const head = `the previous engine run ended with ${total} ${plural(total, 'approval', 'approvals')} still waiting${recordedAs}, ` +
53
+ `and the engine converged ${plural(total, 'it', 'them')} to denied`;
54
+ const body = needsHumanReasons.length > 0 ? needsHumanClauses(needsHumanReasons, total) : `${RECORDED}, ${plural(total, 'it', 'they')} can be re-run`;
55
+ const droppedClause = dropped > 0 ? ` (${dropped} further ${plural(dropped, 'record', 'records')} could not be read.)` : '';
56
+ return `${head}; ${body}. ${NO_AUTO_RERUN}.${droppedClause}`;
57
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
2
+ import type { PermissionResult } from '@sema-agent/agent-types';
2
3
  import type { ApprovalsListEnvelope } from './crashConverged.js';
3
4
  import { type SuspendedReopenReading } from './suspendedReopen.js';
4
5
  export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
@@ -8,9 +9,9 @@ export interface HitlPermissionDecisionLike {
8
9
  message?: string;
9
10
  updatedInput?: unknown;
10
11
  }
11
- export type HitlCanUseToolFn<D extends HitlPermissionDecisionLike = HitlPermissionDecisionLike> = (tool: {
12
+ export type HitlCanUseToolFn<D extends HitlPermissionDecisionLike = HitlPermissionDecisionLike, Ctx = never, Msg = never> = (tool: {
12
13
  name: string;
13
- }, input: Record<string, unknown>, toolUseContext: never, assistantMessage: never, toolUseID: string, forceDecision?: D) => Promise<D>;
14
+ }, input: Record<string, unknown>, toolUseContext: Ctx, assistantMessage: Msg, toolUseID: string, forceDecision?: D) => Promise<D>;
14
15
  export interface ApprovalsResourceLike {
15
16
  list(opts?: {
16
17
  signal?: AbortSignal;
@@ -147,8 +148,10 @@ export type GatePrompt<D extends HitlPermissionDecisionLike = HitlPermissionDeci
147
148
  toolUseID: string;
148
149
  gate: CheckpointGate | null;
149
150
  }) => Promise<D>;
150
- export declare function makeHitlCanUseTool<D extends HitlPermissionDecisionLike = HitlPermissionDecisionLike>(bridge: HitlBridge, prompt: GatePrompt<D>): HitlCanUseToolFn<D>;
151
- export interface HitlBackendDenyDecision {
151
+ export declare function makeHitlCanUseTool<D extends HitlPermissionDecisionLike = HitlPermissionDecisionLike, Ctx = unknown, Msg = unknown>(bridge: HitlBridge, prompt: GatePrompt<D>): HitlCanUseToolFn<D, Ctx, Msg>;
152
+ export interface HitlBackendDenyDecision extends Extract<PermissionResult, {
153
+ behavior: 'deny';
154
+ }> {
152
155
  behavior: 'deny';
153
156
  message: string;
154
157
  decisionReason: {
@@ -1,6 +1,7 @@
1
1
  import type { AgentEvent } from '@sema-agent/sdk';
2
2
  import { type GateCurrentPending, type HitlFailureStage } from './hitlBridge.js';
3
3
  import { type QuestionAnswer } from '../liveQuestionStore.js';
4
+ import { type FsApprovalOutcome } from './toolApprovalWire.js';
4
5
  import type { AskAnsweredOutput, GateLedger } from './gateLedger.js';
5
6
  import { type AskGateWireDeps, type GatePark } from './frameRouter.js';
6
7
  import { type DecideReceiptView } from '../decideReceipt.js';
@@ -29,12 +30,16 @@ export type GateOutcome = {
29
30
  reason: string;
30
31
  code?: GateFailureCode | undefined;
31
32
  errorCode?: string | undefined;
33
+ retriable?: true;
32
34
  retryExhausted?: true;
33
35
  currentPending?: GateCurrentPending | undefined;
34
36
  };
35
37
  export declare function isAlreadyResolvedGateReason(reason: unknown): boolean;
36
- export declare function isGateStandingErrorCode(code: string | null | undefined): boolean;
37
- export declare function isCodeClassifiedGateFailure(code: string | null | undefined): boolean;
38
+ export declare function isAlreadyResolvedFailure(outcome: Extract<GateOutcome | FsApprovalOutcome, {
39
+ kind: 'failed';
40
+ }>): boolean;
41
+ export declare function isGateStandingErrorCode(code: string | null | undefined, retriable?: boolean): boolean;
42
+ export declare function isCodeClassifiedGateFailure(code: string | null | undefined, retriable?: boolean): boolean;
38
43
  export declare function toAnsweredOutput(questions: unknown[], answer: QuestionAnswer): AskAnsweredOutput;
39
44
  export type ParkResolution = {
40
45
  kind: 'reattach';
@@ -1,6 +1,6 @@
1
1
  import { putOwnKey } from '../ownKey.js';
2
2
  import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, NO_QUESTION_OVERLAY_REASON, findPendingForTask, isNoDecisionSurfaceReason, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
3
- import { PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode } from '../engineErrorCodes.js';
3
+ import { CHECKPOINT_UNSUPPORTED_VERSION, PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode, refusalSaysStillPending } from '../engineErrorCodes.js';
4
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
5
5
  import { hostLog } from '../host.js';
6
6
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
@@ -40,21 +40,31 @@ function alreadyResolvedReasonToken(reason) {
40
40
  const r = reason.toLowerCase();
41
41
  return ALREADY_RESOLVED_REASON_TOKENS.find((t) => r.includes(t));
42
42
  }
43
- function isAlreadyResolvedFailure(outcome) {
43
+ export function isAlreadyResolvedFailure(outcome) {
44
+ try {
45
+ return alreadyResolvedFailureVerdict(outcome);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function alreadyResolvedFailureVerdict(outcome) {
52
+ if (typeof outcome !== 'object' || outcome === null)
53
+ return false;
44
54
  const code = 'code' in outcome && isGateFailureCode(outcome.code) ? outcome.code : undefined;
45
55
  if (code !== undefined)
46
56
  return code === 'no_pending';
47
- if ('errorCode' in outcome && isCodeClassifiedGateFailure(outcome.errorCode))
57
+ if ('errorCode' in outcome && isCodeClassifiedGateFailure(outcome.errorCode, Object.hasOwn(outcome, 'retriable') && outcome.retriable === true))
48
58
  return false;
49
59
  if ('errorCode' in outcome && isDecideParkMovedCode(outcome.errorCode))
50
60
  return true;
51
61
  return isAlreadyResolvedGateReason(outcome.reason);
52
62
  }
53
- export function isGateStandingErrorCode(code) {
54
- return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt';
63
+ export function isGateStandingErrorCode(code, retriable) {
64
+ return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt' || (code === CHECKPOINT_UNSUPPORTED_VERSION && retriable === true);
55
65
  }
56
- export function isCodeClassifiedGateFailure(code) {
57
- return isGateStandingErrorCode(code) || code === PARKED_RESUME_STARTUP_FAILED;
66
+ export function isCodeClassifiedGateFailure(code, retriable) {
67
+ return isGateStandingErrorCode(code, retriable) || code === PARKED_RESUME_STARTUP_FAILED;
58
68
  }
59
69
  export function toAnsweredOutput(questions, answer) {
60
70
  const answers = {};
@@ -153,6 +163,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
153
163
  const code = e instanceof HitlSafetyError && isGateFailureCode(e.code) ? e.code : undefined;
154
164
  const currentPending = readDecideCurrentPending(e);
155
165
  const wireCode = readWireErrorCode(e);
166
+ const wireRetriable = typeof e === 'object' && e !== null && refusalSaysStillPending(e);
156
167
  return {
157
168
  kind: 'failed',
158
169
  stage: 'decide',
@@ -162,6 +173,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
162
173
  ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
163
174
  ...(currentPending !== undefined ? { currentPending } : {}),
164
175
  ...(wireCode !== undefined ? { errorCode: wireCode } : {}),
176
+ ...(wireRetriable ? { retriable: true } : {}),
165
177
  };
166
178
  }
167
179
  }
@@ -43,7 +43,7 @@ export interface PlanReviewOutcomeMeta {
43
43
  decision: 'approve' | 'reject';
44
44
  effect: PlanReviewDecisionEffect;
45
45
  }
46
- export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect): string;
46
+ export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect, cause?: 'stored_form_unsupported'): string;
47
47
  export type PlanReviewWireClient = {
48
48
  assistant: Pick<AgentClient['assistant'], 'planReview'>;
49
49
  runs: Pick<AgentClient['runs'], 'get'>;
@@ -6,6 +6,8 @@ import { engineWireTarget } from '../engineWireTarget.js';
6
6
  import { enqueuePlanReviewOutcome } from '../notifications.js';
7
7
  import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
8
8
  import { decideFailureNote } from '../decideFailureNote.js';
9
+ import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError } from '../resumeRefusalCopy.js';
10
+ import { REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
9
11
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
10
12
  import { engineCapString } from '../engineCapsCache.js';
11
13
  import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
@@ -303,7 +305,10 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
303
305
  return false;
304
306
  }
305
307
  }
306
- export function planReviewOutcomeTail(taskId, decision, effect) {
308
+ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
309
+ if (effect === 'still_parked' && cause === 'stored_form_unsupported') {
310
+ return `Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}, and do not ${decision} it again: deciding again will not take effect while it is stored in this form. Tell the user the way forward this outcome gives — cancel this task's run, then submit the work again as a new task.`;
311
+ }
307
312
  switch (effect) {
308
313
  case 'took_effect':
309
314
  return decision === 'approve'
@@ -500,6 +505,7 @@ function planReviewClientsFor(resolved) {
500
505
  async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore) {
501
506
  let outcome;
502
507
  let effect = 'unconfirmed';
508
+ let stillParkedCause;
503
509
  let stillParkedProven = false;
504
510
  let statusSeen;
505
511
  const wire = opts?.wire;
@@ -543,7 +549,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
543
549
  }
544
550
  catch (e1) {
545
551
  const conflict = e1;
546
- if (mode === 'default' && conflict?.status === 400 && conflict?.errorCode === 'request.field_conflict') {
552
+ if (mode === 'default' && conflict?.status === 400 && conflict?.errorCode === REQUEST_FIELD_CONFLICT) {
547
553
  hostLog('debug', 'planReviewWire: permissionModeAfter "default" refused as not applicable — re-sending the approval without the key (same meaning)');
548
554
  body = await sendOnce(undefined, () => client.assistant.planReview(taskId, reason !== undefined ? { decision, reason } : { decision }));
549
555
  }
@@ -610,7 +616,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
610
616
  const status = e?.status;
611
617
  const refusedCode = e?.errorCode;
612
618
  const noAnswer = typeof status === 'number' || typeof e !== 'object' ? null : noAnswerKindOf(e);
613
- if (status === 400 && refusedCode === 'request.field_conflict' && permissionModeAfter === 'acceptEdits') {
619
+ if (status === 400 && refusedCode === REQUEST_FIELD_CONFLICT && permissionModeAfter === 'acceptEdits') {
614
620
  hostLog('debug', 'planReviewWire: approve + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode');
615
621
  effect = 'still_parked';
616
622
  stillParkedProven = true;
@@ -625,6 +631,14 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
625
631
  `This ${decision} was not applied: the engine no longer has a pending plan review for this task — an earlier decision most likely already went through (or the review expired). ` +
626
632
  'This is NOT a rejection of the plan and NOT a failure of the task; the answer just given simply had nothing left to act on.';
627
633
  }
634
+ else if (checkpointUnsupportedVersionFromError(e) !== null) {
635
+ const cu = checkpointUnsupportedVersionFromError(e);
636
+ hostLog('debug', `planReviewWire: ${decision} → 409 checkpoint.unsupported_version (the server cannot continue this parked review in its stored form; still pending)`);
637
+ effect = 'still_parked';
638
+ stillParkedProven = true;
639
+ stillParkedCause = 'stored_form_unsupported';
640
+ outcome = `This ${decision} was not applied. ${checkpointUnsupportedVersionContent(cu, { parkKind: 'plan_review', runId: taskId })}`;
641
+ }
628
642
  else if (typeof status === 'number' && corruptStoredRowWhere(e) !== undefined) {
629
643
  const where = corruptStoredRowWhere(e);
630
644
  hostLog('debug', `planReviewWire: ${decision} → ${String(status)} corrupt (stored resume context unreadable)`);
@@ -651,7 +665,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
651
665
  }
652
666
  }
653
667
  }
654
- const tail = planReviewOutcomeTail(taskId, decision, effect);
668
+ const tail = planReviewOutcomeTail(taskId, decision, effect, stillParkedCause);
655
669
  const prompt = `<plan-review-outcome>\n${outcome}\n${tail}\n</plan-review-outcome>`;
656
670
  const onOutcome = opts?.onOutcome;
657
671
  settle(effect === 'not_sent' || (effect === 'still_parked' && stillParkedProven));
@@ -105,6 +105,11 @@ export declare function tightenSessionPolicy(facade: SessionPolicyFacade, req: S
105
105
  signal?: AbortSignal;
106
106
  }): Promise<SessionPolicyTightenOutcome>;
107
107
  export declare function sessionPolicyTightenNotice(outcome: SessionPolicyTightenOutcome): string;
108
+ export declare function sessionPolicyReadNotice(outcome: Exclude<SessionPolicyReadOutcome, {
109
+ kind: 'present';
110
+ }> | {
111
+ kind: 'no_target';
112
+ }): string;
108
113
  export interface SessionPolicyRemovedEntry {
109
114
  readonly field: 'toolDeny' | 'toolAllow';
110
115
  readonly rule: string;
@@ -344,6 +344,69 @@ export function sessionPolicyTightenNotice(outcome) {
344
344
  }
345
345
  }
346
346
  }
347
+ const READ_NOTICE_GENERIC = 'This session’s rule record could not be shown, so what it holds is not known. Try reading it again.';
348
+ const READ_NOTICE_NO_TARGET = 'There is no engine connection for this session, so its rule record cannot be read and what it holds is not known. Look again once it is connected.';
349
+ function readFailureSentence(kind) {
350
+ switch (kind) {
351
+ case 'face-unavailable':
352
+ return 'This deployment does not keep per-session tool rules, so there is no session rule record to read.';
353
+ case 'loosen-forbidden':
354
+ return 'The engine answered this read with a refusal that only applies to relaxing rules, so this session’s rule record could not be read. Try again; if it keeps happening, whoever runs the engine needs to look at it.';
355
+ case 'store-corrupt':
356
+ return 'The engine could not establish which version this session’s rules are at, so it would not hand the record over. This needs an operator to look at the rule store.';
357
+ case 'cas-conflict':
358
+ return 'The engine reported a version clash on this session’s rule record, so it could not be read this time. Try again.';
359
+ case 'session-ownerless':
360
+ return 'This session has no owner, so it has no rule record to read.';
361
+ case 'session-not-found':
362
+ return 'This session is not readable from here — it may not exist, or it may belong to someone else — so its rule record cannot be shown. Check that you are connected to the engine and the session you meant, or continue in a new session.';
363
+ case 'request-rejected':
364
+ return 'The engine rejected the request to read this session’s rule record as malformed, so it could not be read. Sending the same request again will not change that; check which session was asked for.';
365
+ case 'legacy-tool-name':
366
+ return 'The engine answered this read with a refusal about a tool name it no longer accepts, so this session’s rule record could not be read. Try again; if it keeps happening, whoever runs the engine needs to look at it.';
367
+ case 'unauthorized':
368
+ return 'This call could not present a verifiable identity, so this session’s rule record could not be read.';
369
+ case 'error':
370
+ return 'The engine refused to hand over this session’s rule record for a reason this version has no wording for, so it could not be read. Try again; if it keeps happening, whoever runs the engine needs to look at it.';
371
+ case 'no-verdict':
372
+ return 'No clear answer came back from the engine when this session’s rule record was read, so what it holds is not known — that is not the same as holding nothing. Try again.';
373
+ }
374
+ }
375
+ function readMalformedSentence(why) {
376
+ switch (why) {
377
+ case 'envelope':
378
+ return 'The engine answered, but not in a form this version can read as a session rule record, so what the record holds is not known. Try again; if it keeps happening, this version and the engine may be out of step.';
379
+ case 'rev':
380
+ return 'The engine’s answer for this session’s rule record carried no usable version, so this version will not show it — a record it cannot place may still hold restrictions that are in effect. Try again.';
381
+ case 'rule_field':
382
+ return 'One of the lists in this session’s rule record came back in a form this version cannot read, so none of the record is shown rather than part of it — a partly shown record could leave out restrictions that are in effect.';
383
+ }
384
+ }
385
+ function ownRead(o, key) {
386
+ const d = Object.getOwnPropertyDescriptor(o, key);
387
+ return d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
388
+ }
389
+ export function sessionPolicyReadNotice(outcome) {
390
+ try {
391
+ if (outcome === null || typeof outcome !== 'object')
392
+ return READ_NOTICE_GENERIC;
393
+ const arm = ownRead(outcome, 'kind');
394
+ if (arm === 'failed') {
395
+ const failure = ownRead(outcome, 'failure');
396
+ if (failure === null || typeof failure !== 'object')
397
+ return READ_NOTICE_GENERIC;
398
+ return readFailureSentence(ownRead(failure, 'kind')) ?? READ_NOTICE_GENERIC;
399
+ }
400
+ if (arm === 'malformed')
401
+ return readMalformedSentence(ownRead(outcome, 'why')) ?? READ_NOTICE_GENERIC;
402
+ if (arm === 'no_target')
403
+ return READ_NOTICE_NO_TARGET;
404
+ return READ_NOTICE_GENERIC;
405
+ }
406
+ catch {
407
+ return READ_NOTICE_GENERIC;
408
+ }
409
+ }
347
410
  function withoutRefusedEntries(prior, mounted) {
348
411
  const REFUSABLE_NAME_FIELDS = ['toolDeny', 'toolAllow'];
349
412
  const isRefusable = (f) => REFUSABLE_NAME_FIELDS.includes(f);
@@ -1,4 +1,4 @@
1
- import { RESUME_REOPEN_CODES, RESUME_ENV_FAILED, RESUME_TOOL_UNAVAILABLE } from '../engineErrorCodes.js';
1
+ import { RESUME_REOPEN_CODES, RESUME_ENV_FAILED, RESUME_TOOL_UNAVAILABLE, refusalSaysStillPending } from '../engineErrorCodes.js';
2
2
  const UNSTATED = Object.freeze({ kind: 'unstated' });
3
3
  const NOT_REOPENED = Object.freeze({ kind: 'not_reopened' });
4
4
  export function suspendedReopenOf(ev) {
@@ -25,7 +25,7 @@ export function resumeReopenFromError(err) {
25
25
  const code = err.errorCode;
26
26
  if (typeof code !== 'string' || !RESUME_REOPEN_CODES.includes(code))
27
27
  return null;
28
- if (err.retriable !== true)
28
+ if (!refusalSaysStillPending(err))
29
29
  return null;
30
30
  const reason = code.slice('resume.'.length);
31
31
  return { code, reason, retriable: true };
@@ -37,6 +37,7 @@ export type FsApprovalOutcome = {
37
37
  reason: string;
38
38
  code?: 'no_pending' | undefined;
39
39
  errorCode?: string | undefined;
40
+ retriable?: true;
40
41
  safetyCode?: HitlSafetyCode | undefined;
41
42
  retryExhausted?: true;
42
43
  currentPending?: GateCurrentPending | undefined;
@@ -131,6 +132,7 @@ export interface ApprovalCardRequest {
131
132
  hasBidiControls?: true;
132
133
  persistedRuleShadowed?: string;
133
134
  probeCause?: unknown;
135
+ probeReason?: string;
134
136
  ruleEvidence?: unknown;
135
137
  requiresRealApproval?: true;
136
138
  irreversibleParkGate?: true;
@@ -180,6 +182,7 @@ export interface ToolApprovalFrame {
180
182
  ruleStoreUnreadable?: string;
181
183
  persistedRuleShadowed?: string;
182
184
  probeCause?: unknown;
185
+ probeReason?: string;
183
186
  ruleEvidence?: unknown;
184
187
  requiresRealApproval?: true;
185
188
  mandated?: true;
@@ -204,7 +207,7 @@ export interface ToolApprovalDelegation {
204
207
  depth: number;
205
208
  agentName?: string;
206
209
  }
207
- export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "ruleStoreUnreadable", "readRootCandidate", "mandated", "mandate", "origin", "askClass", "classes", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
210
+ export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "ruleStoreUnreadable", "readRootCandidate", "mandated", "mandate", "origin", "askClass", "classes", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "probeReason", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
208
211
  export interface ApprovalMandatedFacts {
209
212
  readonly mandated?: true;
210
213
  readonly ruleOffersAbsence?: string;