@harness-engineering/orchestrator 0.2.5 → 0.2.7

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 CHANGED
@@ -119,10 +119,10 @@ The main class that ties everything together:
119
119
 
120
120
  ### Tracker Adapters
121
121
 
122
- | Export | Description |
123
- | ------------------------ | ---------------------------------------- |
124
- | `RoadmapTrackerAdapter` | Reads issues from `docs/roadmap.md` |
125
- | `LinearTrackerExtension` | Extends tracking with Linear integration |
122
+ | Export | Description |
123
+ | ------------------------ | ---------------------------------------------------- |
124
+ | `RoadmapTrackerAdapter` | Reads issues from `docs/roadmap.md` |
125
+ | `LinearTrackerExtension` | Extends tracking with Linear integration _(planned)_ |
126
126
 
127
127
  ### Prompt Rendering
128
128
 
package/dist/index.d.mts CHANGED
@@ -344,6 +344,70 @@ declare class ClaudeBackend implements AgentBackend {
344
344
  healthCheck(): Promise<Result<void, AgentError>>;
345
345
  }
346
346
 
347
+ interface OpenAIBackendConfig {
348
+ /** OpenAI model to use. Defaults to 'gpt-4o'. */
349
+ model?: string;
350
+ /** API key. Defaults to process.env.OPENAI_API_KEY. */
351
+ apiKey?: string;
352
+ }
353
+ interface OpenAISession extends AgentSession {
354
+ systemPrompt?: string;
355
+ }
356
+ declare class OpenAIBackend implements AgentBackend {
357
+ readonly name = "openai";
358
+ private config;
359
+ private client;
360
+ private cacheAdapter;
361
+ constructor(config?: OpenAIBackendConfig);
362
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
363
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
364
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
365
+ healthCheck(): Promise<Result<void, AgentError>>;
366
+ }
367
+
368
+ interface GeminiBackendConfig {
369
+ /** Gemini model to use. Defaults to 'gemini-2.0-flash'. */
370
+ model?: string;
371
+ /** API key. Defaults to process.env.GEMINI_API_KEY or process.env.GOOGLE_API_KEY. */
372
+ apiKey?: string;
373
+ }
374
+ interface GeminiSession extends AgentSession {
375
+ systemPrompt?: string;
376
+ }
377
+ declare class GeminiBackend implements AgentBackend {
378
+ readonly name = "gemini";
379
+ private config;
380
+ private cacheAdapter;
381
+ constructor(config?: GeminiBackendConfig);
382
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
383
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
384
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
385
+ healthCheck(): Promise<Result<void, AgentError>>;
386
+ }
387
+
388
+ interface AnthropicBackendConfig {
389
+ /** Anthropic model to use. Defaults to 'claude-sonnet-4-20250514'. */
390
+ model?: string;
391
+ /** API key. Defaults to process.env.ANTHROPIC_API_KEY. */
392
+ apiKey?: string;
393
+ /** Maximum output tokens. Defaults to 4096. */
394
+ maxTokens?: number;
395
+ }
396
+ interface AnthropicSession extends AgentSession {
397
+ systemPrompt?: string;
398
+ }
399
+ declare class AnthropicBackend implements AgentBackend {
400
+ readonly name = "anthropic";
401
+ private config;
402
+ private client;
403
+ private cacheAdapter;
404
+ constructor(config?: AnthropicBackendConfig);
405
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
406
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
407
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
408
+ healthCheck(): Promise<Result<void, AgentError>>;
409
+ }
410
+
347
411
  declare class PromptRenderer {
348
412
  private engine;
349
413
  constructor();
@@ -440,4 +504,4 @@ declare function launchTUI(orchestrator: Orchestrator): {
440
504
  waitUntilExit: () => Promise<void>;
441
505
  };
442
506
 
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 };
507
+ export { type AgentUpdateEvent, AnthropicBackend, type AnthropicBackendConfig, type AnthropicSession, type ApplyEventResult, ClaudeBackend, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, GeminiBackend, type GeminiBackendConfig, type GeminiSession, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MockBackend, OpenAIBackend, type OpenAIBackendConfig, type OpenAISession, 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 };
package/dist/index.d.ts CHANGED
@@ -344,6 +344,70 @@ declare class ClaudeBackend implements AgentBackend {
344
344
  healthCheck(): Promise<Result<void, AgentError>>;
345
345
  }
346
346
 
347
+ interface OpenAIBackendConfig {
348
+ /** OpenAI model to use. Defaults to 'gpt-4o'. */
349
+ model?: string;
350
+ /** API key. Defaults to process.env.OPENAI_API_KEY. */
351
+ apiKey?: string;
352
+ }
353
+ interface OpenAISession extends AgentSession {
354
+ systemPrompt?: string;
355
+ }
356
+ declare class OpenAIBackend implements AgentBackend {
357
+ readonly name = "openai";
358
+ private config;
359
+ private client;
360
+ private cacheAdapter;
361
+ constructor(config?: OpenAIBackendConfig);
362
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
363
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
364
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
365
+ healthCheck(): Promise<Result<void, AgentError>>;
366
+ }
367
+
368
+ interface GeminiBackendConfig {
369
+ /** Gemini model to use. Defaults to 'gemini-2.0-flash'. */
370
+ model?: string;
371
+ /** API key. Defaults to process.env.GEMINI_API_KEY or process.env.GOOGLE_API_KEY. */
372
+ apiKey?: string;
373
+ }
374
+ interface GeminiSession extends AgentSession {
375
+ systemPrompt?: string;
376
+ }
377
+ declare class GeminiBackend implements AgentBackend {
378
+ readonly name = "gemini";
379
+ private config;
380
+ private cacheAdapter;
381
+ constructor(config?: GeminiBackendConfig);
382
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
383
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
384
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
385
+ healthCheck(): Promise<Result<void, AgentError>>;
386
+ }
387
+
388
+ interface AnthropicBackendConfig {
389
+ /** Anthropic model to use. Defaults to 'claude-sonnet-4-20250514'. */
390
+ model?: string;
391
+ /** API key. Defaults to process.env.ANTHROPIC_API_KEY. */
392
+ apiKey?: string;
393
+ /** Maximum output tokens. Defaults to 4096. */
394
+ maxTokens?: number;
395
+ }
396
+ interface AnthropicSession extends AgentSession {
397
+ systemPrompt?: string;
398
+ }
399
+ declare class AnthropicBackend implements AgentBackend {
400
+ readonly name = "anthropic";
401
+ private config;
402
+ private client;
403
+ private cacheAdapter;
404
+ constructor(config?: AnthropicBackendConfig);
405
+ startSession(params: SessionStartParams): Promise<Result<AgentSession, AgentError>>;
406
+ runTurn(session: AgentSession, params: TurnParams): AsyncGenerator<AgentEvent, TurnResult, void>;
407
+ stopSession(_session: AgentSession): Promise<Result<void, AgentError>>;
408
+ healthCheck(): Promise<Result<void, AgentError>>;
409
+ }
410
+
347
411
  declare class PromptRenderer {
348
412
  private engine;
349
413
  constructor();
@@ -440,4 +504,4 @@ declare function launchTUI(orchestrator: Orchestrator): {
440
504
  waitUntilExit: () => Promise<void>;
441
505
  };
442
506
 
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 };
507
+ export { type AgentUpdateEvent, AnthropicBackend, type AnthropicBackendConfig, type AnthropicSession, type ApplyEventResult, ClaudeBackend, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, GeminiBackend, type GeminiBackendConfig, type GeminiSession, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MockBackend, OpenAIBackend, type OpenAIBackendConfig, type OpenAISession, 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 };
package/dist/index.js CHANGED
@@ -30,9 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AnthropicBackend: () => AnthropicBackend,
33
34
  ClaudeBackend: () => ClaudeBackend,
