@mikode13/harness 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +5 -0
  2. package/dist/agent/domain/agent.d.ts +8 -1
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/errors.d.ts +1 -1
  5. package/dist/agent/domain/errors.js +1 -1
  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 +2 -2
  10. package/dist/engines/claude/infrastructure/model/claudeAgent.js +3 -3
  11. package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
  12. package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +2 -2
  13. package/dist/engines/codex/infrastructure/model/codexAgent.js +4 -4
  14. package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
  15. package/dist/engines/domain/model/llmAgent.d.ts +23 -6
  16. package/dist/engines/domain/model/llmAgent.js +157 -31
  17. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  18. package/dist/factory/infrastructure/agentFactory.d.ts +19 -13
  19. package/dist/factory/infrastructure/agentFactory.js +48 -23
  20. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  21. package/dist/llm/domain/conversation.d.ts +4 -3
  22. package/dist/llm/domain/conversation.js +5 -4
  23. package/dist/llm/domain/conversation.js.map +1 -1
  24. package/dist/llm/domain/errors.d.ts +1 -1
  25. package/dist/llm/domain/errors.js +1 -1
  26. package/dist/llm/domain/errors.js.map +1 -1
  27. package/dist/llm/domain/llm.d.ts +6 -2
  28. package/dist/llm/domain/llm.js.map +1 -1
  29. package/dist/llm/domain/message.d.ts +30 -4
  30. package/dist/llm/domain/message.js.map +1 -1
  31. package/dist/llm/domain/tool.d.ts +17 -0
  32. package/dist/llm/domain/tool.js +2 -0
  33. package/dist/llm/domain/tool.js.map +1 -0
  34. package/dist/llm/infrastructure/claudeLLMClient.d.ts +25 -0
  35. package/dist/llm/infrastructure/claudeLLMClient.js +248 -0
  36. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -0
  37. package/dist/llm/infrastructure/openAILLMClient.d.ts +6 -2
  38. package/dist/llm/infrastructure/openAILLMClient.js +92 -16
  39. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  40. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +2 -2
  41. package/dist/orchestration/domain/model/orchestratorAgent.js +3 -3
  42. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  43. package/dist/retry/domain/model/retryingAgent.d.ts +2 -2
  44. package/dist/retry/domain/model/retryingAgent.js +4 -4
  45. package/dist/retry/domain/model/retryingAgent.js.map +1 -1
  46. package/package.json +8 -2
