@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
@@ -44,6 +44,13 @@ export type ProgressEvent = {
44
44
  } | {
45
45
  type: 'agentMessage';
46
46
  message: string;
47
+ }
48
+ /** `id` pairs a call's end with its start when one step calls the same tool more than once. */
49
+ | {
50
+ type: 'tool';
51
+ id: string;
52
+ name: string;
53
+ status: 'in_progress' | 'completed' | 'error';
47
54
  } | {
48
55
  type: 'todoList';
49
56
  items: {
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number }\n\t| { type: 'reasoning'; message: string }\n\t| { type: 'search'; query: string }\n\t| { type: 'fileChange'; changes: { path: string; kind: 'add' | 'update' | 'delete' }[] }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string }\n\t| { type: 'agentMessage'; message: string }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[] }\n\t| { type: 'turnStarted' }\n\t| { type: 'turnEnded' };\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see ./errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError` must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse>;\n}\n"]}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number }\n\t| { type: 'reasoning'; message: string }\n\t| { type: 'search'; query: string }\n\t| { type: 'fileChange'; changes: { path: string; kind: 'add' | 'update' | 'delete' }[] }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string }\n\t| { type: 'agentMessage'; message: string }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| { type: 'tool'; id: string; name: string; status: 'in_progress' | 'completed' | 'error' }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[] }\n\t| { type: 'turnStarted' }\n\t| { type: 'turnEnded' };\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see ./errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError` must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse>;\n}\n"]}
@@ -1,17 +1,34 @@
1
1
  import type { Agent, AgentResponse, Callback } from '#src/agent/domain/agent';
2
2
  import type { LLMClient } from '#src/llm/domain/llm';
3
3
  import type { Message } from '#src/llm/domain/message';
4
+ import type { Tool } from '#src/tools/domain/tool';
4
5
  /**
5
6
  * An agent whose conversation is owned by MiKode: every run sends the whole context to a
6
- * stateless `LLMClient`. Turn boundaries are the consumer's to emit, as with the other
7
- * engines; this agent only narrates what the model produced.
7
+ * stateless `LLMClient`. Within a run the model may call the agent's tools; the agent runs them
8
+ * and sends their results back until the model answers without calling one. Turn boundaries
9
+ * are the consumer's to emit, as with the other engines; this agent only narrates what the
10
+ * model produced and what its tools did.
8
11
  */