35
+ GeminiBackend: () => GeminiBackend,
34
36
  LinearGraphQLStub: () => LinearGraphQLStub,
35
37
  MockBackend: () => MockBackend,
38
+ OpenAIBackend: () => OpenAIBackend,
36
39
  Orchestrator: () => Orchestrator,
37
40
  PromptRenderer: () => PromptRenderer,
38
41
  RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
@@ -68,15 +71,20 @@ function calculateRetryDelay(attempt, type, maxRetryBackoffMs = DEFAULT_MAX_RETR
68
71
  }
69
72
 
70
73
  // src/core/candidate-selection.ts
74
+ function comparePriority(a, b) {
75
+ const pa = a.priority ?? Number.MAX_SAFE_INTEGER;
76
+ const pb = b.priority ?? Number.MAX_SAFE_INTEGER;
77
+ return pa !== pb ? pa - pb : null;
78
+ }
79
+ function compareCreatedAt(a, b) {
80
+ const ca = a.createdAt ?? "\uFFFF";
81
+ const cb = b.createdAt ?? "\uFFFF";
82
+ if (ca === cb) return null;
83
+ return ca < cb ? -1 : 1;
84
+ }
71
85
  function sortCandidates(issues) {
72
86
  return [...issues].sort((a, b) => {
73
- const pa = a.priority ?? Number.MAX_SAFE_INTEGER;
74
- const pb = b.priority ?? Number.MAX_SAFE_INTEGER;
75
- if (pa !== pb) return pa - pb;
76
- const ca = a.createdAt ?? "\uFFFF";
77
- const cb = b.createdAt ?? "\uFFFF";
78
- if (ca !== cb) return ca < cb ? -1 : 1;
79
- return a.identifier.localeCompare(b.identifier);
87
+ return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
80
88
  });
81
89
  }
