@jmoyers/harness 0.1.10 → 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.
- package/README.md +31 -35
- package/package.json +31 -11
- package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
- package/packages/harness-ai/src/stream-text.ts +13 -91
- package/packages/harness-ui/src/frame-primitives.ts +158 -0
- package/packages/harness-ui/src/index.ts +18 -0
- package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
- package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
- package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
- package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
- package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
- package/packages/harness-ui/src/interaction/input.ts +420 -0
- package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
- package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
- package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
- package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
- package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
- package/packages/harness-ui/src/kit.ts +476 -0
- package/packages/harness-ui/src/layout.ts +238 -0
- package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
- package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
- package/packages/harness-ui/src/surface.ts +252 -0
- package/packages/harness-ui/src/text-layout.ts +210 -0
- package/packages/nim-core/src/contracts.ts +239 -0
- package/packages/nim-core/src/event-store.ts +299 -0
- package/packages/nim-core/src/events.ts +53 -0
- package/packages/nim-core/src/index.ts +9 -0
- package/packages/nim-core/src/provider-router.ts +129 -0
- package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
- package/packages/nim-core/src/runtime-factory.ts +49 -0
- package/packages/nim-core/src/runtime.ts +1797 -0
- package/packages/nim-core/src/session-store.ts +516 -0
- package/packages/nim-core/src/telemetry.ts +48 -0
- package/packages/nim-test-tui/src/index.ts +150 -0
- package/packages/nim-ui-core/src/index.ts +1 -0
- package/packages/nim-ui-core/src/projection.ts +87 -0
- package/scripts/codex-live-mux-runtime.ts +2 -3721
- package/scripts/control-plane-daemon.ts +24 -2
- package/scripts/harness-bin.js +5 -0
- package/scripts/harness-commands.ts +300 -0
- package/scripts/harness-runtime.ts +82 -0
- package/scripts/harness.ts +33 -3007
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/cli/gateway/runtime.ts +1872 -0
- package/src/cli/parsing/flags.ts +23 -0
- package/src/cli/parsing/session.ts +42 -0
- package/src/cli/runtime/context.ts +193 -0
- package/src/cli/runtime-app/application.ts +392 -0
- package/src/cli/runtime-infra/gateway-control.ts +729 -0
- package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
- package/src/cli/workflows/runtime.ts +965 -0
- package/src/clients/tui/left-rail-interactions.ts +519 -0
- package/src/clients/tui/main-pane-interactions.ts +509 -0
- package/src/clients/tui/modal-input-routing.ts +71 -0
- package/src/clients/tui/render-snapshot-adapter.ts +88 -0
- package/src/clients/web/synced-selectors.ts +132 -0
- package/src/codex/live-session.ts +82 -29
- package/src/config/config-core.ts +361 -10
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/prompt/thread-title-namer.ts +49 -23
- package/src/control-plane/session-summary.ts +10 -81
- package/src/control-plane/status/reducer-base.ts +12 -12
- package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
- package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
- package/src/control-plane/stream-client.ts +12 -2
- package/src/control-plane/stream-command-parser.ts +83 -143
- package/src/control-plane/stream-protocol.ts +53 -37
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +943 -80
- package/src/control-plane/stream-session-runtime-types.ts +41 -0
- package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
- package/src/core/state/observed-stream-cursor.ts +43 -0
- package/src/core/state/synced-observed-state.ts +273 -0
- package/src/core/store/harness-synced-store.ts +81 -0
- package/src/diff/budget.ts +136 -0
- package/src/diff/build.ts +289 -0
- package/src/diff/chunker.ts +146 -0
- package/src/diff/git-invoke.ts +315 -0
- package/src/diff/git-parse.ts +472 -0
- package/src/diff/hash.ts +70 -0
- package/src/diff/index.ts +24 -0
- package/src/diff/normalize.ts +134 -0
- package/src/diff/types.ts +178 -0
- package/src/diff-ui/args.ts +346 -0
- package/src/diff-ui/commands.ts +123 -0
- package/src/diff-ui/finder.ts +94 -0
- package/src/diff-ui/highlight.ts +127 -0
- package/src/diff-ui/index.ts +2 -0
- package/src/diff-ui/model.ts +141 -0
- package/src/diff-ui/pager.ts +412 -0
- package/src/diff-ui/render.ts +337 -0
- package/src/diff-ui/runtime.ts +379 -0
- package/src/diff-ui/state.ts +224 -0
- package/src/diff-ui/types.ts +236 -0
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +76 -4
- package/src/mux/control-plane-op-queue.ts +93 -7
- package/src/mux/conversation-rail.ts +28 -71
- package/src/mux/dual-pane-core.ts +13 -13
- package/src/mux/harness-core-ui.ts +313 -42
- package/src/mux/input-shortcuts.ts +22 -112
- package/src/mux/keybinding-catalog.ts +340 -0
- package/src/mux/keybinding-registry.ts +103 -0
- package/src/mux/live-mux/command-menu-open-in.ts +280 -0
- package/src/mux/live-mux/command-menu.ts +167 -4
- package/src/mux/live-mux/conversation-state.ts +13 -0
- package/src/mux/live-mux/directory-resolution.ts +1 -1
- package/src/mux/live-mux/git-parsing.ts +16 -0
- package/src/mux/live-mux/git-snapshot.ts +33 -2
- package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
- package/src/mux/live-mux/home-pane-drop.ts +1 -1
- package/src/mux/live-mux/home-pane-pointer.ts +10 -0
- package/src/mux/live-mux/input-forwarding.ts +59 -2
- package/src/mux/live-mux/left-nav-activation.ts +124 -7
- package/src/mux/live-mux/left-nav.ts +35 -0
- package/src/mux/live-mux/link-click.ts +292 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
- package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
- package/src/mux/live-mux/modal-input-reducers.ts +106 -8
- package/src/mux/live-mux/modal-overlays.ts +210 -31
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
- package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
- package/src/mux/live-mux/pointer-routing.ts +5 -2
- package/src/mux/live-mux/project-pane-pointer.ts +8 -0
- package/src/mux/live-mux/rail-layout.ts +33 -30
- package/src/mux/live-mux/release-notes.ts +383 -0
- package/src/mux/live-mux/render-trace-analysis.ts +52 -7
- package/src/mux/live-mux/repository-folding.ts +3 -0
- package/src/mux/live-mux/selection.ts +0 -4
- package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
- package/src/mux/project-pane-github-review.ts +271 -0
- package/src/mux/render-frame.ts +4 -0
- package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
- package/src/mux/task-composer.ts +21 -14
- package/src/mux/task-focused-pane.ts +118 -117
- package/src/mux/task-screen-keybindings.ts +19 -82
- package/src/mux/workspace-rail-model.ts +270 -104
- package/src/mux/workspace-rail.ts +45 -22
- package/src/pty/session-broker.ts +1 -1
- package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
- package/src/services/control-plane.ts +50 -32
- package/src/services/conversation-lifecycle.ts +118 -87
- package/src/services/conversation-startup-hydration.ts +20 -12
- package/src/services/directory-hydration.ts +21 -16
- package/src/services/event-persistence.ts +7 -0
- package/src/services/left-rail-pointer-handler.ts +329 -0
- package/src/services/mux-ui-state-persistence.ts +5 -1
- package/src/services/recording.ts +34 -26
- package/src/services/runtime-command-menu-agent-tools.ts +1 -1
- package/src/services/runtime-control-actions.ts +79 -61
- package/src/services/runtime-control-plane-ops.ts +122 -83
- package/src/services/runtime-conversation-actions.ts +40 -26
- package/src/services/runtime-conversation-activation.ts +82 -30
- package/src/services/runtime-conversation-starter.ts +80 -48
- package/src/services/runtime-conversation-title-edit.ts +91 -80
- package/src/services/runtime-envelope-handler.ts +107 -105
- package/src/services/runtime-git-state.ts +42 -29
- package/src/services/runtime-layout-resize.ts +3 -1
- package/src/services/runtime-left-rail-render.ts +99 -63
- package/src/services/runtime-nim-cli-session.ts +438 -0
- package/src/services/runtime-nim-session.ts +705 -0
- package/src/services/runtime-nim-tool-bridge.ts +141 -0
- package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
- package/src/services/runtime-process-wiring.ts +29 -36
- package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
- package/src/services/runtime-render-flush.ts +63 -70
- package/src/services/runtime-render-lifecycle.ts +65 -64
- package/src/services/runtime-render-orchestrator.ts +55 -45
- package/src/services/runtime-render-pipeline.ts +106 -103
- package/src/services/runtime-render-state.ts +62 -49
- package/src/services/runtime-repository-actions.ts +97 -70
- package/src/services/runtime-right-pane-render.ts +80 -53
- package/src/services/runtime-shutdown.ts +38 -35
- package/src/services/runtime-stream-subscriptions.ts +35 -27
- package/src/services/runtime-task-composer-persistence.ts +71 -59
- package/src/services/runtime-task-composer-snapshot.ts +14 -0
- package/src/services/runtime-task-editor-actions.ts +46 -29
- package/src/services/runtime-task-pane-actions.ts +220 -134
- package/src/services/runtime-task-pane-shortcuts.ts +323 -123
- package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
- package/src/services/runtime-workspace-observed-events.ts +33 -184
- package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
- package/src/services/session-diagnostics-store.ts +217 -0
- package/src/services/startup-background-resume.ts +26 -21
- package/src/services/startup-orchestrator.ts +16 -13
- package/src/services/startup-paint-tracker.ts +29 -21
- package/src/services/startup-persisted-conversation-queue.ts +19 -13
- package/src/services/startup-settled-gate.ts +25 -15
- package/src/services/startup-shutdown.ts +18 -22
- package/src/services/startup-state-hydration.ts +44 -34
- package/src/services/startup-visibility.ts +12 -4
- package/src/services/task-pane-selection-actions.ts +89 -72
- package/src/services/task-planning-hydration.ts +24 -18
- package/src/services/task-planning-observed-events.ts +50 -52
- package/src/services/workspace-observed-events.ts +66 -63
- package/src/storage/storage-lifecycle-core.ts +438 -0
- package/src/store/control-plane-store-normalize.ts +33 -242
- package/src/store/control-plane-store-types.ts +1 -35
- package/src/store/control-plane-store.ts +396 -56
- package/src/store/event-store.ts +397 -3
- package/src/terminal/snapshot-oracle.ts +207 -94
- package/src/ui/mux-theme.ts +112 -8
- package/src/ui/panes/home-gridfire.ts +40 -31
- package/src/ui/panes/home.ts +10 -2
- package/src/ui/panes/nim.ts +315 -0
- package/src/mux/live-mux/actions-task.ts +0 -115
- package/src/mux/live-mux/left-rail-actions.ts +0 -118
- package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
- package/src/mux/live-mux/left-rail-pointer.ts +0 -74
- package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
- package/src/services/runtime-directory-actions.ts +0 -164
- package/src/services/runtime-input-pipeline.ts +0 -50
- package/src/services/runtime-input-router.ts +0 -189
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -119
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -278
- package/src/services/runtime-task-pane.ts +0 -62
- package/src/services/runtime-workspace-actions.ts +0 -158
- package/src/ui/conversation-input-forwarder.ts +0 -114
- package/src/ui/conversation-selection-input.ts +0 -103
- package/src/ui/global-shortcut-input.ts +0 -89
- package/src/ui/input.ts +0 -238
- package/src/ui/kit.ts +0 -509
- package/src/ui/left-nav-input.ts +0 -80
- package/src/ui/left-rail-pointer-input.ts +0 -148
- package/src/ui/repository-fold-input.ts +0 -91
- package/src/ui/surface.ts +0 -224
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import type { NimModelRef } from './contracts.ts';
|
|
5
|
+
|
|
6
|
+
export type NimPersistedSession = {
|
|
7
|
+
readonly sessionId: string;
|
|
8
|
+
readonly tenantId: string;
|
|
9
|
+
readonly userId: string;
|
|
10
|
+
readonly model: NimModelRef;
|
|
11
|
+
readonly lane: string;
|
|
12
|
+
readonly soulHash?: string;
|
|
13
|
+
readonly skillsSnapshotVersion?: number;
|
|
14
|
+
readonly eventSeq: number;
|
|
15
|
+
readonly lastRunId?: string;
|
|
16
|
+
readonly followups: readonly NimPersistedFollowUp[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NimPersistedIdempotency = {
|
|
20
|
+
readonly idempotencyKey: string;
|
|
21
|
+
readonly runId: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type NimPersistedFollowUp = {
|
|
25
|
+
readonly queueId: string;
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly priority: 'normal' | 'high';
|
|
28
|
+
readonly dedupeKey: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface NimSessionStore {
|
|
32
|
+
upsertSession(session: NimPersistedSession): void;
|
|
33
|
+
getSession(sessionId: string): NimPersistedSession | undefined;
|
|
34
|
+
listSessions(tenantId: string, userId: string): readonly NimPersistedSession[];
|
|
35
|
+
upsertIdempotency(sessionId: string, idempotencyKey: string, runId: string): void;
|
|
36
|
+
getRunIdByIdempotency(sessionId: string, idempotencyKey: string): string | undefined;
|
|
37
|
+
listIdempotency(sessionId: string): readonly NimPersistedIdempotency[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class InMemoryNimSessionStore implements NimSessionStore {
|
|
41
|
+
private sessions = new Map<string, NimPersistedSession>();
|
|
42
|
+
private idempotencyBySession = new Map<string, Map<string, string>>();
|
|
43
|
+
|
|
44
|
+
public upsertSession(session: NimPersistedSession): void {
|
|
45
|
+
this.sessions.set(session.sessionId, session);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public getSession(sessionId: string): NimPersistedSession | undefined {
|
|
49
|
+
return this.sessions.get(sessionId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public listSessions(tenantId: string, userId: string): readonly NimPersistedSession[] {
|
|
53
|
+
return Array.from(this.sessions.values()).filter(
|
|
54
|
+
(session) => session.tenantId === tenantId && session.userId === userId,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public upsertIdempotency(sessionId: string, idempotencyKey: string, runId: string): void {
|
|
59
|
+
let map = this.idempotencyBySession.get(sessionId);
|
|
60
|
+
if (map === undefined) {
|
|
61
|
+
map = new Map<string, string>();
|
|
62
|
+
this.idempotencyBySession.set(sessionId, map);
|
|
63
|
+
}
|
|
64
|
+
if (map.has(idempotencyKey)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
map.set(idempotencyKey, runId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getRunIdByIdempotency(sessionId: string, idempotencyKey: string): string | undefined {
|
|
71
|
+
return this.idempotencyBySession.get(sessionId)?.get(idempotencyKey);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public listIdempotency(sessionId: string): readonly NimPersistedIdempotency[] {
|
|
75
|
+
const map = this.idempotencyBySession.get(sessionId);
|
|
76
|
+
if (map === undefined) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return Array.from(map.entries()).map(([idempotencyKey, runId]) => ({
|
|
80
|
+
idempotencyKey,
|
|
81
|
+
runId,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface StatementLike {
|
|
87
|
+
run: (...params: unknown[]) => unknown;
|
|
88
|
+
get: (...params: unknown[]) => unknown;
|
|
89
|
+
all: (...params: unknown[]) => unknown[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class WrappedStatement {
|
|
93
|
+
private readonly statement: StatementLike;
|
|
94
|
+
|
|
95
|
+
public constructor(statement: StatementLike) {
|
|
96
|
+
this.statement = statement;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public run(...params: unknown[]): unknown {
|
|
100
|
+
return this.statement.run(...params);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public get(...params: unknown[]): unknown {
|
|
104
|
+
const value = this.statement.get(...params);
|
|
105
|
+
return value === null ? undefined : value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public all(...params: unknown[]): unknown[] {
|
|
109
|
+
return this.statement.all(...params);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface SqliteDatabaseLike {
|
|
114
|
+
close: () => void;
|
|
115
|
+
exec: (sql: string) => unknown;
|
|
116
|
+
prepare: (sql: string) => StatementLike;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type BunSqliteModule = {
|
|
120
|
+
Database: new (path: string) => SqliteDatabaseLike;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
interface SqliteRuntime {
|
|
124
|
+
readonly bunVersion: string | undefined;
|
|
125
|
+
readonly loadModule: (specifier: 'bun:sqlite') => unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const require = createRequire(import.meta.url);
|
|
129
|
+
const defaultRuntime: SqliteRuntime = {
|
|
130
|
+
bunVersion: process.versions.bun,
|
|
131
|
+
loadModule: (specifier) => require(specifier) as unknown,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function createDatabaseForRuntime(
|
|
135
|
+
path: string,
|
|
136
|
+
runtime: SqliteRuntime = defaultRuntime,
|
|
137
|
+
): SqliteDatabaseLike {
|
|
138
|
+
if (runtime.bunVersion === undefined) {
|
|
139
|
+
throw new Error('bun runtime is required for sqlite access');
|
|
140
|
+
}
|
|
141
|
+
const module = runtime.loadModule('bun:sqlite') as BunSqliteModule;
|
|
142
|
+
return new module.Database(path);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class DatabaseSync {
|
|
146
|
+
private readonly database: SqliteDatabaseLike;
|
|
147
|
+
|
|
148
|
+
public constructor(path: string, runtime: SqliteRuntime = defaultRuntime) {
|
|
149
|
+
this.database = createDatabaseForRuntime(path, runtime);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public close(): void {
|
|
153
|
+
this.database.close();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public exec(sql: string): void {
|
|
157
|
+
this.database.exec(sql);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public prepare(sql: string): WrappedStatement {
|
|
161
|
+
return new WrappedStatement(this.database.prepare(sql));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const NIM_SESSION_STORE_SCHEMA_VERSION = 2;
|
|
166
|
+
|
|
167
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
168
|
+
if (typeof value !== 'object' || value === null) {
|
|
169
|
+
throw new Error('expected sqlite row object');
|
|
170
|
+
}
|
|
171
|
+
return value as Record<string, unknown>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function asString(value: unknown, field: string): string {
|
|
175
|
+
if (typeof value !== 'string') {
|
|
176
|
+
throw new Error(`expected string for ${field}`);
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function asFollowupPriority(value: unknown, field: string): 'normal' | 'high' {
|
|
182
|
+
if (value === 'normal' || value === 'high') {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`expected follow-up priority for ${field}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function asStringOrUndefined(value: unknown, field: string): string | undefined {
|
|
189
|
+
if (value === null || value === undefined) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return asString(value, field);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function asNonNegativeInteger(value: unknown, field: string): number {
|
|
196
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
197
|
+
throw new Error(`expected non-negative integer for ${field}`);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function asNonNegativeIntegerOrUndefined(value: unknown, field: string): number | undefined {
|
|
203
|
+
if (value === null || value === undefined) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
return asNonNegativeInteger(value, field);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseFollowupsJson(value: unknown): readonly NimPersistedFollowUp[] {
|
|
210
|
+
const json = asString(value, 'followups_json');
|
|
211
|
+
let parsed: unknown;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(json);
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error('invalid followups_json');
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(parsed)) {
|
|
218
|
+
throw new Error('expected followups_json array');
|
|
219
|
+
}
|
|
220
|
+
return parsed.map((item, index) => {
|
|
221
|
+
const row = asRecord(item);
|
|
222
|
+
return {
|
|
223
|
+
queueId: asString(row.queueId, `followups_json[${String(index)}].queueId`),
|
|
224
|
+
text: asString(row.text, `followups_json[${String(index)}].text`),
|
|
225
|
+
priority: asFollowupPriority(row.priority, `followups_json[${String(index)}].priority`),
|
|
226
|
+
dedupeKey: asString(row.dedupeKey, `followups_json[${String(index)}].dedupeKey`),
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parsePersistedSessionRow(row: unknown): NimPersistedSession {
|
|
232
|
+
const value = asRecord(row);
|
|
233
|
+
const soulHash = asStringOrUndefined(value.soul_hash, 'soul_hash');
|
|
234
|
+
const skillsSnapshotVersion = asNonNegativeIntegerOrUndefined(
|
|
235
|
+
value.skills_snapshot_version,
|
|
236
|
+
'skills_snapshot_version',
|
|
237
|
+
);
|
|
238
|
+
const lastRunId = asStringOrUndefined(value.last_run_id, 'last_run_id');
|
|
239
|
+
const followups = parseFollowupsJson(value.followups_json);
|
|
240
|
+
return {
|
|
241
|
+
sessionId: asString(value.session_id, 'session_id'),
|
|
242
|
+
tenantId: asString(value.tenant_id, 'tenant_id'),
|
|
243
|
+
userId: asString(value.user_id, 'user_id'),
|
|
244
|
+
model: asString(value.model, 'model') as NimModelRef,
|
|
245
|
+
lane: asString(value.lane, 'lane'),
|
|
246
|
+
...(soulHash !== undefined ? { soulHash } : {}),
|
|
247
|
+
...(skillsSnapshotVersion !== undefined ? { skillsSnapshotVersion } : {}),
|
|
248
|
+
eventSeq: asNonNegativeInteger(value.event_seq, 'event_seq'),
|
|
249
|
+
...(lastRunId !== undefined ? { lastRunId } : {}),
|
|
250
|
+
followups,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseIdempotencyRow(row: unknown): NimPersistedIdempotency {
|
|
255
|
+
const value = asRecord(row);
|
|
256
|
+
return {
|
|
257
|
+
idempotencyKey: asString(value.idempotency_key, 'idempotency_key'),
|
|
258
|
+
runId: asString(value.run_id, 'run_id'),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function preparePath(filePath: string): string {
|
|
263
|
+
if (filePath === ':memory:') {
|
|
264
|
+
return filePath;
|
|
265
|
+
}
|
|
266
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
267
|
+
return filePath;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export class NimSqliteSessionStore implements NimSessionStore {
|
|
271
|
+
private readonly db: DatabaseSync;
|
|
272
|
+
|
|
273
|
+
public constructor(filePath = ':memory:') {
|
|
274
|
+
this.db = new DatabaseSync(preparePath(filePath));
|
|
275
|
+
this.configureConnection();
|
|
276
|
+
this.initializeSchema();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public close(): void {
|
|
280
|
+
this.db.close();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public upsertSession(session: NimPersistedSession): void {
|
|
284
|
+
this.db
|
|
285
|
+
.prepare(
|
|
286
|
+
`
|
|
287
|
+
INSERT INTO nim_sessions (
|
|
288
|
+
session_id,
|
|
289
|
+
tenant_id,
|
|
290
|
+
user_id,
|
|
291
|
+
model,
|
|
292
|
+
lane,
|
|
293
|
+
soul_hash,
|
|
294
|
+
skills_snapshot_version,
|
|
295
|
+
event_seq,
|
|
296
|
+
last_run_id,
|
|
297
|
+
followups_json
|
|
298
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
299
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
300
|
+
tenant_id = excluded.tenant_id,
|
|
301
|
+
user_id = excluded.user_id,
|
|
302
|
+
model = excluded.model,
|
|
303
|
+
lane = excluded.lane,
|
|
304
|
+
soul_hash = excluded.soul_hash,
|
|
305
|
+
skills_snapshot_version = excluded.skills_snapshot_version,
|
|
306
|
+
event_seq = excluded.event_seq,
|
|
307
|
+
last_run_id = excluded.last_run_id,
|
|
308
|
+
followups_json = excluded.followups_json
|
|
309
|
+
`,
|
|
310
|
+
)
|
|
311
|
+
.run(
|
|
312
|
+
session.sessionId,
|
|
313
|
+
session.tenantId,
|
|
314
|
+
session.userId,
|
|
315
|
+
session.model,
|
|
316
|
+
session.lane,
|
|
317
|
+
session.soulHash ?? null,
|
|
318
|
+
session.skillsSnapshotVersion ?? null,
|
|
319
|
+
session.eventSeq,
|
|
320
|
+
session.lastRunId ?? null,
|
|
321
|
+
JSON.stringify(session.followups),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
public getSession(sessionId: string): NimPersistedSession | undefined {
|
|
326
|
+
const row = this.db
|
|
327
|
+
.prepare(
|
|
328
|
+
`
|
|
329
|
+
SELECT
|
|
330
|
+
session_id,
|
|
331
|
+
tenant_id,
|
|
332
|
+
user_id,
|
|
333
|
+
model,
|
|
334
|
+
lane,
|
|
335
|
+
soul_hash,
|
|
336
|
+
skills_snapshot_version,
|
|
337
|
+
event_seq,
|
|
338
|
+
last_run_id,
|
|
339
|
+
followups_json
|
|
340
|
+
FROM nim_sessions
|
|
341
|
+
WHERE session_id = ?
|
|
342
|
+
LIMIT 1
|
|
343
|
+
`,
|
|
344
|
+
)
|
|
345
|
+
.get(sessionId);
|
|
346
|
+
if (row === undefined) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
return parsePersistedSessionRow(row);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public listSessions(tenantId: string, userId: string): readonly NimPersistedSession[] {
|
|
353
|
+
const rows = this.db
|
|
354
|
+
.prepare(
|
|
355
|
+
`
|
|
356
|
+
SELECT
|
|
357
|
+
session_id,
|
|
358
|
+
tenant_id,
|
|
359
|
+
user_id,
|
|
360
|
+
model,
|
|
361
|
+
lane,
|
|
362
|
+
soul_hash,
|
|
363
|
+
skills_snapshot_version,
|
|
364
|
+
event_seq,
|
|
365
|
+
last_run_id,
|
|
366
|
+
followups_json
|
|
367
|
+
FROM nim_sessions
|
|
368
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
369
|
+
ORDER BY session_id ASC
|
|
370
|
+
`,
|
|
371
|
+
)
|
|
372
|
+
.all(tenantId, userId);
|
|
373
|
+
return rows.map((row) => parsePersistedSessionRow(row));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public upsertIdempotency(sessionId: string, idempotencyKey: string, runId: string): void {
|
|
377
|
+
this.db
|
|
378
|
+
.prepare(
|
|
379
|
+
`
|
|
380
|
+
INSERT OR IGNORE INTO nim_session_idempotency (
|
|
381
|
+
session_id,
|
|
382
|
+
idempotency_key,
|
|
383
|
+
run_id
|
|
384
|
+
) VALUES (?, ?, ?)
|
|
385
|
+
`,
|
|
386
|
+
)
|
|
387
|
+
.run(sessionId, idempotencyKey, runId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public getRunIdByIdempotency(sessionId: string, idempotencyKey: string): string | undefined {
|
|
391
|
+
const row = this.db
|
|
392
|
+
.prepare(
|
|
393
|
+
`
|
|
394
|
+
SELECT run_id
|
|
395
|
+
FROM nim_session_idempotency
|
|
396
|
+
WHERE session_id = ? AND idempotency_key = ?
|
|
397
|
+
LIMIT 1
|
|
398
|
+
`,
|
|
399
|
+
)
|
|
400
|
+
.get(sessionId, idempotencyKey);
|
|
401
|
+
if (row === undefined) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
return asString(asRecord(row).run_id, 'run_id');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public listIdempotency(sessionId: string): readonly NimPersistedIdempotency[] {
|
|
408
|
+
const rows = this.db
|
|
409
|
+
.prepare(
|
|
410
|
+
`
|
|
411
|
+
SELECT idempotency_key, run_id
|
|
412
|
+
FROM nim_session_idempotency
|
|
413
|
+
WHERE session_id = ?
|
|
414
|
+
ORDER BY idempotency_key ASC
|
|
415
|
+
`,
|
|
416
|
+
)
|
|
417
|
+
.all(sessionId);
|
|
418
|
+
return rows.map((row) => parseIdempotencyRow(row));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private configureConnection(): void {
|
|
422
|
+
this.db.exec('PRAGMA journal_mode = WAL;');
|
|
423
|
+
this.db.exec('PRAGMA synchronous = NORMAL;');
|
|
424
|
+
this.db.exec('PRAGMA busy_timeout = 5000;');
|
|
425
|
+
this.db.exec('PRAGMA foreign_keys = ON;');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private initializeSchema(): void {
|
|
429
|
+
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
430
|
+
try {
|
|
431
|
+
const currentVersion = this.readSchemaVersion();
|
|
432
|
+
if (currentVersion > NIM_SESSION_STORE_SCHEMA_VERSION) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`nim session store schema version ${String(currentVersion)} is newer than supported version ${String(NIM_SESSION_STORE_SCHEMA_VERSION)}`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (currentVersion < 1) {
|
|
438
|
+
this.applySchemaV1();
|
|
439
|
+
}
|
|
440
|
+
if (currentVersion < 2) {
|
|
441
|
+
this.applySchemaV2();
|
|
442
|
+
}
|
|
443
|
+
this.writeSchemaVersion(NIM_SESSION_STORE_SCHEMA_VERSION);
|
|
444
|
+
this.db.exec('COMMIT');
|
|
445
|
+
} catch (error) {
|
|
446
|
+
this.db.exec('ROLLBACK');
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private applySchemaV1(): void {
|
|
452
|
+
this.db.exec(`
|
|
453
|
+
CREATE TABLE IF NOT EXISTS nim_sessions (
|
|
454
|
+
session_id TEXT PRIMARY KEY,
|
|
455
|
+
tenant_id TEXT NOT NULL,
|
|
456
|
+
user_id TEXT NOT NULL,
|
|
457
|
+
model TEXT NOT NULL,
|
|
458
|
+
lane TEXT NOT NULL,
|
|
459
|
+
soul_hash TEXT,
|
|
460
|
+
skills_snapshot_version INTEGER,
|
|
461
|
+
event_seq INTEGER NOT NULL,
|
|
462
|
+
last_run_id TEXT
|
|
463
|
+
);
|
|
464
|
+
`);
|
|
465
|
+
this.db.exec(`
|
|
466
|
+
CREATE INDEX IF NOT EXISTS idx_nim_sessions_scope
|
|
467
|
+
ON nim_sessions (tenant_id, user_id, session_id);
|
|
468
|
+
`);
|
|
469
|
+
this.db.exec(`
|
|
470
|
+
CREATE TABLE IF NOT EXISTS nim_session_idempotency (
|
|
471
|
+
session_id TEXT NOT NULL,
|
|
472
|
+
idempotency_key TEXT NOT NULL,
|
|
473
|
+
run_id TEXT NOT NULL,
|
|
474
|
+
PRIMARY KEY (session_id, idempotency_key),
|
|
475
|
+
FOREIGN KEY (session_id) REFERENCES nim_sessions(session_id) ON DELETE CASCADE
|
|
476
|
+
);
|
|
477
|
+
`);
|
|
478
|
+
this.db.exec(`
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_nim_session_idempotency_session
|
|
480
|
+
ON nim_session_idempotency (session_id, idempotency_key);
|
|
481
|
+
`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private applySchemaV2(): void {
|
|
485
|
+
if (!this.tableHasColumn('nim_sessions', 'followups_json')) {
|
|
486
|
+
this.db.exec(`
|
|
487
|
+
ALTER TABLE nim_sessions
|
|
488
|
+
ADD COLUMN followups_json TEXT NOT NULL DEFAULT '[]';
|
|
489
|
+
`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private readSchemaVersion(): number {
|
|
494
|
+
const row = this.db.prepare('PRAGMA user_version;').get();
|
|
495
|
+
if (row === undefined) {
|
|
496
|
+
throw new Error('failed to read nim session store schema version');
|
|
497
|
+
}
|
|
498
|
+
const version = asRecord(row).user_version;
|
|
499
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 0) {
|
|
500
|
+
throw new Error(`invalid nim session store schema version value: ${String(version)}`);
|
|
501
|
+
}
|
|
502
|
+
return version;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private writeSchemaVersion(version: number): void {
|
|
506
|
+
this.db.exec(`PRAGMA user_version = ${String(version)};`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private tableHasColumn(tableName: string, columnName: string): boolean {
|
|
510
|
+
const rows = this.db.prepare(`PRAGMA table_info(${tableName});`).all();
|
|
511
|
+
return rows.some((row) => {
|
|
512
|
+
const name = asRecord(row).name;
|
|
513
|
+
return typeof name === 'string' && name === columnName;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { NimTelemetrySink } from './contracts.ts';
|
|
4
|
+
import { parseNimEventEnvelope, type NimEventEnvelope } from './events.ts';
|
|
5
|
+
|
|
6
|
+
export type NimJsonlTelemetrySinkInput = {
|
|
7
|
+
readonly filePath: string;
|
|
8
|
+
readonly mode?: 'append' | 'truncate';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class NimJsonlTelemetrySink implements NimTelemetrySink {
|
|
12
|
+
public readonly name: string;
|
|
13
|
+
private readonly filePath: string;
|
|
14
|
+
|
|
15
|
+
public constructor(input: NimJsonlTelemetrySinkInput) {
|
|
16
|
+
this.filePath = input.filePath;
|
|
17
|
+
this.name = `jsonl:${this.filePath}`;
|
|
18
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
19
|
+
if (input.mode !== 'append') {
|
|
20
|
+
writeFileSync(this.filePath, '', 'utf8');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public record(event: NimEventEnvelope): void {
|
|
25
|
+
appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readNimJsonlTelemetry(filePath: string): NimEventEnvelope[] {
|
|
30
|
+
const content = readFileSync(filePath, 'utf8').trim();
|
|
31
|
+
if (content.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const lines = content.split('\n');
|
|
35
|
+
return lines.map((line, index) => {
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`invalid Nim telemetry JSONL at line ${String(index + 1)}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return parseNimEventEnvelope(parsed);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(`invalid Nim telemetry event envelope at line ${String(index + 1)}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { NimEventEnvelope } from '../../nim-core/src/events.ts';
|
|
2
|
+
import type { NimRuntime, NimUiEvent } from '../../nim-core/src/contracts.ts';
|
|
3
|
+
import { projectEventToUiEvents, type NimUiMode } from '../../nim-ui-core/src/projection.ts';
|
|
4
|
+
|
|
5
|
+
export type TestTuiFrame = {
|
|
6
|
+
readonly mode: NimUiMode;
|
|
7
|
+
readonly runId: string;
|
|
8
|
+
readonly lines: readonly string[];
|
|
9
|
+
readonly state: 'thinking' | 'tool-calling' | 'responding' | 'idle';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CollectNimTestTuiFrameInput = {
|
|
13
|
+
readonly runtime: NimRuntime;
|
|
14
|
+
readonly tenantId: string;
|
|
15
|
+
readonly sessionId: string;
|
|
16
|
+
readonly mode: NimUiMode;
|
|
17
|
+
readonly fromEventIdExclusive?: string;
|
|
18
|
+
readonly timeoutMs?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type CollectNimTestTuiFrameResult = {
|
|
22
|
+
readonly frame: TestTuiFrame;
|
|
23
|
+
readonly lastEventId?: string;
|
|
24
|
+
readonly projectedEventCount: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class NimTestTuiController {
|
|
28
|
+
private mode: NimUiMode;
|
|
29
|
+
private runId: string;
|
|
30
|
+
private state: 'thinking' | 'tool-calling' | 'responding' | 'idle';
|
|
31
|
+
private lines: string[];
|
|
32
|
+
private pendingAssistantText: string;
|
|
33
|
+
|
|
34
|
+
public constructor(input: { mode: NimUiMode; runId: string }) {
|
|
35
|
+
this.mode = input.mode;
|
|
36
|
+
this.runId = input.runId;
|
|
37
|
+
this.state = 'idle';
|
|
38
|
+
this.lines = [];
|
|
39
|
+
this.pendingAssistantText = '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public consume(event: NimEventEnvelope): readonly NimUiEvent[] {
|
|
43
|
+
if (event.run_id.length > 0) {
|
|
44
|
+
this.runId = event.run_id;
|
|
45
|
+
}
|
|
46
|
+
const projected = projectEventToUiEvents(event, this.mode);
|
|
47
|
+
for (const item of projected) {
|
|
48
|
+
if (item.type === 'assistant.state') {
|
|
49
|
+
this.state = item.state;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (item.type === 'assistant.text.delta') {
|
|
53
|
+
this.pendingAssistantText += item.text;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (item.type === 'assistant.text.message') {
|
|
57
|
+
this.pendingAssistantText = '';
|
|
58
|
+
this.lines.push(item.text);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (item.type === 'tool.activity') {
|
|
62
|
+
this.lines.push(`[tool:${item.phase}] ${item.toolName}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (item.type === 'system.notice') {
|
|
66
|
+
this.lines.push(`[notice] ${item.text}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return projected;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public snapshot(): TestTuiFrame {
|
|
73
|
+
const lines =
|
|
74
|
+
this.pendingAssistantText.length > 0
|
|
75
|
+
? [...this.lines, this.pendingAssistantText]
|
|
76
|
+
: this.lines.slice();
|
|
77
|
+
return {
|
|
78
|
+
mode: this.mode,
|
|
79
|
+
runId: this.runId,
|
|
80
|
+
lines,
|
|
81
|
+
state: this.state,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function collectNimTestTuiFrame(
|
|
87
|
+
input: CollectNimTestTuiFrameInput,
|
|
88
|
+
): Promise<CollectNimTestTuiFrameResult> {
|
|
89
|
+
const stream = input.runtime.streamEvents({
|
|
90
|
+
tenantId: input.tenantId,
|
|
91
|
+
sessionId: input.sessionId,
|
|
92
|
+
...(input.fromEventIdExclusive !== undefined
|
|
93
|
+
? { fromEventIdExclusive: input.fromEventIdExclusive }
|
|
94
|
+
: {}),
|
|
95
|
+
fidelity: 'semantic',
|
|
96
|
+
});
|
|
97
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
98
|
+
const controller = new NimTestTuiController({
|
|
99
|
+
mode: input.mode,
|
|
100
|
+
runId: input.sessionId,
|
|
101
|
+
});
|
|
102
|
+
const timeoutMs = input.timeoutMs ?? 5000;
|
|
103
|
+
const deadline = Date.now() + timeoutMs;
|
|
104
|
+
let sawActiveState = false;
|
|
105
|
+
let projectedEventCount = 0;
|
|
106
|
+
let lastEventId: string | undefined;
|
|
107
|
+
try {
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
const remaining = deadline - Date.now();
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
const next = await Promise.race([
|
|
112
|
+
iterator.next(),
|
|
113
|
+
new Promise<never>((_, reject) => {
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
reject(new Error('timed out waiting for Nim test TUI idle frame'));
|
|
116
|
+
}, remaining);
|
|
117
|
+
}),
|
|
118
|
+
]).finally(() => {
|
|
119
|
+
if (timer !== undefined) {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
if (next.done) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
lastEventId = next.value.event_id;
|
|
127
|
+
const projected = controller.consume(next.value);
|
|
128
|
+
projectedEventCount += projected.length;
|
|
129
|
+
for (const item of projected) {
|
|
130
|
+
if (item.type !== 'assistant.state') {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (item.state !== 'idle') {
|
|
134
|
+
sawActiveState = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (sawActiveState) {
|
|
138
|
+
return {
|
|
139
|
+
frame: controller.snapshot(),
|
|
140
|
+
...(lastEventId !== undefined ? { lastEventId } : {}),
|
|
141
|
+
projectedEventCount,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new Error('timed out waiting for Nim test TUI idle frame');
|
|
147
|
+
} finally {
|
|
148
|
+
await iterator.return?.();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './projection.ts';
|