@mikode13/harness 1.0.1 → 2.1.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.
Files changed (50) hide show
  1. package/README.md +10 -2
  2. package/dist/agent/domain/agent.d.ts +13 -3
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/errors.d.ts +29 -6
  5. package/dist/agent/domain/errors.js +35 -0
  6. package/dist/agent/domain/errors.js.map +1 -1
  7. package/dist/agent/domain/providerFailure.js +1 -1
  8. package/dist/agent/domain/providerFailure.js.map +1 -1
  9. package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +3 -3
  10. package/dist/engines/claude/infrastructure/model/claudeAgent.js +76 -51
  11. package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
  12. package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +3 -3
  13. package/dist/engines/codex/infrastructure/model/codexAgent.js +57 -32
  14. package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
  15. package/dist/engines/domain/model/llmAgent.d.ts +17 -0
  16. package/dist/engines/domain/model/llmAgent.js +63 -0
  17. package/dist/engines/domain/model/llmAgent.js.map +1 -0
  18. package/dist/factory/infrastructure/agentFactory.d.ts +29 -4
  19. package/dist/factory/infrastructure/agentFactory.js +53 -8
  20. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/llm/domain/conversation.d.ts +20 -0
  24. package/dist/llm/domain/conversation.js +26 -0
  25. package/dist/llm/domain/conversation.js.map +1 -0
  26. package/dist/llm/domain/errors.d.ts +4 -0
  27. package/dist/llm/domain/errors.js +5 -0
  28. package/dist/llm/domain/errors.js.map +1 -0
  29. package/dist/llm/domain/llm.d.ts +18 -0
  30. package/dist/llm/domain/llm.js +2 -0
  31. package/dist/llm/domain/llm.js.map +1 -0
  32. package/dist/llm/domain/message.d.ts +11 -0
  33. package/dist/llm/domain/message.js +2 -0
  34. package/dist/llm/domain/message.js.map +1 -0
  35. package/dist/llm/infrastructure/claudeLLMClient.d.ts +21 -0
  36. package/dist/llm/infrastructure/claudeLLMClient.js +189 -0
  37. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -0
  38. package/dist/llm/infrastructure/openAILLMClient.d.ts +22 -0
  39. package/dist/llm/infrastructure/openAILLMClient.js +172 -0
  40. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -0
  41. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +4 -3
  42. package/dist/orchestration/domain/model/orchestratorAgent.js +36 -17
  43. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  44. package/dist/retry/domain/model/retryingAgent.d.ts +12 -4
  45. package/dist/retry/domain/model/retryingAgent.js +42 -12
  46. package/dist/retry/domain/model/retryingAgent.js.map +1 -1
  47. package/dist/shared/domain/tokens.d.ts +26 -0
  48. package/dist/shared/domain/tokens.js +19 -0
  49. package/dist/shared/domain/tokens.js.map +1 -0
  50. package/package.json +9 -1