@@ -0,0 +1,17 @@
1
+ export interface JSONSchema {
2
+ type: 'object';
3
+ [keyword: string]: unknown;
4
+ }
5
+ export interface ToolDefinition {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: JSONSchema;
9
+ }
10
+ export interface Tool extends ToolDefinition {
11
+ /**
12
+ * Runs the tool on the input the model sent. The input is unknown because the model can
13
+ * send anything: validating it is the tool's job, and a rejection returns to the model as
14
+ * an error result.
15
+ */
16
+ execute(input: unknown, signal: AbortSignal): Promise<string>;
17
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../../../src/llm/domain/tool.ts"],"names":[],"mappings":"","sourcesContent":["export interface JSONSchema {\n\ttype: 'object';\n\t[keyword: string]: unknown;\n}\n\nexport interface ToolDefinition {\n\tname: string;\n\tdescription: string;\n\tinputSchema: JSONSchema;\n}\n\nexport interface Tool extends ToolDefinition {\n\t/**\n\t * Runs the tool on the input the model sent. The input is unknown because the model can\n\t * send anything: validating it is the tool's job, and a rejection returns to the model as\n\t * an error result.\n\t */\n\texecute(input: unknown, signal: AbortSignal): Promise<string>;\n}\n"]}
@@ -0,0 +1,25 @@
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
+ import type { ToolDefinition } from '../domain/tool.ts';
5
+ export declare const claudeLLMModels: readonly ["claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5"];
6
+ export type ClaudeLLMModel = (typeof claudeLLMModels)[number];
7
+ /**
8
+ * A stateless `LLMClient` on the Anthropic Messages API, which keeps nothing between calls:
9
+ * every call carries the whole context, so the conversation is MiKode's, not the provider's.
10
+ */
11
+ export declare class ClaudeLLMClient 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, tools }: {
22
+ context: Message[];
23
+ tools: ToolDefinition[];
24
+ }, signal: AbortSignal): Promise<LLMResponse>;
25
+ }
@@ -0,0 +1,248 @@
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
+ // This client's label on the thinking blocks it keeps for replay; see `ProviderDataPart`.
17
+ const claudeSource = 'claude';
18
+ /**
19
+ * Anthropic rejects a `tool_use` whose input is not an object, with a 400 on every later call.
20
+ * Another provider's call can hold anything else, such as OpenAI arguments that did not parse;
21
+ * its result already told the model the call failed, so sending it as empty loses nothing.
22
+ */
23
+ function toClaudeInputObject(input) {
24
+ return typeof input === 'object' && input !== null && !Array.isArray(input) ? input : {};
25
+ }
26
+ /** Thinking crosses back only as the signed block this client kept, never as its text. */
27
+ function toClaudeBlocks(part) {
28
+ switch (part.type) {
29
+ case 'text':
30
+ // The API rejects an empty text block.
31
+ return part.text ? [{ type: 'text', text: part.text }] : [];
32
+ case 'toolCall':
33
+ return [
34
+ { type: 'tool_use', id: part.id, name: part.name, input: toClaudeInputObject(part.input) },
35
+ ];
36
+ case 'toolResult':
37
+ return [
38
+ {
39
+ type: 'tool_result',
40
+ tool_use_id: part.callId,
41
+ content: part.output,
42
+ is_error: part.isError,
43
+ },
44
+ ];
45
+ case 'providerData':
46
+ // Another provider's block would be rejected; losing it only costs that provider's reasoning.
47
+ return part.source === claudeSource
48
+ ? [part.data]
49
+ : [];
50
+ case 'reasoning':
51
+ // Unsigned text cannot be replayed as thinking. The API accepts a turn without it.
52
+ return [];
53
+ }
54
+ }
55
+ /**
56
+ * Tool results travel in a user turn, as the API requires. A message left with no blocks is
57
+ * left out whole; the API joins the consecutive user turns that leaves.
58
+ */
59
+ function toClaudeInput(message) {
60
+ const content = message.content.flatMap(toClaudeBlocks);
61
+ if (content.length === 0) {
62
+ return [];
63
+ }
64
+ return [{ role: message.role === 'tool' ? 'user' : message.role, content }];
65
+ }
66
+ /**
67
+ * Strict: the API constrains the model to the schema, so its input always parses and matches.
68
+ * A schema outside what strict mode supports fails the request with a 400.
69
+ */
70
+ function toClaudeTool({ name, description, inputSchema }) {
71
+ return { name, description, input_schema: inputSchema, strict: true };
72
+ }
73
+ function describeBlock(block, logger) {
74
+ switch (block.type) {
75
+ case 'text':
76
+ return [{ type: 'text', text: block.text }];
77
+ case 'thinking': {
78
+ // Kept signed for replay; the summarized text is only for narration.
79
+ const signed = {
80
+ type: 'thinking',
81
+ thinking: block.thinking,
82
+ signature: block.signature,
83
+ };
84
+ const replay = { type: 'providerData', source: claudeSource, data: signed };
85
+ return block.thinking ? [{ type: 'reasoning', text: block.thinking }, replay] : [replay];
86
+ }
87
+ case 'redacted_thinking': {
88
+ // Encrypted by design: nothing to narrate, but it still goes back.
89
+ const redacted = { type: 'redacted_thinking', data: block.data };
90
+ return [{ type: 'providerData', source: claudeSource, data: redacted }];
91
+ }
92
+ case 'tool_use':
93
+ return [{ type: 'toolCall', id: block.id, name: block.name, input: block.input }];
94
+ default:
95
+ // Server tools are never offered, so any other block is output this client does not expect.
96
+ treatErrors(() => {
97
+ logger.warn(block, 'Claude returned a content block the client does not map');
98
+ }, classifyHostFailure, 'Claude client logger failed while mapping a response');
99
+ return [];
100
+ }
101
+ }
102
+ function toStopReason(response) {
103
+ switch (response.stop_reason) {
104
+ // `tool_use` is a complete answer that asks for tools: running them is the agent's call.
105
+ case 'end_turn':
106
+ case 'tool_use':
107
+ return 'completed';
108
+ case 'max_tokens':
109
+ return 'truncated';
110
+ case 'refusal':
111
+ return 'refused';
112
+ case 'model_context_window_exceeded':
113
+ throw new MaxContextError('The context no longer fits the Claude model', {
114
+ cause: 'The response stopped at the model context window.',
115
+ });
116
+ default:
117
+ // `pause_turn` and `stop_sequence` need server tools or stop sequences, and this
118
+ // client sends neither.
119
+ throw new UnrecoverableError('The Claude response stopped unexpectedly', {
120
+ cause: `The response stopped with "${response.stop_reason ?? 'unknown'}".`,
121
+ });
122
+ }
123
+ }
124
+ /** Unlike OpenAI, Anthropic counts cache reads and writes apart from `input_tokens`. */
125
+ function toTokens(usage, logger) {
126
+ if (!usage) {
127
+ // Zeros would report a call that may have been billed as free.
128
+ treatErrors(() => {
129
+ logger.warn('Claude returned a response without usage; it cannot be accounted for');
130
+ }, classifyHostFailure, 'Claude client logger failed while reading usage');
131
+ return undefined;
132
+ }
133
+ return {
134
+ inputTokens: usage.input_tokens,
135
+ readCacheTokens: usage.cache_read_input_tokens ?? 0,
136
+ writtenCacheTokens: usage.cache_creation_input_tokens ?? 0,
137
+ outputTokens: usage.output_tokens,
138
+ };
139
+ }
140
+ function toLLMResponse(response, logger) {
141
+ let usage;
142
+ try {
143
+ // Inside the try: it warns when the usage is missing, and that warning can throw too.
144
+ usage = toTokens(response.usage, logger);
145
+ return {
146
+ message: {
147
+ role: 'assistant',
148
+ content: response.content.flatMap(block => describeBlock(block, logger)),
149
+ },
150
+ usage,
151
+ stopReason: toStopReason(response),
152
+ };
153
+ }
154
+ catch (error) {
155
+ // The call was billed whether the response failed or a logger did while mapping it, and
156
+ // an answer without usage leaves the run's count unknown.
157
+ throw withSpentTokens(error, usage, !response.usage);
158
+ }
159
+ }
160
+ /**
161
+ * The SDK resolves credentials lazily, so a missing key surfaces on the first call, as a plain
162
+ * `Error` the SDK gives no class of its own. An `AnthropicError` that is not an `APIError` never
163
+ * carries an answer from the API: on this client's path it is the SDK refusing to make the
164
+ * call, such as an unresolved profile or a request too large to send without streaming.
165
+ */
166
+ function isLocalSDKFailure(error) {
167
+ return ((error instanceof AnthropicError && !(error instanceof APIError)) ||
168
+ (error instanceof Error && error.message.startsWith('Could not resolve authentication method')));
169
+ }
170
+ function classifyClaudeFailure(error) {
171
+ if (isLocalSDKFailure(error)) {
172
+ // The API never answered, and calling again would fail the same way.
173
+ return new UnrecoverableError('The Claude client cannot send the request', {
174
+ cause: describeFailure(error),
175
+ });
176
+ }
177
+ if (error instanceof APIError) {
178
+ // Anthropic gives an overlong prompt no code of its own, only a 400 with this message.
179
+ if (error.status === 400 && error.message.includes('prompt is too long')) {
180
+ return new MaxContextError('The context no longer fits the Claude model', {
181
+ cause: describeFailure(error),
182
+ });
183
+ }
184
+ if (unrecoverableErrorTypes.some(type => type === error.type) ||
185
+ unrecoverableStatuses.some(status => status === error.status)) {
186
+ return new UnrecoverableError('Claude rejected the request', {
187
+ cause: describeFailure(error),
188
+ });
189
+ }
190
+ }
191
+ // A rate limit, an overload (529), a 5xx or a network failure: a later attempt can succeed.
192
+ return classifyProviderFailure(error, 'The Claude request failed');
193
+ }
194
+ /**
195
+ * A stateless `LLMClient` on the Anthropic Messages API, which keeps nothing between calls:
196
+ * every call carries the whole context, so the conversation is MiKode's, not the provider's.
197
+ */
198
+ export class ClaudeLLMClient {
199
+ client;
200
+ model;
201
+ systemPrompt;
202
+ logger;
203
+ // `model` is untyped on purpose: this adapter is the one that knows what Claude supports.
204
+ constructor({ model, systemPrompt, logger, }) {
205
+ if (!isOneOf(claudeLLMModels, model)) {
206
+ throw new InvalidAgentConfigError(`"${model}" is not a Claude API model; expected one of: ${claudeLLMModels.join(', ')}`);
207
+ }
208
+ try {
209
+ // The SDK's default retries stay on, as OpenAI's do. See "The provider SDKs keep their
210
+ // transport retries under RetryingAgent" in decisions.md.
211
+ this.client = new Anthropic();
212
+ }
213
+ catch (error) {
214
+ // Conflicting credential options, for example: building it again cannot fix them. A
215
+ // missing key does not fail here but on the first call; see `isLocalSDKFailure`.
216
+ throw new UnrecoverableError('The Claude client could not be set up', {
217
+ cause: describeFailure(error),
218
+ });
219
+ }
220
+ this.model = model;
221
+ this.systemPrompt = systemPrompt;
222
+ this.logger = logger;
223
+ }
224
+ async send({ context, tools }, signal) {
225
+ let response;
226
+ try {
227
+ response = await this.client.messages.create({
228
+ model: this.model,
229
+ max_tokens: maxOutputTokens,
230
+ system: this.systemPrompt,
231
+ messages: context.flatMap(toClaudeInput),
232
+ // Left out when empty, so a request without tools stays as it always was.
233
+ ...(tools.length > 0 && { tools: tools.map(toClaudeTool) }),
234
+ thinking: { type: 'adaptive', display: 'summarized' },
235
+ // Anthropic caches only on request. This marks the last block, so the next call
236
+ // reads everything before it from the cache instead of paying for it again.
237
+ cache_control: { type: 'ephemeral' },
238
+ }, { signal });
239
+ }
240
+ catch (error) {
241
+ // The SDK's abort error is named 'Error', so it would pass for a provider failure.
242
+ signal.throwIfAborted();
243
+ throw classifyClaudeFailure(error);
244
+ }
245
+ return treatErrors(() => toLLMResponse(response, this.logger), classifyLocalFailure, 'Claude response mapping failed');
246
+ }
247
+ }
248
+ //# 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\tContentBlockParam,\n\tMessage as AnthropicMessage,\n\tMessageParam,\n\tRedactedThinkingBlockParam,\n\tThinkingBlockParam,\n\tTool as ClaudeTool,\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';\nimport type { ToolDefinition } from '../domain/tool.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// This client's label on the thinking blocks it keeps for replay; see `ProviderDataPart`.\nconst claudeSource = 'claude';\n\n/**\n * Anthropic rejects a `tool_use` whose input is not an object, with a 400 on every later call.\n * Another provider's call can hold anything else, such as OpenAI arguments that did not parse;\n * its result already told the model the call failed, so sending it as empty loses nothing.\n */\nfunction toClaudeInputObject(input: unknown): unknown {\n\treturn typeof input === 'object' && input !== null && !Array.isArray(input) ? input : {};\n}\n\n/** Thinking crosses back only as the signed block this client kept, never as its text. */\nfunction toClaudeBlocks(part: MessagePart): ContentBlockParam[] {\n\tswitch (part.type) {\n\t\tcase 'text':\n\t\t\t// The API rejects an empty text block.\n\t\t\treturn part.text ? [{ type: 'text', text: part.text }] : [];\n\t\tcase 'toolCall':\n\t\t\treturn [\n\t\t\t\t{ type: 'tool_use', id: part.id, name: part.name, input: toClaudeInputObject(part.input) },\n\t\t\t];\n\t\tcase 'toolResult':\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'tool_result',\n\t\t\t\t\ttool_use_id: part.callId,\n\t\t\t\t\tcontent: part.output,\n\t\t\t\t\tis_error: part.isError,\n\t\t\t\t},\n\t\t\t];\n\t\tcase 'providerData':\n\t\t\t// Another provider's block would be rejected; losing it only costs that provider's reasoning.\n\t\t\treturn part.source === claudeSource\n\t\t\t\t? [part.data as ThinkingBlockParam | RedactedThinkingBlockParam]\n\t\t\t\t: [];\n\t\tcase 'reasoning':\n\t\t\t// Unsigned text cannot be replayed as thinking. The API accepts a turn without it.\n\t\t\treturn [];\n\t}\n}\n\n/**\n * Tool results travel in a user turn, as the API requires. A message left with no blocks is\n * left out whole; the API joins the consecutive user turns that leaves.\n */\nfunction toClaudeInput(message: Message): MessageParam[] {\n\tconst content = message.content.flatMap(toClaudeBlocks);\n\tif (content.length === 0) {\n\t\treturn [];\n\t}\n\n\treturn [{ role: message.role === 'tool' ? 'user' : message.role, content }];\n}\n\n/**\n * Strict: the API constrains the model to the schema, so its input always parses and matches.\n * A schema outside what strict mode supports fails the request with a 400.\n */\nfunction toClaudeTool({ name, description, inputSchema }: ToolDefinition): ClaudeTool {\n\treturn { name, description, input_schema: inputSchema, strict: true };\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// Kept signed for replay; the summarized text is only for narration.\n\t\t\tconst signed: ThinkingBlockParam = {\n\t\t\t\ttype: 'thinking',\n\t\t\t\tthinking: block.thinking,\n\t\t\t\tsignature: block.signature,\n\t\t\t};\n\t\t\tconst replay: MessagePart = { type: 'providerData', source: claudeSource, data: signed };\n\t\t\treturn block.thinking ? [{ type: 'reasoning', text: block.thinking }, replay] : [replay];\n\t\t}\n\t\tcase 'redacted_thinking': {\n\t\t\t// Encrypted by design: nothing to narrate, but it still goes back.\n\t\t\tconst redacted: RedactedThinkingBlockParam = { type: 'redacted_thinking', data: block.data };\n\t\t\treturn [{ type: 'providerData', source: claudeSource, data: redacted }];\n\t\t}\n\t\tcase 'tool_use':\n\t\t\treturn [{ type: 'toolCall', id: block.id, name: block.name, input: block.input }];\n\t\tdefault:\n\t\t\t// Server tools are never offered, 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\t// `tool_use` is a complete answer that asks for tools: running them is the agent's call.\n\t\tcase 'end_turn':\n\t\tcase 'tool_use':\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// `pause_turn` and `stop_sequence` need server tools or stop sequences, and this\n\t\t\t// client sends neither.\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(\n\t\t{ context, tools }: { context: Message[]; tools: ToolDefinition[] },\n\t\tsignal: AbortSignal,\n\t): 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\t// Left out when empty, so a request without tools stays as it always was.\n\t\t\t\t\t...(tools.length > 0 && { tools: tools.map(toClaudeTool) }),\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"]}
@@ -1,6 +1,7 @@
1
- import type { ILogger } from '../../shared/domain/logger.ts';
1
+ import type { ILogger } from '#src/shared/domain/logger';
2
2
  import type { LLMClient, LLMResponse } from '../domain/llm.ts';