9
12
  export declare class LLMAgent implements Agent {
10
13
  private readonly llmClient;
11
14
  private readonly conversation;
12
- constructor({ llmClient, messages }: {
15
+ private readonly tools;
16
+ private readonly toolDefinitions;
17
+ private readonly maxSteps;
18
+ constructor({ llmClient, messages, tools, maxSteps, }: {
13
19
  llmClient: LLMClient;
14
20
  messages?: Message[];
21
+ tools?: Tool[];
22
+ /** How many calls to the model one run may make before it fails. */
23
+ maxSteps?: number;
15
24
  });
25
+ private llmCall;
26
+ /**
27
+ * Always answers the call: a missing tool or a failing one becomes an error result, so the
28
+ * model can correct itself. Only a cancellation escapes, whatever error the tool turned it into.
29
+ */
30
+ private runTool;
31
+ /** One at a time and in order, so their effects and their narration never interleave. */
32
+ private runTools;
16
33
  run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse>;
17
34
  }
@@ -1,30 +1,76 @@
1
- import { UnrecoverableError, withSpentTokens } from '#src/agent/domain/errors';
2
- import { classifyHostFailure, classifyProviderFailure, treatErrors, } from '#src/agent/domain/providerFailure';
1
+ import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpentTokens, } from '#src/agent/domain/errors';
2
+ import { classifyHostFailure, classifyProviderFailure, describeFailure, treatErrors, } from '#src/agent/domain/providerFailure';
3
3
  import { Conversation } from '#src/llm/domain/conversation';
4
+ import { addTokens } from '#src/shared/domain/tokens';
4
5
  function describePart(part) {
5
- return part.type === 'text'
6
- ? { type: 'agentMessage', message: part.text }
7
- : { type: 'reasoning', message: part.text };
6
+ switch (part.type) {
7
+ case 'text':
8
+ return { type: 'agentMessage', message: part.text };
9
+ case 'reasoning':
10
+ return { type: 'reasoning', message: part.text };
11
+ case 'toolCall':
12
+ return { type: 'tool', id: part.id, name: part.name, status: 'in_progress' };
13
+ case 'toolResult':
14
+ return {
15
+ type: 'tool',
16
+ id: part.callId,
17
+ name: part.name,
18
+ status: part.isError ? 'error' : 'completed',
19
+ };
20
+ case 'providerData':
21
+ // Opaque by design: its readable side, if any, arrives as a reasoning part.
22
+ return undefined;
23
+ }
24
+ }
25
+ function narrate(part, callback) {
26
+ const event = describePart(part);
27
+ if (!event) {
28
+ return;
29
+ }
30
+ treatErrors(() => {
31
+ callback(event);
32
+ }, classifyHostFailure, 'LLM agent progress callback failed');
8
33
  }
9
34
  /**
10
35
  * An agent whose conversation is owned by MiKode: every run sends the whole context to a
11
- * stateless `LLMClient`. Turn boundaries are the consumer's to emit, as with the other
12
- * engines; this agent only narrates what the model produced.
36
+ * stateless `LLMClient`. Within a run the model may call the agent's tools; the agent runs them
37
+ * and sends their results back until the model answers without calling one. Turn boundaries
38
+ * are the consumer's to emit, as with the other engines; this agent only narrates what the
39
+ * model produced and what its tools did.
13
40
  */
14
41
  export class LLMAgent {
15
42
  llmClient;
16
43
  conversation;
17
- constructor({ llmClient, messages }) {
44
+ tools;
45
+ toolDefinitions;
46
+ maxSteps;
47
+ constructor({ llmClient, messages, tools = [], maxSteps = 25, }) {
48
+ if (!Number.isInteger(maxSteps) || maxSteps < 1) {
49
+ throw new InvalidAgentConfigError(`maxSteps must be a positive integer; got ${String(maxSteps)}`);
50
+ }
18
51
  this.llmClient = llmClient;
19
52
  this.conversation = new Conversation(messages);
53
+ this.maxSteps = maxSteps;
54
+ this.tools = new Map(tools.map(tool => [tool.name, tool]));
55
+ if (this.tools.size !== tools.length) {
56
+ // The model calls tools by name, so a second one with the same name could never run.
57
+ throw new InvalidAgentConfigError('Two tools share a name; each name must be unique');
58
+ }
59
+ // The client only describes the tools to the model; running them stays with the agent.
60
+ this.toolDefinitions = tools.map(({ name, description, inputSchema }) => ({
61
+ name,
62
+ description,
63
+ inputSchema,
64
+ }));
20
65
  }
21
- async run(prompt, signal, callback) {
22
- const start = Date.now();
23
- const userMessage = { role: 'user', content: [{ type: 'text', text: prompt }] };
66
+ async llmCall(runMessages, signal) {
24
67
  let response;
25
68
  try {
26
- // The prompt is sent as a copy so the one recorded below is the one the agent built.
27
- response = await this.llmClient.send([...this.conversation.getContext(), structuredClone(userMessage)], signal);
69
+ // Sent as a copy, so what the run records is what the agent built, whatever the client does.
70
+ response = await this.llmClient.send({
71
+ context: [...this.conversation.getContext(), ...structuredClone(runMessages)],
72
+ tools: this.toolDefinitions,
73
+ }, signal);
28
74
  }
29
75
  catch (error) {
30
76
  throw classifyProviderFailure(error, 'The LLM call failed');
@@ -37,27 +83,107 @@ export class LLMAgent {
37
83
  usageUnreported: !response.usage,
38
84
  });
39
85
  }
40
- this.conversation.addExchange(userMessage, response.message);
86
+ return response;
87
+ }
88
+ /**
89
+ * Always answers the call: a missing tool or a failing one becomes an error result, so the
90
+ * model can correct itself. Only a cancellation escapes, whatever error the tool turned it into.
91
+ */
92
+ async runTool(call, signal) {
93
+ const result = { type: 'toolResult', callId: call.id, name: call.name };
94
+ const tool = this.tools.get(call.name);
95
+ if (!tool) {
96
+ return {
97
+ ...result,
98
+ output: `There is no tool named "${call.name}". Available tools: ${[...this.tools.keys()].join(', ') || 'none'}.`,
99
+ isError: true,
100
+ };
101
+ }
102
+ // Checked again at the last moment: the consumer may cancel on the announcement itself.
103
+ signal.throwIfAborted();
41
104
  try {
42
- for (const part of response.message.content) {
43
- treatErrors(() => {
44
- callback(describePart(part));
45
- }, classifyHostFailure, 'LLM agent progress callback failed');
105
+ return { ...result, output: await tool.execute(call.input, signal), isError: false };
106
+ }
107
+ catch (error) {
108
+ signal.throwIfAborted();
109
+ return { ...result, output: describeFailure(error), isError: true };
110
+ }
111
+ }
112
+ /** One at a time and in order, so their effects and their narration never interleave. */
113
+ async runTools(calls, signal, callback) {
114
+ const results = [];
115
+ for (const call of calls) {
116
+ // A call before this one may have ignored the cancellation; this one must not start,
117
+ // nor be announced as running.
118
+ signal.throwIfAborted();
119
+ // Announced only as its turn comes, so a consumer never shows a call as running early.
120
+ narrate(call, callback);
121
+ const result = await this.runTool(call, signal);
122
+ narrate(result, callback);
123
+ results.push(result);
124
+ }
125
+ return { role: 'tool', content: results };
126
+ }
127
+ async run(prompt, signal, callback) {
128
+ const start = Date.now();
129
+ const runMessages = [{ role: 'user', content: [{ type: 'text', text: prompt }] }];
130
+ let tokens;
131
+ // Once one call went unreported the run's total is unknown, however many report later.
132
+ let unreported = false;
133
+ let toolRan = false;
134
+ try {
135
+ for (let step = 1; step <= this.maxSteps; step++) {
136
+ const response = await this.llmCall(runMessages, signal);
137
+ tokens = addTokens(tokens, response.usage);
138
+ unreported ||= !response.usage;
139
+ runMessages.push(response.message);
140
+ // Tool calls are narrated as they start, in `runTools`, and never if they do not run.
141
+ for (const part of response.message.content) {
142
+ if (part.type !== 'toolCall')
143
+ narrate(part, callback);
144
+ }
145
+ const calls = response.message.content.filter(part => part.type === 'toolCall');
146
+ if (calls.length === 0) {
147
+ // Recorded only now, so a failed run leaves no tool call without its result.
148
+ this.conversation.addRun(runMessages);
149
+ return {
150
+ // Only the final answer: text from earlier steps was narrated as it came.
151
+ response: response.message.content
152
+ .filter(part => part.type === 'text')
153
+ .map(part => part.text)
154
+ .join('\n'),
155
+ tokens: unreported ? undefined : tokens,
156
+ duration: (Date.now() - start) / 1000,
157
+ };
158
+ }
159
+ // On the last step no call is left to send the results to, so the tools do not run.
160
+ if (step < this.maxSteps) {
161
+ // Only a call to a tool the agent has reaches `execute`; a missing one has no
162
+ // effect a retry could repeat.
163
+ toolRan ||= calls.some(call => this.tools.has(call.name));
164
+ runMessages.push(await this.runTools(calls, signal, callback));
165
+ }
46
166
  }
167
+ throw new UnrecoverableError('The LLM did not finish within its step limit', {
168
+ cause: `The model was still calling tools after ${String(this.maxSteps)} steps.`,
169
+ });
47
170
  }
48
171
  catch (error) {
49
- // The answer already arrived, so the run spent its tokens whatever the consumer did.
50
- throw withSpentTokens(error, response.usage, !response.usage);
51
- }
52
- const text = response.message.content
53
- .filter(part => part.type === 'text')
54
- .map(part => part.text)
55
- .join('\n');
56
- return {
57
- response: text,
58
- tokens: response.usage,
59
- duration: (Date.now() - start) / 1000,
60
- };
172
+ let failure = error;
173
+ if (toolRan && error instanceof RecoverableError) {
174
+ // RetryingAgent would run the whole prompt again, and with it every tool this run
175
+ // already executed, so a failure after one ran must not invite a retry.
176
+ const unrecoverable = new UnrecoverableError(error.message, {
177
+ cause: error.cause,
178
+ tokens: error.tokens,
179
+ usageUnreported: error.usageUnreported,
180
+ });
181
+ unrecoverable.stack = error.stack;
182
+ failure = unrecoverable;
183
+ }
184
+ // The run spent the tokens of every step before the one that failed.
185
+ throw withSpentTokens(failure, tokens, unreported);
186
+ }
61
187
  }
62
188
  }
63
189
  //# sourceMappingURL=llmAgent.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"llmAgent.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/llmAgent.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentResponse, Callback, ProgressEvent } from '#src/agent/domain/agent';\nimport { UnrecoverableError, withSpentTokens } from '#src/agent/domain/errors';\nimport {\n\tclassifyHostFailure,\n\tclassifyProviderFailure,\n\ttreatErrors,\n} from '#src/agent/domain/providerFailure';\nimport { Conversation } from '#src/llm/domain/conversation';\nimport type { LLMClient, LLMResponse } from '#src/llm/domain/llm';\nimport type { Message, MessagePart } from '#src/llm/domain/message';\n\nfunction describePart(part: MessagePart): ProgressEvent {\n\treturn part.type === 'text'\n\t\t? { type: 'agentMessage', message: part.text }\n\t\t: { type: 'reasoning', message: part.text };\n}\n\n/**\n * An agent whose conversation is owned by MiKode: every run sends the whole context to a\n * stateless `LLMClient`. Turn boundaries are the consumer's to emit, as with the other\n * engines; this agent only narrates what the model produced.\n */\nexport class LLMAgent implements Agent {\n\tprivate readonly llmClient: LLMClient;\n\tprivate readonly conversation: Conversation;\n\n\tconstructor({ llmClient, messages }: { llmClient: LLMClient; messages?: Message[] }) {\n\t\tthis.llmClient = llmClient;\n\t\tthis.conversation = new Conversation(messages);\n\t}\n\n\tasync run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse> {\n\t\tconst start = Date.now();\n\t\tconst userMessage: Message = { role: 'user', content: [{ type: 'text', text: prompt }] };\n\n\t\tlet response: LLMResponse;\n\t\ttry {\n\t\t\t// The prompt is sent as a copy so the one recorded below is the one the agent built.\n\t\t\tresponse = await this.llmClient.send(\n\t\t\t\t[...this.conversation.getContext(), structuredClone(userMessage)],\n\t\t\t\tsignal,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow classifyProviderFailure(error, 'The LLM call failed');\n\t\t}\n\n\t\tif (response.stopReason !== 'completed') {\n\t\t\tthrow new UnrecoverableError('The LLM stopped before completing its answer', {\n\t\t\t\tcause: `The model stopped with \"${response.stopReason}\".`,\n\t\t\t\t// The call was billed even though its answer is unusable.\n\t\t\t\ttokens: response.usage,\n\t\t\t\tusageUnreported: !response.usage,\n\t\t\t});\n\t\t}\n\n\t\tthis.conversation.addExchange(userMessage, response.message);\n\n\t\ttry {\n\t\t\tfor (const part of response.message.content) {\n\t\t\t\ttreatErrors(\n\t\t\t\t\t() => {\n\t\t\t\t\t\tcallback(describePart(part));\n\t\t\t\t\t},\n\t\t\t\t\tclassifyHostFailure,\n\t\t\t\t\t'LLM agent progress callback failed',\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// The answer already arrived, so the run spent its tokens whatever the consumer did.\n\t\t\tthrow withSpentTokens(error, response.usage, !response.usage);\n\t\t}\n\n\t\tconst text = response.message.content\n\t\t\t.filter(part => part.type === 'text')\n\t\t\t.map(part => part.text)\n\t\t\t.join('\\n');\n\n\t\treturn {\n\t\t\tresponse: text,\n\t\t\ttokens: response.usage,\n\t\t\tduration: (Date.now() - start) / 1000,\n\t\t};\n\t}\n}\n"]}
1
+ {"version":3,"file":"llmAgent.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/llmAgent.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentResponse, Callback, ProgressEvent } from '#src/agent/domain/agent';\nimport {\n\tInvalidAgentConfigError,\n\tRecoverableError,\n\tUnrecoverableError,\n\twithSpentTokens,\n} from '#src/agent/domain/errors';\nimport {\n\tclassifyHostFailure,\n\tclassifyProviderFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/agent/domain/providerFailure';\nimport { Conversation } from '#src/llm/domain/conversation';\nimport type { LLMClient, LLMResponse } from '#src/llm/domain/llm';\nimport type { Message, MessagePart, ToolCallPart, ToolResultPart } from '#src/llm/domain/message';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { Tool } from '#src/tools/domain/tool';\nimport { addTokens, type Tokens } from '#src/shared/domain/tokens';\n\nfunction describePart(part: MessagePart): ProgressEvent | undefined {\n\tswitch (part.type) {\n\t\tcase 'text':\n\t\t\treturn { type: 'agentMessage', message: part.text };\n\t\tcase 'reasoning':\n\t\t\treturn { type: 'reasoning', message: part.text };\n\t\tcase 'toolCall':\n\t\t\treturn { type: 'tool', id: part.id, name: part.name, status: 'in_progress' };\n\t\tcase 'toolResult':\n\t\t\treturn {\n\t\t\t\ttype: 'tool',\n\t\t\t\tid: part.callId,\n\t\t\t\tname: part.name,\n\t\t\t\tstatus: part.isError ? 'error' : 'completed',\n\t\t\t};\n\t\tcase 'providerData':\n\t\t\t// Opaque by design: its readable side, if any, arrives as a reasoning part.\n\t\t\treturn undefined;\n\t}\n}\n\nfunction narrate(part: MessagePart, callback: Callback): void {\n\tconst event = describePart(part);\n\tif (!event) {\n\t\treturn;\n\t}\n\n\ttreatErrors(\n\t\t() => {\n\t\t\tcallback(event);\n\t\t},\n\t\tclassifyHostFailure,\n\t\t'LLM agent progress callback failed',\n\t);\n}\n\n/**\n * An agent whose conversation is owned by MiKode: every run sends the whole context to a\n * stateless `LLMClient`. Within a run the model may call the agent's tools; the agent runs them\n * and sends their results back until the model answers without calling one. Turn boundaries\n * are the consumer's to emit, as with the other engines; this agent only narrates what the\n * model produced and what its tools did.\n */\nexport class LLMAgent implements Agent {\n\tprivate readonly llmClient: LLMClient;\n\tprivate readonly conversation: Conversation;\n\tprivate readonly tools: Map<string, Tool>;\n\tprivate readonly toolDefinitions: ToolDefinition[];\n\tprivate readonly maxSteps: number;\n\n\tconstructor({\n\t\tllmClient,\n\t\tmessages,\n\t\ttools = [],\n\t\tmaxSteps = 25,\n\t}: {\n\t\tllmClient: LLMClient;\n\t\tmessages?: Message[];\n\t\ttools?: Tool[];\n\t\t/** How many calls to the model one run may make before it fails. */\n\t\tmaxSteps?: number;\n\t}) {\n\t\tif (!Number.isInteger(maxSteps) || maxSteps < 1) {\n\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t`maxSteps must be a positive integer; got ${String(maxSteps)}`,\n\t\t\t);\n\t\t}\n\n\t\tthis.llmClient = llmClient;\n\t\tthis.conversation = new Conversation(messages);\n\t\tthis.maxSteps = maxSteps;\n\t\tthis.tools = new Map(tools.map(tool => [tool.name, tool]));\n\t\tif (this.tools.size !== tools.length) {\n\t\t\t// The model calls tools by name, so a second one with the same name could never run.\n\t\t\tthrow new InvalidAgentConfigError('Two tools share a name; each name must be unique');\n\t\t}\n\t\t// The client only describes the tools to the model; running them stays with the agent.\n\t\tthis.toolDefinitions = tools.map(({ name, description, inputSchema }) => ({\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tinputSchema,\n\t\t}));\n\t}\n\n\tprivate async llmCall(runMessages: Message[], signal: AbortSignal): Promise<LLMResponse> {\n\t\tlet response: LLMResponse;\n\t\ttry {\n\t\t\t// Sent as a copy, so what the run records is what the agent built, whatever the client does.\n\t\t\tresponse = await this.llmClient.send(\n\t\t\t\t{\n\t\t\t\t\tcontext: [...this.conversation.getContext(), ...structuredClone(runMessages)],\n\t\t\t\t\ttools: this.toolDefinitions,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow classifyProviderFailure(error, 'The LLM call failed');\n\t\t}\n\n\t\tif (response.stopReason !== 'completed') {\n\t\t\tthrow new UnrecoverableError('The LLM stopped before completing its answer', {\n\t\t\t\tcause: `The model stopped with \"${response.stopReason}\".`,\n\t\t\t\t// The call was billed even though its answer is unusable.\n\t\t\t\ttokens: response.usage,\n\t\t\t\tusageUnreported: !response.usage,\n\t\t\t});\n\t\t}\n\n\t\treturn response;\n\t}\n\n\t/**\n\t * Always answers the call: a missing tool or a failing one becomes an error result, so the\n\t * model can correct itself. Only a cancellation escapes, whatever error the tool turned it into.\n\t */\n\tprivate async runTool(call: ToolCallPart, signal: AbortSignal): Promise<ToolResultPart> {\n\t\tconst result = { type: 'toolResult' as const, callId: call.id, name: call.name };\n\t\tconst tool = this.tools.get(call.name);\n\t\tif (!tool) {\n\t\t\treturn {\n\t\t\t\t...result,\n\t\t\t\toutput: `There is no tool named \"${call.name}\". Available tools: ${[...this.tools.keys()].join(', ') || 'none'}.`,\n\t\t\t\tisError: true,\n\t\t\t};\n\t\t}\n\n\t\t// Checked again at the last moment: the consumer may cancel on the announcement itself.\n\t\tsignal.throwIfAborted();\n\t\ttry {\n\t\t\treturn { ...result, output: await tool.execute(call.input, signal), isError: false };\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\treturn { ...result, output: describeFailure(error), isError: true };\n\t\t}\n\t}\n\n\t/** One at a time and in order, so their effects and their narration never interleave. */\n\tprivate async runTools(\n\t\tcalls: ToolCallPart[],\n\t\tsignal: AbortSignal,\n\t\tcallback: Callback,\n\t): Promise<Message> {\n\t\tconst results: MessagePart[] = [];\n\t\tfor (const call of calls) {\n\t\t\t// A call before this one may have ignored the cancellation; this one must not start,\n\t\t\t// nor be announced as running.\n\t\t\tsignal.throwIfAborted();\n\t\t\t// Announced only as its turn comes, so a consumer never shows a call as running early.\n\t\t\tnarrate(call, callback);\n\t\t\tconst result = await this.runTool(call, signal);\n\t\t\tnarrate(result, callback);\n\t\t\tresults.push(result);\n\t\t}\n\n\t\treturn { role: 'tool', content: results };\n\t}\n\n\tasync run(prompt: string, signal: AbortSignal, callback: Callback): Promise<AgentResponse> {\n\t\tconst start = Date.now();\n\t\tconst runMessages: Message[] = [{ role: 'user', content: [{ type: 'text', text: prompt }] }];\n\t\tlet tokens: Tokens | undefined;\n\t\t// Once one call went unreported the run's total is unknown, however many report later.\n\t\tlet unreported = false;\n\t\tlet toolRan = false;\n\n\t\ttry {\n\t\t\tfor (let step = 1; step <= this.maxSteps; step++) {\n\t\t\t\tconst response = await this.llmCall(runMessages, signal);\n\t\t\t\ttokens = addTokens(tokens, response.usage);\n\t\t\t\tunreported ||= !response.usage;\n\t\t\t\trunMessages.push(response.message);\n\n\t\t\t\t// Tool calls are narrated as they start, in `runTools`, and never if they do not run.\n\t\t\t\tfor (const part of response.message.content) {\n\t\t\t\t\tif (part.type !== 'toolCall') narrate(part, callback);\n\t\t\t\t}\n\n\t\t\t\tconst calls = response.message.content.filter(part => part.type === 'toolCall');\n\t\t\t\tif (calls.length === 0) {\n\t\t\t\t\t// Recorded only now, so a failed run leaves no tool call without its result.\n\t\t\t\t\tthis.conversation.addRun(runMessages);\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\t// Only the final answer: text from earlier steps was narrated as it came.\n\t\t\t\t\t\tresponse: response.message.content\n\t\t\t\t\t\t\t.filter(part => part.type === 'text')\n\t\t\t\t\t\t\t.map(part => part.text)\n\t\t\t\t\t\t\t.join('\\n'),\n\t\t\t\t\t\ttokens: unreported ? undefined : tokens,\n\t\t\t\t\t\tduration: (Date.now() - start) / 1000,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// On the last step no call is left to send the results to, so the tools do not run.\n\t\t\t\tif (step < this.maxSteps) {\n\t\t\t\t\t// Only a call to a tool the agent has reaches `execute`; a missing one has no\n\t\t\t\t\t// effect a retry could repeat.\n\t\t\t\t\ttoolRan ||= calls.some(call => this.tools.has(call.name));\n\t\t\t\t\trunMessages.push(await this.runTools(calls, signal, callback));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthrow new UnrecoverableError('The LLM did not finish within its step limit', {\n\t\t\t\tcause: `The model was still calling tools after ${String(this.maxSteps)} steps.`,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlet failure = error;\n\t\t\tif (toolRan && error instanceof RecoverableError) {\n\t\t\t\t// RetryingAgent would run the whole prompt again, and with it every tool this run\n\t\t\t\t// already executed, so a failure after one ran must not invite a retry.\n\t\t\t\tconst unrecoverable = new UnrecoverableError(error.message, {\n\t\t\t\t\tcause: error.cause,\n\t\t\t\t\ttokens: error.tokens,\n\t\t\t\t\tusageUnreported: error.usageUnreported,\n\t\t\t\t});\n\t\t\t\tunrecoverable.stack = error.stack;\n\t\t\t\tfailure = unrecoverable;\n\t\t\t}\n\n\t\t\t// The run spent the tokens of every step before the one that failed.\n\t\t\tthrow withSpentTokens(failure, tokens, unreported);\n\t\t}\n\t}\n}\n"]}
@@ -8,10 +8,11 @@ export declare class Conversation {
8
8
  private readonly messages;
9
9
  constructor(messages?: Message[]);
10
10
  /**
11
- * Records a prompt and its answer together, once the call succeeded. A failed call
12
- * records nothing, so a retry of the same prompt cannot appear twice.
11
+ * Records a whole run at once, once it succeeded: the prompt, every step the model took and
12
+ * every tool result. A failed run records nothing, so a retry cannot repeat its prompt, and
13
+ * no tool call is ever kept without its result.
13
14
  */
14
- addExchange(prompt: Message, answer: Message): void;
15
+ addRun(messages: Message[]): void;
15
16
  /**
16
17
  * A deep copy, like everything the conversation takes in: a client that rewrites the
17
18
  * messages it was sent cannot change what the next turn sends.
@@ -9,11 +9,12 @@ export class Conversation {
9
9
  this.messages = structuredClone(messages);
10
10
  }
11
11
  /**
12
- * Records a prompt and its answer together, once the call succeeded. A failed call
13
- * records nothing, so a retry of the same prompt cannot appear twice.
12
+ * Records a whole run at once, once it succeeded: the prompt, every step the model took and
13
+ * every tool result. A failed run records nothing, so a retry cannot repeat its prompt, and
14
+ * no tool call is ever kept without its result.
14
15
  */
15
- addExchange(prompt, answer) {
16
- this.messages.push(structuredClone(prompt), structuredClone(answer));
16
+ addRun(messages) {
17
+ this.messages.push(...structuredClone(messages));
17
18
  }
18
19
  /**
19
20
  * A deep copy, like everything the conversation takes in: a client that rewrites the
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.js","sourceRoot":"","sources":["../../../src/llm/domain/conversation.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,YAAY;IACP,QAAQ,CAAY;IAErC,YAAY,WAAsB,EAAE;QACnC,IAAI,CAAC,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,MAAe,EAAE,MAAe;QAC3C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,CAAC;IAED;;;OAGG;IACH,UAAU;QACT,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;CACD","sourcesContent":["import type { Message } from './message.ts';\n\n/**\n * What one agent remembers across its runs. The LLM never sees this object, only the\n * context built from it, so how the context is derived (compaction, later) can change\n * without touching either side.\n */\nexport class Conversation {\n\tprivate readonly messages: Message[];\n\n\tconstructor(messages: Message[] = []) {\n\t\tthis.messages = structuredClone(messages);\n\t}\n\n\t/**\n\t * Records a prompt and its answer together, once the call succeeded. A failed call\n\t * records nothing, so a retry of the same prompt cannot appear twice.\n\t */\n\taddExchange(prompt: Message, answer: Message): void {\n\t\tthis.messages.push(structuredClone(prompt), structuredClone(answer));\n\t}\n\n\t/**\n\t * A deep copy, like everything the conversation takes in: a client that rewrites the\n\t * messages it was sent cannot change what the next turn sends.\n\t */\n\tgetContext(): Message[] {\n\t\treturn structuredClone(this.messages);\n\t}\n}\n"]}
1
+ {"version":3,"file":"conversation.js","sourceRoot":"","sources":["../../../src/llm/domain/conversation.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,YAAY;IACP,QAAQ,CAAY;IAErC,YAAY,WAAsB,EAAE;QACnC,IAAI,CAAC,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,QAAmB;QACzB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClD,CAAC;IAED;;;OAGG;IACH,UAAU;QACT,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;CACD","sourcesContent":["import type { Message } from './message.ts';\n\n/**\n * What one agent remembers across its runs. The LLM never sees this object, only the\n * context built from it, so how the context is derived (compaction, later) can change\n * without touching either side.\n */\nexport class Conversation {\n\tprivate readonly messages: Message[];\n\n\tconstructor(messages: Message[] = []) {\n\t\tthis.messages = structuredClone(messages);\n\t}\n\n\t/**\n\t * Records a whole run at once, once it succeeded: the prompt, every step the model took and\n\t * every tool result. A failed run records nothing, so a retry cannot repeat its prompt, and\n\t * no tool call is ever kept without its result.\n\t */\n\taddRun(messages: Message[]): void {\n\t\tthis.messages.push(...structuredClone(messages));\n\t}\n\n\t/**\n\t * A deep copy, like everything the conversation takes in: a client that rewrites the\n\t * messages it was sent cannot change what the next turn sends.\n\t */\n\tgetContext(): Message[] {\n\t\treturn structuredClone(this.messages);\n\t}\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { Tokens } from '#src/shared/domain/tokens';
2
2
  import type { Message } from './message.ts';
3
+ import type { ToolDefinition } from './tool.ts';
3
4
  export type StopReason = 'completed' | 'truncated' | 'refused';
4
5
  export interface LLMResponse {
5
6
  message: Message & {
@@ -14,5 +15,8 @@ export interface LLMResponse {
14
15
  stopReason: StopReason;
15
16
  }
16
17
  export interface LLMClient {
17
- send(context: Message[], signal: AbortSignal): Promise<LLMResponse>;
18
+ send({ context, tools }: {
19
+ context: Message[];
20
+ tools: ToolDefinition[];
21
+ }, signal: AbortSignal): Promise<LLMResponse>;
18
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../../src/llm/domain/llm.ts"],"names":[],"mappings":"","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Message } from './message.ts';\n\nexport type StopReason = 'completed' | 'truncated' | 'refused';\n\nexport interface LLMResponse {\n\tmessage: Message & { role: 'assistant' };\n\t/**\n\t * Missing when the provider did not report it. The call was made and may have been billed,\n\t * so the agent keeps the answer and only its tokens become unknown; a failure raised from\n\t * such a response is marked `usageUnreported`.\n\t */\n\tusage: Tokens | undefined;\n\tstopReason: StopReason;\n}\n\nexport interface LLMClient {\n\tsend(context: Message[], signal: AbortSignal): Promise<LLMResponse>;\n}\n"]}
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../../src/llm/domain/llm.ts"],"names":[],"mappings":"","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Message } from './message.ts';\nimport type { ToolDefinition } from './tool.ts';\n\nexport type StopReason = 'completed' | 'truncated' | 'refused';\n\nexport interface LLMResponse {\n\tmessage: Message & { role: 'assistant' };\n\t/**\n\t * Missing when the provider did not report it. The call was made and may have been billed,\n\t * so the agent keeps the answer and only its tokens become unknown; a failure raised from\n\t * such a response is marked `usageUnreported`.\n\t */\n\tusage: Tokens | undefined;\n\tstopReason: StopReason;\n}\n\nexport interface LLMClient {\n\tsend(\n\t\t{ context, tools }: { context: Message[]; tools: ToolDefinition[] },\n\t\tsignal: AbortSignal,\n\t): Promise<LLMResponse>;\n}\n"]}
@@ -1,11 +1,37 @@
1
- export type MessagePart = {
1
+ export type MessagePart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart | ProviderDataPart;
2
+ export interface TextPart {
2
3
  type: 'text';
3
4
  text: string;
4
- } | {
5
+ }
6
+ export interface ReasoningPart {
5
7
  type: 'reasoning';
6
8
  text: string;
7
- };
9
+ }
10
+ export interface ToolCallPart {
11
+ id: string;
12
+ type: 'toolCall';
13
+ name: string;
14
+ input: unknown;
15
+ }
16
+ export interface ToolResultPart {
17
+ callId: string;
18
+ type: 'toolResult';
19
+ name: string;
20
+ output: string;
21
+ isError: boolean;
22
+ }
23
+ /**
24
+ * A block only the client that produced it can read back, such as signed or encrypted
25
+ * reasoning, kept whole so that client can send it again on the next call. `source` is the
26
+ * client's own label and the domain gives it no meaning: every other client leaves the part out.
27
+ * `data` must stay plain JSON, since the conversation is copied and will be persisted.
28
+ */
29
+ export interface ProviderDataPart {
30
+ type: 'providerData';
31
+ source: string;
32
+ data: unknown;
33
+ }
8
34
  export interface Message {
9
- role: 'user' | 'assistant';
35
+ role: 'user' | 'assistant' | 'tool';
10
36
  content: MessagePart[];
11
37
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message.js","sourceRoot":"","sources":["../../../src/llm/domain/message.ts"],"names":[],"mappings":"","sourcesContent":["export type MessagePart = { type: 'text'; text: string } | { type: 'reasoning'; text: string };\n\nexport interface Message {\n\trole: 'user' | 'assistant';\n\tcontent: MessagePart[];\n}\n"]}
1
+ {"version":3,"file":"message.js","sourceRoot":"","sources":["../../../src/llm/domain/message.ts"],"names":[],"mappings":"","sourcesContent":["export type MessagePart =\n\tTextPart | ReasoningPart | ToolCallPart | ToolResultPart | ProviderDataPart;\n\nexport interface TextPart {\n\ttype: 'text';\n\ttext: string;\n}\n\nexport interface ReasoningPart {\n\ttype: 'reasoning';\n\ttext: string;\n}\n\nexport interface ToolCallPart {\n\tid: string;\n\ttype: 'toolCall';\n\tname: string;\n\tinput: unknown;\n}\n\nexport interface ToolResultPart {\n\tcallId: string;\n\ttype: 'toolResult';\n\tname: string;\n\toutput: string;\n\tisError: boolean;\n}\n\n/**\n * A block only the client that produced it can read back, such as signed or encrypted\n * reasoning, kept whole so that client can send it again on the next call. `source` is the\n * client's own label and the domain gives it no meaning: every other client leaves the part out.\n * `data` must stay plain JSON, since the conversation is copied and will be persisted.\n */\nexport interface ProviderDataPart {\n\ttype: 'providerData';\n\tsource: string;\n\tdata: unknown;\n}\n\nexport interface Message {\n\trole: 'user' | 'assistant' | 'tool';\n\tcontent: MessagePart[];\n}\n"]}
@@ -0,0 +1,10 @@
1
+ export interface JSONSchema {
2
+ type: 'object';
3
+ [keyword: string]: unknown;
4
+ }
5
+ /** What a client describes to the model. Running the tool is not the model's concern. */
6
+ export interface ToolDefinition {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: JSONSchema;
10
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../../../src/llm/domain/tool.ts"],"names":[],"mappings":"","sourcesContent":["export interface JSONSchema {\n\ttype: 'object';\n\t[keyword: string]: unknown;\n}\n\n/** What a client describes to the model. Running the tool is not the model's concern. */\nexport interface ToolDefinition {\n\tname: string;\n\tdescription: string;\n\tinputSchema: JSONSchema;\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 claudeLLMModels: readonly ["claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5"];
5
6
  export type ClaudeLLMModel = (typeof claudeLLMModels)[number];
6
7
  /**
@@ -17,5 +18,8 @@ export declare class ClaudeLLMClient implements LLMClient {
17
18
  systemPrompt: string;
18
19
  logger: ILogger;
19
20
  });
20
- send(context: Message[], signal: AbortSignal): Promise<LLMResponse>;
21
+ send({ context, tools }: {
22
+ context: Message[];
23
+ tools: ToolDefinition[];
24
+ }, signal: AbortSignal): Promise<LLMResponse>;
21
25
  }