@@ -0,0 +1,189 @@
1
+ import Anthropic, { AnthropicError, APIError } from '@anthropic-ai/sdk';
2
+ import { InvalidAgentConfigError, UnrecoverableError, withSpentTokens, } from '#src/agent/domain/errors';
3
+ import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, describeFailure, treatErrors, } from '#src/agent/domain/providerFailure';
4
+ import { isOneOf } from '#src/shared/domain/isOneOf';
5
+ import { MaxContextError } from "../domain/errors.js";
6
+ // The Messages API takes full model IDs, not the Agent SDK's aliases, so this list is its own.
7
+ export const claudeLLMModels = ['claude-opus-5-5', 'claude-fable-5-1', 'claude-sonnet-5'];
8
+ // Adaptive thinking spends from this budget too. Above roughly 21,000 the SDK refuses a request
9
+ // that is not streamed, because it could outlast its ten-minute timeout.
10
+ const maxOutputTokens = 16_000;
11
+ // The request itself is wrong or not allowed, so sending it again cannot succeed.
12
+ const unrecoverableStatuses = [400, 401, 403, 404, 413, 422];
13
+ // The account is out of credit, or its key cannot make the request. Resending cannot fix
14
+ // either, whatever status the error arrives with.
15
+ const unrecoverableErrorTypes = ['billing_error', 'authentication_error', 'permission_error'];
16
+ /**
17
+ * Only the text crosses back. Anthropic needs a thinking block again only to continue a turn
18
+ * that called a tool, and none is sent yet, so a reasoning part stays in the conversation for
19
+ * narration and is left out of the request. A message with no text is left out whole; the API
20
+ * joins the consecutive user turns that leaves.
21
+ */
22
+ function toClaudeInput(message) {
23
+ const text = message.content
24
+ .filter(part => part.type === 'text')
25
+ .map(part => part.text)
26
+ .join('\n');
27
+ return text ? [{ role: message.role, content: text }] : [];
28
+ }
29
+ function describeBlock(block, logger) {
30
+ switch (block.type) {
31
+ case 'text':
32
+ return [{ type: 'text', text: block.text }];
33
+ case 'thinking':
34
+ // Summarized thinking; the signature only matters for replay, which is not done.
35
+ return block.thinking ? [{ type: 'reasoning', text: block.thinking }] : [];
36
+ case 'redacted_thinking':
37
+ // Encrypted by design: there is nothing to narrate.
38
+ return [];
39
+ default:
40
+ // No tools are sent yet, so any other block is output this client does not expect.
41
+ treatErrors(() => {
42
+ logger.warn(block, 'Claude returned a content block the client does not map');
43
+ }, classifyHostFailure, 'Claude client logger failed while mapping a response');
44
+ return [];
45
+ }
46
+ }
47
+ function toStopReason(response) {
48
+ switch (response.stop_reason) {
49
+ case 'end_turn':
50
+ return 'completed';
51
+ case 'max_tokens':
52
+ return 'truncated';
53
+ case 'refusal':
54
+ return 'refused';
55
+ case 'model_context_window_exceeded':
56
+ throw new MaxContextError('The context no longer fits the Claude model', {
57
+ cause: 'The response stopped at the model context window.',
58
+ });
59
+ default:
60
+ // `tool_use`, `pause_turn` and `stop_sequence` need tools, server tools or stop
61
+ // sequences, and this client sends none of them.
62
+ throw new UnrecoverableError('The Claude response stopped unexpectedly', {
63
+ cause: `The response stopped with "${response.stop_reason ?? 'unknown'}".`,
64
+ });
65
+ }
66
+ }
67
+ /** Unlike OpenAI, Anthropic counts cache reads and writes apart from `input_tokens`. */
68
+ function toTokens(usage, logger) {
69
+ if (!usage) {
70
+ // Zeros would report a call that may have been billed as free.
71
+ treatErrors(() => {
72
+ logger.warn('Claude returned a response without usage; it cannot be accounted for');
73
+ }, classifyHostFailure, 'Claude client logger failed while reading usage');
74
+ return undefined;
75
+ }
76
+ return {
77
+ inputTokens: usage.input_tokens,
78
+ readCacheTokens: usage.cache_read_input_tokens ?? 0,
79
+ writtenCacheTokens: usage.cache_creation_input_tokens ?? 0,
80
+ outputTokens: usage.output_tokens,
81
+ };
82
+ }
83
+ function toLLMResponse(response, logger) {
84
+ let usage;
85
+ try {
86
+ // Inside the try: it warns when the usage is missing, and that warning can throw too.
87
+ usage = toTokens(response.usage, logger);
88
+ return {
89
+ message: {
90
+ role: 'assistant',
91
+ content: response.content.flatMap(block => describeBlock(block, logger)),
92
+ },
93
+ usage,
94
+ stopReason: toStopReason(response),
95
+ };
96
+ }
97
+ catch (error) {
98
+ // The call was billed whether the response failed or a logger did while mapping it, and
99
+ // an answer without usage leaves the run's count unknown.
100
+ throw withSpentTokens(error, usage, !response.usage);
101
+ }
102
+ }
103
+ /**
104
+ * The SDK resolves credentials lazily, so a missing key surfaces on the first call, as a plain
105
+ * `Error` the SDK gives no class of its own. An `AnthropicError` that is not an `APIError` never
106
+ * carries an answer from the API: on this client's path it is the SDK refusing to make the
107
+ * call, such as an unresolved profile or a request too large to send without streaming.
108
+ */
109
+ function isLocalSDKFailure(error) {
110
+ return ((error instanceof AnthropicError && !(error instanceof APIError)) ||
111
+ (error instanceof Error && error.message.startsWith('Could not resolve authentication method')));
112
+ }
113
+ function classifyClaudeFailure(error) {
114
+ if (isLocalSDKFailure(error)) {
115
+ // The API never answered, and calling again would fail the same way.
116
+ return new UnrecoverableError('The Claude client cannot send the request', {
117
+ cause: describeFailure(error),
118
+ });
119
+ }
120
+ if (error instanceof APIError) {
121
+ // Anthropic gives an overlong prompt no code of its own, only a 400 with this message.
122
+ if (error.status === 400 && error.message.includes('prompt is too long')) {
123
+ return new MaxContextError('The context no longer fits the Claude model', {
124
+ cause: describeFailure(error),
125
+ });
126
+ }
127
+ if (unrecoverableErrorTypes.some(type => type === error.type) ||
128
+ unrecoverableStatuses.some(status => status === error.status)) {
129
+ return new UnrecoverableError('Claude rejected the request', {
130
+ cause: describeFailure(error),
131
+ });
132
+ }
133
+ }
134
+ // A rate limit, an overload (529), a 5xx or a network failure: a later attempt can succeed.
135
+ return classifyProviderFailure(error, 'The Claude request failed');
136
+ }
137
+ /**
138
+ * A stateless `LLMClient` on the Anthropic Messages API, which keeps nothing between calls:
139
+ * every call carries the whole context, so the conversation is MiKode's, not the provider's.
140
+ */
141
+ export class ClaudeLLMClient {
142
+ client;
143
+ model;
144
+ systemPrompt;
145
+ logger;
146
+ // `model` is untyped on purpose: this adapter is the one that knows what Claude supports.
147
+ constructor({ model, systemPrompt, logger, }) {
148
+ if (!isOneOf(claudeLLMModels, model)) {
149
+ throw new InvalidAgentConfigError(`"${model}" is not a Claude API model; expected one of: ${claudeLLMModels.join(', ')}`);
150
+ }
151
+ try {
152
+ // The SDK's default retries stay on, as OpenAI's do. See "The provider SDKs keep their
153
+ // transport retries under RetryingAgent" in decisions.md.
154
+ this.client = new Anthropic();
155
+ }
156
+ catch (error) {
157
+ // Conflicting credential options, for example: building it again cannot fix them. A
158
+ // missing key does not fail here but on the first call; see `isLocalSDKFailure`.
159
+ throw new UnrecoverableError('The Claude client could not be set up', {
160
+ cause: describeFailure(error),
161
+ });
162
+ }
163
+ this.model = model;
164
+ this.systemPrompt = systemPrompt;
165
+ this.logger = logger;
166
+ }
167
+ async send(context, signal) {
168
+ let response;
169
+ try {
170
+ response = await this.client.messages.create({
171
+ model: this.model,
172
+ max_tokens: maxOutputTokens,
173
+ system: this.systemPrompt,
174
+ messages: context.flatMap(toClaudeInput),
175
+ thinking: { type: 'adaptive', display: 'summarized' },
176
+ // Anthropic caches only on request. This marks the last block, so the next call
177
+ // reads everything before it from the cache instead of paying for it again.
178
+ cache_control: { type: 'ephemeral' },
179
+ }, { signal });
180
+ }
181
+ catch (error) {
182
+ // The SDK's abort error is named 'Error', so it would pass for a provider failure.
183
+ signal.throwIfAborted();
184
+ throw classifyClaudeFailure(error);
185
+ }
186
+ return treatErrors(() => toLLMResponse(response, this.logger), classifyLocalFailure, 'Claude response mapping failed');
187
+ }
188
+ }
189
+ //# sourceMappingURL=claudeLLMClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claudeLLMClient.js","sourceRoot":"","sources":["../../../src/llm/infrastructure/claudeLLMClient.ts"],"names":[],"mappings":"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","sourcesContent":["import Anthropic, { AnthropicError, APIError } from '@anthropic-ai/sdk';\nimport type {\n\tContentBlock,\n\tMessage as AnthropicMessage,\n\tMessageParam,\n\tUsage,\n} from '@anthropic-ai/sdk/resources/messages';\nimport {\n\tInvalidAgentConfigError,\n\tUnrecoverableError,\n\twithSpentTokens,\n} from '#src/agent/domain/errors';\nimport {\n\tclassifyHostFailure,\n\tclassifyLocalFailure,\n\tclassifyProviderFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/agent/domain/providerFailure';\nimport { isOneOf } from '#src/shared/domain/isOneOf';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport type { Tokens } from '#src/shared/domain/tokens';\nimport { MaxContextError } from '../domain/errors.ts';\nimport type { LLMClient, LLMResponse, StopReason } from '../domain/llm.ts';\nimport type { Message, MessagePart } from '../domain/message.ts';\n\n// The Messages API takes full model IDs, not the Agent SDK's aliases, so this list is its own.\nexport const claudeLLMModels = ['claude-opus-5-5', 'claude-fable-5-1', 'claude-sonnet-5'] as const;\nexport type ClaudeLLMModel = (typeof claudeLLMModels)[number];\n\n// Adaptive thinking spends from this budget too. Above roughly 21,000 the SDK refuses a request\n// that is not streamed, because it could outlast its ten-minute timeout.\nconst maxOutputTokens = 16_000;\n\n// The request itself is wrong or not allowed, so sending it again cannot succeed.\nconst unrecoverableStatuses = [400, 401, 403, 404, 413, 422];\n\n// The account is out of credit, or its key cannot make the request. Resending cannot fix\n// either, whatever status the error arrives with.\nconst unrecoverableErrorTypes = ['billing_error', 'authentication_error', 'permission_error'];\n\n/**\n * Only the text crosses back. Anthropic needs a thinking block again only to continue a turn\n * that called a tool, and none is sent yet, so a reasoning part stays in the conversation for\n * narration and is left out of the request. A message with no text is left out whole; the API\n * joins the consecutive user turns that leaves.\n */\nfunction toClaudeInput(message: Message): MessageParam[] {\n\tconst text = message.content\n\t\t.filter(part => part.type === 'text')\n\t\t.map(part => part.text)\n\t\t.join('\\n');\n\n\treturn text ? [{ role: message.role, content: text }] : [];\n}\n\nfunction describeBlock(block: ContentBlock, logger: ILogger): MessagePart[] {\n\tswitch (block.type) {\n\t\tcase 'text':\n\t\t\treturn [{ type: 'text', text: block.text }];\n\t\tcase 'thinking':\n\t\t\t// Summarized thinking; the signature only matters for replay, which is not done.\n\t\t\treturn block.thinking ? [{ type: 'reasoning', text: block.thinking }] : [];\n\t\tcase 'redacted_thinking':\n\t\t\t// Encrypted by design: there is nothing to narrate.\n\t\t\treturn [];\n\t\tdefault:\n\t\t\t// No tools are sent yet, so any other block is output this client does not expect.\n\t\t\ttreatErrors(\n\t\t\t\t() => {\n\t\t\t\t\tlogger.warn(block, 'Claude returned a content block the client does not map');\n\t\t\t\t},\n\t\t\t\tclassifyHostFailure,\n\t\t\t\t'Claude client logger failed while mapping a response',\n\t\t\t);\n\t\t\treturn [];\n\t}\n}\n\nfunction toStopReason(response: AnthropicMessage): StopReason {\n\tswitch (response.stop_reason) {\n\t\tcase 'end_turn':\n\t\t\treturn 'completed';\n\t\tcase 'max_tokens':\n\t\t\treturn 'truncated';\n\t\tcase 'refusal':\n\t\t\treturn 'refused';\n\t\tcase 'model_context_window_exceeded':\n\t\t\tthrow new MaxContextError('The context no longer fits the Claude model', {\n\t\t\t\tcause: 'The response stopped at the model context window.',\n\t\t\t});\n\t\tdefault:\n\t\t\t// `tool_use`, `pause_turn` and `stop_sequence` need tools, server tools or stop\n\t\t\t// sequences, and this client sends none of them.\n\t\t\tthrow new UnrecoverableError('The Claude response stopped unexpectedly', {\n\t\t\t\tcause: `The response stopped with \"${response.stop_reason ?? 'unknown'}\".`,\n\t\t\t});\n\t}\n}\n\n/** Unlike OpenAI, Anthropic counts cache reads and writes apart from `input_tokens`. */\nfunction toTokens(usage: Usage | undefined, logger: ILogger): Tokens | undefined {\n\tif (!usage) {\n\t\t// Zeros would report a call that may have been billed as free.\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn('Claude returned a response without usage; it cannot be accounted for');\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Claude client logger failed while reading usage',\n\t\t);\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tinputTokens: usage.input_tokens,\n\t\treadCacheTokens: usage.cache_read_input_tokens ?? 0,\n\t\twrittenCacheTokens: usage.cache_creation_input_tokens ?? 0,\n\t\toutputTokens: usage.output_tokens,\n\t};\n}\n\nfunction toLLMResponse(response: AnthropicMessage, logger: ILogger): LLMResponse {\n\tlet usage: Tokens | undefined;\n\n\ttry {\n\t\t// Inside the try: it warns when the usage is missing, and that warning can throw too.\n\t\tusage = toTokens(response.usage, logger);\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\trole: 'assistant',\n\t\t\t\tcontent: response.content.flatMap(block => describeBlock(block, logger)),\n\t\t\t},\n\t\t\tusage,\n\t\t\tstopReason: toStopReason(response),\n\t\t};\n\t} catch (error) {\n\t\t// The call was billed whether the response failed or a logger did while mapping it, and\n\t\t// an answer without usage leaves the run's count unknown.\n\t\tthrow withSpentTokens(error, usage, !response.usage);\n\t}\n}\n\n/**\n * The SDK resolves credentials lazily, so a missing key surfaces on the first call, as a plain\n * `Error` the SDK gives no class of its own. An `AnthropicError` that is not an `APIError` never\n * carries an answer from the API: on this client's path it is the SDK refusing to make the\n * call, such as an unresolved profile or a request too large to send without streaming.\n */\nfunction isLocalSDKFailure(error: unknown): boolean {\n\treturn (\n\t\t(error instanceof AnthropicError && !(error instanceof APIError)) ||\n\t\t(error instanceof Error && error.message.startsWith('Could not resolve authentication method'))\n\t);\n}\n\nfunction classifyClaudeFailure(error: unknown): Error {\n\tif (isLocalSDKFailure(error)) {\n\t\t// The API never answered, and calling again would fail the same way.\n\t\treturn new UnrecoverableError('The Claude client cannot send the request', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n\tif (error instanceof APIError) {\n\t\t// Anthropic gives an overlong prompt no code of its own, only a 400 with this message.\n\t\tif (error.status === 400 && error.message.includes('prompt is too long')) {\n\t\t\treturn new MaxContextError('The context no longer fits the Claude model', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t\tif (\n\t\t\tunrecoverableErrorTypes.some(type => type === error.type) ||\n\t\t\tunrecoverableStatuses.some(status => status === error.status)\n\t\t) {\n\t\t\treturn new UnrecoverableError('Claude rejected the request', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t}\n\n\t// A rate limit, an overload (529), a 5xx or a network failure: a later attempt can succeed.\n\treturn classifyProviderFailure(error, 'The Claude request failed');\n}\n\n/**\n * A stateless `LLMClient` on the Anthropic Messages API, which keeps nothing between calls:\n * every call carries the whole context, so the conversation is MiKode's, not the provider's.\n */\nexport class ClaudeLLMClient implements LLMClient {\n\tprivate readonly client: Anthropic;\n\tprivate readonly model: ClaudeLLMModel;\n\tprivate readonly systemPrompt: string;\n\tprivate readonly logger: ILogger;\n\n\t// `model` is untyped on purpose: this adapter is the one that knows what Claude supports.\n\tconstructor({\n\t\tmodel,\n\t\tsystemPrompt,\n\t\tlogger,\n\t}: {\n\t\tmodel: string;\n\t\tsystemPrompt: string;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!isOneOf(claudeLLMModels, model)) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`\"${model}\" is not a Claude API model; expected one of: ${claudeLLMModels.join(', ')}`,\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\t// The SDK's default retries stay on, as OpenAI's do. See \"The provider SDKs keep their\n\t\t\t// transport retries under RetryingAgent\" in decisions.md.\n\t\t\tthis.client = new Anthropic();\n\t\t} catch (error) {\n\t\t\t// Conflicting credential options, for example: building it again cannot fix them. A\n\t\t\t// missing key does not fail here but on the first call; see `isLocalSDKFailure`.\n\t\t\tthrow new UnrecoverableError('The Claude client could not be set up', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\n\t\tthis.model = model;\n\t\tthis.systemPrompt = systemPrompt;\n\t\tthis.logger = logger;\n\t}\n\n\tasync send(context: Message[], signal: AbortSignal): Promise<LLMResponse> {\n\t\tlet response: AnthropicMessage;\n\n\t\ttry {\n\t\t\tresponse = await this.client.messages.create(\n\t\t\t\t{\n\t\t\t\t\tmodel: this.model,\n\t\t\t\t\tmax_tokens: maxOutputTokens,\n\t\t\t\t\tsystem: this.systemPrompt,\n\t\t\t\t\tmessages: context.flatMap(toClaudeInput),\n\t\t\t\t\tthinking: { type: 'adaptive', display: 'summarized' },\n\t\t\t\t\t// Anthropic caches only on request. This marks the last block, so the next call\n\t\t\t\t\t// reads everything before it from the cache instead of paying for it again.\n\t\t\t\t\tcache_control: { type: 'ephemeral' },\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The SDK's abort error is named 'Error', so it would pass for a provider failure.\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow classifyClaudeFailure(error);\n\t\t}\n\n\t\treturn treatErrors(\n\t\t\t() => toLLMResponse(response, this.logger),\n\t\t\tclassifyLocalFailure,\n\t\t\t'Claude response mapping failed',\n\t\t);\n\t}\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { ILogger } from '#src/shared/domain/logger';
2
+ import type { LLMClient, LLMResponse } from '../domain/llm.ts';
3
+ import type { Message } from '../domain/message.ts';
4
+ export declare const openAIModels: readonly ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.6-terra"];
5
+ export type OpenAIModel = (typeof openAIModels)[number];
6
+ /**
7
+ * A stateless `LLMClient` on the OpenAI Responses API. With `store: false` OpenAI keeps
8
+ * nothing between calls, so every call carries the whole context: the conversation is
9
+ * MiKode's, not the provider's.
10
+ */
11
+ export declare class OpenAILLMClient implements LLMClient {
12
+ private readonly client;
13
+ private readonly model;
14
+ private readonly systemPrompt;
15
+ private readonly logger;
16
+ constructor({ model, systemPrompt, logger, }: {
17
+ model: string;
18
+ systemPrompt: string;
19
+ logger: ILogger;
20
+ });
21
+ send(context: Message[], signal: AbortSignal): Promise<LLMResponse>;
22
+ }
@@ -0,0 +1,172 @@
1
+ import OpenAI, { APIError } from 'openai';
2
+ import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpentTokens, } from '#src/agent/domain/errors';
3
+ import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, describeFailure, treatErrors, } from '#src/agent/domain/providerFailure';
4
+ import { isOneOf } from '#src/shared/domain/isOneOf';
5
+ import { MaxContextError } from "../domain/errors.js";
6
+ // The SDK types `model` as a plain string, so this list is maintained by hand.
7
+ export const openAIModels = [
8
+ 'gpt-6-astra',
9
+ 'gpt-5.6-sol',
10
+ 'gpt-5.6-luna',
11
+ 'gpt-5.6-terra',
12
+ ];
13
+ // The request itself is wrong or not allowed, so sending it again cannot succeed.
14
+ const unrecoverableStatuses = [400, 401, 403, 404, 422];
15
+ // Out of credit or over a spend limit. These arrive as a 429 like a rate limit, but the account
16
+ // stays blocked however often the request is sent.
17
+ const exhaustedQuotaCodes = [
18
+ 'insufficient_quota',
19
+ 'credit_balance_exhausted',
20
+ 'project_spend_limit_exceeded',
21
+ ];
22
+ // The codes OpenAI gives a failed response that a new attempt can succeed past.
23
+ const transientResponseErrorCodes = ['server_error', 'rate_limit_exceeded'];
24
+ /**
25
+ * Only the text crosses back. OpenAI replays reasoning only from the encrypted item it
26
+ * returned, which a `Message` does not keep, so a reasoning part stays in the conversation
27
+ * for narration and is left out of the request. A message with no text is left out whole.
28
+ */
29
+ function toOpenAIInput(message) {
30
+ const text = message.content
31
+ .filter(part => part.type === 'text')
32
+ .map(part => part.text)
33
+ .join('\n');
34
+ return text ? [{ role: message.role, content: text }] : [];
35
+ }
36
+ function describeItem(item, logger) {
37
+ switch (item.type) {
38
+ case 'message':
39
+ // A refusal is not an answer: it only sets the stop reason.
40
+ return item.content.flatMap(content => content.type === 'output_text' ? [{ type: 'text', text: content.text }] : []);
41
+ case 'reasoning': {
42
+ // The summary is the readable part; the encrypted content is opaque by design.
43
+ const text = item.summary.map(summary => summary.text).join('\n');
44
+ return text ? [{ type: 'reasoning', text }] : [];
45
+ }
46
+ default:
47
+ // No tools are sent yet, so any other item is output this client does not expect.
48
+ treatErrors(() => {
49
+ logger.warn(item, 'OpenAI returned an output item the client does not map');
50
+ }, classifyHostFailure, 'OpenAI client logger failed while mapping a response');
51
+ return [];
52
+ }
53
+ }
54
+ function toStopReason(response) {
55
+ if (response.status === 'incomplete') {
56
+ return response.incomplete_details?.reason === 'content_filter' ? 'refused' : 'truncated';
57
+ }
58
+ if (response.status !== 'completed') {
59
+ const cause = response.error?.message ??
60
+ `The response ended with status "${response.status ?? 'unknown'}".`;
61
+ // The same failure is retried when it arrives as an SDK error, so it must be here too.
62
+ if (response.error && transientResponseErrorCodes.includes(response.error.code)) {
63
+ throw new RecoverableError('The OpenAI response failed', { cause });
64
+ }
65
+ throw new UnrecoverableError('The OpenAI response did not complete', { cause });
66
+ }
67
+ const refused = response.output.some(item => item.type === 'message' && item.content.some(content => content.type === 'refusal'));
68
+ return refused ? 'refused' : 'completed';
69
+ }
70
+ /** OpenAI counts both cache reads and cache writes inside `input_tokens`. */
71
+ function toTokens(usage, logger) {
72
+ if (!usage) {
73
+ // Zeros would report a call that may have been billed as free.
74
+ treatErrors(() => {
75
+ logger.warn('OpenAI returned a response without usage; it cannot be accounted for');
76
+ }, classifyHostFailure, 'OpenAI client logger failed while reading usage');
77
+ return undefined;
78
+ }
79
+ const { cached_tokens, cache_write_tokens } = usage.input_tokens_details;
80
+ return {
81
+ inputTokens: usage.input_tokens - cached_tokens - cache_write_tokens,
82
+ readCacheTokens: cached_tokens,
83
+ writtenCacheTokens: cache_write_tokens,
84
+ outputTokens: usage.output_tokens,
85
+ };
86
+ }
87
+ function toLLMResponse(response, logger) {
88
+ let usage;
89
+ try {
90
+ // Inside the try: it warns when the usage is missing, and that warning can throw too.
91
+ usage = toTokens(response.usage, logger);
92
+ return {
93
+ message: {
94
+ role: 'assistant',
95
+ content: response.output.flatMap(item => describeItem(item, logger)),
96
+ },
97
+ usage,
98
+ stopReason: toStopReason(response),
99
+ };
100
+ }
101
+ catch (error) {
102
+ // The call was billed whether the response failed or a logger did while mapping it, and
103
+ // an answer without usage leaves the run's count unknown.
104
+ throw withSpentTokens(error, usage, !response.usage);
105
+ }
106
+ }
107
+ function classifyOpenAIFailure(error) {
108
+ if (error instanceof APIError) {
109
+ if (error.code === 'context_length_exceeded') {
110
+ return new MaxContextError('The context no longer fits the OpenAI model', {
111
+ cause: describeFailure(error),
112
+ });
113
+ }
114
+ if (exhaustedQuotaCodes.some(code => code === error.code) ||
115
+ unrecoverableStatuses.some(status => status === error.status)) {
116
+ return new UnrecoverableError('OpenAI rejected the request', {
117
+ cause: describeFailure(error),
118
+ });
119
+ }
120
+ }
121
+ return classifyProviderFailure(error, 'The OpenAI request failed');
122
+ }
123
+ /**
124
+ * A stateless `LLMClient` on the OpenAI Responses API. With `store: false` OpenAI keeps
125
+ * nothing between calls, so every call carries the whole context: the conversation is
126
+ * MiKode's, not the provider's.
127
+ */
128
+ export class OpenAILLMClient {
129
+ client;
130
+ model;
131
+ systemPrompt;
132
+ logger;
133
+ // `model` is untyped on purpose: this adapter is the one that knows what OpenAI supports.
134
+ constructor({ model, systemPrompt, logger, }) {
135
+ if (!isOneOf(openAIModels, model)) {
136
+ throw new InvalidAgentConfigError(`"${model}" is not an OpenAI model; expected one of: ${openAIModels.join(', ')}`);
137
+ }
138
+ try {
139
+ // The SDK's default retries stay on: they back off and honour `Retry-After`, which
140
+ // `RetryingAgent` does not. See "The provider SDKs keep their transport retries" in decisions.md.
141
+ this.client = new OpenAI();
142
+ }
143
+ catch (error) {
144
+ // A missing API key, for example: building the client again cannot fix it.
145
+ throw new UnrecoverableError('The OpenAI client could not be set up', {
146
+ cause: describeFailure(error),
147
+ });
148
+ }
149
+ this.model = model;
150
+ this.systemPrompt = systemPrompt;
151
+ this.logger = logger;
152
+ }
153
+ async send(context, signal) {
154
+ let response;
155
+ try {
156
+ response = await this.client.responses.create({
157
+ model: this.model,
158
+ instructions: this.systemPrompt,
159
+ input: context.flatMap(toOpenAIInput),
160
+ store: false,
161
+ reasoning: { summary: 'auto' },
162
+ }, { signal });
163
+ }
164
+ catch (error) {
165
+ // The SDK's abort error is named 'Error', so it would pass for a provider failure.
166
+ signal.throwIfAborted();
167
+ throw classifyOpenAIFailure(error);
168
+ }
169
+ return treatErrors(() => toLLMResponse(response, this.logger), classifyLocalFailure, 'OpenAI response mapping failed');
170
+ }
171
+ }
172
+ //# sourceMappingURL=openAILLMClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openAILLMClient.js","sourceRoot":"","sources":["../../../src/llm/infrastructure/openAILLMClient.ts"],"names":[],"mappings":"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","sourcesContent":["import OpenAI, { APIError } from 'openai';\nimport type {\n\tResponse,\n\tResponseInputItem,\n\tResponseOutputItem,\n\tResponseUsage,\n} from 'openai/resources/responses/responses';\nimport {\n\tInvalidAgentConfigError,\n\tRecoverableError,\n\tUnrecoverableError,\n\twithSpentTokens,\n} from '#src/agent/domain/errors';\nimport {\n\tclassifyHostFailure,\n\tclassifyLocalFailure,\n\tclassifyProviderFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/agent/domain/providerFailure';\nimport { isOneOf } from '#src/shared/domain/isOneOf';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport type { Tokens } from '#src/shared/domain/tokens';\nimport { MaxContextError } from '../domain/errors.ts';\nimport type { LLMClient, LLMResponse, StopReason } from '../domain/llm.ts';\nimport type { Message, MessagePart } from '../domain/message.ts';\n\n// The SDK types `model` as a plain string, so this list is maintained by hand.\nexport const openAIModels = [\n\t'gpt-6-astra',\n\t'gpt-5.6-sol',\n\t'gpt-5.6-luna',\n\t'gpt-5.6-terra',\n] as const;\nexport type OpenAIModel = (typeof openAIModels)[number];\n\n// The request itself is wrong or not allowed, so sending it again cannot succeed.\nconst unrecoverableStatuses = [400, 401, 403, 404, 422];\n\n// Out of credit or over a spend limit. These arrive as a 429 like a rate limit, but the account\n// stays blocked however often the request is sent.\nconst exhaustedQuotaCodes = [\n\t'insufficient_quota',\n\t'credit_balance_exhausted',\n\t'project_spend_limit_exceeded',\n];\n\n// The codes OpenAI gives a failed response that a new attempt can succeed past.\nconst transientResponseErrorCodes = ['server_error', 'rate_limit_exceeded'];\n\n/**\n * Only the text crosses back. OpenAI replays reasoning only from the encrypted item it\n * returned, which a `Message` does not keep, so a reasoning part stays in the conversation\n * for narration and is left out of the request. A message with no text is left out whole.\n */\nfunction toOpenAIInput(message: Message): ResponseInputItem[] {\n\tconst text = message.content\n\t\t.filter(part => part.type === 'text')\n\t\t.map(part => part.text)\n\t\t.join('\\n');\n\n\treturn text ? [{ role: message.role, content: text }] : [];\n}\n\nfunction describeItem(item: ResponseOutputItem, logger: ILogger): MessagePart[] {\n\tswitch (item.type) {\n\t\tcase 'message':\n\t\t\t// A refusal is not an answer: it only sets the stop reason.\n\t\t\treturn item.content.flatMap(content =>\n\t\t\t\tcontent.type === 'output_text' ? [{ type: 'text' as const, text: content.text }] : [],\n\t\t\t);\n\t\tcase 'reasoning': {\n\t\t\t// The summary is the readable part; the encrypted content is opaque by design.\n\t\t\tconst text = item.summary.map(summary => summary.text).join('\\n');\n\t\t\treturn text ? [{ type: 'reasoning', text }] : [];\n\t\t}\n\t\tdefault:\n\t\t\t// No tools are sent yet, so any other item is output this client does not expect.\n\t\t\ttreatErrors(\n\t\t\t\t() => {\n\t\t\t\t\tlogger.warn(item, 'OpenAI returned an output item the client does not map');\n\t\t\t\t},\n\t\t\t\tclassifyHostFailure,\n\t\t\t\t'OpenAI client logger failed while mapping a response',\n\t\t\t);\n\t\t\treturn [];\n\t}\n}\n\nfunction toStopReason(response: Response): StopReason {\n\tif (response.status === 'incomplete') {\n\t\treturn response.incomplete_details?.reason === 'content_filter' ? 'refused' : 'truncated';\n\t}\n\tif (response.status !== 'completed') {\n\t\tconst cause =\n\t\t\tresponse.error?.message ??\n\t\t\t`The response ended with status \"${response.status ?? 'unknown'}\".`;\n\t\t// The same failure is retried when it arrives as an SDK error, so it must be here too.\n\t\tif (response.error && transientResponseErrorCodes.includes(response.error.code)) {\n\t\t\tthrow new RecoverableError('The OpenAI response failed', { cause });\n\t\t}\n\t\tthrow new UnrecoverableError('The OpenAI response did not complete', { cause });\n\t}\n\n\tconst refused = response.output.some(\n\t\titem => item.type === 'message' && item.content.some(content => content.type === 'refusal'),\n\t);\n\treturn refused ? 'refused' : 'completed';\n}\n\n/** OpenAI counts both cache reads and cache writes inside `input_tokens`. */\nfunction toTokens(usage: ResponseUsage | undefined, logger: ILogger): Tokens | undefined {\n\tif (!usage) {\n\t\t// Zeros would report a call that may have been billed as free.\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn('OpenAI returned a response without usage; it cannot be accounted for');\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'OpenAI client logger failed while reading usage',\n\t\t);\n\t\treturn undefined;\n\t}\n\n\tconst { cached_tokens, cache_write_tokens } = usage.input_tokens_details;\n\treturn {\n\t\tinputTokens: usage.input_tokens - cached_tokens - cache_write_tokens,\n\t\treadCacheTokens: cached_tokens,\n\t\twrittenCacheTokens: cache_write_tokens,\n\t\toutputTokens: usage.output_tokens,\n\t};\n}\n\nfunction toLLMResponse(response: Response, logger: ILogger): LLMResponse {\n\tlet usage: Tokens | undefined;\n\n\ttry {\n\t\t// Inside the try: it warns when the usage is missing, and that warning can throw too.\n\t\tusage = toTokens(response.usage, logger);\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\trole: 'assistant',\n\t\t\t\tcontent: response.output.flatMap(item => describeItem(item, logger)),\n\t\t\t},\n\t\t\tusage,\n\t\t\tstopReason: toStopReason(response),\n\t\t};\n\t} catch (error) {\n\t\t// The call was billed whether the response failed or a logger did while mapping it, and\n\t\t// an answer without usage leaves the run's count unknown.\n\t\tthrow withSpentTokens(error, usage, !response.usage);\n\t}\n}\n\nfunction classifyOpenAIFailure(error: unknown): Error {\n\tif (error instanceof APIError) {\n\t\tif (error.code === 'context_length_exceeded') {\n\t\t\treturn new MaxContextError('The context no longer fits the OpenAI model', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t\tif (\n\t\t\texhaustedQuotaCodes.some(code => code === error.code) ||\n\t\t\tunrecoverableStatuses.some(status => status === error.status)\n\t\t) {\n\t\t\treturn new UnrecoverableError('OpenAI rejected the request', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn classifyProviderFailure(error, 'The OpenAI request failed');\n}\n\n/**\n * A stateless `LLMClient` on the OpenAI Responses API. With `store: false` OpenAI keeps\n * nothing between calls, so every call carries the whole context: the conversation is\n * MiKode's, not the provider's.\n */\nexport class OpenAILLMClient implements LLMClient {\n\tprivate readonly client: OpenAI;\n\tprivate readonly model: OpenAIModel;\n\tprivate readonly systemPrompt: string;\n\tprivate readonly logger: ILogger;\n\n\t// `model` is untyped on purpose: this adapter is the one that knows what OpenAI supports.\n\tconstructor({\n\t\tmodel,\n\t\tsystemPrompt,\n\t\tlogger,\n\t}: {\n\t\tmodel: string;\n\t\tsystemPrompt: string;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!isOneOf(openAIModels, model)) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`\"${model}\" is not an OpenAI model; expected one of: ${openAIModels.join(', ')}`,\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\t// The SDK's default retries stay on: they back off and honour `Retry-After`, which\n\t\t\t// `RetryingAgent` does not. See \"The provider SDKs keep their transport retries\" in decisions.md.\n\t\t\tthis.client = new OpenAI();\n\t\t} catch (error) {\n\t\t\t// A missing API key, for example: building the client again cannot fix it.\n\t\t\tthrow new UnrecoverableError('The OpenAI client could not be set up', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\n\t\tthis.model = model;\n\t\tthis.systemPrompt = systemPrompt;\n\t\tthis.logger = logger;\n\t}\n\n\tasync send(context: Message[], signal: AbortSignal): Promise<LLMResponse> {\n\t\tlet response: Response;\n\n\t\ttry {\n\t\t\tresponse = await this.client.responses.create(\n\t\t\t\t{\n\t\t\t\t\tmodel: this.model,\n\t\t\t\t\tinstructions: this.systemPrompt,\n\t\t\t\t\tinput: context.flatMap(toOpenAIInput),\n\t\t\t\t\tstore: false,\n\t\t\t\t\treasoning: { summary: 'auto' },\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The SDK's abort error is named 'Error', so it would pass for a provider failure.\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow classifyOpenAIFailure(error);\n\t\t}\n\n\t\treturn treatErrors(\n\t\t\t() => toLLMResponse(response, this.logger),\n\t\t\tclassifyLocalFailure,\n\t\t\t'OpenAI response mapping failed',\n\t\t);\n\t}\n}\n"]}
@@ -1,7 +1,7 @@
1
- import type { Agent, AgentResponse, Callback } from '../../../agent/domain/agent.ts';
1
+ import type { Agent, AgentResponse, Callback } from '#src/agent/domain/agent';
2
2
  import type { ReviewerDecision } from './reviewerDecision.ts';