82
90
  function isEligible(issue, state, activeStates, terminalStates) {
@@ -760,6 +768,21 @@ var MockBackend = class {
760
768
  var import_node_child_process2 = require("child_process");
761
769
  var readline = __toESM(require("readline"));
762
770
  var import_types8 = require("@harness-engineering/types");
771
+ function resolveExitCode(code, command, resolve) {
772
+ if (code === 0) {
773
+ resolve((0, import_types8.Ok)(void 0));
774
+ } else {
775
+ resolve(
776
+ (0, import_types8.Err)({
777
+ category: "agent_not_found",
778
+ message: `Claude command '${command}' not found or failed`
779
+ })
780
+ );
781
+ }
782
+ }
783
+ function resolveSpawnError(command, resolve) {
784
+ resolve((0, import_types8.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
785
+ }
763
786
  var ClaudeBackend = class {
764
787
  name = "claude";
765
788
  command;
@@ -818,27 +841,338 @@ var ClaudeBackend = class {
818
841
  async healthCheck() {
819
842
  return new Promise((resolve) => {
820
843
  const child = (0, import_node_child_process2.spawn)(this.command, ["--version"]);
821
- child.on("exit", (code) => {
822
- if (code === 0) {
823
- resolve((0, import_types8.Ok)(void 0));
824
- } else {
825
- resolve(
826
- (0, import_types8.Err)({
827
- category: "agent_not_found",
828
- message: `Claude command '${this.command}' not found or failed`
829
- })
830
- );
844
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve));
845
+ child.on("error", () => resolveSpawnError(this.command, resolve));
846
+ });
847
+ }
848
+ };
849
+
850
+ // src/agent/backends/openai.ts
851
+ var import_openai = __toESM(require("openai"));
852
+ var import_types9 = require("@harness-engineering/types");
853
+ var import_core2 = require("@harness-engineering/core");
854
+ var OpenAIBackend = class {
855
+ name = "openai";
856
+ config;
857
+ client;
858
+ cacheAdapter;
859
+ constructor(config = {}) {
860
+ this.config = {
861
+ model: config.model ?? "gpt-4o",
862
+ apiKey: config.apiKey ?? process.env.OPENAI_API_KEY ?? ""
863
+ };
864
+ this.client = new import_openai.default({ apiKey: this.config.apiKey });
865
+ this.cacheAdapter = new import_core2.OpenAICacheAdapter();
866
+ }
867
+ async startSession(params) {
868
+ if (!this.config.apiKey) {
869
+ return (0, import_types9.Err)({
870
+ category: "agent_not_found",
871
+ message: "OPENAI_API_KEY is not set"
872
+ });
873
+ }
874
+ const session = {
875
+ sessionId: `openai-session-${Date.now()}`,
876
+ workspacePath: params.workspacePath,
877
+ backendName: this.name,
878
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
879
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
880
+ };
881
+ return (0, import_types9.Ok)(session);
882
+ }
883
+ async *runTurn(session, params) {
884
+ const openAISession = session;
885
+ const messages = [];
886
+ if (openAISession.systemPrompt) {
887
+ messages.push({ role: "system", content: openAISession.systemPrompt });
888
+ }
889
+ messages.push({ role: "user", content: params.prompt });
890
+ let inputTokens = 0;
891
+ let outputTokens = 0;
892
+ let totalTokens = 0;
893
+ let cacheCreationTokens = 0;
894
+ let cacheReadTokens = 0;
895
+ try {
896
+ const stream = await this.client.chat.completions.create({
897
+ model: this.config.model,
898
+ messages,
899
+ stream: true,
900
+ stream_options: { include_usage: true }
901
+ });
902
+ for await (const chunk of stream) {
903
+ const delta = chunk.choices[0]?.delta;
904
+ if (delta?.content) {
905
+ const event = {
906
+ type: "text",
907
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
908
+ content: delta.content,
909
+ sessionId: session.sessionId
910
+ };
911
+ yield event;
831
912
  }
913
+ if (chunk.usage) {
914
+ inputTokens = chunk.usage.prompt_tokens ?? 0;
915
+ outputTokens = chunk.usage.completion_tokens ?? 0;
916
+ totalTokens = chunk.usage.total_tokens ?? 0;
917
+ const cacheUsage = this.cacheAdapter.parseCacheUsage(chunk);
918
+ cacheCreationTokens = cacheUsage.cacheCreationTokens;
919
+ cacheReadTokens = cacheUsage.cacheReadTokens;
920
+ }
921
+ }
922
+ } catch (err) {
923
+ const errorMessage = err instanceof Error ? err.message : "OpenAI request failed";
924
+ yield {
925
+ type: "error",
926
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
927
+ content: errorMessage,
928
+ sessionId: session.sessionId
929
+ };
930
+ return {
931
+ success: false,
932
+ sessionId: session.sessionId,
933
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
934
+ error: errorMessage
935
+ };
936
+ }
937
+ return {
938
+ success: true,
939
+ sessionId: session.sessionId,
940
+ usage: {
941
+ inputTokens,
942
+ outputTokens,
943
+ totalTokens,
944
+ cacheCreationTokens,
945
+ cacheReadTokens
946
+ }
947
+ };
948
+ }
949
+ async stopSession(_session) {
950
+ return (0, import_types9.Ok)(void 0);
951
+ }
952
+ async healthCheck() {
953
+ try {
954
+ await this.client.models.list();
955
+ return (0, import_types9.Ok)(void 0);
956
+ } catch (err) {
957
+ return (0, import_types9.Err)({
958
+ category: "response_error",
959
+ message: err instanceof Error ? err.message : "OpenAI health check failed"
832
960
  });
833
- child.on("error", () => {
834
- resolve(
835
- (0, import_types8.Err)({
836
- category: "agent_not_found",
837
- message: `Claude command '${this.command}' not found`
838
- })
839
- );
961
+ }
962
+ }
963
+ };
964
+
965
+ // src/agent/backends/gemini.ts
966
+ var import_generative_ai = require("@google/generative-ai");
967
+ var import_types10 = require("@harness-engineering/types");
968
+ var import_core3 = require("@harness-engineering/core");
969
+ var GeminiBackend = class {
970
+ name = "gemini";
971
+ config;
972
+ cacheAdapter;
973
+ constructor(config = {}) {
974
+ this.config = {
975
+ model: config.model ?? "gemini-2.0-flash",
976
+ apiKey: config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? ""
977
+ };
978
+ this.cacheAdapter = new import_core3.GeminiCacheAdapter();
979
+ }
980
+ async startSession(params) {
981
+ if (!this.config.apiKey) {
982
+ return (0, import_types10.Err)({
983
+ category: "agent_not_found",
984
+ message: "GEMINI_API_KEY is not set"
840
985
  });
841
- });
986
+ }
987
+ const session = {
988
+ sessionId: `gemini-session-${Date.now()}`,
989
+ workspacePath: params.workspacePath,
990
+ backendName: this.name,
991
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
992
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
993
+ };
994
+ return (0, import_types10.Ok)(session);
995
+ }
996
+ async *runTurn(session, params) {
997
+ const geminiSession = session;
998
+ let inputTokens = 0;
999
+ let outputTokens = 0;
1000
+ let totalTokens = 0;
1001
+ let cacheCreationTokens = 0;
1002
+ let cacheReadTokens = 0;
1003
+ try {
1004
+ const genAI = new import_generative_ai.GoogleGenerativeAI(this.config.apiKey);
1005
+ const model = genAI.getGenerativeModel({
1006
+ model: this.config.model,
1007
+ ...geminiSession.systemPrompt !== void 0 && {
1008
+ systemInstruction: geminiSession.systemPrompt
1009
+ }
1010
+ });
1011
+ const result = await model.generateContentStream(params.prompt);
1012
+ for await (const chunk of result.stream) {
1013
+ const text = chunk.text();
1014
+ if (text) {
1015
+ const event = {
1016
+ type: "text",
1017
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1018
+ content: text,
1019
+ sessionId: session.sessionId
1020
+ };
1021
+ yield event;
1022
+ }
1023
+ if (chunk.usageMetadata) {
1024
+ inputTokens = chunk.usageMetadata.promptTokenCount ?? 0;
1025
+ outputTokens = chunk.usageMetadata.candidatesTokenCount ?? 0;
1026
+ totalTokens = chunk.usageMetadata.totalTokenCount ?? 0;
1027
+ const cacheUsage = this.cacheAdapter.parseCacheUsage(chunk);
1028
+ cacheCreationTokens = cacheUsage.cacheCreationTokens;
1029
+ cacheReadTokens = cacheUsage.cacheReadTokens;
1030
+ }
1031
+ }
1032
+ } catch (err) {
1033
+ const errorMessage = err instanceof Error ? err.message : "Gemini request failed";
1034
+ yield {
1035
+ type: "error",
1036
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1037
+ content: errorMessage,
1038
+ sessionId: session.sessionId
1039
+ };
1040
+ return {
1041
+ success: false,
1042
+ sessionId: session.sessionId,
1043
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
1044
+ error: errorMessage
1045
+ };
1046
+ }
1047
+ return {
1048
+ success: true,
1049
+ sessionId: session.sessionId,
1050
+ usage: {
1051
+ inputTokens,
1052
+ outputTokens,
1053
+ totalTokens,
1054
+ cacheCreationTokens,
1055
+ cacheReadTokens
1056
+ }
1057
+ };
1058
+ }
1059
+ async stopSession(_session) {
1060
+ return (0, import_types10.Ok)(void 0);
1061
+ }
1062
+ async healthCheck() {
1063
+ try {
1064
+ const genAI = new import_generative_ai.GoogleGenerativeAI(this.config.apiKey);
1065
+ genAI.getGenerativeModel({ model: this.config.model });
1066
+ return (0, import_types10.Ok)(void 0);
1067
+ } catch (err) {
1068
+ return (0, import_types10.Err)({
1069
+ category: "response_error",
1070
+ message: err instanceof Error ? err.message : "Gemini health check failed"
1071
+ });
1072
+ }
1073
+ }
1074
+ };
1075
+
1076
+ // src/agent/backends/anthropic.ts
1077
+ var import_sdk = __toESM(require("@anthropic-ai/sdk"));
1078
+ var import_types11 = require("@harness-engineering/types");
1079
+ var import_core4 = require("@harness-engineering/core");
1080
+ var AnthropicBackend = class {
1081
+ name = "anthropic";
1082
+ config;
1083
+ client;
1084
+ cacheAdapter;
1085
+ constructor(config = {}) {
1086
+ this.config = {
1087
+ model: config.model ?? "claude-sonnet-4-20250514",
1088
+ apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
1089
+ maxTokens: config.maxTokens ?? 4096
1090
+ };
1091
+ this.client = new import_sdk.default({ apiKey: this.config.apiKey });
1092
+ this.cacheAdapter = new import_core4.AnthropicCacheAdapter();
1093
+ }
1094
+ async startSession(params) {
1095
+ if (!this.config.apiKey) {
1096
+ return (0, import_types11.Err)({
1097
+ category: "agent_not_found",
1098
+ message: "ANTHROPIC_API_KEY is not set"
1099
+ });
1100
+ }
1101
+ const session = {
1102
+ sessionId: `anthropic-session-${Date.now()}`,
1103
+ workspacePath: params.workspacePath,
1104
+ backendName: this.name,
1105
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1106
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
1107
+ };
1108
+ return (0, import_types11.Ok)(session);
1109
+ }
1110
+ async *runTurn(session, params) {
1111
+ const anthropicSession = session;
1112
+ const systemBlocks = anthropicSession.systemPrompt ? [
1113
+ this.cacheAdapter.wrapSystemBlock(
1114
+ anthropicSession.systemPrompt,
1115
+ "session"
1116
+ )
1117
+ ] : void 0;
1118
+ try {
1119
+ const stream = this.client.messages.stream({
1120
+ model: this.config.model,
1121
+ max_tokens: this.config.maxTokens,
1122
+ ...systemBlocks && { system: systemBlocks },
1123
+ messages: [{ role: "user", content: params.prompt }]
1124
+ });
1125
+ for await (const event of stream) {
1126
+ if (event.type === "content_block_delta" && "text" in event.delta) {
1127
+ yield {
1128
+ type: "text",
1129
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1130
+ content: event.delta.text,
1131
+ sessionId: session.sessionId
1132
+ };
1133
+ }
1134
+ }
1135
+ const finalMessage = await stream.finalMessage();
1136
+ const { input_tokens, output_tokens } = finalMessage.usage;
1137
+ const cacheUsage = this.cacheAdapter.parseCacheUsage(finalMessage);
1138
+ return {
1139
+ success: true,
1140
+ sessionId: session.sessionId,
1141
+ usage: {
1142
+ inputTokens: input_tokens,
1143
+ outputTokens: output_tokens,
1144
+ totalTokens: input_tokens + output_tokens,
1145
+ cacheCreationTokens: cacheUsage.cacheCreationTokens,
1146
+ cacheReadTokens: cacheUsage.cacheReadTokens
1147
+ }
1148
+ };
1149
+ } catch (err) {
1150
+ const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
1151
+ yield {
1152
+ type: "error",
1153
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1154
+ content: errorMessage,
1155
+ sessionId: session.sessionId
1156
+ };
1157
+ return {
1158
+ success: false,
1159
+ sessionId: session.sessionId,
1160
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
1161
+ error: errorMessage
1162
+ };
1163
+ }
1164
+ }
1165
+ async stopSession(_session) {
1166
+ return (0, import_types11.Ok)(void 0);
1167
+ }
1168
+ async healthCheck() {
1169
+ if (!this.config.apiKey) {
1170
+ return (0, import_types11.Err)({
1171
+ category: "response_error",
1172
+ message: "ANTHROPIC_API_KEY is not set"
1173
+ });
1174
+ }
1175
+ return (0, import_types11.Ok)(void 0);
842
1176
  }
843
1177
  };
844
1178
 
@@ -866,6 +1200,7 @@ var PromptRenderer = class {
866
1200
 
867
1201
  // src/orchestrator.ts
868
1202
  var import_node_events = require("events");
1203
+ var import_core6 = require("@harness-engineering/core");
869
1204
 
870
1205
  // src/agent/runner.ts
871
1206
  var AgentRunner = class {
@@ -892,7 +1227,13 @@ var AgentRunner = class {
892
1227
  let lastResult = {
893
1228
  success: false,
894
1229
  sessionId: session.sessionId,
895
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
1230
+ usage: {
1231
+ inputTokens: 0,
1232
+ outputTokens: 0,
1233
+ totalTokens: 0,
1234
+ cacheCreationTokens: 0,
1235
+ cacheReadTokens: 0
1236
+ }
896
1237
  };
897
1238
  try {
898
1239
  while (currentTurn < this.options.maxTurns) {
@@ -991,7 +1332,7 @@ var StructuredLogger = class {
991
1332
  // src/workspace/config-scanner.ts
992
1333
  var import_node_fs = require("fs");
993
1334
  var import_node_path = require("path");
994
- var import_core2 = require("@harness-engineering/core");
1335
+ var import_core5 = require("@harness-engineering/core");
995
1336
  var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
996
1337
  function scanSingleFile(filePath, targetDir, scanner) {
997
1338
  if (!(0, import_node_fs.existsSync)(filePath)) return null;
@@ -1001,28 +1342,27 @@ function scanSingleFile(filePath, targetDir, scanner) {
1001
1342
  } catch {
1002
1343
  return null;
1003
1344
  }
1004
- const injectionFindings = (0, import_core2.scanForInjection)(content);
1005
- const findings = (0, import_core2.mapInjectionFindings)(injectionFindings);
1345
+ const injectionFindings = (0, import_core5.scanForInjection)(content);
1346
+ const findings = (0, import_core5.mapInjectionFindings)(injectionFindings);
1006
1347
  const secFindings = scanner.scanContent(content, filePath);
1007
- findings.push(...(0, import_core2.mapSecurityFindings)(secFindings, findings));
1348
+ findings.push(...(0, import_core5.mapSecurityFindings)(secFindings, findings));
1008
1349
  return {
1009
1350
  file: (0, import_node_path.relative)(targetDir, filePath).replaceAll("\\", "/"),
1010
1351
  findings,
1011
- overallSeverity: (0, import_core2.computeOverallSeverity)(findings)
1352
+ overallSeverity: (0, import_core5.computeOverallSeverity)(findings)
1012
1353
  };
1013
1354
  }
1014
1355
  async function scanWorkspaceConfig(workspacePath) {
1015
- const scanner = new import_core2.SecurityScanner((0, import_core2.parseSecurityConfig)({}));
1356
+ const scanner = new import_core5.SecurityScanner((0, import_core5.parseSecurityConfig)({}));
1016
1357
  const results = [];
1017
1358
  for (const configFile of CONFIG_FILES) {
1018
1359
  const result = scanSingleFile((0, import_node_path.join)(workspacePath, configFile), workspacePath, scanner);
1019
1360
  if (result) results.push(result);
1020
1361
  }
1021
- return { exitCode: (0, import_core2.computeScanExitCode)(results), results };
1362
+ return { exitCode: (0, import_core5.computeScanExitCode)(results), results };
1022
1363
  }
1023
1364
 
1024
1365
  // src/orchestrator.ts
1025
- var import_core4 = require("@harness-engineering/core");
1026
1366
  var Orchestrator = class extends import_node_events.EventEmitter {
1027
1367
  state;
1028
1368
  config;
@@ -1070,6 +1410,21 @@ var Orchestrator = class extends import_node_events.EventEmitter {
1070
1410
  return new MockBackend();
1071
1411
  } else if (this.config.agent.backend === "claude") {
1072
1412
  return new ClaudeBackend(this.config.agent.command);
1413
+ } else if (this.config.agent.backend === "openai") {
1414
+ return new OpenAIBackend({
1415
+ ...this.config.agent.model !== void 0 && { model: this.config.agent.model },
1416
+ ...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
1417
+ });
1418
+ } else if (this.config.agent.backend === "gemini") {
1419
+ return new GeminiBackend({
1420
+ ...this.config.agent.model !== void 0 && { model: this.config.agent.model },
1421
+ ...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
1422
+ });
1423
+ } else if (this.config.agent.backend === "anthropic") {
1424
+ return new AnthropicBackend({
1425
+ ...this.config.agent.model !== void 0 && { model: this.config.agent.model },
1426
+ ...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
1427
+ });
1073
1428
  }
1074
1429
  throw new Error(`Unsupported agent backend: ${this.config.agent.backend}`);
1075
1430
  }
@@ -1171,7 +1526,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
1171
1526
  ...f.line !== void 0 ? { line: f.line } : {}
1172
1527
  }))
1173
1528
  );
1174
- (0, import_core4.writeTaint)(
1529
+ (0, import_core6.writeTaint)(
1175
1530
  workspacePath,
1176
1531
  issue.id,
1177
1532
  "Medium-severity injection patterns found in workspace config files",
@@ -1368,12 +1723,14 @@ var AgentsTable = ({ agents }) => {
1368
1723
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, underline: true, children: "Active Agents" }),
1369
1724
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Box, { flexDirection: "row", borderStyle: "single", borderColor: "gray", children: [
1370
1725
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Identifier" }) }),
1726
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 12, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Backend" }) }),
1371
1727
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Phase" }) }),
1372
1728
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 10, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Tokens" }) }),
1373
1729
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { flexGrow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { bold: true, children: "Message" }) })
1374
1730
  ] }),
1375
1731
  agents.map((agent) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink3.Box, { flexDirection: "row", children: [
1376
1732
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { children: agent.identifier }) }),
1733
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 12, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "blue", children: agent.session?.backendName || "-" }) }),
1377
1734
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 20, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "cyan", children: agent.phase }) }),
1378
1735
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { width: 10, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "yellow", children: agent.session?.totalTokens || 0 }) }),
1379
1736
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Box, { flexGrow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { wrap: "truncate-end", children: agent.session?.lastMessage || "-" }) })
@@ -1425,9 +1782,12 @@ function launchTUI(orchestrator) {
1425
1782
  }
