@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,1797 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type {
3
+ AbortTurnInput,
4
+ CompactSessionInput,
5
+ CompactionResult,
6
+ ListSessionsInput,
7
+ ListSessionsResult,
8
+ MemorySnapshot,
9
+ MemoryStore,
10
+ NimModelRef,
11
+ NimProvider,
12
+ NimRuntime,
13
+ NimTelemetrySink,
14
+ NimToolDefinition,
15
+ NimToolPolicy,
16
+ NimUiEvent,
17
+ QueueTurnInput,
18
+ QueueTurnResult,
19
+ ReplayEventsInput,
20
+ ReplayEventsResult,
21
+ ResumeSessionInput,
22
+ SendTurnInput,
23
+ SessionHandle,
24
+ SkillSource,
25
+ SkillsSnapshot,
26
+ SoulSnapshot,
27
+ SoulSource,
28
+ StartSessionInput,
29
+ SteerTurnInput,
30
+ SteerTurnResult,
31
+ StreamEventsInput,
32
+ StreamUiInput,
33
+ SwitchModelInput,
34
+ TurnHandle,
35
+ TurnResult,
36
+ } from './contracts.ts';
37
+ import type { NimEventEnvelope } from './events.ts';
38
+ import {
39
+ NimProviderRouter,
40
+ type NimProviderDriver,
41
+ type NimProviderTurnEvent,
42
+ } from './provider-router.ts';
43
+ import { projectEventToUiEvents } from '../../nim-ui-core/src/projection.ts';
44
+ import { InMemoryNimEventStore, type NimEventStore } from './event-store.ts';
45
+ import {
46
+ InMemoryNimSessionStore,
47
+ type NimPersistedSession,
48
+ type NimSessionStore,
49
+ } from './session-store.ts';
50
+
51
+ type SessionState = {
52
+ readonly sessionId: string;
53
+ readonly tenantId: string;
54
+ readonly userId: string;
55
+ model: NimModelRef;
56
+ lane: string;
57
+ soulHash?: string;
58
+ skillsSnapshotVersion?: number;
59
+ eventSeq: number;
60
+ lastRunId?: string;
61
+ activeRunId?: string;
62
+ queuedTurns: QueueItem[];
63
+ idempotencyToRunId: Map<string, string>;
64
+ };
65
+
66
+ type QueueItem = {
67
+ readonly queueId: string;
68
+ readonly text: string;
69
+ readonly priority: 'normal' | 'high';
70
+ readonly dedupeKey: string;
71
+ };
72
+
73
+ type AbortReason = 'manual' | 'timeout' | 'policy' | 'signal';
74
+ type ToolBlockReason = 'policy-deny' | 'policy-allow-miss' | 'tool-unavailable';
75
+
76
+ type RunState = {
77
+ readonly runId: string;
78
+ readonly sessionId: string;
79
+ readonly idempotencyKey: string;
80
+ readonly input: string;
81
+ readonly traceId: string;
82
+ readonly abortController: AbortController;
83
+ readonly soulHash?: string;
84
+ readonly skillsHash?: string;
85
+ readonly skillsSnapshotVersion?: number;
86
+ readonly memoryHash?: string;
87
+ stepCounter: number;
88
+ active: boolean;
89
+ streaming: boolean;
90
+ compacting: boolean;
91
+ aborted: boolean;
92
+ abortReason?: AbortReason;
93
+ abortSignalCleanup?: () => void;
94
+ steers: string[];
95
+ assistantOutputBuffer: string;
96
+ assistantOutputDeltaCount: number;
97
+ resolveDone: (result: TurnResult) => void;
98
+ done: Promise<TurnResult>;
99
+ };
100
+
101
+ type EventSubscriber = {
102
+ readonly input: StreamEventsInput;
103
+ readonly fromEvent?: NimEventEnvelope;
104
+ readonly queue: AsyncPushQueue<NimEventEnvelope>;
105
+ };
106
+
107
+ const MAX_QUEUED_TURNS_PER_SESSION = 64;
108
+ const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
109
+
110
+ class AsyncPushQueue<T> {
111
+ private values: T[] = [];
112
+ private waiters: Array<(value: IteratorResult<T>) => void> = [];
113
+ private closed = false;
114
+
115
+ public push(value: T): void {
116
+ if (this.closed) {
117
+ return;
118
+ }
119
+ const waiter = this.waiters.shift();
120
+ if (waiter !== undefined) {
121
+ waiter({ done: false, value });
122
+ return;
123
+ }
124
+ this.values.push(value);
125
+ }
126
+
127
+ public close(): void {
128
+ this.closed = true;
129
+ while (this.waiters.length > 0) {
130
+ const waiter = this.waiters.shift();
131
+ waiter?.({ done: true, value: undefined });
132
+ }
133
+ }
134
+
135
+ public async next(): Promise<IteratorResult<T>> {
136
+ if (this.values.length > 0) {
137
+ const value = this.values.shift() as T;
138
+ return { done: false, value };
139
+ }
140
+ if (this.closed) {
141
+ return { done: true, value: undefined };
142
+ }
143
+ return await new Promise<IteratorResult<T>>((resolve) => {
144
+ this.waiters.push(resolve);
145
+ });
146
+ }
147
+ }
148
+
149
+ function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
150
+ let resolve!: (value: T) => void;
151
+ const promise = new Promise<T>((innerResolve) => {
152
+ resolve = innerResolve;
153
+ });
154
+ return { promise, resolve };
155
+ }
156
+
157
+ function sleep(ms: number): Promise<void> {
158
+ return new Promise((resolve) => setTimeout(resolve, ms));
159
+ }
160
+
161
+ export interface InMemoryNimRuntimeOptions {
162
+ providerRouter?: NimProviderRouter;
163
+ telemetrySinks?: readonly NimTelemetrySink[];
164
+ eventStore?: NimEventStore;
165
+ sessionStore?: NimSessionStore;
166
+ }
167
+
168
+ export class InMemoryNimRuntime implements NimRuntime {
169
+ private sessions = new Map<string, SessionState>();
170
+ private runs = new Map<string, RunState>();
171
+ private providerRouter: NimProviderRouter;
172
+ private tools: readonly NimToolDefinition[] = [];
173
+ private toolPolicy: NimToolPolicy = {
174
+ hash: 'policy-default',
175
+ allow: [],
176
+ deny: [],
177
+ };
178
+ private soulSources: SoulSource[] = [];
179
+ private skillSources: SkillSource[] = [];
180
+ private memoryStores: MemoryStore[] = [];
181
+ private eventStore: NimEventStore;
182
+ private sessionStore: NimSessionStore;
183
+ private subscribers = new Map<string, EventSubscriber>();
184
+ private telemetrySinks: NimTelemetrySink[] = [];
185
+ private globalLane: Promise<void> = Promise.resolve();
186
+ private sessionLanes = new Map<string, Promise<void>>();
187
+
188
+ public constructor(input?: InMemoryNimRuntimeOptions) {
189
+ this.providerRouter = input?.providerRouter ?? new NimProviderRouter();
190
+ if (input?.telemetrySinks !== undefined) {
191
+ this.telemetrySinks = [...input.telemetrySinks];
192
+ }
193
+ this.eventStore = input?.eventStore ?? new InMemoryNimEventStore();
194
+ this.sessionStore = input?.sessionStore ?? new InMemoryNimSessionStore();
195
+ }
196
+
197
+ public async startSession(input: StartSessionInput): Promise<SessionHandle> {
198
+ this.providerRouter.resolveModel(input.model);
199
+ const sessionId = randomUUID();
200
+ const lane = input.lane ?? `session:${sessionId}`;
201
+ const soulHash = this.currentSoulHash();
202
+ const skillsSnapshot = this.currentSkillsSnapshot();
203
+ const next: SessionState = {
204
+ sessionId,
205
+ tenantId: input.tenantId,
206
+ userId: input.userId,
207
+ model: input.model,
208
+ lane,
209
+ ...(soulHash !== undefined ? { soulHash } : {}),
210
+ ...(skillsSnapshot !== undefined ? { skillsSnapshotVersion: skillsSnapshot.version } : {}),
211
+ eventSeq: 0,
212
+ queuedTurns: [],
213
+ idempotencyToRunId: new Map<string, string>(),
214
+ };
215
+ this.sessions.set(sessionId, next);
216
+ this.persistSession(next);
217
+ this.appendSessionEvent(next, {
218
+ type: 'session.started',
219
+ runId: '',
220
+ turnId: '',
221
+ stepId: 'step:session-started',
222
+ source: 'system',
223
+ idempotencyKey: 'session-started',
224
+ data: {
225
+ model: input.model,
226
+ },
227
+ });
228
+ return this.toHandle(next);
229
+ }
230
+
231
+ public async resumeSession(input: ResumeSessionInput): Promise<SessionHandle> {
232
+ const state = this.requireSession(input.sessionId);
233
+ if (state.tenantId !== input.tenantId || state.userId !== input.userId) {
234
+ throw new Error('session access denied');
235
+ }
236
+ this.appendSessionEvent(state, {
237
+ type: 'session.resumed',
238
+ runId: state.lastRunId ?? '',
239
+ turnId: '',
240
+ stepId: 'step:session-resumed',
241
+ source: 'system',
242
+ idempotencyKey: 'session-resumed',
243
+ });
244
+ return this.toHandle(state);
245
+ }
246
+
247
+ public async listSessions(input: ListSessionsInput): Promise<ListSessionsResult> {
248
+ const sessions = this.sessionStore
249
+ .listSessions(input.tenantId, input.userId)
250
+ .map((session) => this.toHandleFromPersisted(session));
251
+ return {
252
+ sessions,
253
+ };
254
+ }
255
+
256
+ public registerTools(tools: readonly NimToolDefinition[]): void {
257
+ this.tools = tools.slice();
258
+ }
259
+
260
+ public setToolPolicy(policy: NimToolPolicy): void {
261
+ this.toolPolicy = policy;
262
+ }
263
+
264
+ public registerProvider(provider: NimProvider): void {
265
+ this.providerRouter.registerProvider(provider);
266
+ }
267
+
268
+ public registerTelemetrySink(sink: NimTelemetrySink): void {
269
+ this.telemetrySinks.push(sink);
270
+ }
271
+
272
+ public registerProviderDriver(driver: NimProviderDriver): void {
273
+ this.providerRouter.registerDriver(driver);
274
+ }
275
+
276
+ public async switchModel(input: SwitchModelInput): Promise<void> {
277
+ this.providerRouter.resolveModel(input.model);
278
+ const session = this.requireSession(input.sessionId);
279
+ session.model = input.model;
280
+ this.appendSessionEvent(session, {
281
+ type: 'provider.model.switch.completed',
282
+ source: 'system',
283
+ runId: session.lastRunId ?? '',
284
+ turnId: '',
285
+ stepId: 'step:model-switch',
286
+ idempotencyKey: 'model-switch',
287
+ data: {
288
+ model: input.model,
289
+ reason: input.reason,
290
+ },
291
+ });
292
+ }
293
+
294
+ public registerSoulSource(source: SoulSource): void {
295
+ this.soulSources.push(source);
296
+ const soulHash = this.currentSoulHash();
297
+ for (const session of this.sessions.values()) {
298
+ if (soulHash === undefined) {
299
+ delete session.soulHash;
300
+ } else {
301
+ session.soulHash = soulHash;
302
+ }
303
+ this.persistSession(session);
304
+ }
305
+ }
306
+
307
+ public registerSkillSource(source: SkillSource): void {
308
+ this.skillSources.push(source);
309
+ const snapshot = this.currentSkillsSnapshot();
310
+ for (const session of this.sessions.values()) {
311
+ if (snapshot === undefined) {
312
+ delete session.skillsSnapshotVersion;
313
+ } else {
314
+ session.skillsSnapshotVersion = snapshot.version;
315
+ }
316
+ this.persistSession(session);
317
+ }
318
+ }
319
+
320
+ public registerMemoryStore(store: MemoryStore): void {
321
+ this.memoryStores.push(store);
322
+ }
323
+
324
+ public async loadSoul(): Promise<SoulSnapshot> {
325
+ return {
326
+ hash: `soul:${this.soulSources.length}`,
327
+ };
328
+ }
329
+
330
+ public async loadSkills(): Promise<SkillsSnapshot> {
331
+ return {
332
+ hash: `skills:${this.skillSources.length}`,
333
+ version: this.skillSources.length,
334
+ };
335
+ }
336
+
337
+ public async loadMemory(): Promise<MemorySnapshot> {
338
+ return {
339
+ hash: `memory:${this.memoryStores.length}`,
340
+ };
341
+ }
342
+
343
+ public async sendTurn(input: SendTurnInput): Promise<TurnHandle> {
344
+ const session = this.requireSession(input.sessionId);
345
+ const currentSoulHash = this.currentSoulHash();
346
+ const currentSkillsSnapshot = this.currentSkillsSnapshot();
347
+ const currentMemoryHash = this.currentMemoryHash();
348
+ if (currentSoulHash === undefined) {
349
+ delete session.soulHash;
350
+ } else {
351
+ session.soulHash = currentSoulHash;
352
+ }
353
+ if (currentSkillsSnapshot === undefined) {
354
+ delete session.skillsSnapshotVersion;
355
+ } else {
356
+ session.skillsSnapshotVersion = currentSkillsSnapshot.version;
357
+ }
358
+
359
+ const existingRunId =
360
+ session.idempotencyToRunId.get(input.idempotencyKey) ??
361
+ this.sessionStore.getRunIdByIdempotency(session.sessionId, input.idempotencyKey);
362
+ if (existingRunId !== undefined) {
363
+ const existing = this.runs.get(existingRunId);
364
+ if (existing !== undefined) {
365
+ this.appendSessionEvent(session, {
366
+ type: 'turn.idempotency.reused',
367
+ source: 'system',
368
+ runId: existingRunId,
369
+ turnId: existingRunId,
370
+ stepId: 'step:idempotency-reused',
371
+ idempotencyKey: input.idempotencyKey,
372
+ });
373
+ return {
374
+ runId: existing.runId,
375
+ sessionId: existing.sessionId,
376
+ idempotencyKey: existing.idempotencyKey,
377
+ done: existing.done,
378
+ };
379
+ }
380
+ const resolved = this.resolveStoredTurnResult(session, existingRunId);
381
+ if (resolved === undefined) {
382
+ this.appendSessionEvent(session, {
383
+ type: 'turn.idempotency.unresolved',
384
+ source: 'system',
385
+ runId: existingRunId,
386
+ turnId: existingRunId,
387
+ stepId: 'step:idempotency-unresolved',
388
+ idempotencyKey: input.idempotencyKey,
389
+ });
390
+ throw new Error(`idempotency run is non-terminal: ${existingRunId}`);
391
+ }
392
+ session.idempotencyToRunId.set(input.idempotencyKey, existingRunId);
393
+ this.sessionStore.upsertIdempotency(session.sessionId, input.idempotencyKey, existingRunId);
394
+ this.appendSessionEvent(session, {
395
+ type: 'turn.idempotency.reused',
396
+ source: 'system',
397
+ runId: existingRunId,
398
+ turnId: existingRunId,
399
+ stepId: 'step:idempotency-reused',
400
+ idempotencyKey: input.idempotencyKey,
401
+ });
402
+ return {
403
+ runId: existingRunId,
404
+ sessionId: session.sessionId,
405
+ idempotencyKey: input.idempotencyKey,
406
+ done: Promise.resolve(resolved),
407
+ };
408
+ }
409
+
410
+ const runId = randomUUID();
411
+ const turnDeferred = deferred<TurnResult>();
412
+ const run: RunState = {
413
+ runId,
414
+ sessionId: session.sessionId,
415
+ idempotencyKey: input.idempotencyKey,
416
+ input: input.input,
417
+ traceId: randomUUID(),
418
+ abortController: new AbortController(),
419
+ ...(session.soulHash !== undefined ? { soulHash: session.soulHash } : {}),
420
+ ...(currentSkillsSnapshot?.hash !== undefined
421
+ ? { skillsHash: currentSkillsSnapshot.hash }
422
+ : {}),
423
+ ...(session.skillsSnapshotVersion !== undefined
424
+ ? { skillsSnapshotVersion: session.skillsSnapshotVersion }
425
+ : {}),
426
+ ...(currentMemoryHash !== undefined ? { memoryHash: currentMemoryHash } : {}),
427
+ stepCounter: 0,
428
+ active: true,
429
+ streaming: true,
430
+ compacting: false,
431
+ aborted: false,
432
+ steers: [],
433
+ assistantOutputBuffer: '',
434
+ assistantOutputDeltaCount: 0,
435
+ resolveDone: turnDeferred.resolve,
436
+ done: turnDeferred.promise,
437
+ };
438
+
439
+ if (input.abortSignal !== undefined) {
440
+ const onAbort = () => {
441
+ this.requestAbort(session, run, 'signal');
442
+ };
443
+ if (input.abortSignal.aborted) {
444
+ onAbort();
445
+ } else {
446
+ input.abortSignal.addEventListener('abort', onAbort, { once: true });
447
+ run.abortSignalCleanup = () => {
448
+ input.abortSignal?.removeEventListener('abort', onAbort);
449
+ };
450
+ }
451
+ }
452
+
453
+ this.runs.set(runId, run);
454
+ session.lastRunId = runId;
455
+ session.activeRunId = runId;
456
+ session.idempotencyToRunId.set(input.idempotencyKey, runId);
457
+ this.sessionStore.upsertIdempotency(session.sessionId, input.idempotencyKey, runId);
458
+ this.persistSession(session);
459
+
460
+ void this.enqueueSessionAndGlobal(session.sessionId, async () => {
461
+ await this.executeRun(session, run);
462
+ });
463
+
464
+ return {
465
+ runId: run.runId,
466
+ sessionId: run.sessionId,
467
+ idempotencyKey: run.idempotencyKey,
468
+ done: run.done,
469
+ };
470
+ }
471
+
472
+ public async abortTurn(input: AbortTurnInput): Promise<void> {
473
+ const run = this.runs.get(input.runId);
474
+ if (run === undefined) {
475
+ return;
476
+ }
477
+ const session = this.requireSession(run.sessionId);
478
+ this.requestAbort(session, run, input.reason ?? 'manual');
479
+ }
480
+
481
+ public async steerTurn(input: SteerTurnInput): Promise<SteerTurnResult> {
482
+ const session = this.requireSession(input.sessionId);
483
+ const activeRun =
484
+ input.runId !== undefined
485
+ ? this.runs.get(input.runId)
486
+ : session.activeRunId !== undefined
487
+ ? this.runs.get(session.activeRunId)
488
+ : undefined;
489
+
490
+ if (activeRun === undefined || !activeRun.active) {
491
+ return {
492
+ accepted: false,
493
+ reason: 'no-active-run',
494
+ };
495
+ }
496
+ if (!activeRun.streaming) {
497
+ return {
498
+ accepted: false,
499
+ reason: 'not-streaming',
500
+ };
501
+ }
502
+ if (activeRun.compacting) {
503
+ return {
504
+ accepted: false,
505
+ reason: 'compacting',
506
+ };
507
+ }
508
+
509
+ this.appendRunEvent(session, activeRun, {
510
+ type: 'turn.steer.requested',
511
+ source: 'system',
512
+ steerStrategy: 'append',
513
+ data: {
514
+ strategy: 'append',
515
+ },
516
+ });
517
+ activeRun.steers.push(input.text);
518
+ this.appendRunEvent(session, activeRun, {
519
+ type: 'turn.steer.accepted',
520
+ source: 'system',
521
+ steerStrategy: 'append',
522
+ data: {
523
+ strategy: 'append',
524
+ },
525
+ });
526
+ return {
527
+ accepted: true,
528
+ };
529
+ }
530
+
531
+ public async queueTurn(input: QueueTurnInput): Promise<QueueTurnResult> {
532
+ const session = this.requireSession(input.sessionId);
533
+ if (input.text.trim().length === 0) {
534
+ return {
535
+ queued: false,
536
+ reason: 'invalid-state',
537
+ };
538
+ }
539
+
540
+ if (session.queuedTurns.length >= MAX_QUEUED_TURNS_PER_SESSION) {
541
+ return {
542
+ queued: false,
543
+ reason: 'queue-full',
544
+ };
545
+ }
546
+
547
+ const dedupeKey = input.dedupeKey ?? input.text;
548
+ const duplicate = session.queuedTurns.some((item) => item.dedupeKey === dedupeKey);
549
+ if (duplicate) {
550
+ return {
551
+ queued: false,
552
+ reason: 'duplicate',
553
+ };
554
+ }
555
+
556
+ const queueItem: QueueItem = {
557
+ queueId: randomUUID(),
558
+ text: input.text,
559
+ priority: input.priority ?? 'normal',
560
+ dedupeKey,
561
+ };
562
+
563
+ let position = 0;
564
+ if (queueItem.priority === 'high') {
565
+ session.queuedTurns.unshift(queueItem);
566
+ position = 0;
567
+ } else {
568
+ session.queuedTurns.push(queueItem);
569
+ position = session.queuedTurns.length - 1;
570
+ }
571
+
572
+ const run = session.activeRunId !== undefined ? this.runs.get(session.activeRunId) : undefined;
573
+ this.appendSessionEvent(session, {
574
+ type: 'turn.queue.enqueued',
575
+ source: 'system',
576
+ runId: run?.runId ?? '',
577
+ turnId: run?.runId ?? '',
578
+ stepId: 'step:queue-enqueued',
579
+ idempotencyKey: run?.idempotencyKey ?? 'queue',
580
+ queueId: queueItem.queueId,
581
+ queuePosition: position,
582
+ });
583
+
584
+ return {
585
+ queued: true,
586
+ queueId: queueItem.queueId,
587
+ position,
588
+ };
589
+ }
590
+
591
+ public async compactSession(input: CompactSessionInput): Promise<CompactionResult> {
592
+ const session = this.requireSession(input.sessionId);
593
+ const run = session.activeRunId !== undefined ? this.runs.get(session.activeRunId) : undefined;
594
+
595
+ if (run !== undefined && run.active) {
596
+ run.compacting = true;
597
+ this.appendRunEvent(session, run, {
598
+ type: 'provider.context.compaction.started',
599
+ source: 'provider',
600
+ state: 'thinking',
601
+ data: {
602
+ trigger: input.trigger,
603
+ includeMemoryFlush: input.includeMemoryFlush ?? false,
604
+ },
605
+ });
606
+ } else {
607
+ this.appendSessionEvent(session, {
608
+ type: 'provider.context.compaction.started',
609
+ source: 'provider',
610
+ runId: session.lastRunId ?? '',
611
+ turnId: session.lastRunId ?? '',
612
+ stepId: 'step:compaction-started',
613
+ idempotencyKey: 'compaction',
614
+ data: {
615
+ trigger: input.trigger,
616
+ includeMemoryFlush: input.includeMemoryFlush ?? false,
617
+ },
618
+ });
619
+ }
620
+
621
+ await sleep(1);
622
+
623
+ if (run !== undefined && run.active) {
624
+ this.appendRunEvent(session, run, {
625
+ type: 'provider.context.compaction.completed',
626
+ source: 'provider',
627
+ state: 'thinking',
628
+ data: {
629
+ trigger: input.trigger,
630
+ },
631
+ });
632
+ run.compacting = false;
633
+ } else {
634
+ this.appendSessionEvent(session, {
635
+ type: 'provider.context.compaction.completed',
636
+ source: 'provider',
637
+ runId: session.lastRunId ?? '',
638
+ turnId: session.lastRunId ?? '',
639
+ stepId: 'step:compaction-completed',
640
+ idempotencyKey: 'compaction',
641
+ data: {
642
+ trigger: input.trigger,
643
+ },
644
+ });
645
+ }
646
+
647
+ const sessionEvents = this.eventStore.list({
648
+ tenantId: session.tenantId,
649
+ sessionId: session.sessionId,
650
+ });
651
+ const summaryEventId = sessionEvents[sessionEvents.length - 1]?.event_id;
652
+ if (summaryEventId === undefined) {
653
+ return {
654
+ compacted: true,
655
+ };
656
+ }
657
+ return {
658
+ compacted: true,
659
+ summaryEventId,
660
+ };
661
+ }
662
+
663
+ public streamEvents(input: StreamEventsInput): AsyncIterable<NimEventEnvelope> {
664
+ const queue = new AsyncPushQueue<NimEventEnvelope>();
665
+ const id = randomUUID();
666
+ const subscribers = this.subscribers;
667
+ const fromEvent = this.resolveFromEvent(input.fromEventIdExclusive);
668
+ const subscriber: EventSubscriber = {
669
+ input,
670
+ ...(fromEvent !== undefined ? { fromEvent } : {}),
671
+ queue,
672
+ };
673
+
674
+ this.subscribers.set(id, subscriber);
675
+
676
+ const initialEvents = this.eventStore.list({
677
+ tenantId: input.tenantId,
678
+ ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
679
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
680
+ });
681
+ for (const event of initialEvents) {
682
+ if (!this.shouldDeliverEvent(input, event, fromEvent)) {
683
+ continue;
684
+ }
685
+ queue.push(event);
686
+ }
687
+
688
+ return {
689
+ [Symbol.asyncIterator]: () => {
690
+ return {
691
+ async next() {
692
+ return await queue.next();
693
+ },
694
+ async return() {
695
+ subscribers.delete(id);
696
+ queue.close();
697
+ return { done: true, value: undefined };
698
+ },
699
+ };
700
+ },
701
+ };
702
+ }
703
+
704
+ public streamUi(input: StreamUiInput): AsyncIterable<NimUiEvent> {
705
+ const source = this.streamEvents({
706
+ tenantId: input.tenantId,
707
+ ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
708
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
709
+ fidelity: 'semantic',
710
+ });
711
+ const mode = input.mode;
712
+ return {
713
+ async *[Symbol.asyncIterator]() {
714
+ for await (const event of source) {
715
+ const projected = projectEventToUiEvents(event, mode);
716
+ for (const item of projected) {
717
+ yield item;
718
+ }
719
+ }
720
+ },
721
+ };
722
+ }
723
+
724
+ public async replayEvents(input: ReplayEventsInput): Promise<ReplayEventsResult> {
725
+ const fromEvent = this.resolveFromEvent(input.fromEventIdExclusive);
726
+ const toEvent = this.resolveToEvent(input.toEventIdInclusive);
727
+ const streamInput: StreamEventsInput = {
728
+ tenantId: input.tenantId,
729
+ ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
730
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
731
+ ...(input.fidelity !== undefined ? { fidelity: input.fidelity } : {}),
732
+ ...(input.includeThoughtDeltas !== undefined
733
+ ? { includeThoughtDeltas: input.includeThoughtDeltas }
734
+ : {}),
735
+ ...(input.includeToolArgumentDeltas !== undefined
736
+ ? { includeToolArgumentDeltas: input.includeToolArgumentDeltas }
737
+ : {}),
738
+ };
739
+
740
+ const events = this.eventStore.list({
741
+ tenantId: input.tenantId,
742
+ ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
743
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
744
+ });
745
+ const filtered = events.filter((event) => {
746
+ if (!this.matchesSubscriberEvent(streamInput, event)) {
747
+ return false;
748
+ }
749
+ if (!this.matchesFidelityFilter(streamInput, event)) {
750
+ return false;
751
+ }
752
+ if (!this.matchesReplayWindow(event, fromEvent, toEvent)) {
753
+ return false;
754
+ }
755
+ return true;
756
+ });
757
+
758
+ return {
759
+ events: filtered,
760
+ };
761
+ }
762
+
763
+ private async executeRun(session: SessionState, run: RunState): Promise<void> {
764
+ try {
765
+ this.appendRunEvent(session, run, {
766
+ type: 'turn.started',
767
+ source: 'system',
768
+ state: 'responding',
769
+ });
770
+ this.emitRunContextSnapshotEvents(session, run);
771
+
772
+ const requestedToolName = this.requestedToolName(run.input);
773
+ const exposedTools = this.resolveExposedTools();
774
+ const blockedToolReason =
775
+ requestedToolName === undefined
776
+ ? undefined
777
+ : this.resolveToolBlockReason(requestedToolName, exposedTools);
778
+ if (requestedToolName !== undefined && blockedToolReason !== undefined) {
779
+ this.appendRunEvent(session, run, {
780
+ type: 'tool.policy.blocked',
781
+ source: 'system',
782
+ state: 'responding',
783
+ data: {
784
+ toolName: requestedToolName,
785
+ reason: blockedToolReason,
786
+ },
787
+ });
788
+ }
789
+
790
+ const autoCompaction = await this.runAutoOverflowCompactionIfNeeded(session, run);
791
+ let terminalState: 'completed' | 'aborted' | 'failed';
792
+ if (autoCompaction === 'failed' || autoCompaction === 'aborted') {
793
+ terminalState = autoCompaction;
794
+ } else {
795
+ const resolvedModel = this.providerRouter.resolveModel(session.model);
796
+ terminalState =
797
+ resolvedModel.driver === undefined
798
+ ? await this.executeRunWithMockProvider(
799
+ session,
800
+ run,
801
+ exposedTools,
802
+ requestedToolName,
803
+ blockedToolReason,
804
+ )
805
+ : await this.executeRunWithProviderDriver(
806
+ session,
807
+ run,
808
+ resolvedModel.driver,
809
+ resolvedModel.parsedModel.providerModelId,
810
+ exposedTools,
811
+ );
812
+ }
813
+
814
+ this.appendRunEvent(session, run, {
815
+ type: 'assistant.state.changed',
816
+ source: 'system',
817
+ state: 'idle',
818
+ });
819
+ await this.finalizeRun(session, run, terminalState);
820
+ } catch (error) {
821
+ if (run.active) {
822
+ this.appendRunEvent(session, run, {
823
+ type: 'turn.failed',
824
+ source: 'system',
825
+ state: 'idle',
826
+ data: {
827
+ message: error instanceof Error ? error.message : String(error),
828
+ },
829
+ });
830
+ await this.finalizeRun(session, run, 'failed');
831
+ }
832
+ }
833
+ }
834
+
835
+ private async executeRunWithMockProvider(
836
+ session: SessionState,
837
+ run: RunState,
838
+ exposedTools: readonly NimToolDefinition[],
839
+ requestedToolName?: string,
840
+ blockedToolReason?: ToolBlockReason,
841
+ ): Promise<'completed' | 'aborted'> {
842
+ this.appendRunEvent(session, run, {
843
+ type: 'assistant.state.changed',
844
+ source: 'system',
845
+ state: 'thinking',
846
+ });
847
+ this.appendRunEvent(session, run, {
848
+ type: 'provider.thinking.started',
849
+ source: 'provider',
850
+ state: 'thinking',
851
+ });
852
+
853
+ await sleep(15);
854
+ if (run.aborted) {
855
+ return 'aborted';
856
+ }
857
+
858
+ const shouldUseTool = run.input.includes('use-tool');
859
+ if (shouldUseTool) {
860
+ const toolCallId = randomUUID();
861
+ const toolName = requestedToolName ?? exposedTools[0]?.name ?? 'mock-tool';
862
+ const canInvokeTool =
863
+ blockedToolReason === undefined && exposedTools.some((tool) => tool.name === toolName);
864
+
865
+ if (canInvokeTool) {
866
+ this.appendRunEvent(session, run, {
867
+ type: 'assistant.state.changed',
868
+ source: 'system',
869
+ state: 'tool-calling',
870
+ toolCallId,
871
+ });
872
+ this.appendRunEvent(session, run, {
873
+ type: 'tool.call.started',
874
+ source: 'tool',
875
+ state: 'tool-calling',
876
+ toolCallId,
877
+ data: {
878
+ toolName,
879
+ },
880
+ });
881
+
882
+ await sleep(1);
883
+ if (run.aborted) {
884
+ return 'aborted';
885
+ }
886
+
887
+ this.appendRunEvent(session, run, {
888
+ type: 'tool.call.completed',
889
+ source: 'tool',
890
+ state: 'tool-calling',
891
+ toolCallId,
892
+ data: {
893
+ toolName,
894
+ },
895
+ });
896
+ this.appendRunEvent(session, run, {
897
+ type: 'tool.result.emitted',
898
+ source: 'tool',
899
+ state: 'responding',
900
+ toolCallId,
901
+ });
902
+ }
903
+ }
904
+
905
+ this.appendRunEvent(session, run, {
906
+ type: 'provider.thinking.completed',
907
+ source: 'provider',
908
+ state: 'responding',
909
+ });
910
+
911
+ if (run.aborted) {
912
+ return 'aborted';
913
+ }
914
+
915
+ const steerSuffix = run.steers.length > 0 ? ` [steer:${run.steers.join(' | ')}]` : '';
916
+ this.appendAssistantOutputDelta(session, run, `echo:${run.input}${steerSuffix}`);
917
+ this.flushAssistantOutputMessage(session, run);
918
+ this.appendRunEvent(session, run, {
919
+ type: 'assistant.output.completed',
920
+ source: 'provider',
921
+ state: 'responding',
922
+ });
923
+
924
+ return 'completed';
925
+ }
926
+
927
+ private async executeRunWithProviderDriver(
928
+ session: SessionState,
929
+ run: RunState,
930
+ driver: NimProviderDriver,
931
+ providerModelId: string,
932
+ exposedTools: readonly NimToolDefinition[],
933
+ ): Promise<'completed' | 'aborted' | 'failed'> {
934
+ let terminalState: 'completed' | 'aborted' | 'failed' = 'completed';
935
+ for await (const providerEvent of driver.runTurn({
936
+ modelRef: session.model,
937
+ providerModelId,
938
+ input: run.input,
939
+ tools: exposedTools,
940
+ abortSignal: run.abortController.signal,
941
+ })) {
942
+ if (run.aborted) {
943
+ return 'aborted';
944
+ }
945
+ terminalState = this.appendProviderTurnEvent(session, run, providerEvent);
946
+ if (terminalState === 'failed') {
947
+ return terminalState;
948
+ }
949
+ }
950
+ return run.aborted ? 'aborted' : terminalState;
951
+ }
952
+
953
+ private emitRunContextSnapshotEvents(session: SessionState, run: RunState): void {
954
+ if (run.soulHash === undefined) {
955
+ this.appendRunEvent(session, run, {
956
+ type: 'soul.snapshot.missing',
957
+ source: 'soul',
958
+ state: 'responding',
959
+ data: {
960
+ reason: 'no-soul-source',
961
+ },
962
+ });
963
+ } else {
964
+ this.appendRunEvent(session, run, {
965
+ type: 'soul.snapshot.loaded',
966
+ source: 'soul',
967
+ state: 'responding',
968
+ data: {
969
+ hash: run.soulHash,
970
+ },
971
+ });
972
+ }
973
+
974
+ if (run.skillsSnapshotVersion === undefined || run.skillsHash === undefined) {
975
+ this.appendRunEvent(session, run, {
976
+ type: 'skills.snapshot.missing',
977
+ source: 'skill',
978
+ state: 'responding',
979
+ data: {
980
+ reason: 'no-skill-source',
981
+ },
982
+ });
983
+ } else {
984
+ this.appendRunEvent(session, run, {
985
+ type: 'skills.snapshot.loaded',
986
+ source: 'skill',
987
+ state: 'responding',
988
+ data: {
989
+ hash: run.skillsHash,
990
+ version: run.skillsSnapshotVersion,
991
+ },
992
+ });
993
+ }
994
+
995
+ if (run.memoryHash === undefined) {
996
+ this.appendRunEvent(session, run, {
997
+ type: 'memory.snapshot.missing',
998
+ source: 'memory',
999
+ state: 'responding',
1000
+ data: {
1001
+ reason: 'no-memory-store',
1002
+ },
1003
+ });
1004
+ } else {
1005
+ this.appendRunEvent(session, run, {
1006
+ type: 'memory.snapshot.loaded',
1007
+ source: 'memory',
1008
+ state: 'responding',
1009
+ data: {
1010
+ hash: run.memoryHash,
1011
+ },
1012
+ });
1013
+ }
1014
+ }
1015
+
1016
+ private resolveOverflowMode(input: string): 'none' | 'recoverable' | 'fatal' {
1017
+ if (input.includes('force-overflow-fail')) {
1018
+ return 'fatal';
1019
+ }
1020
+ if (input.includes('force-overflow-recover')) {
1021
+ return 'recoverable';
1022
+ }
1023
+ return 'none';
1024
+ }
1025
+
1026
+ private async runAutoOverflowCompactionIfNeeded(
1027
+ session: SessionState,
1028
+ run: RunState,
1029
+ ): Promise<'continue' | 'aborted' | 'failed'> {
1030
+ const mode = this.resolveOverflowMode(run.input);
1031
+ if (mode === 'none') {
1032
+ return 'continue';
1033
+ }
1034
+
1035
+ for (let attempt = 1; attempt <= MAX_OVERFLOW_COMPACTION_ATTEMPTS; attempt += 1) {
1036
+ this.appendRunEvent(session, run, {
1037
+ type: 'provider.context.compaction.started',
1038
+ source: 'provider',
1039
+ state: 'thinking',
1040
+ data: {
1041
+ trigger: 'overflow',
1042
+ attempt,
1043
+ maxAttempts: MAX_OVERFLOW_COMPACTION_ATTEMPTS,
1044
+ },
1045
+ });
1046
+
1047
+ await sleep(1);
1048
+ if (run.aborted) {
1049
+ return 'aborted';
1050
+ }
1051
+
1052
+ if (mode === 'recoverable') {
1053
+ this.appendRunEvent(session, run, {
1054
+ type: 'provider.context.compaction.completed',
1055
+ source: 'provider',
1056
+ state: 'responding',
1057
+ data: {
1058
+ trigger: 'overflow',
1059
+ attempt,
1060
+ },
1061
+ });
1062
+ return 'continue';
1063
+ }
1064
+
1065
+ if (attempt < MAX_OVERFLOW_COMPACTION_ATTEMPTS) {
1066
+ this.appendRunEvent(session, run, {
1067
+ type: 'provider.context.compaction.retry',
1068
+ source: 'provider',
1069
+ state: 'thinking',
1070
+ data: {
1071
+ trigger: 'overflow',
1072
+ attempt,
1073
+ nextAttempt: attempt + 1,
1074
+ },
1075
+ });
1076
+ continue;
1077
+ }
1078
+
1079
+ this.appendRunEvent(session, run, {
1080
+ type: 'provider.context.compaction.failed',
1081
+ source: 'provider',
1082
+ state: 'idle',
1083
+ data: {
1084
+ trigger: 'overflow',
1085
+ attempt,
1086
+ reason: 'overflow-retries-exhausted',
1087
+ },
1088
+ });
1089
+ this.appendRunEvent(session, run, {
1090
+ type: 'turn.failed',
1091
+ source: 'system',
1092
+ state: 'idle',
1093
+ data: {
1094
+ message: 'context overflow after compaction retries',
1095
+ },
1096
+ });
1097
+ return 'failed';
1098
+ }
1099
+
1100
+ return 'failed';
1101
+ }
1102
+
1103
+ private appendProviderTurnEvent(
1104
+ session: SessionState,
1105
+ run: RunState,
1106
+ event: NimProviderTurnEvent,
1107
+ ): 'completed' | 'failed' {
1108
+ if (event.type === 'provider.thinking.started') {
1109
+ this.appendRunEvent(session, run, {
1110
+ type: 'assistant.state.changed',
1111
+ source: 'system',
1112
+ state: 'thinking',
1113
+ });
1114
+ this.appendRunEvent(session, run, {
1115
+ type: 'provider.thinking.started',
1116
+ source: 'provider',
1117
+ state: 'thinking',
1118
+ });
1119
+ return 'completed';
1120
+ }
1121
+
1122
+ if (event.type === 'provider.thinking.delta') {
1123
+ this.appendRunEvent(session, run, {
1124
+ type: 'provider.thinking.delta',
1125
+ source: 'provider',
1126
+ state: 'thinking',
1127
+ data: {
1128
+ text: event.text,
1129
+ },
1130
+ });
1131
+ return 'completed';
1132
+ }
1133
+
1134
+ if (event.type === 'provider.thinking.completed') {
1135
+ this.appendRunEvent(session, run, {
1136
+ type: 'provider.thinking.completed',
1137
+ source: 'provider',
1138
+ state: 'responding',
1139
+ });
1140
+ this.appendRunEvent(session, run, {
1141
+ type: 'assistant.state.changed',
1142
+ source: 'system',
1143
+ state: 'responding',
1144
+ });
1145
+ return 'completed';
1146
+ }
1147
+
1148
+ if (event.type === 'tool.call.started') {
1149
+ this.appendRunEvent(session, run, {
1150
+ type: 'assistant.state.changed',
1151
+ source: 'system',
1152
+ state: 'tool-calling',
1153
+ toolCallId: event.toolCallId,
1154
+ });
1155
+ this.appendRunEvent(session, run, {
1156
+ type: 'tool.call.started',
1157
+ source: 'tool',
1158
+ state: 'tool-calling',
1159
+ toolCallId: event.toolCallId,
1160
+ data: {
1161
+ toolName: event.toolName,
1162
+ },
1163
+ });
1164
+ return 'completed';
1165
+ }
1166
+
1167
+ if (event.type === 'tool.call.arguments.delta') {
1168
+ this.appendRunEvent(session, run, {
1169
+ type: 'tool.call.arguments.delta',
1170
+ source: 'tool',
1171
+ state: 'tool-calling',
1172
+ toolCallId: event.toolCallId,
1173
+ data: {
1174
+ delta: event.delta,
1175
+ },
1176
+ });
1177
+ return 'completed';
1178
+ }
1179
+
1180
+ if (event.type === 'tool.call.completed') {
1181
+ this.appendRunEvent(session, run, {
1182
+ type: 'tool.call.completed',
1183
+ source: 'tool',
1184
+ state: 'tool-calling',
1185
+ toolCallId: event.toolCallId,
1186
+ data: {
1187
+ toolName: event.toolName,
1188
+ },
1189
+ });
1190
+ this.appendRunEvent(session, run, {
1191
+ type: 'assistant.state.changed',
1192
+ source: 'system',
1193
+ state: 'responding',
1194
+ });
1195
+ return 'completed';
1196
+ }
1197
+
1198
+ if (event.type === 'tool.call.failed') {
1199
+ this.appendRunEvent(session, run, {
1200
+ type: 'tool.call.failed',
1201
+ source: 'tool',
1202
+ state: 'responding',
1203
+ toolCallId: event.toolCallId,
1204
+ data: {
1205
+ toolName: event.toolName,
1206
+ error: event.error,
1207
+ },
1208
+ });
1209
+ this.appendRunEvent(session, run, {
1210
+ type: 'assistant.state.changed',
1211
+ source: 'system',
1212
+ state: 'responding',
1213
+ });
1214
+ return 'completed';
1215
+ }
1216
+
1217
+ if (event.type === 'tool.result.emitted') {
1218
+ this.appendRunEvent(session, run, {
1219
+ type: 'tool.result.emitted',
1220
+ source: 'tool',
1221
+ state: 'responding',
1222
+ toolCallId: event.toolCallId,
1223
+ data: {
1224
+ toolName: event.toolName,
1225
+ ...(event.output !== undefined ? { output: event.output } : {}),
1226
+ },
1227
+ });
1228
+ return 'completed';
1229
+ }
1230
+
1231
+ if (event.type === 'assistant.output.delta') {
1232
+ this.appendAssistantOutputDelta(session, run, event.text);
1233
+ return 'completed';
1234
+ }
1235
+
1236
+ if (event.type === 'assistant.output.completed') {
1237
+ this.flushAssistantOutputMessage(session, run);
1238
+ this.appendRunEvent(session, run, {
1239
+ type: 'assistant.output.completed',
1240
+ source: 'provider',
1241
+ state: 'responding',
1242
+ });
1243
+ return 'completed';
1244
+ }
1245
+
1246
+ if (event.type === 'provider.turn.error') {
1247
+ this.appendRunEvent(session, run, {
1248
+ type: 'turn.failed',
1249
+ source: 'system',
1250
+ state: 'idle',
1251
+ data: {
1252
+ message: event.message,
1253
+ },
1254
+ });
1255
+ return 'failed';
1256
+ }
1257
+
1258
+ if (event.type === 'provider.turn.finished' && event.finishReason === 'error') {
1259
+ this.appendRunEvent(session, run, {
1260
+ type: 'turn.failed',
1261
+ source: 'system',
1262
+ state: 'idle',
1263
+ data: {
1264
+ message: 'provider finished with error',
1265
+ },
1266
+ });
1267
+ return 'failed';
1268
+ }
1269
+
1270
+ return 'completed';
1271
+ }
1272
+
1273
+ private async finalizeRun(
1274
+ session: SessionState,
1275
+ run: RunState,
1276
+ state: 'completed' | 'aborted' | 'failed',
1277
+ ): Promise<void> {
1278
+ if (!run.active) {
1279
+ return;
1280
+ }
1281
+
1282
+ run.active = false;
1283
+ run.streaming = false;
1284
+ run.compacting = false;
1285
+ run.abortSignalCleanup?.();
1286
+ delete run.abortSignalCleanup;
1287
+
1288
+ if (session.activeRunId === run.runId) {
1289
+ delete session.activeRunId;
1290
+ }
1291
+
1292
+ if (state === 'completed') {
1293
+ this.flushAssistantOutputMessage(session, run);
1294
+ }
1295
+
1296
+ if (state === 'aborted') {
1297
+ this.appendRunEvent(session, run, {
1298
+ type: 'turn.abort.completed',
1299
+ source: 'system',
1300
+ state: 'idle',
1301
+ data: {
1302
+ reason: run.abortReason ?? 'manual',
1303
+ },
1304
+ });
1305
+ }
1306
+
1307
+ this.appendRunEvent(session, run, {
1308
+ type: 'turn.completed',
1309
+ source: 'system',
1310
+ state: 'idle',
1311
+ data: {
1312
+ terminalState: state,
1313
+ },
1314
+ });
1315
+
1316
+ run.resolveDone({
1317
+ runId: run.runId,
1318
+ terminalState: state,
1319
+ });
1320
+
1321
+ if (session.queuedTurns.length > 0) {
1322
+ const queueItem = session.queuedTurns.shift() as QueueItem;
1323
+ this.appendSessionEvent(session, {
1324
+ type: 'turn.queue.dequeued',
1325
+ source: 'system',
1326
+ runId: run.runId,
1327
+ turnId: run.runId,
1328
+ stepId: 'step:queue-dequeued',
1329
+ idempotencyKey: run.idempotencyKey,
1330
+ queueId: queueItem.queueId,
1331
+ queuePosition: 0,
1332
+ });
1333
+ await this.sendTurn({
1334
+ sessionId: session.sessionId,
1335
+ input: queueItem.text,
1336
+ idempotencyKey: `queue:${queueItem.queueId}`,
1337
+ });
1338
+ }
1339
+ }
1340
+
1341
+ private requestAbort(session: SessionState, run: RunState, reason: AbortReason): void {
1342
+ if (run.aborted) {
1343
+ return;
1344
+ }
1345
+
1346
+ run.aborted = true;
1347
+ run.abortReason = reason;
1348
+ run.abortController.abort(reason);
1349
+
1350
+ this.appendRunEvent(session, run, {
1351
+ type: 'turn.abort.requested',
1352
+ source: 'system',
1353
+ state: 'responding',
1354
+ data: {
1355
+ reason,
1356
+ },
1357
+ });
1358
+
1359
+ this.appendRunEvent(session, run, {
1360
+ type: 'turn.abort.propagated',
1361
+ source: 'system',
1362
+ state: 'responding',
1363
+ data: {
1364
+ reason,
1365
+ },
1366
+ });
1367
+ }
1368
+
1369
+ private appendRunEvent(
1370
+ session: SessionState,
1371
+ run: RunState,
1372
+ input: {
1373
+ type: string;
1374
+ source: NimEventEnvelope['source'];
1375
+ state?: NimEventEnvelope['state'];
1376
+ steerStrategy?: NimEventEnvelope['steer_strategy'];
1377
+ toolCallId?: string;
1378
+ data?: Record<string, unknown>;
1379
+ },
1380
+ ): void {
1381
+ run.stepCounter += 1;
1382
+ const event: NimEventEnvelope = {
1383
+ event_id: randomUUID(),
1384
+ event_seq: session.eventSeq + 1,
1385
+ ts: new Date().toISOString(),
1386
+ tenant_id: session.tenantId,
1387
+ user_id: session.userId,
1388
+ workspace_id: 'workspace-local',
1389
+ session_id: session.sessionId,
1390
+ run_id: run.runId,
1391
+ turn_id: run.runId,
1392
+ step_id: `step:${run.stepCounter}`,
1393
+ ...(input.toolCallId !== undefined ? { tool_call_id: input.toolCallId } : {}),
1394
+ source: input.source,
1395
+ type: input.type,
1396
+ payload_hash: `hash:${run.runId}:${run.stepCounter}`,
1397
+ idempotency_key: run.idempotencyKey,
1398
+ lane: session.lane,
1399
+ ...(input.steerStrategy !== undefined ? { steer_strategy: input.steerStrategy } : {}),
1400
+ policy_hash: this.toolPolicy.hash,
1401
+ ...(run.skillsSnapshotVersion !== undefined
1402
+ ? { skills_snapshot_version: run.skillsSnapshotVersion }
1403
+ : {}),
1404
+ ...(run.soulHash !== undefined ? { soul_hash: run.soulHash } : {}),
1405
+ trace_id: run.traceId,
1406
+ span_id: randomUUID(),
1407
+ ...(input.state !== undefined ? { state: input.state } : {}),
1408
+ ...(input.data !== undefined ? { data: input.data } : {}),
1409
+ };
1410
+ this.emitEvent(session, event);
1411
+ }
1412
+
1413
+ private appendAssistantOutputDelta(session: SessionState, run: RunState, text: string): void {
1414
+ if (text.length === 0) {
1415
+ return;
1416
+ }
1417
+ run.assistantOutputBuffer += text;
1418
+ run.assistantOutputDeltaCount += 1;
1419
+ this.appendRunEvent(session, run, {
1420
+ type: 'assistant.output.delta',
1421
+ source: 'provider',
1422
+ state: 'responding',
1423
+ data: {
1424
+ text,
1425
+ },
1426
+ });
1427
+ }
1428
+
1429
+ private flushAssistantOutputMessage(session: SessionState, run: RunState): void {
1430
+ if (run.assistantOutputDeltaCount === 0) {
1431
+ return;
1432
+ }
1433
+ this.appendRunEvent(session, run, {
1434
+ type: 'assistant.output.message',
1435
+ source: 'provider',
1436
+ state: 'responding',
1437
+ data: {
1438
+ text: run.assistantOutputBuffer,
1439
+ },
1440
+ });
1441
+ run.assistantOutputBuffer = '';
1442
+ run.assistantOutputDeltaCount = 0;
1443
+ }
1444
+
1445
+ private appendSessionEvent(
1446
+ session: SessionState,
1447
+ input: {
1448
+ type: string;
1449
+ source: NimEventEnvelope['source'];
1450
+ runId: string;
1451
+ turnId: string;
1452
+ stepId: string;
1453
+ idempotencyKey: string;
1454
+ queueId?: string;
1455
+ queuePosition?: number;
1456
+ data?: Record<string, unknown>;
1457
+ },
1458
+ ): void {
1459
+ const event: NimEventEnvelope = {
1460
+ event_id: randomUUID(),
1461
+ event_seq: session.eventSeq + 1,
1462
+ ts: new Date().toISOString(),
1463
+ tenant_id: session.tenantId,
1464
+ user_id: session.userId,
1465
+ workspace_id: 'workspace-local',
1466
+ session_id: session.sessionId,
1467
+ run_id: input.runId,
1468
+ turn_id: input.turnId,
1469
+ step_id: input.stepId,
1470
+ source: input.source,
1471
+ type: input.type,
1472
+ payload_hash: `hash:${input.stepId}:${session.eventSeq + 1}`,
1473
+ idempotency_key: input.idempotencyKey,
1474
+ lane: session.lane,
1475
+ policy_hash: this.toolPolicy.hash,
1476
+ trace_id: input.runId.length > 0 ? input.runId : 'trace:session',
1477
+ span_id: randomUUID(),
1478
+ ...(input.queueId !== undefined ? { queue_id: input.queueId } : {}),
1479
+ ...(input.queuePosition !== undefined ? { queue_position: input.queuePosition } : {}),
1480
+ ...(input.data !== undefined ? { data: input.data } : {}),
1481
+ };
1482
+ this.emitEvent(session, event);
1483
+ }
1484
+
1485
+ private emitEvent(session: SessionState, event: NimEventEnvelope): void {
1486
+ session.eventSeq += 1;
1487
+ const finalized: NimEventEnvelope = {
1488
+ ...event,
1489
+ event_seq: session.eventSeq,
1490
+ };
1491
+ this.eventStore.append(finalized);
1492
+ this.persistSession(session);
1493
+ this.dispatchTelemetry(finalized);
1494
+
1495
+ for (const subscriber of this.subscribers.values()) {
1496
+ if (!this.shouldDeliverEvent(subscriber.input, finalized, subscriber.fromEvent)) {
1497
+ continue;
1498
+ }
1499
+ subscriber.queue.push(finalized);
1500
+ }
1501
+ }
1502
+
1503
+ private dispatchTelemetry(event: NimEventEnvelope): void {
1504
+ for (const sink of this.telemetrySinks) {
1505
+ sink.record(event);
1506
+ }
1507
+ }
1508
+
1509
+ private shouldDeliverEvent(
1510
+ input: StreamEventsInput,
1511
+ event: NimEventEnvelope,
1512
+ fromEvent?: NimEventEnvelope,
1513
+ ): boolean {
1514
+ if (!this.matchesSubscriberEvent(input, event)) {
1515
+ return false;
1516
+ }
1517
+ if (!this.matchesFidelityFilter(input, event)) {
1518
+ return false;
1519
+ }
1520
+
1521
+ if (fromEvent === undefined) {
1522
+ return true;
1523
+ }
1524
+
1525
+ if (event.session_id !== fromEvent.session_id) {
1526
+ return true;
1527
+ }
1528
+
1529
+ return event.event_seq > fromEvent.event_seq;
1530
+ }
1531
+
1532
+ private matchesFidelityFilter(input: StreamEventsInput, event: NimEventEnvelope): boolean {
1533
+ if (event.type === 'provider.thinking.delta' && input.includeThoughtDeltas !== true) {
1534
+ return false;
1535
+ }
1536
+ if (event.type === 'tool.call.arguments.delta' && input.includeToolArgumentDeltas !== true) {
1537
+ return false;
1538
+ }
1539
+ if (input.fidelity === 'semantic' && event.type === 'provider.raw.delta') {
1540
+ return false;
1541
+ }
1542
+ return true;
1543
+ }
1544
+
1545
+ private matchesSubscriberEvent(input: StreamEventsInput, event: NimEventEnvelope): boolean {
1546
+ if (event.tenant_id !== input.tenantId) {
1547
+ return false;
1548
+ }
1549
+ if (input.sessionId !== undefined && event.session_id !== input.sessionId) {
1550
+ return false;
1551
+ }
1552
+ if (input.runId !== undefined && event.run_id !== input.runId) {
1553
+ return false;
1554
+ }
1555
+ return true;
1556
+ }
1557
+
1558
+ private currentSoulHash(): string | undefined {
1559
+ if (this.soulSources.length === 0) {
1560
+ return undefined;
1561
+ }
1562
+ return `soul:${this.soulSources.length}`;
1563
+ }
1564
+
1565
+ private currentSkillsSnapshot(): { hash: string; version: number } | undefined {
1566
+ const version = this.skillSources.length;
1567
+ if (version === 0) {
1568
+ return undefined;
1569
+ }
1570
+ return {
1571
+ hash: `skills:${version}`,
1572
+ version,
1573
+ };
1574
+ }
1575
+
1576
+ private currentMemoryHash(): string | undefined {
1577
+ if (this.memoryStores.length === 0) {
1578
+ return undefined;
1579
+ }
1580
+ return `memory:${this.memoryStores.length}`;
1581
+ }
1582
+
1583
+ private requestedToolName(input: string): string | undefined {
1584
+ const match = /(?:^|\s)use-tool(?:\s+([A-Za-z0-9._:-]+))?/u.exec(input);
1585
+ if (match === null) {
1586
+ return undefined;
1587
+ }
1588
+ const namedTool = match[1];
1589
+ if (typeof namedTool === 'string' && namedTool.length > 0) {
1590
+ return namedTool;
1591
+ }
1592
+ return this.tools[0]?.name;
1593
+ }
1594
+
1595
+ private resolveExposedTools(): readonly NimToolDefinition[] {
1596
+ const denySet = new Set(this.toolPolicy.deny);
1597
+ const hasAllowList = this.toolPolicy.allow.length > 0;
1598
+ const allowSet = new Set(this.toolPolicy.allow);
1599
+ return this.tools.filter((tool) => {
1600
+ if (denySet.has(tool.name)) {
1601
+ return false;
1602
+ }
1603
+ if (hasAllowList && !allowSet.has(tool.name)) {
1604
+ return false;
1605
+ }
1606
+ return true;
1607
+ });
1608
+ }
1609
+
1610
+ private resolveToolBlockReason(
1611
+ requestedToolName: string,
1612
+ exposedTools: readonly NimToolDefinition[],
1613
+ ): ToolBlockReason | undefined {
1614
+ const isRegistered = this.tools.some((tool) => tool.name === requestedToolName);
1615
+ if (!isRegistered) {
1616
+ return 'tool-unavailable';
1617
+ }
1618
+ if (this.toolPolicy.deny.includes(requestedToolName)) {
1619
+ return 'policy-deny';
1620
+ }
1621
+ if (this.toolPolicy.allow.length > 0 && !this.toolPolicy.allow.includes(requestedToolName)) {
1622
+ return 'policy-allow-miss';
1623
+ }
1624
+ const isExposed = exposedTools.some((tool) => tool.name === requestedToolName);
1625
+ return isExposed ? undefined : 'tool-unavailable';
1626
+ }
1627
+
1628
+ private resolveFromEvent(fromEventIdExclusive?: string): NimEventEnvelope | undefined {
1629
+ if (fromEventIdExclusive === undefined) {
1630
+ return undefined;
1631
+ }
1632
+ return this.eventStore.getById(fromEventIdExclusive);
1633
+ }
1634
+
1635
+ private resolveToEvent(toEventIdInclusive?: string): NimEventEnvelope | undefined {
1636
+ if (toEventIdInclusive === undefined) {
1637
+ return undefined;
1638
+ }
1639
+ return this.eventStore.getById(toEventIdInclusive);
1640
+ }
1641
+
1642
+ private matchesReplayWindow(
1643
+ event: NimEventEnvelope,
1644
+ fromEvent?: NimEventEnvelope,
1645
+ toEvent?: NimEventEnvelope,
1646
+ ): boolean {
1647
+ if (fromEvent !== undefined && event.session_id === fromEvent.session_id) {
1648
+ if (event.event_seq <= fromEvent.event_seq) {
1649
+ return false;
1650
+ }
1651
+ }
1652
+ if (toEvent !== undefined && event.session_id === toEvent.session_id) {
1653
+ if (event.event_seq > toEvent.event_seq) {
1654
+ return false;
1655
+ }
1656
+ }
1657
+ return true;
1658
+ }
1659
+
1660
+ private resolveStoredTurnResult(session: SessionState, runId: string): TurnResult | undefined {
1661
+ const events = this.eventStore.list({
1662
+ tenantId: session.tenantId,
1663
+ sessionId: session.sessionId,
1664
+ runId,
1665
+ });
1666
+ let completion: NimEventEnvelope | undefined;
1667
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1668
+ const candidate = events[index];
1669
+ if (candidate?.type === 'turn.completed') {
1670
+ completion = candidate;
1671
+ break;
1672
+ }
1673
+ }
1674
+ if (completion?.data !== undefined) {
1675
+ const terminalState = completion.data.terminalState;
1676
+ if (
1677
+ terminalState === 'completed' ||
1678
+ terminalState === 'aborted' ||
1679
+ terminalState === 'failed'
1680
+ ) {
1681
+ return {
1682
+ runId,
1683
+ terminalState,
1684
+ };
1685
+ }
1686
+ }
1687
+ return undefined;
1688
+ }
1689
+
1690
+ private requireSession(sessionId: string): SessionState {
1691
+ const session = this.sessions.get(sessionId);
1692
+ if (session !== undefined) {
1693
+ return session;
1694
+ }
1695
+ const persisted = this.sessionStore.getSession(sessionId);
1696
+ if (persisted === undefined) {
1697
+ throw new Error(`session not found: ${sessionId}`);
1698
+ }
1699
+ const hydrated = this.hydrateSessionState(persisted);
1700
+ this.sessions.set(sessionId, hydrated);
1701
+ return hydrated;
1702
+ }
1703
+
1704
+ private toHandle(state: SessionState): SessionHandle {
1705
+ return {
1706
+ sessionId: state.sessionId,
1707
+ tenantId: state.tenantId,
1708
+ userId: state.userId,
1709
+ model: state.model,
1710
+ lane: state.lane,
1711
+ ...(state.soulHash !== undefined ? { soulHash: state.soulHash } : {}),
1712
+ ...(state.skillsSnapshotVersion !== undefined
1713
+ ? { skillsSnapshotVersion: state.skillsSnapshotVersion }
1714
+ : {}),
1715
+ };
1716
+ }
1717
+
1718
+ private toHandleFromPersisted(state: NimPersistedSession): SessionHandle {
1719
+ return {
1720
+ sessionId: state.sessionId,
1721
+ tenantId: state.tenantId,
1722
+ userId: state.userId,
1723
+ model: state.model,
1724
+ lane: state.lane,
1725
+ ...(state.soulHash !== undefined ? { soulHash: state.soulHash } : {}),
1726
+ ...(state.skillsSnapshotVersion !== undefined
1727
+ ? { skillsSnapshotVersion: state.skillsSnapshotVersion }
1728
+ : {}),
1729
+ };
1730
+ }
1731
+
1732
+ private persistSession(state: SessionState): void {
1733
+ this.sessionStore.upsertSession({
1734
+ sessionId: state.sessionId,
1735
+ tenantId: state.tenantId,
1736
+ userId: state.userId,
1737
+ model: state.model,
1738
+ lane: state.lane,
1739
+ ...(state.soulHash !== undefined ? { soulHash: state.soulHash } : {}),
1740
+ ...(state.skillsSnapshotVersion !== undefined
1741
+ ? { skillsSnapshotVersion: state.skillsSnapshotVersion }
1742
+ : {}),
1743
+ eventSeq: state.eventSeq,
1744
+ ...(state.lastRunId !== undefined ? { lastRunId: state.lastRunId } : {}),
1745
+ followups: state.queuedTurns.map((queuedTurn) => ({
1746
+ queueId: queuedTurn.queueId,
1747
+ text: queuedTurn.text,
1748
+ priority: queuedTurn.priority,
1749
+ dedupeKey: queuedTurn.dedupeKey,
1750
+ })),
1751
+ });
1752
+ }
1753
+
1754
+ private hydrateSessionState(state: NimPersistedSession): SessionState {
1755
+ const idempotencyToRunId = new Map<string, string>();
1756
+ for (const entry of this.sessionStore.listIdempotency(state.sessionId)) {
1757
+ idempotencyToRunId.set(entry.idempotencyKey, entry.runId);
1758
+ }
1759
+ return {
1760
+ sessionId: state.sessionId,
1761
+ tenantId: state.tenantId,
1762
+ userId: state.userId,
1763
+ model: state.model,
1764
+ lane: state.lane,
1765
+ ...(state.soulHash !== undefined ? { soulHash: state.soulHash } : {}),
1766
+ ...(state.skillsSnapshotVersion !== undefined
1767
+ ? { skillsSnapshotVersion: state.skillsSnapshotVersion }
1768
+ : {}),
1769
+ eventSeq: state.eventSeq,
1770
+ ...(state.lastRunId !== undefined ? { lastRunId: state.lastRunId } : {}),
1771
+ queuedTurns: state.followups.map((queuedTurn) => ({
1772
+ queueId: queuedTurn.queueId,
1773
+ text: queuedTurn.text,
1774
+ priority: queuedTurn.priority,
1775
+ dedupeKey: queuedTurn.dedupeKey,
1776
+ })),
1777
+ idempotencyToRunId,
1778
+ };
1779
+ }
1780
+
1781
+ private async enqueueSessionAndGlobal(
1782
+ sessionId: string,
1783
+ task: () => Promise<void>,
1784
+ ): Promise<void> {
1785
+ const sessionLane = this.sessionLanes.get(sessionId) ?? Promise.resolve();
1786
+ const nextSessionLane = sessionLane
1787
+ .catch(() => undefined)
1788
+ .then(async () => {
1789
+ const nextGlobal = this.globalLane.catch(() => undefined).then(task);
1790
+ this.globalLane = nextGlobal;
1791
+ await nextGlobal;
1792
+ });
1793
+
1794
+ this.sessionLanes.set(sessionId, nextSessionLane);
1795
+ await nextSessionLane;
1796
+ }
1797
+ }