@harness-engineering/orchestrator 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,443 @@
1
+ import { Issue, AgentEvent, TokenUsage, WorkflowConfig, Result, WorkflowDefinition, IssueTrackerClient, TrackerConfig, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult } from '@harness-engineering/types';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ /**
5
+ * Run attempt lifecycle phases (internal to orchestrator).
6
+ * Tracks the current phase of a single run attempt.
7
+ */
8
+ type RunAttemptPhase = 'PreparingWorkspace' | 'BuildingPrompt' | 'LaunchingAgent' | 'InitializingSession' | 'StreamingTurn' | 'Finishing' | 'Succeeded' | 'Failed' | 'TimedOut' | 'Stalled' | 'CanceledByReconciliation';
9
+ /**
10
+ * Live session metadata tracked while an agent subprocess is running.
11
+ */
12
+ interface LiveSession {
13
+ sessionId: string;
14
+ backendName: string;
15
+ agentPid: number | null;
16
+ startedAt: string;
17
+ lastEvent: string | null;
18
+ lastTimestamp: string | null;
19
+ lastMessage: string | null;
20
+ inputTokens: number;
21
+ outputTokens: number;
22
+ totalTokens: number;
23
+ lastReportedInputTokens: number;
24
+ lastReportedOutputTokens: number;
25
+ lastReportedTotalTokens: number;
26
+ turnCount: number;
27
+ }
28
+ /**
29
+ * Entry in the running map: one active worker.
30
+ */
31
+ interface RunningEntry {
32
+ issueId: string;
33
+ identifier: string;
34
+ issue: Issue;
35
+ attempt: number | null;
36
+ workspacePath: string;
37
+ startedAt: string;
38
+ phase: RunAttemptPhase;
39
+ session: LiveSession | null;
40
+ }
41
+ /**
42
+ * Entry in the retry queue.
43
+ */
44
+ interface RetryEntry {
45
+ issueId: string;
46
+ identifier: string;
47
+ attempt: number;
48
+ dueAtMs: number;
49
+ error: string | null;
50
+ }
51
+ /**
52
+ * Token totals for observability.
53
+ */
54
+ interface TokenTotals {
55
+ inputTokens: number;
56
+ outputTokens: number;
57
+ totalTokens: number;
58
+ secondsRunning: number;
59
+ }
60
+ /**
61
+ * Rate limit snapshot (populated by agent events).
62
+ */
63
+ interface RateLimitSnapshot {
64
+ requestsRemaining: number | null;
65
+ requestsLimit: number | null;
66
+ tokensRemaining: number | null;
67
+ tokensLimit: number | null;
68
+ }
69
+ /**
70
+ * Single authoritative in-memory orchestrator state.
71
+ */
72
+ interface OrchestratorState {
73
+ pollIntervalMs: number;
74
+ maxConcurrentAgents: number;
75
+ running: Map<string, RunningEntry>;
76
+ claimed: Set<string>;
77
+ retryAttempts: Map<string, RetryEntry>;
78
+ completed: Set<string>;
79
+ tokenTotals: TokenTotals;
80
+ rateLimits: RateLimitSnapshot;
81
+ }
82
+
83
+ /**
84
+ * Discriminated union of events that drive the orchestrator state machine.
85
+ * All events are data -- the caller constructs them from I/O results.
86
+ */
87
+ type OrchestratorEvent = TickEvent | WorkerExitEvent | AgentUpdateEvent | RetryFiredEvent | StallDetectedEvent;
88
+ interface TickEvent {
89
+ type: 'tick';
90
+ candidates: Issue[];
91
+ runningStates: Map<string, Issue>;
92
+ /** Caller-supplied wall clock (ms since epoch). Keeps state machine pure. */
93
+ nowMs: number;
94
+ }
95
+ interface WorkerExitEvent {
96
+ type: 'worker_exit';
97
+ issueId: string;
98
+ reason: 'normal' | 'error';
99
+ error?: string | undefined;
100
+ attempt: number | null;
101
+ }
102
+ interface AgentUpdateEvent {
103
+ type: 'agent_update';
104
+ issueId: string;
105
+ event: AgentEvent;
106
+ }
107
+ interface RetryFiredEvent {
108
+ type: 'retry_fired';
109
+ issueId: string;
110
+ candidates: Issue[];
111
+ /** Caller-supplied wall clock (ms since epoch). Keeps state machine pure. */
112
+ nowMs: number;
113
+ }
114
+ interface StallDetectedEvent {
115
+ type: 'stall_detected';
116
+ issueId: string;
117
+ }
118
+ /**
119
+ * Discriminated union of side effects returned by the state machine.
120
+ * These are data describing what to do -- the orchestrator loop executes them.
121
+ */
122
+ type SideEffect = DispatchEffect | StopEffect | ScheduleRetryEffect | ReleaseClaimEffect | CleanWorkspaceEffect | UpdateTokensEffect | EmitLogEffect;
123
+ interface DispatchEffect {
124
+ type: 'dispatch';
125
+ issue: Issue;
126
+ attempt: number | null;
127
+ }
128
+ interface StopEffect {
129
+ type: 'stop';
130
+ issueId: string;
131
+ reason: string;
132
+ }
133
+ interface ScheduleRetryEffect {
134
+ type: 'scheduleRetry';
135
+ issueId: string;
136
+ identifier: string;
137
+ attempt: number;
138
+ delayMs: number;
139
+ error: string | null;
140
+ }
141
+ interface ReleaseClaimEffect {
142
+ type: 'releaseClaim';
143
+ issueId: string;
144
+ }
145
+ interface CleanWorkspaceEffect {
146
+ type: 'cleanWorkspace';
147
+ issueId: string;
148
+ identifier: string;
149
+ }
150
+ interface UpdateTokensEffect {
151
+ type: 'updateTokens';
152
+ issueId: string;
153
+ usage: TokenUsage;
154
+ }
155
+ interface EmitLogEffect {
156
+ type: 'emitLog';
157
+ level: 'info' | 'warn' | 'error';
158
+ message: string;
159
+ context?: Record<string, unknown>;
160
+ }
161
+
162
+ /**
163
+ * Calculate retry delay based on attempt number and retry type.
164
+ *
165
+ * Continuation retries: fixed 1000ms delay.
166
+ * Failure retries: exponential backoff 10000 * 2^(attempt-1), capped at maxRetryBackoffMs.
167
+ */
168
+ declare function calculateRetryDelay(attempt: number, type: 'continuation' | 'failure', maxRetryBackoffMs?: number): number;
169
+
170
+ /**
171
+ * Sort candidates by dispatch priority (stable sort).
172
+ * 1. priority ascending (1..4 preferred; null sorts last)
173
+ * 2. createdAt oldest first (null sorts last)
174
+ * 3. identifier lexicographic tie-breaker
175
+ */
176
+ declare function sortCandidates(issues: readonly Issue[]): Issue[];
177
+ /**
178
+ * Check if a single issue is dispatch-eligible.
179
+ * State comparisons are case-insensitive.
180
+ */
181
+ declare function isEligible(issue: Issue, state: OrchestratorState, activeStates: string[], terminalStates: string[]): boolean;
182
+ /**
183
+ * Select and sort eligible candidates from a list of issues.
184
+ */
185
+ declare function selectCandidates(issues: readonly Issue[], state: OrchestratorState, activeStates: string[], terminalStates: string[]): Issue[];
186
+
187
+ /**
188
+ * Get the number of available global concurrency slots.
189
+ */
190
+ declare function getAvailableSlots(state: OrchestratorState): number;
191
+ /**
192
+ * Count running entries by normalized (lowercase) issue state.
193
+ */
194
+ declare function getPerStateCount(running: ReadonlyMap<string, RunningEntry>): Map<string, number>;
195
+ /**
196
+ * Check if dispatching an issue in the given state is allowed
197
+ * by both global and per-state concurrency limits.
198
+ */
199
+ declare function canDispatch(state: OrchestratorState, issueState: string, maxConcurrentAgentsByState: Record<string, number>): boolean;
200
+
201
+ /**
202
+ * Reconcile running issues against their current tracker states.
203
+ *
204
+ * For each running issue found in runningStates:
205
+ * - Terminal state -> stop + cleanWorkspace + releaseClaim
206
+ * - Neither active nor terminal -> stop + releaseClaim (no workspace cleanup)
207
+ * - Still active -> no effects (keep running)
208
+ *
209
+ * Issues not found in runningStates are left running (state refresh may have
210
+ * partially failed; retry next tick per spec).
211
+ */
212
+ declare function reconcile(state: OrchestratorState, runningStates: ReadonlyMap<string, Issue>, activeStates: string[], terminalStates: string[]): SideEffect[];
213
+
214
+ interface ApplyEventResult {
215
+ nextState: OrchestratorState;
216
+ effects: SideEffect[];
217
+ }
218
+ /**
219
+ * Pure state machine transition function.
220
+ *
221
+ * Takes the current state, an event, and config.
222
+ * Returns the next state and a list of side effects to execute.
223
+ * No I/O is performed -- all side effects are returned as data.
224
+ */
225
+ declare function applyEvent(state: OrchestratorState, event: OrchestratorEvent, config: WorkflowConfig): ApplyEventResult;
226
+
227
+ /**
228
+ * Create an empty OrchestratorState initialized from config.
229
+ */
230
+ declare function createEmptyState(config: WorkflowConfig): OrchestratorState;
231
+
232
+ declare class WorkflowLoader {
233
+ loadWorkflow(filePath: string): Promise<Result<WorkflowDefinition, Error>>;
234
+ }
235
+
236
+ declare function validateWorkflowConfig(config: unknown): Result<WorkflowConfig, Error>;
237
+ declare function getDefaultConfig(): WorkflowConfig;
238
+
239
+ /**
240
+ * Adapter for using a markdown roadmap file as an issue tracker.
241
+ *
242
+ * This adapter parses a standard Harness roadmap file, extracts features,
243
+ * and maps them to the internal Issue model using deterministic hashing
244
+ * for identifiers.
245
+ */
246
+ declare class RoadmapTrackerAdapter implements IssueTrackerClient {
247
+ private config;
248
+ /**
249
+ * Creates a new RoadmapTrackerAdapter.
250
+ *
251
+ * @param config - The tracker configuration including the file path
252
+ */
253
+ constructor(config: TrackerConfig);
254
+ /**
255
+ * Fetches all issues that are in an "active" state according to the config.
256
+ */
257
+ fetchCandidateIssues(): Promise<Result<Issue[], Error>>;
258
+ /**
259
+ * Fetches issues that match any of the given state names.
260
+ *
261
+ * @param stateNames - List of statuses to filter by
262
+ */
263
+ fetchIssuesByStates(stateNames: string[]): Promise<Result<Issue[], Error>>;
264
+ /**
265
+ * Fetches full issue details for a list of identifiers.
266
+ *
267
+ * @param issueIds - List of issue IDs to fetch
268
+ */
269
+ fetchIssueStatesByIds(issueIds: string[]): Promise<Result<Map<string, Issue>, Error>>;
270
+ /**
271
+ * Maps a raw RoadmapFeature from the parser to the unified Issue model.
272
+ */
273
+ private mapFeatureToIssue;
274
+ /**
275
+ * Generates a deterministic, URL-safe identifier for a feature name.
276
+ */
277
+ private generateId;
278
+ }
279
+
280
+ /**
281
+ * Interface for Linear GraphQL tool extension.
282
+ * This is a stub implementation for Phase 4.
283
+ */
284
+ interface LinearGraphQLExtension {
285
+ query(query: string, variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
286
+ }
287
+ declare class LinearGraphQLStub implements LinearGraphQLExtension {
288
+ query(query: string, _variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
289
+ }
290
+
291
+ declare class WorkspaceManager {
292
+ private config;
293
+ constructor(config: WorkspaceConfig);
294
+ /**
295
+ * Sanitizes an issue identifier to be safe for use as a directory name.
296
+ */
297
+ sanitizeIdentifier(identifier: string): string;
298
+ /**
299
+ * Resolves the full path for an issue's workspace.
300
+ */
301
+ resolvePath(identifier: string): string;
302
+ /**
303
+ * Ensures the workspace directory exists.
304
+ */
305
+ ensureWorkspace(identifier: string): Promise<Result<string, Error>>;
306
+ /**
307
+ * Checks if a workspace exists.
308
+ */
309
+ exists(identifier: string): Promise<boolean>;
310
+ /**
311
+ * Removes a workspace directory.
312
+ */
313
+ removeWorkspace(identifier: string): Promise<Result<void, Error>>;
314
+ }
315
+
316
+ declare class WorkspaceHooks {
317
+ private config;
318
+ constructor(config: HooksConfig);
319
+ /**
320
+ * Executes a shell hook in the specified workspace directory.
321
+ */
322
+ executeHook(hookName: keyof Omit<HooksConfig, 'timeoutMs'>, cwd: string): Promise<Result<void, Error>>;
323
+ afterCreate(cwd: string): Promise<Result<void, Error>>;
324
+ beforeRun(cwd: string): Promise<Result<void, Error>>;
325
+ afterRun(cwd: string): Promise<Result<void, Error>>;
326
+ beforeRemove(cwd: string): Promise<Result<void, Error>>;
327
+ }
328
+
329
+ declare class MockBackend implements AgentBackend {
330
+ readonly name = "mock";
331
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
332
+ runTurn(session: AgentSession, _params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
333
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
334
+ healthCheck(): Promise<Result<void, AgentError>>;
335
+ }
336
+
337
+ declare class ClaudeBackend implements AgentBackend {
338
+ readonly name = "claude";
339
+ private command;
340
+ constructor(command?: string);
341
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
342
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
343
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
344
+ healthCheck(): Promise<Result<void, AgentError>>;
345
+ }
346
+
347
+ declare class PromptRenderer {
348
+ private engine;
349
+ constructor();
350
+ render(template: string, context: Record<string, unknown>): Promise<string>;
351
+ }
352
+
353
+ /**
354
+ * The central orchestrator that manages the lifecycle of coding agents.
355
+ *
356
+ * It polls an issue tracker for candidate tasks, manages ephemeral workspaces,
357
+ * runs agents to resolve issues, and updates the tracker with progress.
358
+ *
359
+ * @fires Orchestrator#state_change Emitted when the internal state machine transitions
360
+ * @fires Orchestrator#agent_event Emitted when an agent produces an output or thought
361
+ */
362
+ declare class Orchestrator extends EventEmitter {
363
+ private state;
364
+ private config;
365
+ private tracker;
366
+ private workspace;
367
+ private hooks;
368
+ private runner;
369
+ private renderer;
370
+ private promptTemplate;
371
+ private server?;
372
+ private interval?;
373
+ private logger;
374
+ /**
375
+ * Creates a new Orchestrator instance.
376
+ *
377
+ * @param config - The workflow configuration
378
+ * @param promptTemplate - The template used to generate agent instructions
379
+ * @param overrides - Optional dependency overrides for testing or custom behavior
380
+ */
381
+ constructor(config: WorkflowConfig, promptTemplate: string, overrides?: {
382
+ tracker?: IssueTrackerClient;
383
+ backend?: AgentBackend;
384
+ });
385
+ private createTracker;
386
+ private createBackend;
387
+ /**
388
+ * Run a single tick of the orchestrator loop.
389
+ *
390
+ * This method fetches the latest issue states, applies them to the state machine,
391
+ * and executes any resulting side effects (dispatching new agents, stopping agents, etc.).
392
+ */
393
+ tick(): Promise<void>;
394
+ /**
395
+ * Processes a side effect generated by the state machine.
396
+ *
397
+ * @param effect - The effect to handle
398
+ */
399
+ private handleEffect;
400
+ /**
401
+ * Dispatches a new agent to work on an issue.
402
+ *
403
+ * @param issue - The issue to resolve
404
+ * @param attempt - The retry attempt number
405
+ */
406
+ private dispatchIssue;
407
+ /**
408
+ * Runs an agent session in a background task to avoid blocking the main loop.
409
+ */
410
+ private runAgentInBackgroundTask;
411
+ /**
412
+ * Informs the state machine that an agent worker has exited.
413
+ */
414
+ private emitWorkerExit;
415
+ /**
416
+ * Stops execution for a specific issue.
417
+ *
418
+ * @param issueId - The ID of the issue to stop
419
+ */
420
+ private stopIssue;
421
+ /**
422
+ * Starts the polling loop and the internal HTTP server.
423
+ */
424
+ start(): void;
425
+ /**
426
+ * Stops the orchestrator, clearing the polling interval and stopping the server.
427
+ */
428
+ stop(): Promise<void>;
429
+ /**
430
+ * Returns a point-in-time snapshot of the orchestrator's internal state.
431
+ */
432
+ getSnapshot(): Record<string, unknown>;
433
+ }
434
+
435
+ /**
436
+ * Launches the Ink TUI for the given Orchestrator instance.
437
+ * Returns a function to wait for the TUI to exit.
438
+ */
439
+ declare function launchTUI(orchestrator: Orchestrator): {
440
+ waitUntilExit: () => Promise<void>;
441
+ };
442
+
443
+ export { type AgentUpdateEvent, type ApplyEventResult, ClaudeBackend, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MockBackend, Orchestrator, type OrchestratorEvent, type OrchestratorState, PromptRenderer, type RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type TickEvent, type TokenTotals, type UpdateTokensEffect, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, applyEvent, calculateRetryDelay, canDispatch, createEmptyState, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, reconcile, selectCandidates, sortCandidates, validateWorkflowConfig };