1426
1783
  // Annotate the CommonJS export names for ESM import in node:
1427
1784
  0 && (module.exports = {
1785
+ AnthropicBackend,
1428
1786
  ClaudeBackend,
1787
+ GeminiBackend,
1429
1788
  LinearGraphQLStub,
1430
1789
  MockBackend,
1790
+ OpenAIBackend,
1431
1791
  Orchestrator,
1432
1792
  PromptRenderer,
1433
1793
  RoadmapTrackerAdapter,
package/dist/index.mjs CHANGED
@@ -11,15 +11,20 @@ function calculateRetryDelay(attempt, type, maxRetryBackoffMs = DEFAULT_MAX_RETR
11
11
  }
12
12
 
13
13
  // src/core/candidate-selection.ts
14
+ function comparePriority(a, b) {
15
+ const pa = a.priority ?? Number.MAX_SAFE_INTEGER;
16
+ const pb = b.priority ?? Number.MAX_SAFE_INTEGER;
17
+ return pa !== pb ? pa - pb : null;
18
+ }
19
+ function compareCreatedAt(a, b) {
20
+ const ca = a.createdAt ?? "\uFFFF";
21
+ const cb = b.createdAt ?? "\uFFFF";
22
+ if (ca === cb) return null;
23
+ return ca < cb ? -1 : 1;
24
+ }
14
25
  function sortCandidates(issues) {
15
26
  return [...issues].sort((a, b) => {
16
- const pa = a.priority ?? Number.MAX_SAFE_INTEGER;
17
- const pb = b.priority ?? Number.MAX_SAFE_INTEGER;
18
- if (pa !== pb) return pa - pb;
19
- const ca = a.createdAt ?? "\uFFFF";
20
- const cb = b.createdAt ?? "\uFFFF";
21
- if (ca !== cb) return ca < cb ? -1 : 1;
22
- return a.identifier.localeCompare(b.identifier);
27
+ return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
23
28
  });
24
29
  }
25
30
  function isEligible(issue, state, activeStates, terminalStates) {
@@ -711,6 +716,21 @@ import {
711
716
  Ok as Ok8,
712
717
  Err as Err6
713
718
  } from "@harness-engineering/types";
719
+ function resolveExitCode(code, command, resolve) {
720
+ if (code === 0) {
721
+ resolve(Ok8(void 0));
722
+ } else {
723
+ resolve(
724
+ Err6({
725
+ category: "agent_not_found",
726
+ message: `Claude command '${command}' not found or failed`
727
+ })
728
+ );
729
+ }
730
+ }
731
+ function resolveSpawnError(command, resolve) {
732
+ resolve(Err6({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
733
+ }
714
734
  var ClaudeBackend = class {
715
735
  name = "claude";
716
736
  command;
@@ -769,27 +789,347 @@ var ClaudeBackend = class {
769
789
  async healthCheck() {
770
790
  return new Promise((resolve) => {
771
791
  const child = spawn2(this.command, ["--version"]);
772
- child.on("exit", (code) => {
773
- if (code === 0) {
774
- resolve(Ok8(void 0));
775
- } else {
776
- resolve(
777
- Err6({
778
- category: "agent_not_found",
779
- message: `Claude command '${this.command}' not found or failed`
780
- })
781
- );
792
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve));
793
+ child.on("error", () => resolveSpawnError(this.command, resolve));
794
+ });
795
+ }
796
+ };
797
+
798
+ // src/agent/backends/openai.ts
799
+ import OpenAI from "openai";
800
+ import {
801
+ Ok as Ok9,
802
+ Err as Err7
803
+ } from "@harness-engineering/types";
804
+ import { OpenAICacheAdapter } from "@harness-engineering/core";
805
+ var OpenAIBackend = class {
806
+ name = "openai";
807
+ config;
808
+ client;
809
+ cacheAdapter;
810
+ constructor(config = {}) {
811
+ this.config = {
812
+ model: config.model ?? "gpt-4o",
813
+ apiKey: config.apiKey ?? process.env.OPENAI_API_KEY ?? ""
814
+ };
815
+ this.client = new OpenAI({ apiKey: this.config.apiKey });
816
+ this.cacheAdapter = new OpenAICacheAdapter();
817
+ }
818
+ async startSession(params) {
819
+ if (!this.config.apiKey) {
820
+ return Err7({
821
+ category: "agent_not_found",
822
+ message: "OPENAI_API_KEY is not set"
823
+ });
824
+ }
825
+ const session = {
826
+ sessionId: `openai-session-${Date.now()}`,
827
+ workspacePath: params.workspacePath,
828
+ backendName: this.name,
829
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
830
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
831
+ };
832
+ return Ok9(session);
833
+ }
834
+ async *runTurn(session, params) {
835
+ const openAISession = session;
836
+ const messages = [];
837
+ if (openAISession.systemPrompt) {
838
+ messages.push({ role: "system", content: openAISession.systemPrompt });
839
+ }
840
+ messages.push({ role: "user", content: params.prompt });
841
+ let inputTokens = 0;
842
+ let outputTokens = 0;
843
+ let totalTokens = 0;
844
+ let cacheCreationTokens = 0;
845
+ let cacheReadTokens = 0;
846
+ try {
847
+ const stream = await this.client.chat.completions.create({
848
+ model: this.config.model,
849
+ messages,
850
+ stream: true,
851
+ stream_options: { include_usage: true }
852
+ });
853
+ for await (const chunk of stream) {
854
+ const delta = chunk.choices[0]?.delta;
855
+ if (delta?.content) {
856
+ const event = {
857
+ type: "text",
858
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
859
+ content: delta.content,
860
+ sessionId: session.sessionId
861
+ };
862
+ yield event;
782
863
  }
864
+ if (chunk.usage) {
865
+ inputTokens = chunk.usage.prompt_tokens ?? 0;
866
+ outputTokens = chunk.usage.completion_tokens ?? 0;
867
+ totalTokens = chunk.usage.total_tokens ?? 0;
868
+ const cacheUsage = this.cacheAdapter.parseCacheUsage(chunk);
869
+ cacheCreationTokens = cacheUsage.cacheCreationTokens;
870
+ cacheReadTokens = cacheUsage.cacheReadTokens;
871
+ }
872
+ }
873
+ } catch (err) {
874
+ const errorMessage = err instanceof Error ? err.message : "OpenAI request failed";
875
+ yield {
876
+ type: "error",
877
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
878
+ content: errorMessage,
879
+ sessionId: session.sessionId
880
+ };
881
+ return {
882
+ success: false,
883
+ sessionId: session.sessionId,
884
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
885
+ error: errorMessage
886
+ };
887
+ }
888
+ return {
889
+ success: true,
890
+ sessionId: session.sessionId,
891
+ usage: {
892
+ inputTokens,
893
+ outputTokens,
894
+ totalTokens,
895
+ cacheCreationTokens,
896
+ cacheReadTokens
897
+ }
898
+ };
899
+ }
900
+ async stopSession(_session) {
901
+ return Ok9(void 0);
902
+ }
903
+ async healthCheck() {
904
+ try {
905
+ await this.client.models.list();
906
+ return Ok9(void 0);
907
+ } catch (err) {
908
+ return Err7({
909
+ category: "response_error",
910
+ message: err instanceof Error ? err.message : "OpenAI health check failed"
783
911
  });
784
- child.on("error", () => {
785
- resolve(
786
- Err6({
787
- category: "agent_not_found",
788
- message: `Claude command '${this.command}' not found`
789
- })
790
- );
912
+ }
913
+ }
914
+ };
915
+
916
+ // src/agent/backends/gemini.ts
917
+ import { GoogleGenerativeAI } from "@google/generative-ai";
918
+ import {
919
+ Ok as Ok10,
920
+ Err as Err8
921
+ } from "@harness-engineering/types";
922
+ import { GeminiCacheAdapter } from "@harness-engineering/core";
923
+ var GeminiBackend = class {
924
+ name = "gemini";
925
+ config;
926
+ cacheAdapter;
927
+ constructor(config = {}) {
928
+ this.config = {
929
+ model: config.model ?? "gemini-2.0-flash",
930
+ apiKey: config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? ""
931
+ };
932
+ this.cacheAdapter = new GeminiCacheAdapter();
933
+ }
934
+ async startSession(params) {
935
+ if (!this.config.apiKey) {
936
+ return Err8({
937
+ category: "agent_not_found",
938
+ message: "GEMINI_API_KEY is not set"
791
939
  });
792
- });
940
+ }
941
+ const session = {
942
+ sessionId: `gemini-session-${Date.now()}`,
943
+ workspacePath: params.workspacePath,
944
+ backendName: this.name,
945
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
946
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
947
+ };
948
+ return Ok10(session);
949
+ }
950
+ async *runTurn(session, params) {
951
+ const geminiSession = session;
952
+ let inputTokens = 0;
953
+ let outputTokens = 0;
954
+ let totalTokens = 0;
955
+ let cacheCreationTokens = 0;
956
+ let cacheReadTokens = 0;
957
+ try {
958
+ const genAI = new GoogleGenerativeAI(this.config.apiKey);
959
+ const model = genAI.getGenerativeModel({
960
+ model: this.config.model,
961
+ ...geminiSession.systemPrompt !== void 0 && {
962
+ systemInstruction: geminiSession.systemPrompt
963
+ }
964
+ });
965
+ const result = await model.generateContentStream(params.prompt);
966
+ for await (const chunk of result.stream) {
967
+ const text = chunk.text();
968
+ if (text) {
969
+ const event = {
970
+ type: "text",
971
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
972
+ content: text,
973
+ sessionId: session.sessionId
974
+ };
975
+ yield event;
976
+ }
977
+ if (chunk.usageMetadata) {
978
+ inputTokens = chunk.usageMetadata.promptTokenCount ?? 0;
979
+ outputTokens = chunk.usageMetadata.candidatesTokenCount ?? 0;
980
+ totalTokens = chunk.usageMetadata.totalTokenCount ?? 0;
981
+ const cacheUsage = this.cacheAdapter.parseCacheUsage(chunk);
982
+ cacheCreationTokens = cacheUsage.cacheCreationTokens;
983
+ cacheReadTokens = cacheUsage.cacheReadTokens;
984
+ }
985
+ }
986
+ } catch (err) {
987
+ const errorMessage = err instanceof Error ? err.message : "Gemini request failed";
988
+ yield {
989
+ type: "error",
990
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
991
+ content: errorMessage,
992
+ sessionId: session.sessionId
993
+ };
994
+ return {
995
+ success: false,
996
+ sessionId: session.sessionId,
997
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
998
+ error: errorMessage
999
+ };
1000
+ }
1001
+ return {
1002
+ success: true,
1003
+ sessionId: session.sessionId,
1004
+ usage: {
1005
+ inputTokens,
1006
+ outputTokens,
1007
+ totalTokens,
1008
+ cacheCreationTokens,
1009
+ cacheReadTokens
1010
+ }
1011
+ };
1012
+ }
1013
+ async stopSession(_session) {
1014
+ return Ok10(void 0);
1015
+ }
1016
+ async healthCheck() {
1017
+ try {
1018
+ const genAI = new GoogleGenerativeAI(this.config.apiKey);
1019
+ genAI.getGenerativeModel({ model: this.config.model });
1020
+ return Ok10(void 0);
1021
+ } catch (err) {
1022
+ return Err8({
1023
+ category: "response_error",
1024
+ message: err instanceof Error ? err.message : "Gemini health check failed"
1025
+ });
1026
+ }
1027
+ }
1028
+ };
1029
+
1030
+ // src/agent/backends/anthropic.ts
1031
+ import Anthropic from "@anthropic-ai/sdk";
1032
+ import {
1033
+ Ok as Ok11,
1034
+ Err as Err9
1035
+ } from "@harness-engineering/types";
1036
+ import { AnthropicCacheAdapter } from "@harness-engineering/core";
1037
+ var AnthropicBackend = class {
1038
+ name = "anthropic";
1039
+ config;
1040
+ client;
1041
+ cacheAdapter;
1042
+ constructor(config = {}) {
1043
+ this.config = {
1044
+ model: config.model ?? "claude-sonnet-4-20250514",
1045
+ apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
1046
+ maxTokens: config.maxTokens ?? 4096
1047
+ };
1048
+ this.client = new Anthropic({ apiKey: this.config.apiKey });
1049
+ this.cacheAdapter = new AnthropicCacheAdapter();
1050
+ }
1051
+ async startSession(params) {
1052
+ if (!this.config.apiKey) {
1053
+ return Err9({
1054
+ category: "agent_not_found",
1055
+ message: "ANTHROPIC_API_KEY is not set"
1056
+ });
1057
+ }
1058
+ const session = {
1059
+ sessionId: `anthropic-session-${Date.now()}`,
1060
+ workspacePath: params.workspacePath,
1061
+ backendName: this.name,
1062
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1063
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
1064
+ };
1065
+ return Ok11(session);
1066
+ }
1067
+ async *runTurn(session, params) {
1068
+ const anthropicSession = session;
1069
+ const systemBlocks = anthropicSession.systemPrompt ? [
1070
+ this.cacheAdapter.wrapSystemBlock(
1071
+ anthropicSession.systemPrompt,
1072
+ "session"
1073
+ )
1074
+ ] : void 0;
1075
+ try {
1076
+ const stream = this.client.messages.stream({
1077
+ model: this.config.model,
1078
+ max_tokens: this.config.maxTokens,
1079
+ ...systemBlocks && { system: systemBlocks },
1080
+ messages: [{ role: "user", content: params.prompt }]
1081
+ });
1082
+ for await (const event of stream) {
1083
+ if (event.type === "content_block_delta" && "text" in event.delta) {
1084
+ yield {
1085
+ type: "text",
1086
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1087
+ content: event.delta.text,
1088
+ sessionId: session.sessionId
1089
+ };
1090
+ }
1091
+ }
1092
+ const finalMessage = await stream.finalMessage();
1093
+ const { input_tokens, output_tokens } = finalMessage.usage;
1094
+ const cacheUsage = this.cacheAdapter.parseCacheUsage(finalMessage);
1095
+ return {
1096
+ success: true,
1097
+ sessionId: session.sessionId,
1098
+ usage: {
1099
+ inputTokens: input_tokens,
1100
+ outputTokens: output_tokens,
1101
+ totalTokens: input_tokens + output_tokens,
1102
+ cacheCreationTokens: cacheUsage.cacheCreationTokens,
1103
+ cacheReadTokens: cacheUsage.cacheReadTokens
1104
+ }
1105
+ };
1106
+ } catch (err) {
1107
+ const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
1108
+ yield {
1109
+ type: "error",
1110
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1111
+ content: errorMessage,
1112
+ sessionId: session.sessionId
1113
+ };
1114
+ return {
1115
+ success: false,
1116
+ sessionId: session.sessionId,
1117
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
1118
+ error: errorMessage
1119
+ };
1120
+ }
1121
+ }
1122
+ async stopSession(_session) {
1123
+ return Ok11(void 0);
1124
+ }
1125
+ async healthCheck() {
1126
+ if (!this.config.apiKey) {
1127
+ return Err9({
1128
+ category: "response_error",
1129
+ message: "ANTHROPIC_API_KEY is not set"
1130
+ });
1131
+ }
1132
+ return Ok11(void 0);
793
1133
  }
794
1134
  };
795
1135
 
@@ -817,6 +1157,7 @@ var PromptRenderer = class {
817
1157
 
818
1158
  // src/orchestrator.ts
819
1159
  import { EventEmitter } from "events";
1160
+ import { writeTaint } from "@harness-engineering/core";
820
1161
 
821
1162
  // src/agent/runner.ts
822
1163
  var AgentRunner = class {
@@ -843,7 +1184,13 @@ var AgentRunner = class {
843
1184
  let lastResult = {
844
1185
  success: false,
845
1186
  sessionId: session.sessionId,
846
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
1187
+ usage: {
1188
+ inputTokens: 0,
1189
+ outputTokens: 0,
1190
+ totalTokens: 0,
1191
+ cacheCreationTokens: 0,
1192
+ cacheReadTokens: 0
1193
+ }
847
1194
  };
848
1195
  try {
849
1196
  while (currentTurn < this.options.maxTurns) {
@@ -981,7 +1328,6 @@ async function scanWorkspaceConfig(workspacePath) {
981
1328
  }
982
1329
 
983
1330
  // src/orchestrator.ts
984
- import { writeTaint } from "@harness-engineering/core";
985
1331
  var Orchestrator = class extends EventEmitter {
986
1332
  state;
987
1333
  config;
@@ -1029,6 +1375,21 @@ var Orchestrator = class extends EventEmitter {
1029
1375
  return new MockBackend();
1030
1376
  } else if (this.config.agent.backend === "claude") {
1031
1377
  return new ClaudeBackend(this.config.agent.command);
1378
+ } else if (this.config.agent.backend === "openai") {
1379
+ return new OpenAIBackend({
1380
+ ...this.config.agent.model !== void 0 && { model: this.config.agent.model },
1381
+ ...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
1382
+ });
1383
+ } else if (this.config.agent.backend === "gemini") {
1384
+ return new GeminiBackend({
1385
+ ...this.config.agent.model !== void 0 && { model: this.config.agent.model },
1386
+ ...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
1387
+ });
1388
+ } else if (this.config.agent.backend === "anthropic") {
1389
+ return new AnthropicBackend({
1390
+ ...this.config.agent.model !== void 0 && { model: this.config.agent.model },
1391
+ ...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
1392
+ });
1032
1393
  }
1033
1394
  throw new Error(`Unsupported agent backend: ${this.config.agent.backend}`);
1034
1395
  }
@@ -1327,12 +1688,14 @@ var AgentsTable = ({ agents }) => {
1327
1688
  /* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, children: "Active Agents" }),
1328
1689
  /* @__PURE__ */ jsxs3(Box3, { flexDirection: "row", borderStyle: "single", borderColor: "gray", children: [
1329
1690
  /* @__PURE__ */ jsx3(Box3, { width: 20, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Identifier" }) }),
1691
+ /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Backend" }) }),
1330
1692
  /* @__PURE__ */ jsx3(Box3, { width: 20, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Phase" }) }),
1331
1693
  /* @__PURE__ */ jsx3(Box3, { width: 10, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Tokens" }) }),
1332
1694
  /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Message" }) })
1333
1695
  ] }),
1334
1696
  agents.map((agent) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "row", children: [
1335
1697
  /* @__PURE__ */ jsx3(Box3, { width: 20, children: /* @__PURE__ */ jsx3(Text3, { children: agent.identifier }) }),
1698
+ /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { color: "blue", children: agent.session?.backendName || "-" }) }),
1336
1699
  /* @__PURE__ */ jsx3(Box3, { width: 20, children: /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: agent.phase }) }),
1337
1700
  /* @__PURE__ */ jsx3(Box3, { width: 10, children: /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: agent.session?.totalTokens || 0 }) }),
1338
1701
  /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, children: /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: agent.session?.lastMessage || "-" }) })
@@ -1383,9 +1746,12 @@ function launchTUI(orchestrator) {
1383
1746
  return { waitUntilExit };
1384
1747
  }
1385
1748
  export {
1749
+ AnthropicBackend,
1386
1750
  ClaudeBackend,
1751
+ GeminiBackend,
1387
1752
  LinearGraphQLStub,
1388
1753
  MockBackend,
1754
+ OpenAIBackend,
1389
1755
  Orchestrator,
1390
1756
  PromptRenderer,
1391
1757
  RoadmapTrackerAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/orchestrator",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Orchestrator daemon for dispatching coding agents to issues",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -36,13 +36,15 @@
36
36
  },
37
37
  "homepage": "https://github.com/Intense-Visions/harness-engineering/tree/main/packages/orchestrator#readme",
38
38
  "dependencies": {
39
- "chokidar": "^3.6.0",
39
+ "@anthropic-ai/sdk": "^0.87.0",
40
+ "@google/generative-ai": "^0.24.0",
40
41
  "ink": "^4.4.1",
41
- "liquidjs": "^10.25.2",
42
+ "liquidjs": "^10.25.3",
43
+ "openai": "^4.0.0",
42
44
  "react": "^18.3.1",
43
45
  "yaml": "^2.8.3",
44
- "@harness-engineering/core": "0.17.0",
45
- "@harness-engineering/types": "0.7.0"
46
+ "@harness-engineering/core": "0.21.4",
47
+ "@harness-engineering/types": "0.9.2"
46
48
  },
47
49
  "devDependencies": {
48
50
  "@types/node": "^22.19.15",