@mikode13/harness 2.1.0 → 2.3.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 (49) hide show
  1. package/dist/agent/domain/agent.d.ts +7 -0
  2. package/dist/agent/domain/agent.js.map +1 -1
  3. package/dist/engines/domain/model/llmAgent.d.ts +20 -3
  4. package/dist/engines/domain/model/llmAgent.js +156 -30
  5. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  6. package/dist/llm/domain/conversation.d.ts +4 -3
  7. package/dist/llm/domain/conversation.js +5 -4
  8. package/dist/llm/domain/conversation.js.map +1 -1
  9. package/dist/llm/domain/llm.d.ts +5 -1
  10. package/dist/llm/domain/llm.js.map +1 -1
  11. package/dist/llm/domain/message.d.ts +30 -4
  12. package/dist/llm/domain/message.js.map +1 -1
  13. package/dist/llm/domain/tool.d.ts +10 -0
  14. package/dist/llm/domain/tool.js +2 -0
  15. package/dist/llm/domain/tool.js.map +1 -0
  16. package/dist/llm/infrastructure/claudeLLMClient.d.ts +5 -1
  17. package/dist/llm/infrastructure/claudeLLMClient.js +78 -19
  18. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  19. package/dist/llm/infrastructure/openAILLMClient.d.ts +5 -1
  20. package/dist/llm/infrastructure/openAILLMClient.js +88 -12
  21. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  22. package/dist/tools/domain/tool.d.ts +9 -0
  23. package/dist/tools/domain/tool.js +2 -0
  24. package/dist/tools/domain/tool.js.map +1 -0
  25. package/dist/tools/domain/workspace.d.ts +48 -0
  26. package/dist/tools/domain/workspace.js +2 -0
  27. package/dist/tools/domain/workspace.js.map +1 -0
  28. package/dist/tools/infrastructure/boundedWorkspace.d.ts +66 -0
  29. package/dist/tools/infrastructure/boundedWorkspace.js +160 -0
  30. package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -0
  31. package/dist/tools/infrastructure/createWorkspace.d.ts +14 -0
  32. package/dist/tools/infrastructure/createWorkspace.js +54 -0
  33. package/dist/tools/infrastructure/createWorkspace.js.map +1 -0
  34. package/dist/tools/infrastructure/defineTool.d.ts +15 -0
  35. package/dist/tools/infrastructure/defineTool.js +87 -0
  36. package/dist/tools/infrastructure/defineTool.js.map +1 -0
  37. package/dist/tools/infrastructure/gitWorkspace.d.ts +13 -0
  38. package/dist/tools/infrastructure/gitWorkspace.js +61 -0
  39. package/dist/tools/infrastructure/gitWorkspace.js.map +1 -0
  40. package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +20 -0
  41. package/dist/tools/infrastructure/ripgrepWorkspace.js +85 -0
  42. package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -0
  43. package/dist/tools/infrastructure/runProcess.d.ts +23 -0
  44. package/dist/tools/infrastructure/runProcess.js +91 -0
  45. package/dist/tools/infrastructure/runProcess.js.map +1 -0
  46. package/dist/tools/infrastructure/workspaceTools.d.ts +4 -0
  47. package/dist/tools/infrastructure/workspaceTools.js +110 -0
  48. package/dist/tools/infrastructure/workspaceTools.js.map +1 -0
  49. package/package.json +2 -1
@@ -13,31 +13,86 @@ const unrecoverableStatuses = [400, 401, 403, 404, 413, 422];
13
13
  // The account is out of credit, or its key cannot make the request. Resending cannot fix
14
14
  // either, whatever status the error arrives with.
15
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';
16
18
  /**
17
- * Only the text crosses back. Anthropic needs a thinking block again only to continue a turn
18
- * that called a tool, and none is sent yet, so a reasoning part stays in the conversation for
19
- * narration and is left out of the request. A message with no text is left out whole; the API
20
- * joins the consecutive user turns that leaves.
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.
21
58
  */
22
59
  function toClaudeInput(message) {
23
- const text = message.content
24
- .filter(part => part.type === 'text')
25
- .map(part => part.text)
26
- .join('\n');
27
- return text ? [{ role: message.role, content: text }] : [];
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 };
28
72
  }
29
73
  function describeBlock(block, logger) {
30
74
  switch (block.type) {
31
75
  case 'text':
32
76
  return [{ type: 'text', text: block.text }];
33
- case 'thinking':
34
- // Summarized thinking; the signature only matters for replay, which is not done.
35
- return block.thinking ? [{ type: 'reasoning', text: block.thinking }] : [];
36
- case 'redacted_thinking':
37
- // Encrypted by design: there is nothing to narrate.
38
- return [];
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 }];
39
94
  default:
40
- // No tools are sent yet, so any other block is output this client does not expect.
95
+ // Server tools are never offered, so any other block is output this client does not expect.
41
96
  treatErrors(() => {
42
97
  logger.warn(block, 'Claude returned a content block the client does not map');
43
98
  }, classifyHostFailure, 'Claude client logger failed while mapping a response');
@@ -46,7 +101,9 @@ function describeBlock(block, logger) {
46
101
  }
47
102
  function toStopReason(response) {
48
103
  switch (response.stop_reason) {
104
+ // `tool_use` is a complete answer that asks for tools: running them is the agent's call.
49
105
  case 'end_turn':
106
+ case 'tool_use':
50
107
  return 'completed';
51
108
  case 'max_tokens':
52
109
  return 'truncated';
@@ -57,8 +114,8 @@ function toStopReason(response) {
57
114
  cause: 'The response stopped at the model context window.',
58
115
  });
59
116
  default:
60
- // `tool_use`, `pause_turn` and `stop_sequence` need tools, server tools or stop
61
- // sequences, and this client sends none of them.
117
+ // `pause_turn` and `stop_sequence` need server tools or stop sequences, and this
118
+ // client sends neither.
62
119
  throw new UnrecoverableError('The Claude response stopped unexpectedly', {
63
120
  cause: `The response stopped with "${response.stop_reason ?? 'unknown'}".`,
64
121
  });
@@ -164,7 +221,7 @@ export class ClaudeLLMClient {
164
221
  this.systemPrompt = systemPrompt;
165
222
  this.logger = logger;
166
223
  }
167
- async send(context, signal) {
224
+ async send({ context, tools }, signal) {
168
225
  let response;
169
226
  try {
170
227
  response = await this.client.messages.create({
@@ -172,6 +229,8 @@ export class ClaudeLLMClient {
172
229
  max_tokens: maxOutputTokens,
173
230
  system: this.systemPrompt,
174
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) }),
175
234
  thinking: { type: 'adaptive', display: 'summarized' },
176
235
  // Anthropic caches only on request. This marks the last block, so the next call
177
236
  // reads everything before it from the cache instead of paying for it again.
@@ -1 +1 @@
1
- {"version":3,"file":"claudeLLMClient.js","sourceRoot":"","sources":["../../../src/llm/infrastructure/claudeLLMClient.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAOxE,OAAO,EACN,uBAAuB,EACvB,kBAAkB,EAClB,eAAe,GACf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,WAAW,GACX,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAGrD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAItD,+FAA+F;AAC/F,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,CAAU,CAAC;AAGnG,gGAAgG;AAChG,yEAAyE;AACzE,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B,kFAAkF;AAClF,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAE7D,yFAAyF;AACzF,kDAAkD;AAClD,MAAM,uBAAuB,GAAG,CAAC,eAAe,EAAE,sBAAsB,EAAE,kBAAkB,CAAC,CAAC;AAE9F;;;;;GAKG;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,aAAa,CAAC,KAAmB,EAAE,MAAe;IAC1D,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,MAAM;YACV,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7C,KAAK,UAAU;YACd,iFAAiF;YACjF,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,KAAK,mBAAmB;YACvB,oDAAoD;YACpD,OAAO,EAAE,CAAC;QACX;YACC,mFAAmF;YACnF,WAAW,CACV,GAAG,EAAE;gBACJ,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,yDAAyD,CAAC,CAAC;YAC/E,CAAC,EACD,mBAAmB,EACnB,sDAAsD,CACtD,CAAC;YACF,OAAO,EAAE,CAAC;IACZ,CAAC;AACF,CAAC;AAED,SAAS,YAAY,CAAC,QAA0B;IAC/C,QAAQ,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC9B,KAAK,UAAU;YACd,OAAO,WAAW,CAAC;QACpB,KAAK,YAAY;YAChB,OAAO,WAAW,CAAC;QACpB,KAAK,SAAS;YACb,OAAO,SAAS,CAAC;QAClB,KAAK,+BAA+B;YACnC,MAAM,IAAI,eAAe,CAAC,6CAA6C,EAAE;gBACxE,KAAK,EAAE,mDAAmD;aAC1D,CAAC,CAAC;QACJ;YACC,gFAAgF;YAChF,iDAAiD;YACjD,MAAM,IAAI,kBAAkB,CAAC,0CAA0C,EAAE;gBACxE,KAAK,EAAE,8BAA8B,QAAQ,CAAC,WAAW,IAAI,SAAS,IAAI;aAC1E,CAAC,CAAC;IACL,CAAC;AACF,CAAC;AAED,wFAAwF;AACxF,SAAS,QAAQ,CAAC,KAAwB,EAAE,MAAe;IAC1D,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,OAAO;QACN,WAAW,EAAE,KAAK,CAAC,YAAY;QAC/B,eAAe,EAAE,KAAK,CAAC,uBAAuB,IAAI,CAAC;QACnD,kBAAkB,EAAE,KAAK,CAAC,2BAA2B,IAAI,CAAC;QAC1D,YAAY,EAAE,KAAK,CAAC,aAAa;KACjC,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,QAA0B,EAAE,MAAe;IACjE,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,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;aACxE;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;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,KAAc;IACxC,OAAO,CACN,CAAC,KAAK,YAAY,cAAc,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC,CAAC;QACjE,CAAC,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,yCAAyC,CAAC,CAAC,CAC/F,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC5C,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,qEAAqE;QACrE,OAAO,IAAI,kBAAkB,CAAC,2CAA2C,EAAE;YAC1E,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;SAC7B,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC/B,uFAAuF;QACvF,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;YAC1E,OAAO,IAAI,eAAe,CAAC,6CAA6C,EAAE;gBACzE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;aAC7B,CAAC,CAAC;QACJ,CAAC;QACD,IACC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;YACzD,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,4FAA4F;IAC5F,OAAO,uBAAuB,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,eAAe;IACV,MAAM,CAAY;IAClB,KAAK,CAAiB;IACtB,YAAY,CAAS;IACrB,MAAM,CAAU;IAEjC,0FAA0F;IAC1F,YAAY,EACX,KAAK,EACL,YAAY,EACZ,MAAM,GAKN;QACA,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,uBAAuB,CAChC,IAAI,KAAK,iDAAiD,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACtF,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACJ,uFAAuF;YACvF,0DAA0D;YAC1D,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,oFAAoF;YACpF,iFAAiF;YACjF,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,QAA0B,CAAC;QAE/B,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAC3C;gBACC,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,eAAe;gBAC3B,MAAM,EAAE,IAAI,CAAC,YAAY;gBACzB,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;gBACxC,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE;gBACrD,gFAAgF;gBAChF,4EAA4E;gBAC5E,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;aACpC,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 Anthropic, { AnthropicError, APIError } from '@anthropic-ai/sdk';\nimport type {\n\tContentBlock,\n\tMessage as AnthropicMessage,\n\tMessageParam,\n\tUsage,\n} from '@anthropic-ai/sdk/resources/messages';\nimport {\n\tInvalidAgentConfigError,\n\tUnrecoverableError,\n\twithSpentTokens,\n} from '#src/agent/domain/errors';\nimport {\n\tclassifyHostFailure,\n\tclassifyLocalFailure,\n\tclassifyProviderFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/agent/domain/providerFailure';\nimport { isOneOf } from '#src/shared/domain/isOneOf';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport type { Tokens } from '#src/shared/domain/tokens';\nimport { MaxContextError } from '../domain/errors.ts';\nimport type { LLMClient, LLMResponse, StopReason } from '../domain/llm.ts';\nimport type { Message, MessagePart } from '../domain/message.ts';\n\n// The Messages API takes full model IDs, not the Agent SDK's aliases, so this list is its own.\nexport const claudeLLMModels = ['claude-opus-5-5', 'claude-fable-5-1', 'claude-sonnet-5'] as const;\nexport type ClaudeLLMModel = (typeof claudeLLMModels)[number];\n\n// Adaptive thinking spends from this budget too. Above roughly 21,000 the SDK refuses a request\n// that is not streamed, because it could outlast its ten-minute timeout.\nconst maxOutputTokens = 16_000;\n\n// The request itself is wrong or not allowed, so sending it again cannot succeed.\nconst unrecoverableStatuses = [400, 401, 403, 404, 413, 422];\n\n// The account is out of credit, or its key cannot make the request. Resending cannot fix\n// either, whatever status the error arrives with.\nconst unrecoverableErrorTypes = ['billing_error', 'authentication_error', 'permission_error'];\n\n/**\n * Only the text crosses back. Anthropic needs a thinking block again only to continue a turn\n * that called a tool, and none is sent yet, so a reasoning part stays in the conversation for\n * narration and is left out of the request. A message with no text is left out whole; the API\n * joins the consecutive user turns that leaves.\n */\nfunction toClaudeInput(message: Message): MessageParam[] {\n\tconst text = message.content\n\t\t.filter(part => part.type === 'text')\n\t\t.map(part => part.text)\n\t\t.join('\\n');\n\n\treturn text ? [{ role: message.role, content: text }] : [];\n}\n\nfunction describeBlock(block: ContentBlock, logger: ILogger): MessagePart[] {\n\tswitch (block.type) {\n\t\tcase 'text':\n\t\t\treturn [{ type: 'text', text: block.text }];\n\t\tcase 'thinking':\n\t\t\t// Summarized thinking; the signature only matters for replay, which is not done.\n\t\t\treturn block.thinking ? [{ type: 'reasoning', text: block.thinking }] : [];\n\t\tcase 'redacted_thinking':\n\t\t\t// Encrypted by design: there is nothing to narrate.\n\t\t\treturn [];\n\t\tdefault:\n\t\t\t// No tools are sent yet, so any other block is output this client does not expect.\n\t\t\ttreatErrors(\n\t\t\t\t() => {\n\t\t\t\t\tlogger.warn(block, 'Claude returned a content block the client does not map');\n\t\t\t\t},\n\t\t\t\tclassifyHostFailure,\n\t\t\t\t'Claude client logger failed while mapping a response',\n\t\t\t);\n\t\t\treturn [];\n\t}\n}\n\nfunction toStopReason(response: AnthropicMessage): StopReason {\n\tswitch (response.stop_reason) {\n\t\tcase 'end_turn':\n\t\t\treturn 'completed';\n\t\tcase 'max_tokens':\n\t\t\treturn 'truncated';\n\t\tcase 'refusal':\n\t\t\treturn 'refused';\n\t\tcase 'model_context_window_exceeded':\n\t\t\tthrow new MaxContextError('The context no longer fits the Claude model', {\n\t\t\t\tcause: 'The response stopped at the model context window.',\n\t\t\t});\n\t\tdefault:\n\t\t\t// `tool_use`, `pause_turn` and `stop_sequence` need tools, server tools or stop\n\t\t\t// sequences, and this client sends none of them.\n\t\t\tthrow new UnrecoverableError('The Claude response stopped unexpectedly', {\n\t\t\t\tcause: `The response stopped with \"${response.stop_reason ?? 'unknown'}\".`,\n\t\t\t});\n\t}\n}\n\n/** Unlike OpenAI, Anthropic counts cache reads and writes apart from `input_tokens`. */\nfunction toTokens(usage: Usage | undefined, logger: ILogger): Tokens | undefined {\n\tif (!usage) {\n\t\t// Zeros would report a call that may have been billed as free.\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn('Claude returned a response without usage; it cannot be accounted for');\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Claude client logger failed while reading usage',\n\t\t);\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tinputTokens: usage.input_tokens,\n\t\treadCacheTokens: usage.cache_read_input_tokens ?? 0,\n\t\twrittenCacheTokens: usage.cache_creation_input_tokens ?? 0,\n\t\toutputTokens: usage.output_tokens,\n\t};\n}\n\nfunction toLLMResponse(response: AnthropicMessage, logger: ILogger): LLMResponse {\n\tlet usage: Tokens | undefined;\n\n\ttry {\n\t\t// Inside the try: it warns when the usage is missing, and that warning can throw too.\n\t\tusage = toTokens(response.usage, logger);\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\trole: 'assistant',\n\t\t\t\tcontent: response.content.flatMap(block => describeBlock(block, logger)),\n\t\t\t},\n\t\t\tusage,\n\t\t\tstopReason: toStopReason(response),\n\t\t};\n\t} catch (error) {\n\t\t// The call was billed whether the response failed or a logger did while mapping it, and\n\t\t// an answer without usage leaves the run's count unknown.\n\t\tthrow withSpentTokens(error, usage, !response.usage);\n\t}\n}\n\n/**\n * The SDK resolves credentials lazily, so a missing key surfaces on the first call, as a plain\n * `Error` the SDK gives no class of its own. An `AnthropicError` that is not an `APIError` never\n * carries an answer from the API: on this client's path it is the SDK refusing to make the\n * call, such as an unresolved profile or a request too large to send without streaming.\n */\nfunction isLocalSDKFailure(error: unknown): boolean {\n\treturn (\n\t\t(error instanceof AnthropicError && !(error instanceof APIError)) ||\n\t\t(error instanceof Error && error.message.startsWith('Could not resolve authentication method'))\n\t);\n}\n\nfunction classifyClaudeFailure(error: unknown): Error {\n\tif (isLocalSDKFailure(error)) {\n\t\t// The API never answered, and calling again would fail the same way.\n\t\treturn new UnrecoverableError('The Claude client cannot send the request', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n\tif (error instanceof APIError) {\n\t\t// Anthropic gives an overlong prompt no code of its own, only a 400 with this message.\n\t\tif (error.status === 400 && error.message.includes('prompt is too long')) {\n\t\t\treturn new MaxContextError('The context no longer fits the Claude model', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t\tif (\n\t\t\tunrecoverableErrorTypes.some(type => type === error.type) ||\n\t\t\tunrecoverableStatuses.some(status => status === error.status)\n\t\t) {\n\t\t\treturn new UnrecoverableError('Claude rejected the request', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t}\n\n\t// A rate limit, an overload (529), a 5xx or a network failure: a later attempt can succeed.\n\treturn classifyProviderFailure(error, 'The Claude request failed');\n}\n\n/**\n * A stateless `LLMClient` on the Anthropic Messages API, which keeps nothing between calls:\n * every call carries the whole context, so the conversation is MiKode's, not the provider's.\n */\nexport class ClaudeLLMClient implements LLMClient {\n\tprivate readonly client: Anthropic;\n\tprivate readonly model: ClaudeLLMModel;\n\tprivate readonly systemPrompt: string;\n\tprivate readonly logger: ILogger;\n\n\t// `model` is untyped on purpose: this adapter is the one that knows what Claude supports.\n\tconstructor({\n\t\tmodel,\n\t\tsystemPrompt,\n\t\tlogger,\n\t}: {\n\t\tmodel: string;\n\t\tsystemPrompt: string;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!isOneOf(claudeLLMModels, model)) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`\"${model}\" is not a Claude API model; expected one of: ${claudeLLMModels.join(', ')}`,\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\t// The SDK's default retries stay on, as OpenAI's do. See \"The provider SDKs keep their\n\t\t\t// transport retries under RetryingAgent\" in decisions.md.\n\t\t\tthis.client = new Anthropic();\n\t\t} catch (error) {\n\t\t\t// Conflicting credential options, for example: building it again cannot fix them. A\n\t\t\t// missing key does not fail here but on the first call; see `isLocalSDKFailure`.\n\t\t\tthrow new UnrecoverableError('The Claude client could not be set up', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\n\t\tthis.model = model;\n\t\tthis.systemPrompt = systemPrompt;\n\t\tthis.logger = logger;\n\t}\n\n\tasync send(context: Message[], signal: AbortSignal): Promise<LLMResponse> {\n\t\tlet response: AnthropicMessage;\n\n\t\ttry {\n\t\t\tresponse = await this.client.messages.create(\n\t\t\t\t{\n\t\t\t\t\tmodel: this.model,\n\t\t\t\t\tmax_tokens: maxOutputTokens,\n\t\t\t\t\tsystem: this.systemPrompt,\n\t\t\t\t\tmessages: context.flatMap(toClaudeInput),\n\t\t\t\t\tthinking: { type: 'adaptive', display: 'summarized' },\n\t\t\t\t\t// Anthropic caches only on request. This marks the last block, so the next call\n\t\t\t\t\t// reads everything before it from the cache instead of paying for it again.\n\t\t\t\t\tcache_control: { type: 'ephemeral' },\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The SDK's abort error is named 'Error', so it would pass for a provider failure.\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow classifyClaudeFailure(error);\n\t\t}\n\n\t\treturn treatErrors(\n\t\t\t() => toLLMResponse(response, this.logger),\n\t\t\tclassifyLocalFailure,\n\t\t\t'Claude response mapping failed',\n\t\t);\n\t}\n}\n"]}
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
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
  }
@@ -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');
@@ -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 '#src/agent/domain/errors';\nimport {\n\tclassifyHostFailure,\n\tclassifyLocalFailure,\n\tclassifyProviderFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/agent/domain/providerFailure';\nimport { isOneOf } from '#src/shared/domain/isOneOf';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport type { Tokens } from '#src/shared/domain/tokens';\nimport { MaxContextError } from '../domain/errors.ts';\nimport type { LLMClient, LLMResponse, StopReason } from '../domain/llm.ts';\nimport type { Message, MessagePart } from '../domain/message.ts';\n\n// The SDK types `model` as a plain string, so this list is maintained by hand.\nexport const openAIModels = [\n\t'gpt-6-astra',\n\t'gpt-5.6-sol',\n\t'gpt-5.6-luna',\n\t'gpt-5.6-terra',\n] as const;\nexport type OpenAIModel = (typeof openAIModels)[number];\n\n// The request itself is wrong or not allowed, so sending it again cannot succeed.\nconst unrecoverableStatuses = [400, 401, 403, 404, 422];\n\n// Out of credit or over a spend limit. These arrive as a 429 like a rate limit, but the account\n// stays blocked however often the request is sent.\nconst exhaustedQuotaCodes = [\n\t'insufficient_quota',\n\t'credit_balance_exhausted',\n\t'project_spend_limit_exceeded',\n];\n\n// The codes OpenAI gives a failed response that a new attempt can succeed past.\nconst transientResponseErrorCodes = ['server_error', 'rate_limit_exceeded'];\n\n/**\n * Only the text crosses back. OpenAI replays reasoning only from the encrypted item it\n * returned, which a `Message` does not keep, so a reasoning part stays in the conversation\n * for narration and is left out of the request. A message with no text is left out whole.\n */\nfunction toOpenAIInput(message: Message): ResponseInputItem[] {\n\tconst text = message.content\n\t\t.filter(part => part.type === 'text')\n\t\t.map(part => part.text)\n\t\t.join('\\n');\n\n\treturn text ? [{ role: message.role, content: text }] : [];\n}\n\nfunction describeItem(item: ResponseOutputItem, logger: ILogger): MessagePart[] {\n\tswitch (item.type) {\n\t\tcase 'message':\n\t\t\t// A refusal is not an answer: it only sets the stop reason.\n\t\t\treturn item.content.flatMap(content =>\n\t\t\t\tcontent.type === 'output_text' ? [{ type: 'text' as const, text: content.text }] : [],\n\t\t\t);\n\t\tcase 'reasoning': {\n\t\t\t// The summary is the readable part; the encrypted content is opaque by design.\n\t\t\tconst text = item.summary.map(summary => summary.text).join('\\n');\n\t\t\treturn text ? [{ type: 'reasoning', text }] : [];\n\t\t}\n\t\tdefault:\n\t\t\t// No tools are sent yet, so any other item is output this client does not expect.\n\t\t\ttreatErrors(\n\t\t\t\t() => {\n\t\t\t\t\tlogger.warn(item, 'OpenAI returned an output item the client does not map');\n\t\t\t\t},\n\t\t\t\tclassifyHostFailure,\n\t\t\t\t'OpenAI client logger failed while mapping a response',\n\t\t\t);\n\t\t\treturn [];\n\t}\n}\n\nfunction toStopReason(response: Response): StopReason {\n\tif (response.status === 'incomplete') {\n\t\treturn response.incomplete_details?.reason === 'content_filter' ? 'refused' : 'truncated';\n\t}\n\tif (response.status !== 'completed') {\n\t\tconst cause =\n\t\t\tresponse.error?.message ??\n\t\t\t`The response ended with status \"${response.status ?? 'unknown'}\".`;\n\t\t// The same failure is retried when it arrives as an SDK error, so it must be here too.\n\t\tif (response.error && transientResponseErrorCodes.includes(response.error.code)) {\n\t\t\tthrow new RecoverableError('The OpenAI response failed', { cause });\n\t\t}\n\t\tthrow new UnrecoverableError('The OpenAI response did not complete', { cause });\n\t}\n\n\tconst refused = response.output.some(\n\t\titem => item.type === 'message' && item.content.some(content => content.type === 'refusal'),\n\t);\n\treturn refused ? 'refused' : 'completed';\n}\n\n/** OpenAI counts both cache reads and cache writes inside `input_tokens`. */\nfunction toTokens(usage: ResponseUsage | undefined, logger: ILogger): Tokens | undefined {\n\tif (!usage) {\n\t\t// Zeros would report a call that may have been billed as free.\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tlogger.warn('OpenAI returned a response without usage; it cannot be accounted for');\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'OpenAI client logger failed while reading usage',\n\t\t);\n\t\treturn undefined;\n\t}\n\n\tconst { cached_tokens, cache_write_tokens } = usage.input_tokens_details;\n\treturn {\n\t\tinputTokens: usage.input_tokens - cached_tokens - cache_write_tokens,\n\t\treadCacheTokens: cached_tokens,\n\t\twrittenCacheTokens: cache_write_tokens,\n\t\toutputTokens: usage.output_tokens,\n\t};\n}\n\nfunction toLLMResponse(response: Response, logger: ILogger): LLMResponse {\n\tlet usage: Tokens | undefined;\n\n\ttry {\n\t\t// Inside the try: it warns when the usage is missing, and that warning can throw too.\n\t\tusage = toTokens(response.usage, logger);\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\trole: 'assistant',\n\t\t\t\tcontent: response.output.flatMap(item => describeItem(item, logger)),\n\t\t\t},\n\t\t\tusage,\n\t\t\tstopReason: toStopReason(response),\n\t\t};\n\t} catch (error) {\n\t\t// The call was billed whether the response failed or a logger did while mapping it, and\n\t\t// an answer without usage leaves the run's count unknown.\n\t\tthrow withSpentTokens(error, usage, !response.usage);\n\t}\n}\n\nfunction classifyOpenAIFailure(error: unknown): Error {\n\tif (error instanceof APIError) {\n\t\tif (error.code === 'context_length_exceeded') {\n\t\t\treturn new MaxContextError('The context no longer fits the OpenAI model', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t\tif (\n\t\t\texhaustedQuotaCodes.some(code => code === error.code) ||\n\t\t\tunrecoverableStatuses.some(status => status === error.status)\n\t\t) {\n\t\t\treturn new UnrecoverableError('OpenAI rejected the request', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn classifyProviderFailure(error, 'The OpenAI request failed');\n}\n\n/**\n * A stateless `LLMClient` on the OpenAI Responses API. With `store: false` OpenAI keeps\n * nothing between calls, so every call carries the whole context: the conversation is\n * MiKode's, not the provider's.\n */\nexport class OpenAILLMClient implements LLMClient {\n\tprivate readonly client: OpenAI;\n\tprivate readonly model: OpenAIModel;\n\tprivate readonly systemPrompt: string;\n\tprivate readonly logger: ILogger;\n\n\t// `model` is untyped on purpose: this adapter is the one that knows what OpenAI supports.\n\tconstructor({\n\t\tmodel,\n\t\tsystemPrompt,\n\t\tlogger,\n\t}: {\n\t\tmodel: string;\n\t\tsystemPrompt: string;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!isOneOf(openAIModels, model)) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`\"${model}\" is not an OpenAI model; expected one of: ${openAIModels.join(', ')}`,\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\t// The SDK's default retries stay on: they back off and honour `Retry-After`, which\n\t\t\t// `RetryingAgent` does not. See \"The provider SDKs keep their transport retries\" in decisions.md.\n\t\t\tthis.client = new OpenAI();\n\t\t} catch (error) {\n\t\t\t// A missing API key, for example: building the client again cannot fix it.\n\t\t\tthrow new UnrecoverableError('The OpenAI client could not be set up', {\n\t\t\t\tcause: describeFailure(error),\n\t\t\t});\n\t\t}\n\n\t\tthis.model = model;\n\t\tthis.systemPrompt = systemPrompt;\n\t\tthis.logger = logger;\n\t}\n\n\tasync send(context: Message[], signal: AbortSignal): Promise<LLMResponse> {\n\t\tlet response: Response;\n\n\t\ttry {\n\t\t\tresponse = await this.client.responses.create(\n\t\t\t\t{\n\t\t\t\t\tmodel: this.model,\n\t\t\t\t\tinstructions: this.systemPrompt,\n\t\t\t\t\tinput: context.flatMap(toOpenAIInput),\n\t\t\t\t\tstore: false,\n\t\t\t\t\treasoning: { summary: 'auto' },\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The SDK's abort error is named 'Error', so it would pass for a provider failure.\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow classifyOpenAIFailure(error);\n\t\t}\n\n\t\treturn treatErrors(\n\t\t\t() => toLLMResponse(response, this.logger),\n\t\t\tclassifyLocalFailure,\n\t\t\t'OpenAI response mapping failed',\n\t\t);\n\t}\n}\n"]}
1
+ {"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"]}
@@ -0,0 +1,9 @@
1
+ import type { ToolDefinition } from '#src/llm/domain/tool';
2
+ export interface Tool extends ToolDefinition {
3
+ /**
4
+ * Runs the tool on the input the model sent. The input is unknown because the model can
5
+ * send anything: validating it is the tool's job, and a rejection returns to the model as
6
+ * an error result.
7
+ */
8
+ execute(input: unknown, signal: AbortSignal): Promise<string>;
9
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../../../src/tools/domain/tool.ts"],"names":[],"mappings":"","sourcesContent":["import type { ToolDefinition } from '#src/llm/domain/tool';\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,48 @@
1
+ export interface TextMatch {
2
+ path: string;
3
+ line: number;
4
+ text: string;
5
+ }
6
+ /**
7
+ * The folder an agent works on, read-only. Every path is relative to its root, written with
8
+ * `/`, and results come sorted. A file `.gitignore` ignores, or a path outside the root, does
9
+ * not exist here: it is never listed, searched or read, whatever scope a query asks for.
10
+ *
11
+ * A binary file is listed, but never searched or read. Every operation stops after a time
12
+ * limit and fails with an error that asks for a narrower query.
13
+ *
14
+ * `total` counts everything the query matched and `truncated` says whether `limit` cut it.
15
+ */
16
+ export interface Workspace {
17
+ listFiles(query: {
18
+ path?: string;
19
+ glob?: string;
20
+ limit: number;
21
+ }, signal: AbortSignal): Promise<{
22
+ files: string[];
23
+ total: number;
24
+ truncated: boolean;
25
+ }>;
26
+ /** `pattern` is a regular expression. A match's `text` is at most 1,000 characters. */
27
+ searchText(query: {
28
+ pattern: string;
29
+ ignoreCase: boolean;
30
+ path?: string;
31
+ glob?: string;
32
+ limit: number;
33
+ }, signal: AbortSignal): Promise<{
34
+ matches: TextMatch[];
35
+ total: number;
36
+ truncated: boolean;
37
+ }>;
38
+ /** `fromLine` starts at 1. */
39
+ readFile(query: {
40
+ path: string;
41
+ fromLine: number;
42
+ lineCount: number;
43
+ }, signal: AbortSignal): Promise<{
44
+ lines: string[];
45
+ totalLines: number;
46
+ truncated: boolean;
47
+ }>;
48
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/tools/domain/workspace.ts"],"names":[],"mappings":"","sourcesContent":["export interface TextMatch {\n\tpath: string;\n\tline: number;\n\ttext: string;\n}\n\n/**\n * The folder an agent works on, read-only. Every path is relative to its root, written with\n * `/`, and results come sorted. A file `.gitignore` ignores, or a path outside the root, does\n * not exist here: it is never listed, searched or read, whatever scope a query asks for.\n *\n * A binary file is listed, but never searched or read. Every operation stops after a time\n * limit and fails with an error that asks for a narrower query.\n *\n * `total` counts everything the query matched and `truncated` says whether `limit` cut it.\n */\nexport interface Workspace {\n\tlistFiles(\n\t\tquery: { path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ files: string[]; total: number; truncated: boolean }>;\n\n\t/** `pattern` is a regular expression. A match's `text` is at most 1,000 characters. */\n\tsearchText(\n\t\tquery: { pattern: string; ignoreCase: boolean; path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ matches: TextMatch[]; total: number; truncated: boolean }>;\n\n\t/** `fromLine` starts at 1. */\n\treadFile(\n\t\tquery: { path: string; fromLine: number; lineCount: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ lines: string[]; totalLines: number; truncated: boolean }>;\n}\n"]}
@@ -0,0 +1,66 @@
1
+ import type { TextMatch, Workspace } from '../domain/workspace.ts';
2
+ /**
3
+ * Everything both implementations share: the boundary and the shape of every answer. A
4
+ * subclass only says which files are visible and where a pattern matches, across the whole
5
+ * root; the scope a model asks for is applied here, to those results, and never handed to the
6
+ * program underneath. ripgrep stops honouring `.gitignore` for a path it is given explicitly,
7
+ * so passing the model's scope down is how a secret would leak. See "`.gitignore` is the
8
+ * boundary of what an agent reads" in decisions.md.
9
+ *
10
+ * Every operation also stops after `timeoutMs`. A timeout is a failure the model reads and
11
+ * narrows its query from, not a cancellation: only the run's own signal cancels the run.
12
+ */
13
+ export declare abstract class BoundedWorkspace implements Workspace {
14
+ protected readonly root: string;
15
+ private readonly timeoutMs;
16
+ constructor({ root, timeoutMs }: {
17
+ root: string;
18
+ timeoutMs?: number;
19
+ });
20
+ /** Every file the program does not ignore, relative to the root, in any order. */
21
+ protected abstract listCandidates(signal: AbortSignal): Promise<string[]>;
22
+ /**
23
+ * Hands `keep` every line matching `pattern` in a file the program does not ignore, in any
24
+ * order, as the program prints it. Nothing is held here: `keep` decides what to store.
25
+ */
26
+ protected abstract findMatches(pattern: string, ignoreCase: boolean, signal: AbortSignal, keep: (match: TextMatch) => void): Promise<void>;
27
+ listFiles(query: {
28
+ path?: string;
29
+ glob?: string;
30
+ limit: number;
31
+ }, signal: AbortSignal): Promise<{
32
+ files: string[];
33
+ total: number;
34
+ truncated: boolean;
35
+ }>;
36
+ searchText(query: {
37
+ pattern: string;
38
+ ignoreCase: boolean;
39
+ path?: string;
40
+ glob?: string;
41
+ limit: number;
42
+ }, signal: AbortSignal): Promise<{
43
+ matches: TextMatch[];
44
+ total: number;
45
+ truncated: boolean;
46
+ }>;
47
+ readFile(query: {
48
+ path: string;
49
+ fromLine: number;
50
+ lineCount: number;
51
+ }, signal: AbortSignal): Promise<{
52
+ lines: string[];
53
+ totalLines: number;
54
+ truncated: boolean;
55
+ }>;
56
+ /**
57
+ * Runs an operation under the run's signal and this workspace's time limit together. The
58
+ * run's cancellation escapes unchanged; the time limit becomes an error the model can act on.
59
+ */
60
+ private withTimeout;
61
+ /** Regular files only: a symlink could point out of the root, and a deleted file is gone. */
62
+ private regularFiles;
63
+ private inScope;
64
+ /** A path relative to the root with `/`, '' for the root itself, or undefined when it leaves it. */
65
+ private toRelative;
66
+ }