3
3
  import type { Message } from '../domain/message.ts';
4
+ import type { ToolDefinition } from '../domain/tool.ts';
4
5
  export declare const openAIModels: readonly ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.6-terra"];
5
6
  export type OpenAIModel = (typeof openAIModels)[number];
6
7
  /**
@@ -18,5 +19,8 @@ export declare class OpenAILLMClient implements LLMClient {
18
19
  systemPrompt: string;
19
20
  logger: ILogger;
20
21
  });
21
- send(context: Message[], signal: AbortSignal): Promise<LLMResponse>;
22
+ send({ context, tools }: {
23
+ context: Message[];
24
+ tools: ToolDefinition[];
25
+ }, signal: AbortSignal): Promise<LLMResponse>;
22
26
  }
@@ -1,7 +1,7 @@
1
1
  import OpenAI, { APIError } from 'openai';
2
- import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpentTokens, } from "../../agent/domain/errors.js";
3
- import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, describeFailure, treatErrors, } from "../../agent/domain/providerFailure.js";
4
- import { isOneOf } from "../../shared/domain/isOneOf.js";
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
5
  import { MaxContextError } from "../domain/errors.js";
6
6
  // The SDK types `model` as a plain string, so this list is maintained by hand.
7
7
  export const openAIModels = [
@@ -21,17 +21,69 @@ const exhaustedQuotaCodes = [
21
21
  ];
22
22
  // The codes OpenAI gives a failed response that a new attempt can succeed past.
23
23
  const transientResponseErrorCodes = ['server_error', 'rate_limit_exceeded'];
24
+ // This client's label on the reasoning items it keeps for replay; see `ProviderDataPart`.
25
+ const openAISource = 'openai';
26
+ // Marks a failed call's output, which would otherwise read the same as a successful one.
27
+ const failedToolPrefix = 'The tool call failed: ';
24
28
  /**
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.
29
+ * OpenAI takes a flat list of items, so every part becomes its own, in order. Reasoning crosses
30
+ * back only as the encrypted item this client kept, which OpenAI requires beside the tool calls
31
+ * that followed it; a reasoning part is its summary, kept for narration.
28
32
  */
