@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.
- package/README.md +5 -0
- package/dist/agent/domain/agent.d.ts +8 -1
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/errors.d.ts +1 -1
- package/dist/agent/domain/errors.js +1 -1
- package/dist/agent/domain/errors.js.map +1 -1
- package/dist/agent/domain/providerFailure.js +1 -1
- package/dist/agent/domain/providerFailure.js.map +1 -1
- package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +2 -2
- package/dist/engines/claude/infrastructure/model/claudeAgent.js +3 -3
- package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
- package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +2 -2
- package/dist/engines/codex/infrastructure/model/codexAgent.js +4 -4
- package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +23 -6
- package/dist/engines/domain/model/llmAgent.js +157 -31
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentFactory.d.ts +19 -13
- package/dist/factory/infrastructure/agentFactory.js +48 -23
- package/dist/factory/infrastructure/agentFactory.js.map +1 -1
- package/dist/llm/domain/conversation.d.ts +4 -3
- package/dist/llm/domain/conversation.js +5 -4
- package/dist/llm/domain/conversation.js.map +1 -1
- package/dist/llm/domain/errors.d.ts +1 -1
- package/dist/llm/domain/errors.js +1 -1
- package/dist/llm/domain/errors.js.map +1 -1
- package/dist/llm/domain/llm.d.ts +6 -2
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/message.d.ts +30 -4
- package/dist/llm/domain/message.js.map +1 -1
- package/dist/llm/domain/tool.d.ts +17 -0
- package/dist/llm/domain/tool.js +2 -0
- package/dist/llm/domain/tool.js.map +1 -0
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +25 -0
- package/dist/llm/infrastructure/claudeLLMClient.js +248 -0
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -0
- package/dist/llm/infrastructure/openAILLMClient.d.ts +6 -2
- package/dist/llm/infrastructure/openAILLMClient.js +92 -16
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +2 -2
- package/dist/orchestration/domain/model/orchestratorAgent.js +3 -3
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/retry/domain/model/retryingAgent.d.ts +2 -2
- package/dist/retry/domain/model/retryingAgent.js +4 -4
- package/dist/retry/domain/model/retryingAgent.js.map +1 -1
- 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 @@
|
|
|
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 '
|
|
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
|
|
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
|
|
3
|
-
import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, describeFailure, treatErrors, } from
|
|
4
|
-
import { isOneOf } from
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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":"AAAA,OAAO,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAO1C,OAAO,EACN,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GACf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACN,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,WAAW,GACX,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAGzD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAItD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,aAAa;IACb,aAAa;IACb,cAAc;IACd,eAAe;CACN,CAAC;AAGX,kFAAkF;AAClF,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAExD,gGAAgG;AAChG,mDAAmD;AACnD,MAAM,mBAAmB,GAAG;IAC3B,oBAAoB;IACpB,0BAA0B;IAC1B,8BAA8B;CAC9B,CAAC;AAEF,gFAAgF;AAChF,MAAM,2BAA2B,GAAG,CAAC,cAAc,EAAE,qBAAqB,CAAC,CAAC;AAE5E;;;;GAIG;AACH,SAAS,aAAa,CAAC,OAAgB;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO;SAC1B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;SACpC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;SACtB,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,SAAS,YAAY,CAAC,IAAwB,EAAE,MAAe;IAC9D,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS;YACb,4DAA4D;YAC5D,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CACrC,OAAO,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CACrF,CAAC;QACH,KAAK,WAAW,CAAC,CAAC,CAAC;YAClB,+EAA+E;YAC/E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,CAAC;QACD;YACC,kFAAkF;YAClF,WAAW,CACV,GAAG,EAAE;gBACJ,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,wDAAwD,CAAC,CAAC;YAC7E,CAAC,EACD,mBAAmB,EACnB,sDAAsD,CACtD,CAAC;YACF,OAAO,EAAE,CAAC;IACZ,CAAC;AACF,CAAC;AAED,SAAS,YAAY,CAAC,QAAkB;IACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QACtC,OAAO,QAAQ,CAAC,kBAAkB,EAAE,MAAM,KAAK,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;IAC3F,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QACrC,MAAM,KAAK,GACV,QAAQ,CAAC,KAAK,EAAE,OAAO;YACvB,mCAAmC,QAAQ,CAAC,MAAM,IAAI,SAAS,IAAI,CAAC;QACrE,uFAAuF;QACvF,IAAI,QAAQ,CAAC,KAAK,IAAI,2BAA2B,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,gBAAgB,CAAC,4BAA4B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,IAAI,kBAAkB,CAAC,sCAAsC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACjF,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CACnC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAC3F,CAAC;IACF,OAAO,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;AAC1C,CAAC;AAED,6EAA6E;AAC7E,SAAS,QAAQ,CAAC,KAAgC,EAAE,MAAe;IAClE,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,+DAA+D;QAC/D,WAAW,CACV,GAAG,EAAE;YACJ,MAAM,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACrF,CAAC,EACD,mBAAmB,EACnB,iDAAiD,CACjD,CAAC;QACF,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,EAAE,aAAa,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAC,oBAAoB,CAAC;IACzE,OAAO;QACN,WAAW,EAAE,KAAK,CAAC,YAAY,GAAG,aAAa,GAAG,kBAAkB;QACpE,eAAe,EAAE,aAAa;QAC9B,kBAAkB,EAAE,kBAAkB;QACtC,YAAY,EAAE,KAAK,CAAC,aAAa;KACjC,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,QAAkB,EAAE,MAAe;IACzD,IAAI,KAAyB,CAAC;IAE9B,IAAI,CAAC;QACJ,sFAAsF;QACtF,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACzC,OAAO;YACN,OAAO,EAAE;gBACR,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;aACpE;YACD,KAAK;YACL,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC;SAClC,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,wFAAwF;QACxF,0DAA0D;QAC1D,MAAM,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,CAAC;AACF,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC5C,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,yBAAyB,EAAE,CAAC;YAC9C,OAAO,IAAI,eAAe,CAAC,6CAA6C,EAAE;gBACzE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;aAC7B,CAAC,CAAC;QACJ,CAAC;QACD,IACC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;YACrD,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,EAC5D,CAAC;YACF,OAAO,IAAI,kBAAkB,CAAC,6BAA6B,EAAE;gBAC5D,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;aAC7B,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAED,OAAO,uBAAuB,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACV,MAAM,CAAS;IACf,KAAK,CAAc;IACnB,YAAY,CAAS;IACrB,MAAM,CAAU;IAEjC,0FAA0F;IAC1F,YAAY,EACX,KAAK,EACL,YAAY,EACZ,MAAM,GAKN;QACA,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,uBAAuB,CAChC,IAAI,KAAK,8CAA8C,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChF,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACJ,mFAAmF;YACnF,8FAA8F;YAC9F,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,2EAA2E;YAC3E,MAAM,IAAI,kBAAkB,CAAC,uCAAuC,EAAE;gBACrE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;aAC7B,CAAC,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAkB,EAAE,MAAmB;QACjD,IAAI,QAAkB,CAAC;QAEvB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAC5C;gBACC,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,YAAY,EAAE,IAAI,CAAC,YAAY;gBAC/B,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;gBACrC,KAAK,EAAE,KAAK;gBACZ,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;aAC9B,EACD,EAAE,MAAM,EAAE,CACV,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,mFAAmF;YACnF,MAAM,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;QAED,OAAO,WAAW,CACjB,GAAG,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAC1C,oBAAoB,EACpB,gCAAgC,CAChC,CAAC;IACH,CAAC;CACD","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 '
|
|
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 '
|
|
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
|
|
2
|
-
import { RecoverableError, UnrecoverableError, withSpentTokens
|
|
3
|
-
import { classifyHostFailure, treatErrors } from
|
|
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---`
|