@sqdr2/dsh-fusion 0.0.0-stage → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/INSTALL.md +89 -0
  2. package/INSTALL.zh.md +86 -0
  3. package/LICENSE +201 -0
  4. package/README.en.md +193 -0
  5. package/README.md +182 -2
  6. package/RELEASING.md +123 -0
  7. package/RELEASING.zh.md +115 -0
  8. package/contracts/fusion.ts +279 -0
  9. package/contracts/storage.sql +86 -0
  10. package/cordis.patch.yml +3 -0
  11. package/docs/COMPATIBILITY.md +162 -0
  12. package/docs/EVIDENCE.md +141 -0
  13. package/dshx.yml +39 -0
  14. package/lib/client.js +1920 -0
  15. package/lib/client.js.map +1 -0
  16. package/lib/dsh-fusion.js +12664 -0
  17. package/lib/types/activity.d.ts +20 -0
  18. package/lib/types/approval/logical.d.ts +10 -0
  19. package/lib/types/benchmark/naive.d.ts +37 -0
  20. package/lib/types/benchmark/native-run.d.ts +68 -0
  21. package/lib/types/benchmark/output-limits.d.ts +19 -0
  22. package/lib/types/bilingual.d.ts +2 -0
  23. package/lib/types/client/ActivityRows.d.ts +8 -0
  24. package/lib/types/client/FusionCache.d.ts +4 -0
  25. package/lib/types/client/FusionHint.d.ts +9 -0
  26. package/lib/types/client/FusionHistory.d.ts +5 -0
  27. package/lib/types/client/FusionPanelIcon.d.ts +13 -0
  28. package/lib/types/client/FusionRecovery.d.ts +5 -0
  29. package/lib/types/client/FusionSettings.d.ts +9 -0
  30. package/lib/types/client/FusionStatus.d.ts +5 -0
  31. package/lib/types/client/FusionTool.d.ts +4 -0
  32. package/lib/types/client/activity-model.d.ts +14 -0
  33. package/lib/types/client/i18n.d.ts +15 -0
  34. package/lib/types/client/index.d.ts +22 -0
  35. package/lib/types/client/settings-events.d.ts +2 -0
  36. package/lib/types/client/status-polling.d.ts +9 -0
  37. package/lib/types/context/guard.d.ts +21 -0
  38. package/lib/types/context/handoff.d.ts +17 -0
  39. package/lib/types/contracts.d.ts +449 -0
  40. package/lib/types/digest.d.ts +5 -0
  41. package/lib/types/dsh-fusion.d.ts +13 -0
  42. package/lib/types/errors.d.ts +28 -0
  43. package/lib/types/evidence/gate.d.ts +23 -0
  44. package/lib/types/evidence/receipts.d.ts +65 -0
  45. package/lib/types/execution/write-lease.d.ts +42 -0
  46. package/lib/types/host/bindings.d.ts +30 -0
  47. package/lib/types/host/cache-defaults.d.ts +48 -0
  48. package/lib/types/host/cache-policy.d.ts +155 -0
  49. package/lib/types/host/change-evidence.d.ts +38 -0
  50. package/lib/types/host/compaction-routing.d.ts +15 -0
  51. package/lib/types/host/coordinator.d.ts +169 -0
  52. package/lib/types/host/evidence-page.d.ts +13 -0
  53. package/lib/types/host/history.d.ts +88 -0
  54. package/lib/types/host/http.d.ts +4 -0
  55. package/lib/types/host/model-output.d.ts +13 -0
  56. package/lib/types/host/native-activity.d.ts +26 -0
  57. package/lib/types/host/native-approvals.d.ts +29 -0
  58. package/lib/types/host/native-auxiliary.d.ts +17 -0
  59. package/lib/types/host/native-budget.d.ts +33 -0
  60. package/lib/types/host/native-checks.d.ts +104 -0
  61. package/lib/types/host/native-compaction.d.ts +8 -0
  62. package/lib/types/host/native-context.d.ts +36 -0
  63. package/lib/types/host/native-effects.d.ts +75 -0
  64. package/lib/types/host/native-evidence.d.ts +4 -0
  65. package/lib/types/host/native-exploration.d.ts +15 -0
  66. package/lib/types/host/native-keepalive.d.ts +82 -0
  67. package/lib/types/host/native-model-control.d.ts +99 -0
  68. package/lib/types/host/native-on-demand-context.d.ts +39 -0
  69. package/lib/types/host/native-request.d.ts +5 -0
  70. package/lib/types/host/native-review.d.ts +11 -0
  71. package/lib/types/host/native-role-sandbox.d.ts +49 -0
  72. package/lib/types/host/native-scopes.d.ts +49 -0
  73. package/lib/types/host/native-selection.d.ts +29 -0
  74. package/lib/types/host/native-task-context.d.ts +39 -0
  75. package/lib/types/host/native-usage.d.ts +46 -0
  76. package/lib/types/host/native-worker-brief.d.ts +74 -0
  77. package/lib/types/host/native-worker-notices.d.ts +15 -0
  78. package/lib/types/host/native-worker.d.ts +44 -0
  79. package/lib/types/host/native-workflow.d.ts +41 -0
  80. package/lib/types/host/route-transport.d.ts +22 -0
  81. package/lib/types/host/settings.d.ts +39 -0
  82. package/lib/types/host/shell.d.ts +11 -0
  83. package/lib/types/host/status.d.ts +7 -0
  84. package/lib/types/host/subagent-seam.d.ts +10 -0
  85. package/lib/types/host/tool-table.d.ts +35 -0
  86. package/lib/types/host/worker-terminal.d.ts +68 -0
  87. package/lib/types/host/workspace-guard.d.ts +13 -0
  88. package/lib/types/host/workspace.d.ts +52 -0
  89. package/lib/types/identity.d.ts +64 -0
  90. package/lib/types/index.d.ts +34 -0
  91. package/lib/types/message-source.d.ts +29 -0
  92. package/lib/types/profile/compactor.d.ts +5 -0
  93. package/lib/types/profile/keepalive.d.ts +3 -0
  94. package/lib/types/profile/resolve.d.ts +17 -0
  95. package/lib/types/prompts.d.ts +8 -0
  96. package/lib/types/recovery/reconcile.d.ts +28 -0
  97. package/lib/types/review/dispatch.d.ts +20 -0
  98. package/lib/types/review/protocol.d.ts +51 -0
  99. package/lib/types/review/ticket.d.ts +32 -0
  100. package/lib/types/status.d.ts +88 -0
  101. package/lib/types/store-migration.d.ts +29 -0
  102. package/lib/types/task/deletion.d.ts +64 -0
  103. package/lib/types/task/reducer.d.ts +6 -0
  104. package/lib/types/task/sqlite-store.d.ts +71 -0
  105. package/lib/types/task/state.d.ts +51 -0
  106. package/lib/types/task/status-index.d.ts +28 -0
  107. package/lib/types/task/store.d.ts +48 -0
  108. package/lib/types/usage/budget.d.ts +27 -0
  109. package/lib/types/usage/ledger.d.ts +66 -0
  110. package/lib/types/usage/normalize.d.ts +50 -0
  111. package/lib/types/usage/pricing.d.ts +18 -0
  112. package/package.json +178 -4
  113. package/profiles/fusion-auto.example.json +45 -0
  114. package/prompts/compact.md +7 -0
  115. package/prompts/lead.md +42 -0
  116. package/prompts/model-lead.md +13 -0
  117. package/prompts/model-worker.md +5 -0
  118. package/prompts/worker.md +21 -0
  119. package/src/activity.ts +23 -0
  120. package/src/approval/logical.ts +44 -0
  121. package/src/benchmark/naive.ts +216 -0
  122. package/src/benchmark/native-run.ts +215 -0
  123. package/src/benchmark/output-limits.ts +33 -0
  124. package/src/bilingual.ts +2 -0
  125. package/src/client/ActivityRows.tsx +59 -0
  126. package/src/client/FusionCache.tsx +130 -0
  127. package/src/client/FusionHint.tsx +39 -0
  128. package/src/client/FusionHistory.tsx +247 -0
  129. package/src/client/FusionPanelIcon.tsx +39 -0
  130. package/src/client/FusionRecovery.tsx +69 -0
  131. package/src/client/FusionSettings.tsx +205 -0
  132. package/src/client/FusionStatus.tsx +103 -0
  133. package/src/client/FusionTool.tsx +66 -0
  134. package/src/client/activity-model.ts +31 -0
  135. package/src/client/i18n.ts +30 -0
  136. package/src/client/index.ts +50 -0
  137. package/src/client/settings-events.ts +3 -0
  138. package/src/client/status-polling.ts +88 -0
  139. package/src/context/guard.ts +81 -0
  140. package/src/context/handoff.ts +28 -0
  141. package/src/contracts.ts +449 -0
  142. package/src/digest.ts +28 -0
  143. package/src/dsh-fusion.ts +321 -0
  144. package/src/errors.ts +34 -0
  145. package/src/evidence/gate.ts +290 -0
  146. package/src/evidence/receipts.ts +175 -0
  147. package/src/execution/write-lease.ts +187 -0
  148. package/src/host/bindings.ts +91 -0
  149. package/src/host/cache-defaults.ts +103 -0
  150. package/src/host/cache-policy.ts +168 -0
  151. package/src/host/change-evidence.ts +156 -0
  152. package/src/host/compaction-routing.ts +46 -0
  153. package/src/host/coordinator.ts +2160 -0
  154. package/src/host/evidence-page.ts +25 -0
  155. package/src/host/history.ts +114 -0
  156. package/src/host/http.ts +30 -0
  157. package/src/host/model-output.ts +22 -0
  158. package/src/host/native-activity.ts +199 -0
  159. package/src/host/native-approvals.ts +196 -0
  160. package/src/host/native-auxiliary.ts +19 -0
  161. package/src/host/native-budget.ts +112 -0
  162. package/src/host/native-checks.ts +474 -0
  163. package/src/host/native-compaction.ts +34 -0
  164. package/src/host/native-context.ts +204 -0
  165. package/src/host/native-effects.ts +260 -0
  166. package/src/host/native-evidence.ts +31 -0
  167. package/src/host/native-exploration.ts +114 -0
  168. package/src/host/native-keepalive.ts +292 -0
  169. package/src/host/native-model-control.ts +298 -0
  170. package/src/host/native-on-demand-context.ts +74 -0
  171. package/src/host/native-request.ts +12 -0
  172. package/src/host/native-review.ts +51 -0
  173. package/src/host/native-role-sandbox.ts +171 -0
  174. package/src/host/native-scopes.ts +203 -0
  175. package/src/host/native-selection.ts +110 -0
  176. package/src/host/native-task-context.ts +264 -0
  177. package/src/host/native-usage.ts +121 -0
  178. package/src/host/native-worker-brief.ts +122 -0
  179. package/src/host/native-worker-notices.ts +72 -0
  180. package/src/host/native-worker.ts +215 -0
  181. package/src/host/native-workflow.ts +103 -0
  182. package/src/host/route-transport.ts +54 -0
  183. package/src/host/settings.ts +93 -0
  184. package/src/host/shell.ts +13 -0
  185. package/src/host/status.ts +112 -0
  186. package/src/host/subagent-seam.ts +20 -0
  187. package/src/host/tool-table.ts +56 -0
  188. package/src/host/worker-terminal.ts +91 -0
  189. package/src/host/workspace-guard.ts +17 -0
  190. package/src/host/workspace.ts +184 -0
  191. package/src/identity.ts +76 -0
  192. package/src/index.ts +50 -0
  193. package/src/message-source.ts +31 -0
  194. package/src/profile/compactor.ts +26 -0
  195. package/src/profile/keepalive.ts +13 -0
  196. package/src/profile/resolve.ts +131 -0
  197. package/src/prompts.ts +32 -0
  198. package/src/recovery/reconcile.ts +38 -0
  199. package/src/review/dispatch.ts +48 -0
  200. package/src/review/protocol.ts +212 -0
  201. package/src/review/ticket.ts +166 -0
  202. package/src/status.ts +55 -0
  203. package/src/store-migration.ts +65 -0
  204. package/src/task/deletion.ts +105 -0
  205. package/src/task/reducer.ts +679 -0
  206. package/src/task/sqlite-store.ts +386 -0
  207. package/src/task/state.ts +72 -0
  208. package/src/task/status-index.ts +198 -0
  209. package/src/task/store.ts +287 -0
  210. package/src/usage/budget.ts +88 -0
  211. package/src/usage/ledger.ts +218 -0
  212. package/src/usage/normalize.ts +308 -0
  213. package/src/usage/pricing.ts +95 -0