33
+ function toOpenAIItems(part, role) {
34
+ switch (part.type) {
35
+ case 'text':
36
+ return part.text ? [{ role, content: part.text }] : [];
37
+ case 'toolCall':
38
+ return [
39
+ {
40
+ type: 'function_call',
41
+ call_id: part.id,
42
+ name: part.name,
43
+ // A string is arguments that did not parse, sent back as the model wrote them.
44
+ arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),
45
+ },
46
+ ];
47
+ case 'toolResult':
48
+ return [
49
+ {
50
+ type: 'function_call_output',
51
+ call_id: part.callId,
52
+ // OpenAI has no error flag, so the text says it; the conversation keeps the output as it was.
53
+ output: part.isError ? `${failedToolPrefix}${part.output}` : part.output,
54
+ },
55
+ ];
56
+ case 'providerData':
57
+ // Another provider's item would be rejected; losing it only costs that provider's reasoning.
58
+ return part.source === openAISource ? [part.data] : [];
59
+ case 'reasoning':
60
+ return [];
61
+ }
62
+ }
29
63
  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 }] : [];
64
+ // A tool message holds only results, which carry no role.
65
+ const role = message.role === 'assistant' ? 'assistant' : 'user';
66
+ return message.content.flatMap(part => toOpenAIItems(part, role));
67
+ }
68
+ /**
69
+ * Strict: the API constrains the model to the schema, so its arguments always parse and match.
70
+ * A schema outside what strict mode supports fails the request with a 400.
71
+ */
72
+ function toOpenAITool({ name, description, inputSchema }) {
73
+ return { type: 'function', name, description, parameters: inputSchema, strict: true };
74
+ }
75
+ /**
76
+ * Strict mode makes arguments that do not parse rare: a response cut short mid-call ends as
77
+ * `truncated` before any tool runs. Should one arrive, the tool gets the raw string and its
78
+ * rejection lets the model correct itself, instead of the run failing.
79
+ */
80
+ function parseArguments(serialized) {
81
+ try {
82
+ return JSON.parse(serialized);
83
+ }
84
+ catch {
85
+ return serialized;
86
+ }
35
87
  }