3
3
  import type { Validator } from '../interface/validator.ts';
4
- import type { ILogger } from '../../../shared/domain/logger.ts';
4
+ import type { ILogger } from '#src/shared/domain/logger';
5
5
  export declare class OrchestratorAgent implements Agent {
6
6
  private plannerAgent;
7
7
  private executorAgent;
@@ -17,7 +17,8 @@ export declare class OrchestratorAgent implements Agent {
17
17
  maxAttempts?: number;
18
18
  logger: ILogger;
19
19
  });
20
- run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse | undefined>;
20
+ run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse>;
21
+ private runRounds;
21
22
  private getReviewerDecision;
22
23
  private warn;
23
24
  }
@@ -1,5 +1,6 @@
1
- import { RecoverableError, UnrecoverableError } from "../../../agent/domain/errors.js";
2
- import { classifyHostFailure, treatErrors } from "../../../agent/domain/providerFailure.js";
1
+ import { addTokens } from '#src/shared/domain/tokens';
2
+ import { RecoverableError, UnrecoverableError, withSpentTokens } from '#src/agent/domain/errors';
3
+ import { classifyHostFailure, treatErrors } from '#src/agent/domain/providerFailure';
3
4
  const getPlannerPrompt = (userPrompt, previousFailureReason) => {
4
5
  const feedback = previousFailureReason
5
6
  ? `\n\nFeedback from the previous attempt:\n---\n${previousFailureReason}\n---`
@@ -43,17 +44,22 @@ Executor response (context only; verify it independently):
43
44
  ${executorResult}
44
45
  ---${retryNotice}`;
45
46
  };
47
+ // Every role's response counts, even an empty one that starts another round: it was billed.
46
48
  function addToTotals(totals, response) {
47
- totals.duration += response.duration;
48
- totals.inputTokens += response.inputTokens;
49
- totals.outputTokens += response.outputTokens;
49
+ if (!response.tokens)
50
+ totals.unreported = true;
51
+ totals.tokens = addTokens(totals.tokens, response.tokens);
52
+ }
53
+ /** Unknown rather than partial: a sum missing a billed call must not look complete. */
54
+ function runTokens(totals) {
55
+ return totals.unreported ? undefined : totals.tokens;
50
56
  }
51
57
  function stripCodeFence(text) {
52
58
  const match = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(text.trim());
53
59
  return match?.[1] ?? text;
54
60
  }
55
61
  function parseReviewerDecision(response, reviewerDecisionValidator) {
56
- if (!response?.response) {
62
+ if (!response.response) {
57
63
  throw new RecoverableError("There's no decision from the reviewer, something went wrong", {
58
64
  cause: 'Reviewer decision is missing.',
59
65
  });
@@ -94,13 +100,30 @@ export class OrchestratorAgent {
94
100
  this.logger = logger;
95
101
  }
96
102
  async run(prompt, signal, callback) {
103
+ const start = Date.now();
97
104
  // Per invocation, not per instance: the CLI keeps one orchestrator for a whole session.
98
- const totals = { duration: 0, inputTokens: 0, outputTokens: 0 };
105
+ const totals = { tokens: undefined, unreported: false };
106
+ try {
107
+ await this.runRounds(prompt, signal, callback, totals);
108
+ }
109
+ catch (error) {
110
+ // A failing role carries its own tokens; the roles before it are in the totals.
111
+ throw withSpentTokens(error, totals.tokens, totals.unreported);
112
+ }
113
+ return {
114
+ response: 'All job has finished',
115
+ tokens: runTokens(totals),
116
+ // The whole run's wall clock, so it matches what the consumer waited.
117
+ duration: (Date.now() - start) / 1000,
118
+ };
119
+ }
120
+ async runRounds(prompt, signal, callback, totals) {
99
121
  let lastFailureReason;
100
122
  for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
101
123
  const isLastAttempt = attempt === this.maxAttempts;
102
124
  const plannerResponse = await this.plannerAgent.run(getPlannerPrompt(prompt, lastFailureReason), signal, callback);
103
- if (!plannerResponse?.response) {
125
+ addToTotals(totals, plannerResponse);
126
+ if (!plannerResponse.response) {
104
127
  lastFailureReason = 'The planner produced no response.';
105
128
  if (isLastAttempt) {
106
129
  throw new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });
@@ -108,9 +131,9 @@ export class OrchestratorAgent {
108
131
  this.warn(`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the planner produced no response; starting another round`);
109
132
  continue;
110
133
  }
111
- addToTotals(totals, plannerResponse);
112
134
  const executorResponse = await this.executorAgent.run(getExecutorPrompt(prompt, plannerResponse.response), signal, callback);
113
- if (!executorResponse?.response) {
135
+ addToTotals(totals, executorResponse);
136
+ if (!executorResponse.response) {
114
137
  lastFailureReason = 'The executor produced no response.';
115
138
  if (isLastAttempt) {
116
139
  throw new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });
@@ -118,11 +141,9 @@ export class OrchestratorAgent {
118
141
  this.warn(`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the executor produced no response; starting another round`);
119
142
  continue;
120
143
  }
121
- addToTotals(totals, executorResponse);
122
144
  const reviewerDecision = await this.getReviewerDecision(prompt, plannerResponse.response, executorResponse.response, signal, callback, totals);
123
- if (reviewerDecision.decision === 'approved') {
124
- return { response: 'All job has finished', ...totals };
125
- }
145
+ if (reviewerDecision.decision === 'approved')
146
+ return;
126
147
  lastFailureReason = reviewerDecision.feedback;
127
148
  if (isLastAttempt) {
128
149
  throw new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });
@@ -140,9 +161,7 @@ export class OrchestratorAgent {
140
161
  let parseFailureReason;
141
162
  for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
142
163
  const reviewerResponse = await this.reviewerAgent.run(getReviewerPrompt(userPrompt, plannerPrompt, executorResult, parseFailureReason), signal, callback);
143
- if (reviewerResponse) {
144
- addToTotals(totals, reviewerResponse);
145
- }
164
+ addToTotals(totals, reviewerResponse);
146
165
  try {
147
166
  return parseReviewerDecision(reviewerResponse, this.reviewerDecisionValidator);
148
167
  }