@@ -0,0 +1,81 @@
1
+ import type { RoleContextPolicy, TokenMeasurement } from '../contracts.js'
2
+ import { digestOf } from '../digest.js'
3
+ import { FUSION_CONTEXT_BUDGET, FusionError } from '../errors.js'
4
+
5
+ export interface RequestParts {
6
+ systemTokens: number
7
+ messageTokens: number
8
+ toolSchemaTokens: number
9
+ cacheReadTokens: number
10
+ reservedOutputTokens: number
11
+ contextWindow: number
12
+ quality: TokenMeasurement['quality']
13
+ }
14
+
15
+ export function safetyTokens(contextWindow: number, policy: RoleContextPolicy, quality: TokenMeasurement['quality']): number {
16
+ const fromFraction = Math.ceil(contextWindow * policy.safetyFraction)
17
+ const base = Math.max(policy.minSafetyTokens, fromFraction)
18
+ if (quality === 'heuristic') return Math.max(base, Math.ceil(base * 1.25))
19
+ return base
20
+ }
21
+
22
+ /** Output reservation that fits the disclosed window. Leaves half the usable window for input. */
23
+ export function fittedOutputReservation(contextWindow: number): number {
24
+ if (!Number.isSafeInteger(contextWindow) || contextWindow < 1) throw new Error('The configured model does not disclose a usable context window')
25
+ const safety = Math.max(2_048, Math.ceil(contextWindow * 0.05))
26
+ const usable = contextWindow - safety
27
+ if (usable < 2) throw new Error('The configured model context window cannot reserve output')
28
+ return Math.floor(usable / 2)
29
+ }
30
+
31
+ export function requestOutputReservation(contextWindow: number, requested?: number, adapterDefault?: number,
32
+ testedCap?: number, fallback = 8_000): number {
33
+ const fitted = fittedOutputReservation(contextWindow)
34
+ for (const limit of [requested, adapterDefault, testedCap, fallback]) {
35
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) throw new Error('Invalid model output token limit')
36
+ }
37
+ // The context window alone does not disclose a provider's output allowance.
38
+ // RC.2 exposes configured defaults, not a universal maximum-output field.
39
+ // With no disclosed/tested cap, use a bounded policy default, never half of
40
+ // a million-token input window as a fabricated provider output allowance.
41
+ return Math.min(requested ?? adapterDefault ?? testedCap ?? fallback,
42
+ adapterDefault ?? Infinity, testedCap ?? Infinity, fitted)
43
+ }
44
+
45
+ export function inputBudget(policy: RoleContextPolicy, contextWindow: number, maxOutput: number, quality: TokenMeasurement['quality'] = 'exact'): number {
46
+ const safety = safetyTokens(contextWindow, policy, quality)
47
+ const hard = contextWindow - maxOutput - safety
48
+ return Math.max(0, Math.min(policy.targetInputTokens, hard))
49
+ }
50
+
51
+ export function measureRequest(parts: RequestParts, policy: RoleContextPolicy): TokenMeasurement {
52
+ const inputTokens = parts.systemTokens + parts.messageTokens + parts.toolSchemaTokens + parts.cacheReadTokens
53
+ return {
54
+ inputTokens,
55
+ reservedOutputTokens: parts.reservedOutputTokens,
56
+ contextWindow: parts.contextWindow,
57
+ safetyTokens: safetyTokens(parts.contextWindow, policy, parts.quality),
58
+ quality: parts.quality,
59
+ requestDigest: digestOf(parts),
60
+ }
61
+ }
62
+
63
+ export function assertFinalBudget(measurement: TokenMeasurement, policy: RoleContextPolicy): void {
64
+ const budget = inputBudget(policy, measurement.contextWindow, measurement.reservedOutputTokens, measurement.quality)
65
+ if (measurement.inputTokens > budget) {
66
+ throw new FusionError(
67
+ FUSION_CONTEXT_BUDGET,
68
+ `final request ${measurement.inputTokens} exceeds budget ${budget} (${measurement.quality})`,
69
+ )
70
+ }
71
+ }
72
+
73
+ export interface RecoveryAttempt {
74
+ readonly projectionTokens: number
75
+ }
76
+
77
+ export function boundedOverflowRecovery(attempts: readonly RecoveryAttempt[]): 'continue' | 'needs-decision' {
78
+ if (attempts.length > 2) return 'needs-decision'
79
+ if (attempts.length === 2 && attempts[1]!.projectionTokens >= attempts[0]!.projectionTokens) return 'needs-decision'
80
+ return 'continue'
81
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Upper bound on recent conversation text carried into a Sidekick brief after compaction: about 10k tokens,
3
+ * enough for the latest exchanges while leaving most of the Sidekick's context for the task itself.
4
+ */
5
+ export const HANDOFF_CONTENT_BYTES = 40_000
6
+
7
+ /** Content bytes only: envelopes and independently retained task facts are extra. */
8
+ export function recentHandoffSuffix<T extends { readonly content: string }>(records: readonly T[]) {
9
+ const lengths = records.map(record => Buffer.byteLength(record.content, 'utf8'))
10
+ let first = records.length, remaining = HANDOFF_CONTENT_BYTES
11
+ for (let index = records.length - 1; index >= 0; index--) {
12
+ const bytes = lengths[index]!
13
+ // Keep the newest whole record, even when it alone exceeds the allowance.
14
+ // Stop at the first older non-fitting record; do not skip it to fill gaps.
15
+ if (index !== records.length - 1 && bytes > remaining) break
16
+ first = index
17
+ remaining = Math.max(0, remaining - bytes)
18
+ }
19
+ return {
20
+ policy: 'recent-whole-records-utf8-v1' as const,
21
+ contentByteBudget: HANDOFF_CONTENT_BYTES,
22
+ totalRecords: records.length,
23
+ omittedRecords: first,
24
+ totalContentBytes: lengths.reduce((sum, bytes) => sum + bytes, 0),
25
+ retainedContentBytes: lengths.slice(first).reduce((sum, bytes) => sum + bytes, 0),
26
+ records: records.slice(first),
27
+ }
28
+ }
@@ -0,0 +1,449 @@
1
+ import type {} from './message-source.js'
2
+ /**
3
+ * Plugin-domain contracts for dsh-fusion.
4
+ * These are proposed types, not existing DSH API declarations.
5
+ * Convert DSH branded IDs only in a version-pinned bridge (not landed).
6
+ */
7
+ export type Brand<T, B extends string> = T & { readonly __brand: B }
8
+ export type TaskId = Brand<string, 'FusionTaskId'>
9
+ export type SessionId = Brand<string, 'DshSessionId'>
10
+ export type WorkOrderId = Brand<string, 'FusionWorkOrderId'>
11
+ export type OperationId = Brand<string, 'FusionOperationId'>
12
+ export type ArtifactId = Brand<string, 'FusionArtifactId'>
13
+ export type SnapshotId = Brand<string, 'FusionSnapshotId'>
14
+ export type Digest = Brand<string, 'Sha256Hex'>
15
+ export type UsdDecimal = Brand<string, 'UsdDecimal'>
16
+ export type EpochId = Brand<string, 'ContextEpochId'>
17
+
18
+ function asBrand<B extends string>(raw: string): Brand<string, B> {
19
+ if (!raw || raw.includes('\0')) throw new TypeError('empty branded id')
20
+ return raw as Brand<string, B>
21
+ }
22
+
23
+ export const TaskId = (raw: string): TaskId => asBrand(raw)
24
+ export const SessionId = (raw: string): SessionId => asBrand(raw)
25
+ export const WorkOrderId = (raw: string): WorkOrderId => asBrand(raw)
26
+ export const OperationId = (raw: string): OperationId => asBrand(raw)
27
+ export const ArtifactId = (raw: string): ArtifactId => asBrand(raw)
28
+ export const SnapshotId = (raw: string): SnapshotId => asBrand(raw)
29
+ export const EpochId = (raw: string): EpochId => asBrand(raw)
30
+ export const UsdDecimal = (raw: string): UsdDecimal => raw as UsdDecimal
31
+
32
+ export interface PhysicalRoute {
33
+ provider: string
34
+ model: string
35
+ reasoningEffort?: string
36
+ }
37
+
38
+ export type RunSelection =
39
+ | { kind: 'model'; route: PhysicalRoute }
40
+ | { kind: 'profile'; profileId: string; requestedVersion?: string }
41
+
42
+ export type Role = 'lead' | 'worker'
43
+ export type Phase =
44
+ | 'READY'
45
+ | 'PLANNING'
46
+ | 'DIRECT'
47
+ | 'WORKER_RUNNING'
48
+ | 'REVIEWING'
49
+ | 'REWORK'
50
+ | 'WAITING_APPROVAL'
51
+ | 'WAITING_USER'
52
+ | 'WAITING_BUDGET'
53
+ | 'RECOVERING'
54
+ | 'PAUSED'
55
+ | 'STOPPING'
56
+ | 'NEEDS_DECISION'
57
+ | 'FAILED'
58
+ | 'COMPLETED'
59
+ | 'CANCELLED'
60
+
61
+ export type VerificationLevel = 'unverified' | 'partial' | 'verified'
62
+ export type ExecutionIntent = 'DIRECT' | 'DELEGATE'
63
+
64
+ export interface SourceRef {
65
+ sessionId: SessionId
66
+ eventSeq: number
67
+ messageId?: string
68
+ }
69
+
70
+ export interface ArtifactRef {
71
+ id: ArtifactId
72
+ digest: Digest
73
+ ownerTaskId: TaskId
74
+ mediaType: string
75
+ bytes: number
76
+ }
77
+
78
+ export interface Constraint {
79
+ id: string
80
+ text: string
81
+ source: SourceRef
82
+ provenance: 'user' | 'policy' | 'inferred'
83
+ mandatory: boolean
84
+ }
85
+
86
+ export interface AcceptanceCriterion {
87
+ id: string
88
+ description: string
89
+ verificationKind: 'test' | 'static-check' | 'human' | 'review'
90
+ mandatory: boolean
91
+ /** Frozen verifier id. When set, only that registered verifier may satisfy the criterion. */
92
+ verifierId?: string
93
+ /** Frozen CheckPlan id. Test/static criteria require a registered plan. */
94
+ planId?: string
95
+ planDigest?: Digest
96
+ }
97
+
98
+ export interface ResourcePolicy {
99
+ maxWorkerSteps: number
100
+ maxReworkRounds: number
101
+ maxCapabilityUpgrades: number
102
+ modelSpendAuthorizationId?: string
103
+ maxApiCostUsd?: UsdDecimal
104
+ /** Legacy serialized planning field, not enforced by the native coordinator.
105
+ * Native runs use the separately approved request/reservation ledger and omit
106
+ * this field; old work orders retain it as historical data, not a hard cap. */
107
+ maxTotalOutputTokens?: number
108
+ commandMaxSeconds: number
109
+ }
110
+
111
+ export interface WorkOrder {
112
+ schemaVersion: 1
113
+ /** Older work orders are implementation work. Exploration never grants a writer. */
114
+ mode?: 'explore' | 'implement' | 'text'
115
+ taskId: TaskId
116
+ id: WorkOrderId
117
+ operationId: OperationId
118
+ revision: number
119
+ goal: string
120
+ constraints: readonly Constraint[]
121
+ acceptance: readonly AcceptanceCriterion[]
122
+ allowedPaths: readonly string[]
123
+ forbiddenActions: readonly string[]
124
+ baseSnapshot: SnapshotId
125
+ evidence: readonly ArtifactRef[]
126
+ decisions: readonly { text: string; source: SourceRef }[]
127
+ uncertainties: readonly string[]
128
+ policy: ResourcePolicy
129
+ }
130
+
131
+ export interface ExplorationSource {
132
+ path: string
133
+ startLine: number
134
+ endLine: number
135
+ }
136
+
137
+ /** File excerpts are captured by the Host, not accepted from model narration. */
138
+ export interface ExplorationReport {
139
+ schemaVersion: 1
140
+ workOrderId: WorkOrderId
141
+ revision: number
142
+ status: WorkerReport['status']
143
+ summary: string
144
+ unresolved: readonly string[]
145
+ snapshot: SnapshotId
146
+ sources: readonly (ExplorationSource & { fileDigest: string; excerpt: ArtifactRef })[]
147
+ }
148
+
149
+ export interface CoverageClaim {
150
+ criterionId: string
151
+ state: 'satisfied' | 'not-satisfied' | 'not-verified'
152
+ evidenceIds: readonly ArtifactId[]
153
+ explanation: string
154
+ }
155
+
156
+ export interface WorkerReport {
157
+ schemaVersion: 1
158
+ workOrderId: WorkOrderId
159
+ revision: number
160
+ status: 'completed' | 'blocked' | 'needs-decision'
161
+ summary: string
162
+ snapshot: SnapshotId
163
+ changeManifest: ArtifactRef
164
+ coverage: readonly CoverageClaim[]
165
+ verification: readonly ArtifactRef[]
166
+ unresolved: readonly string[]
167
+ questions: readonly string[]
168
+ }
169
+
170
+ export type EvidenceKind = 'tool' | 'human-decision' | 'review-accept'
171
+
172
+ export interface ToolEvidence {
173
+ schemaVersion: 1
174
+ taskId: TaskId
175
+ operationId: OperationId
176
+ actor: SessionId
177
+ nativeToolCallId: string
178
+ argvDigest: Digest
179
+ cwdDigest: Digest
180
+ inputSnapshot: SnapshotId
181
+ outputSnapshot?: SnapshotId
182
+ exitCode: number | null
183
+ startedAt: string
184
+ endedAt: string | null
185
+ stdout: ArtifactRef
186
+ stderr: ArtifactRef
187
+ state: 'started' | 'completed' | 'cancelled' | 'outcome-unknown'
188
+ /** Default `tool`. Human/review criteria require their own kinds. */
189
+ kind?: EvidenceKind
190
+ }
191
+
192
+ export interface ContextCheckpoint {
193
+ schemaVersion: 1
194
+ epochId: EpochId
195
+ taskId: TaskId
196
+ revision: number
197
+ goal: string
198
+ mandatoryConstraints: readonly Constraint[]
199
+ snapshot: SnapshotId
200
+ decisions: readonly { text: string; source: SourceRef }[]
201
+ coverage: readonly CoverageClaim[]
202
+ pendingApprovalIds: readonly string[]
203
+ openQuestions: readonly string[]
204
+ evidence: readonly ArtifactRef[]
205
+ sourceSurfaceSeqs: readonly number[]
206
+ digest: Digest
207
+ }
208
+
209
+ export interface RoleContextPolicy {
210
+ targetInputTokens: number
211
+ reserveOutputTokens: number
212
+ minSafetyTokens: number
213
+ safetyFraction: number
214
+ compactToFraction: number
215
+ evidenceReadTokens: number
216
+ maxToolVisibleTokens: number
217
+ }
218
+
219
+ export interface PairProfile {
220
+ schemaVersion: 1
221
+ id: string
222
+ version: string
223
+ digest: Digest
224
+ enabled: boolean
225
+ quality: 'experimental' | 'validated' | 'deprecated'
226
+ /** Missing on frozen v1 sessions; only new selections use native model behavior. */
227
+ interactionMode?: 'model-like'
228
+ /** Frozen per selection. Absence preserves pre-enforcement sessions. */
229
+ workflowPolicy?: 'enforced-v1' | 'enforced-v2' | 'enforced-v3'
230
+ lead: PhysicalRoute
231
+ worker: PhysicalRoute
232
+ workerUpgradePath: readonly PhysicalRoute[]
233
+ promptDigests: Readonly<Record<'lead' | 'worker' | 'compact', Digest>>
234
+ context: Readonly<Record<Role, RoleContextPolicy>>
235
+ /** Explicit per-route opt-in; absence preserves old frozen profile digests. */
236
+ /** Per role: on, off, or 'auto' (armed once the route reports cache reads). Unset: Lead auto, Worker off. */
237
+ cacheKeepalive?: Readonly<Record<Role, boolean | 'auto'>>
238
+ /** Optional explicit summary route; absence preserves the role model and old digests. */
239
+ compactor?: { readonly route: PhysicalRoute; readonly maxOutputTokens: number }
240
+ dataPolicyId: string
241
+ evidenceCampaignIds: readonly string[]
242
+ }
243
+
244
+ export interface LogicalApproval {
245
+ id: string
246
+ taskId: TaskId
247
+ revision: number
248
+ operationId: OperationId
249
+ nativeRequestId?: string
250
+ requestingAgent?: SessionId
251
+ toolName?: string
252
+ callId?: string
253
+ role?: Role
254
+ reason?: string
255
+ argsDigest: Digest
256
+ snapshot: SnapshotId
257
+ permissionPolicyDigest: Digest
258
+ state: 'pending' | 'approved' | 'rejected' | 'withdrawn' | 'consumed'
259
+ decisionBy?: 'human' | 'preauthorized-policy'
260
+ }
261
+
262
+ export interface WriteLease {
263
+ workspaceId: string
264
+ holder: SessionId
265
+ taskId: TaskId
266
+ generation: number
267
+ activeOperationIds: readonly OperationId[]
268
+ }
269
+
270
+ export interface TokenMeasurement {
271
+ inputTokens: number
272
+ reservedOutputTokens: number
273
+ contextWindow: number
274
+ safetyTokens: number
275
+ quality: 'exact' | 'provider-estimated' | 'heuristic'
276
+ requestDigest: Digest
277
+ }
278
+
279
+ export type UsageApplicability = 'not_applicable' | 'unknown' | 'known'
280
+
281
+ export interface NormalizedUsage {
282
+ uncachedInput: number | null
283
+ cacheRead: number | null
284
+ cacheReadStatus: UsageApplicability
285
+ cacheWrite: Readonly<Record<string, number | null>>
286
+ cacheWriteStatus: Readonly<Record<string, UsageApplicability>>
287
+ output: number | null
288
+ /** Informational subset already inside `output`. Do not charge again. */
289
+ reasoningOutputSubset: number | null
290
+ /** Separately billed reasoning. Charge at the reasoning or output rate. */
291
+ reasoningSeparate: number | null
292
+ /** Reasoning tokens whose include-in-output contract is unknown. Never assume free or included. */
293
+ reasoningUnspecified: number | null
294
+ /** Explicit billing relation. Never infer not_applicable from a null quantity. */
295
+ reasoningBilling: ReasoningBilling
296
+ /** Null when the provider contract was not stated. */
297
+ inputIncludesCacheRead: boolean | null
298
+ }
299
+
300
+ export interface ReasoningBilling {
301
+ kind: 'included' | 'separate' | 'unknown' | 'not_applicable'
302
+ tokens: number | null
303
+ }
304
+
305
+ export interface UsageAttempt {
306
+ attemptId: string
307
+ taskId: TaskId
308
+ agentId: SessionId
309
+ role: Role
310
+ purpose: 'conversation' | 'compaction' | 'review' | 'routing' | 'other'
311
+ route: PhysicalRoute
312
+ epochId: EpochId
313
+ serviceTier?: string
314
+ rawUsage?: ArtifactRef
315
+ normalized: NormalizedUsage
316
+ priceCardDigest?: Digest
317
+ apiEquivalentUsd: UsdDecimal | null
318
+ actualBilledUsd: UsdDecimal | null
319
+ status: 'pending' | 'complete' | 'partial' | 'unknown'
320
+ }
321
+
322
+ export interface EventEnvelope<T extends string, P> {
323
+ schemaVersion: 1
324
+ id: string
325
+ taskId: TaskId
326
+ seq: number
327
+ revision: number
328
+ type: T
329
+ createdAt: string
330
+ causeId: string
331
+ payload: P
332
+ }
333
+
334
+ export interface ReviewGate {
335
+ taskId: TaskId
336
+ workOrderId: WorkOrderId
337
+ revision: number
338
+ reportDigest: Digest
339
+ snapshot: SnapshotId
340
+ reviewAttemptId: string
341
+ decision: 'accept' | 'rework' | 'needs-decision' | 'REVIEW_INCOMPLETE'
342
+ terminalStatus: 'accepting' | 'incomplete' | 'rework' | 'needs-decision' | 'legacy-unbound'
343
+ }
344
+
345
+ export interface ReportSubject {
346
+ taskId: TaskId
347
+ workOrderId: WorkOrderId
348
+ revision: number
349
+ reportId: string
350
+ reportDigest: Digest
351
+ snapshot: SnapshotId
352
+ }
353
+
354
+ export interface ReviewReadinessReceipt {
355
+ subject: ReportSubject
356
+ receiptId: string
357
+ receiptDigest: Digest
358
+ verdict: 'review-ready'
359
+ }
360
+
361
+ export interface ReviewTicket {
362
+ id: string
363
+ requestId: string
364
+ generation: number
365
+ subject: ReportSubject
366
+ validationDigest: Digest
367
+ /** Digest of the work order frozen at request time. */
368
+ workOrderDigest: Digest
369
+ }
370
+
371
+ export interface ReviewResultV2 {
372
+ ticketId: string
373
+ requestId: string
374
+ generation: number
375
+ subject: ReportSubject
376
+ terminalEvidenceRef: string
377
+ decision: 'accept' | 'rework' | 'needs-decision' | 'REVIEW_INCOMPLETE'
378
+ protocolValid: boolean
379
+ }
380
+
381
+ export interface ControlState {
382
+ mode: 'running' | 'paused' | 'stop-requested' | 'cancelled' | 'completed'
383
+ pendingApprovalIds: readonly string[]
384
+ budgetBlocked: boolean
385
+ recovering: boolean
386
+ outcomeUnknown: boolean
387
+ }
388
+
389
+ export interface ReviewCompletedPayload {
390
+ decision: 'accept' | 'rework' | 'needs-decision' | 'REVIEW_INCOMPLETE'
391
+ /** Required for a new completion. Decision-only payloads are legacy-unbound. */
392
+ binding?: ReviewResultV2
393
+ }
394
+
395
+ export type FusionEvent =
396
+ | EventEnvelope<'task/created', { parent: SessionId; selection: RunSelection }>
397
+ | EventEnvelope<'requirements/revised', { reason: string }>
398
+ | EventEnvelope<'profile/frozen', { digest: Digest; profileId: string; version: string }>
399
+ | EventEnvelope<'intent/chosen', { intent: ExecutionIntent }>
400
+ | EventEnvelope<'work-order/prepared', { order: WorkOrder; reservedChild: SessionId }>
401
+ | EventEnvelope<'work-order/acceptance-amended', { workOrderId: WorkOrderId; acceptance: readonly AcceptanceCriterion[]; reason: string }>
402
+ | EventEnvelope<'work-order/scope-expanded', { workOrderId: WorkOrderId; allowedPaths: readonly string[]; added: readonly string[]; reason: string }>
403
+ | EventEnvelope<'child/accepted', { operationId: OperationId; child: SessionId; messageId: string }>
404
+ | EventEnvelope<'child/claimed', { operationId: OperationId; child: SessionId }>
405
+ | EventEnvelope<'exploration/recorded', { report: ExplorationReport }>
406
+ | EventEnvelope<'report/submitted', { operationId: OperationId; report: WorkerReport }>
407
+ | EventEnvelope<'report/validated', { operationId: OperationId; report: WorkerReport }>
408
+ | EventEnvelope<'review/requested', { ticket: ReviewTicket }>
409
+ | EventEnvelope<'review/completed', ReviewCompletedPayload>
410
+ | EventEnvelope<'lease/acquired', { lease: WriteLease }>
411
+ | EventEnvelope<'lease/released', { workspaceId: string; generation: number }>
412
+ | EventEnvelope<'approval/pending', { approval: LogicalApproval }>
413
+ | EventEnvelope<'approval/answered', { approvalId: string; state: 'approved' | 'rejected' | 'withdrawn' }>
414
+ | EventEnvelope<'budget/blocked', { reason: string }>
415
+ | EventEnvelope<'budget/unblocked', { authorizationId: string }>
416
+ | EventEnvelope<'checkpoint/committed', { checkpoint: ContextCheckpoint }>
417
+ | EventEnvelope<'recovery/needed', { reason: string }>
418
+ | EventEnvelope<'recovery/reconciled', { snapshot: SnapshotId; evidenceRef: string }>
419
+ | EventEnvelope<'effect/outcome-unknown', { operationId: OperationId; reason: string }>
420
+ | EventEnvelope<'effects/reconciled', { snapshot: SnapshotId; evidenceRef: string; quiescent: true }>
421
+ | EventEnvelope<'task/stop-requested', { intent: 'pause' | 'cancel' }>
422
+ | EventEnvelope<'task/paused', { reason: string }>
423
+ | EventEnvelope<'task/resumed', { reason: string }>
424
+ | EventEnvelope<'task/completed', { snapshot: SnapshotId; verification: VerificationLevel }>
425
+ | EventEnvelope<'task/cancelled', { reason: string }>
426
+
427
+ export type OutboxState =
428
+ | 'prepared'
429
+ | 'dispatched'
430
+ | 'accepted'
431
+ | 'claimed'
432
+ | 'result-recorded'
433
+ | 'delivered'
434
+ | 'acknowledged'
435
+
436
+ export interface OutboxRow {
437
+ operationId: OperationId
438
+ taskId: TaskId
439
+ kind: string
440
+ state: OutboxState
441
+ reservedChild?: SessionId
442
+ nativeMessageId?: string
443
+ resultDigest?: Digest
444
+ payloadRef?: string
445
+ requestId?: string
446
+ ticket?: ReviewTicket
447
+ }
448
+
449
+ export const TERMINAL_PHASES = new Set<Phase>(['COMPLETED', 'CANCELLED', 'FAILED'])
package/src/digest.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'node:crypto'
2
+ import type { Digest } from './contracts.js'
3
+
4
+ export function sha256Hex(data: string | Uint8Array): Digest {
5
+ return createHash('sha256').update(data).digest('hex') as Digest
6
+ }
7
+
8
+ export function sortKeys(value: unknown): unknown {
9
+ if (Array.isArray(value)) return value.map(sortKeys)
10
+ if (value && typeof value === 'object') {
11
+ const record = value as Record<string, unknown>
12
+ return Object.fromEntries(
13
+ Object.keys(record)
14
+ .filter(key => !key.startsWith('_'))
15
+ .sort()
16
+ .map(key => [key, sortKeys(record[key])]),
17
+ )
18
+ }
19
+ return value
20
+ }
21
+
22
+ export function canonicalJson(value: unknown): string {
23
+ return JSON.stringify(sortKeys(value))
24
+ }
25
+
26
+ export function digestOf(value: unknown): Digest {
27
+ return sha256Hex(canonicalJson(value))
28
+ }