36
88
  function describeItem(item, logger) {
37
89
  switch (item.type) {
@@ -39,12 +91,32 @@ function describeItem(item, logger) {
39
91
  // A refusal is not an answer: it only sets the stop reason.
40
92
  return item.content.flatMap(content => content.type === 'output_text' ? [{ type: 'text', text: content.text }] : []);
41
93
  case 'reasoning': {
42
- // The summary is the readable part; the encrypted content is opaque by design.
94
+ // The summary is the readable part; the encrypted item is what OpenAI reads back.
43
95
  const text = item.summary.map(summary => summary.text).join('\n');
44
- return text ? [{ type: 'reasoning', text }] : [];
96
+ const parts = text ? [{ type: 'reasoning', text }] : [];
97
+ // Without its encrypted content the item cannot be replayed: OpenAI stores nothing.
98
+ if (item.encrypted_content) {
99
+ const encrypted = {
100
+ type: 'reasoning',
101
+ id: item.id,
102
+ summary: item.summary,
103
+ encrypted_content: item.encrypted_content,
104
+ };
105
+ parts.push({ type: 'providerData', source: openAISource, data: encrypted });
106
+ }
107
+ return parts;
45
108
  }
109
+ case 'function_call':
110
+ return [
111
+ {
112
+ type: 'toolCall',
113
+ id: item.call_id,
114
+ name: item.name,
115
+ input: parseArguments(item.arguments),
116
+ },
117
+ ];
46
118
  default:
47
- // No tools are sent yet, so any other item is output this client does not expect.
119
+ // No built-in tools are offered, so any other item is output this client does not expect.
48
120
  treatErrors(() => {
49
121
  logger.warn(item, 'OpenAI returned an output item the client does not map');
50
122
  }, classifyHostFailure, 'OpenAI client logger failed while mapping a response');
@@ -137,7 +209,7 @@ export class OpenAILLMClient {
137
209
  }
138
210
  try {
139
211
  // The SDK's default retries stay on: they back off and honour `Retry-After`, which
140
- // `RetryingAgent` does not. See "The OpenAI SDK keeps its transport retries" in decisions.md.
212
+ // `RetryingAgent` does not. See "The provider SDKs keep their transport retries" in decisions.md.
141
213
  this.client = new OpenAI();
142
214
  }
143
215
  catch (error) {
@@ -150,15 +222,19 @@ export class OpenAILLMClient {
150
222
  this.systemPrompt = systemPrompt;
151
223
  this.logger = logger;
152
224
  }
153
- async send(context, signal) {
225
+ async send({ context, tools }, signal) {
154
226
  let response;
155
227
  try {
156
228
  response = await this.client.responses.create({
157
229
  model: this.model,
158
230
  instructions: this.systemPrompt,
159
231
  input: context.flatMap(toOpenAIInput),
232
+ // Left out when empty, so a request without tools stays as it always was.
233
+ ...(tools.length > 0 && { tools: tools.map(toOpenAITool) }),
160
234
  store: false,
161
235
  reasoning: { summary: 'auto' },
236
+ // With `store: false` this is the only way to send the reasoning back.
237
+ include: ['reasoning.encrypted_content'],
162
238
  }, { signal });
163
239
  }
164
240
  catch (error) {
@@ -1 +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 '../../agent/domain/errors.ts';\nimport {\n\tclassifyHostFailure,\n\tclassifyLocalFailure,\n\tclassifyProviderFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '../../agent/domain/providerFailure.ts';\nimport { isOneOf } from '../../shared/domain/isOneOf.ts';\nimport type { ILogger } from '../../shared/domain/logger.ts';\nimport type { Tokens } from '../../shared/domain/tokens.ts';\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 OpenAI SDK keeps its 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
+ {"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\tFunctionTool,\n\tResponse,\n\tResponseInputItem,\n\tResponseOutputItem,\n\tResponseReasoningItem,\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';\nimport type { ToolDefinition } from '../domain/tool.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// This client's label on the reasoning items it keeps for replay; see `ProviderDataPart`.\nconst openAISource = 'openai';\n\n// Marks a failed call's output, which would otherwise read the same as a successful one.\nconst failedToolPrefix = 'The tool call failed: ';\n\n/**\n * OpenAI takes a flat list of items, so every part becomes its own, in order. Reasoning crosses\n * back only as the encrypted item this client kept, which OpenAI requires beside the tool calls\n * that followed it; a reasoning part is its summary, kept for narration.\n */\nfunction toOpenAIItems(part: MessagePart, role: 'user' | 'assistant'): ResponseInputItem[] {\n\tswitch (part.type) {\n\t\tcase 'text':\n\t\t\treturn part.text ? [{ role, content: part.text }] : [];\n\t\tcase 'toolCall':\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'function_call',\n\t\t\t\t\tcall_id: part.id,\n\t\t\t\t\tname: part.name,\n\t\t\t\t\t// A string is arguments that did not parse, sent back as the model wrote them.\n\t\t\t\t\targuments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n\t\t\t\t},\n\t\t\t];\n\t\tcase 'toolResult':\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'function_call_output',\n\t\t\t\t\tcall_id: part.callId,\n\t\t\t\t\t// OpenAI has no error flag, so the text says it; the conversation keeps the output as it was.\n\t\t\t\t\toutput: part.isError ? `${failedToolPrefix}${part.output}` : part.output,\n\t\t\t\t},\n\t\t\t];\n\t\tcase 'providerData':\n\t\t\t// Another provider's item would be rejected; losing it only costs that provider's reasoning.\n\t\t\treturn part.source === openAISource ? [part.data as ResponseReasoningItem] : [];\n\t\tcase 'reasoning':\n\t\t\treturn [];\n\t}\n}\n\nfunction toOpenAIInput(message: Message): ResponseInputItem[] {\n\t// A tool message holds only results, which carry no role.\n\tconst role = message.role === 'assistant' ? 'assistant' : 'user';\n\treturn message.content.flatMap(part => toOpenAIItems(part, role));\n}\n\n/**\n * Strict: the API constrains the model to the schema, so its arguments always parse and match.\n * A schema outside what strict mode supports fails the request with a 400.\n */\nfunction toOpenAITool({ name, description, inputSchema }: ToolDefinition): FunctionTool {\n\treturn { type: 'function', name, description, parameters: inputSchema, strict: true };\n}\n\n/**\n * Strict mode makes arguments that do not parse rare: a response cut short mid-call ends as\n * `truncated` before any tool runs. Should one arrive, the tool gets the raw string and its\n * rejection lets the model correct itself, instead of the run failing.\n */\nfunction parseArguments(serialized: string): unknown {\n\ttry {\n\t\treturn JSON.parse(serialized);\n\t} catch {\n\t\treturn serialized;\n\t}\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 item is what OpenAI reads back.\n\t\t\tconst text = item.summary.map(summary => summary.text).join('\\n');\n\t\t\tconst parts: MessagePart[] = text ? [{ type: 'reasoning', text }] : [];\n\t\t\t// Without its encrypted content the item cannot be replayed: OpenAI stores nothing.\n\t\t\tif (item.encrypted_content) {\n\t\t\t\tconst encrypted: ResponseReasoningItem = {\n\t\t\t\t\ttype: 'reasoning',\n\t\t\t\t\tid: item.id,\n\t\t\t\t\tsummary: item.summary,\n\t\t\t\t\tencrypted_content: item.encrypted_content,\n\t\t\t\t};\n\t\t\t\tparts.push({ type: 'providerData', source: openAISource, data: encrypted });\n\t\t\t}\n\t\t\treturn parts;\n\t\t}\n\t\tcase 'function_call':\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'toolCall',\n\t\t\t\t\tid: item.call_id,\n\t\t\t\t\tname: item.name,\n\t\t\t\t\tinput: parseArguments(item.arguments),\n\t\t\t\t},\n\t\t\t];\n\t\tdefault:\n\t\t\t// No built-in tools are offered, 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(\n\t\t{ context, tools }: { context: Message[]; tools: ToolDefinition[] },\n\t\tsignal: AbortSignal,\n\t): 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\t// Left out when empty, so a request without tools stays as it always was.\n\t\t\t\t\t...(tools.length > 0 && { tools: tools.map(toOpenAITool) }),\n\t\t\t\t\tstore: false,\n\t\t\t\t\treasoning: { summary: 'auto' },\n\t\t\t\t\t// With `store: false` this is the only way to send the reasoning back.\n\t\t\t\t\tinclude: ['reasoning.encrypted_content'],\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;
@@ -1,6 +1,6 @@
1
- import { addTokens } from "../../../shared/domain/tokens.js";
2
- import { RecoverableError, UnrecoverableError, withSpentTokens, } from "../../../agent/domain/errors.js";
3
- 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';
4
4
  const getPlannerPrompt = (userPrompt, previousFailureReason) => {
5
5
  const feedback = previousFailureReason
6
6
  ? `\n\nFeedback from the previous attempt:\n---\n${previousFailureReason}\n---`