@noetaris/harness-anthropic 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,28 @@ import { ObserverAware, Observer, StepContext } from '@noetaris/harness';
5
5
  interface ClaudeOptions {
6
6
  /** Anthropic API key. Defaults to the `ANTHROPIC_API_KEY` environment variable. */
7
7
  apiKey?: string;
8
+ /**
9
+ * Sampling temperature in [0, 1]. Higher values produce more varied output.
10
+ * When absent, the provider default applies.
11
+ */
12
+ temperature?: number;
13
+ /**
14
+ * Maximum number of tokens to generate.
15
+ * Defaults to `4096` when absent.
16
+ */
17
+ maxTokens?: number;
18
+ /**
19
+ * Top-p nucleus sampling probability. When absent, the provider default applies.
20
+ */
21
+ topP?: number;
22
+ /**
23
+ * Extended thinking configuration for supported models.
24
+ * When present, enables extended thinking mode with the specified token budget.
25
+ */
26
+ thinking?: {
27
+ type: 'enabled';
28
+ budgetTokens: number;
29
+ };
8
30
  }
9
31
  /**
10
32
  * {@link LLM} adapter for the Anthropic Messages API.
@@ -22,11 +44,12 @@ interface ClaudeOptions {
22
44
  declare class Claude implements LLM, ObserverAware {
23
45
  private readonly client;
24
46
  private readonly model;
47
+ private readonly options;
25
48
  private observer;
26
49
  private stepContext;
27
50
  /**
28
51
  * @param model - Anthropic model ID, e.g. `'claude-3-5-haiku-20241022'`.
29
- * @param options - Optional API key override.
52
+ * @param options - Optional configuration including API key and generation params.
30
53
  */
31
54
  constructor(model: string, options?: ClaudeOptions);
32
55
  bindObserver(observer: Observer): void;
package/dist/index.js CHANGED
@@ -76,14 +76,16 @@ var ZEROED_STEP_CONTEXT = { agentId: "", sessionId: "", stepName: "" };
76
76
  var Claude = class {
77
77
  client;
78
78
  model;
79
+ options;
79
80
  observer = {};
80
81
  stepContext = ZEROED_STEP_CONTEXT;
81
82
  /**
82
83
  * @param model - Anthropic model ID, e.g. `'claude-3-5-haiku-20241022'`.
83
- * @param options - Optional API key override.
84
+ * @param options - Optional configuration including API key and generation params.
84
85
  */
85
86
  constructor(model, options) {
86
87
  this.model = model;
88
+ this.options = options;
87
89
  this.client = new Anthropic({ apiKey: options?.apiKey });
88
90
  }
89
91
  bindObserver(observer) {
@@ -95,10 +97,15 @@ var Claude = class {
95
97
  async invoke(messages, options) {
96
98
  const translatedMessages = translateMessages(messages);
97
99
  const tools = options?.tools;
100
+ const requestEvent = { modelId: this.model, providerName: "anthropic" };
101
+ this.observer.onEvent?.(this.stepContext, "llm.request", requestEvent);
98
102
  const response = await this.client.messages.create({
99
103
  model: this.model,
100
104
  messages: translatedMessages,
101
- max_tokens: 4096,
105
+ max_tokens: this.options?.maxTokens ?? 4096,
106
+ ...this.options?.temperature !== void 0 ? { temperature: this.options.temperature } : {},
107
+ ...this.options?.topP !== void 0 ? { top_p: this.options.topP } : {},
108
+ ...this.options?.thinking !== void 0 ? { thinking: { type: this.options.thinking.type, budget_tokens: this.options.thinking.budgetTokens } } : {},
102
109
  ...tools !== void 0 ? { tools: translateTools(tools) } : {}
103
110
  });
104
111
  const result = normalizeResponse(response);
@@ -149,6 +156,8 @@ var MockClaude = class {
149
156
  }
150
157
  async invoke(messages, options) {
151
158
  void options;
159
+ const requestEvent = { modelId: "mock", providerName: "mock" };
160
+ this.observer.onEvent?.(this.stepContext, "llm.request", requestEvent);
152
161
  if (this.queue.length === 0) {
153
162
  throw new MockClaudeEmptyQueueError();
154
163
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/claude.ts","../src/mock-claude.ts"],"sourcesContent":["import type { LLM, Message, Tool, ToolCall, LLMResponse, LLMUsageEvent } from '@noetaris/harness-types'\nimport type { ObserverAware, Observer, StepContext } from '@noetaris/harness'\nimport Anthropic from '@anthropic-ai/sdk'\nimport type { Tool as AnthropicSDKTool } from '@anthropic-ai/sdk/resources/messages/messages.js'\n\n/** Options for {@link Claude}. */\nexport interface ClaudeOptions {\n /** Anthropic API key. Defaults to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string\n}\n\ntype AnthropicContentBlock =\n | { type: 'text'; text: string }\n | { type: 'tool_use'; id: string; name: string; input: unknown }\n | { type: 'tool_result'; tool_use_id: string; content: string }\n\ntype AnthropicMessage = {\n role: 'user' | 'assistant'\n content: string | AnthropicContentBlock[]\n}\n\n// Use SDK's Tool type for the translated tools array passed to client.messages.create\ntype AnthropicTool = AnthropicSDKTool\n\nfunction translateMessages(messages: Message[]): AnthropicMessage[] {\n const result: AnthropicMessage[] = []\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n result.push({ role: 'user', content: msg.content })\n } else if (msg.role === 'assistant') {\n if (msg.toolCalls && msg.toolCalls.length > 0) {\n const blocks: AnthropicContentBlock[] = []\n if (msg.content) {\n blocks.push({ type: 'text', text: msg.content })\n }\n for (const tc of msg.toolCalls) {\n blocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input })\n }\n result.push({ role: 'assistant', content: blocks })\n } else {\n result.push({ role: 'assistant', content: msg.content ?? '' })\n }\n } else if (msg.role === 'tool') {\n const toolResultBlock: AnthropicContentBlock = {\n type: 'tool_result',\n tool_use_id: msg.toolCallId,\n content: msg.content,\n }\n\n const last = result[result.length - 1]\n if (last !== undefined && last.role === 'user') {\n // last message is a user message — merge into it\n if (typeof last.content === 'string') {\n // convert string content to array with text block + tool_result\n last.content = [\n { type: 'text', text: last.content },\n toolResultBlock,\n ]\n } else {\n last.content.push(toolResultBlock)\n }\n } else {\n // no preceding user message — wrap in a new user turn\n result.push({ role: 'user', content: [toolResultBlock] })\n }\n }\n }\n\n return result\n}\n\nfunction translateTools(tools: Tool[]): AnthropicTool[] {\n return tools.map((t) => ({\n name: t.name,\n description: t.description,\n // as: harness Tool.inputSchema is Record<string,unknown>; SDK requires InputSchema with type:'object'\n // which is always present at runtime — cannot be statically verified from the generic type\n input_schema: t.inputSchema as AnthropicSDKTool['input_schema'],\n }))\n}\n\nfunction mapStopReason(stopReason: string): LLMResponse['stopReason'] {\n if (stopReason === 'end_turn') return 'end'\n if (stopReason === 'tool_use') return 'tool_use'\n if (stopReason === 'max_tokens') return 'max_tokens'\n return 'end'\n}\n\nfunction normalizeResponse(response: Anthropic.Message): LLMResponse {\n let text = ''\n const toolCalls: ToolCall[] = []\n\n for (const block of response.content) {\n if (block.type === 'text') {\n text += block.text\n } else if (block.type === 'tool_use') {\n toolCalls.push({ id: block.id, name: block.name, input: block.input })\n }\n }\n\n return {\n text,\n toolCalls,\n stopReason: mapStopReason(response.stop_reason ?? ''),\n }\n}\n\nconst ZEROED_STEP_CONTEXT: StepContext = { agentId: '', sessionId: '', stepName: '' }\n\n/**\n * {@link LLM} adapter for the Anthropic Messages API.\n *\n * Implements {@link ObserverAware} — when an observer is bound the adapter\n * emits an `'llm.response'` event carrying an `LLMUsageEvent` payload after\n * each successful invocation.\n *\n * @example\n * ```ts\n * const llm = new Claude('claude-3-5-haiku-20241022')\n * const response = await llm.invoke(messages)\n * ```\n */\nexport class Claude implements LLM, ObserverAware {\n private readonly client: Anthropic\n private readonly model: string\n private observer: Observer = {}\n private stepContext: StepContext = ZEROED_STEP_CONTEXT\n\n /**\n * @param model - Anthropic model ID, e.g. `'claude-3-5-haiku-20241022'`.\n * @param options - Optional API key override.\n */\n constructor(model: string, options?: ClaudeOptions) {\n this.model = model\n this.client = new Anthropic({ apiKey: options?.apiKey })\n }\n\n bindObserver(observer: Observer): void {\n this.observer = observer\n }\n\n setStepContext(ctx: StepContext): void {\n this.stepContext = ctx\n }\n\n async invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> {\n const translatedMessages = translateMessages(messages)\n const tools = options?.tools\n\n const response = await this.client.messages.create({\n model: this.model,\n messages: translatedMessages,\n max_tokens: 4096,\n ...(tools !== undefined ? { tools: translateTools(tools) } : {}),\n })\n\n const result = normalizeResponse(response)\n\n const event: LLMUsageEvent = {\n tokens: { input: response.usage.input_tokens, output: response.usage.output_tokens },\n modelId: this.model,\n stopReason: result.stopReason,\n providerName: 'anthropic',\n }\n this.observer.onEvent?.(this.stepContext, 'llm.response', event)\n\n return result\n }\n}\n","import type { LLM, Message, Tool, LLMResponse, LLMUsageEvent } from '@noetaris/harness-types'\nimport type { ObserverAware, Observer, StepContext } from '@noetaris/harness'\n\n/** Thrown by {@link MockClaude} when `invoke` is called with no responses queued. */\nexport class MockClaudeEmptyQueueError extends Error {\n constructor() {\n super('MockClaude has no responses configured — call new MockClaude(response) or enqueue(response) before invoke')\n this.name = 'MockClaudeEmptyQueueError'\n }\n}\n\nconst ZEROED_STEP_CONTEXT: StepContext = { agentId: '', sessionId: '', stepName: '' }\n\n/**\n * In-memory test double for {@link Claude}.\n *\n * Pre-load one or more {@link LLMResponse} objects; each `invoke()` call\n * dequeues the next response. The last response is **sticky** — once only\n * one remains it repeats indefinitely rather than throwing.\n *\n * `lastMessages` is populated after each `invoke()` call, allowing assertions\n * on the conversation history passed to the adapter.\n *\n * @example\n * ```ts\n * const llm = new MockClaude({ text: 'hello', toolCalls: [], stopReason: 'end' })\n * ```\n */\nexport class MockClaude implements LLM, ObserverAware {\n /** The message list from the most recent `invoke()` call. */\n lastMessages: Message[] = []\n\n private queue: LLMResponse[] = []\n private observer: Observer = {}\n private stepContext: StepContext = ZEROED_STEP_CONTEXT\n\n /**\n * @param responses - One or more responses to queue up front. May also be\n * added later via {@link enqueue}.\n */\n constructor(responses?: LLMResponse | LLMResponse[]) {\n if (responses !== undefined) {\n this.enqueue(responses)\n }\n }\n\n /** Add one or more responses to the end of the queue. */\n enqueue(response: LLMResponse | LLMResponse[]): void {\n const items = Array.isArray(response) ? response : [response]\n this.queue.push(...items)\n }\n\n bindObserver(observer: Observer): void {\n this.observer = observer\n }\n\n setStepContext(ctx: StepContext): void {\n this.stepContext = ctx\n }\n\n async invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> {\n void options\n if (this.queue.length === 0) {\n throw new MockClaudeEmptyQueueError()\n }\n\n // sticky-last: dequeue only when more than one element remains\n const response: LLMResponse = this.queue.length > 1\n ? (this.queue.shift() as LLMResponse) // as: shift() on non-empty array is always defined; length > 1 is checked above\n : (this.queue[0] as LLMResponse) // as: queue.length === 1 guaranteed by the empty check; index 0 is always defined\n\n this.lastMessages = messages\n\n const event: LLMUsageEvent = {\n tokens: { input: 0, output: 0 },\n modelId: 'mock',\n stopReason: response.stopReason,\n providerName: 'mock',\n }\n this.observer.onEvent?.(this.stepContext, 'llm.response', event)\n\n return response\n }\n}\n"],"mappings":"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","names":["ZEROED_STEP_CONTEXT"]}
1
+ {"version":3,"sources":["../src/claude.ts","../src/mock-claude.ts"],"sourcesContent":["import type { LLM, Message, Tool, ToolCall, LLMResponse, LLMUsageEvent, LLMRequestEvent } from '@noetaris/harness-types'\nimport type { ObserverAware, Observer, StepContext } from '@noetaris/harness'\nimport Anthropic from '@anthropic-ai/sdk'\nimport type { Tool as AnthropicSDKTool } from '@anthropic-ai/sdk/resources/messages/messages.js'\n\n/** Options for {@link Claude}. */\nexport interface ClaudeOptions {\n /** Anthropic API key. Defaults to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string\n /**\n * Sampling temperature in [0, 1]. Higher values produce more varied output.\n * When absent, the provider default applies.\n */\n temperature?: number\n /**\n * Maximum number of tokens to generate.\n * Defaults to `4096` when absent.\n */\n maxTokens?: number\n /**\n * Top-p nucleus sampling probability. When absent, the provider default applies.\n */\n topP?: number\n /**\n * Extended thinking configuration for supported models.\n * When present, enables extended thinking mode with the specified token budget.\n */\n thinking?: {\n type: 'enabled'\n budgetTokens: number\n }\n}\n\ntype AnthropicContentBlock =\n | { type: 'text'; text: string }\n | { type: 'tool_use'; id: string; name: string; input: unknown }\n | { type: 'tool_result'; tool_use_id: string; content: string }\n\ntype AnthropicMessage = {\n role: 'user' | 'assistant'\n content: string | AnthropicContentBlock[]\n}\n\n// Use SDK's Tool type for the translated tools array passed to client.messages.create\ntype AnthropicTool = AnthropicSDKTool\n\nfunction translateMessages(messages: Message[]): AnthropicMessage[] {\n const result: AnthropicMessage[] = []\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n result.push({ role: 'user', content: msg.content })\n } else if (msg.role === 'assistant') {\n if (msg.toolCalls && msg.toolCalls.length > 0) {\n const blocks: AnthropicContentBlock[] = []\n if (msg.content) {\n blocks.push({ type: 'text', text: msg.content })\n }\n for (const tc of msg.toolCalls) {\n blocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input })\n }\n result.push({ role: 'assistant', content: blocks })\n } else {\n result.push({ role: 'assistant', content: msg.content ?? '' })\n }\n } else if (msg.role === 'tool') {\n const toolResultBlock: AnthropicContentBlock = {\n type: 'tool_result',\n tool_use_id: msg.toolCallId,\n content: msg.content,\n }\n\n const last = result[result.length - 1]\n if (last !== undefined && last.role === 'user') {\n // last message is a user message — merge into it\n if (typeof last.content === 'string') {\n // convert string content to array with text block + tool_result\n last.content = [\n { type: 'text', text: last.content },\n toolResultBlock,\n ]\n } else {\n last.content.push(toolResultBlock)\n }\n } else {\n // no preceding user message — wrap in a new user turn\n result.push({ role: 'user', content: [toolResultBlock] })\n }\n }\n }\n\n return result\n}\n\nfunction translateTools(tools: Tool[]): AnthropicTool[] {\n return tools.map((t) => ({\n name: t.name,\n description: t.description,\n // as: harness Tool.inputSchema is Record<string,unknown>; SDK requires InputSchema with type:'object'\n // which is always present at runtime — cannot be statically verified from the generic type\n input_schema: t.inputSchema as AnthropicSDKTool['input_schema'],\n }))\n}\n\nfunction mapStopReason(stopReason: string): LLMResponse['stopReason'] {\n if (stopReason === 'end_turn') return 'end'\n if (stopReason === 'tool_use') return 'tool_use'\n if (stopReason === 'max_tokens') return 'max_tokens'\n return 'end'\n}\n\nfunction normalizeResponse(response: Anthropic.Message): LLMResponse {\n let text = ''\n const toolCalls: ToolCall[] = []\n\n for (const block of response.content) {\n if (block.type === 'text') {\n text += block.text\n } else if (block.type === 'tool_use') {\n toolCalls.push({ id: block.id, name: block.name, input: block.input })\n }\n }\n\n return {\n text,\n toolCalls,\n stopReason: mapStopReason(response.stop_reason ?? ''),\n }\n}\n\nconst ZEROED_STEP_CONTEXT: StepContext = { agentId: '', sessionId: '', stepName: '' }\n\n/**\n * {@link LLM} adapter for the Anthropic Messages API.\n *\n * Implements {@link ObserverAware} — when an observer is bound the adapter\n * emits an `'llm.response'` event carrying an `LLMUsageEvent` payload after\n * each successful invocation.\n *\n * @example\n * ```ts\n * const llm = new Claude('claude-3-5-haiku-20241022')\n * const response = await llm.invoke(messages)\n * ```\n */\nexport class Claude implements LLM, ObserverAware {\n private readonly client: Anthropic\n private readonly model: string\n private readonly options: ClaudeOptions | undefined\n private observer: Observer = {}\n private stepContext: StepContext = ZEROED_STEP_CONTEXT\n\n /**\n * @param model - Anthropic model ID, e.g. `'claude-3-5-haiku-20241022'`.\n * @param options - Optional configuration including API key and generation params.\n */\n constructor(model: string, options?: ClaudeOptions) {\n this.model = model\n this.options = options\n this.client = new Anthropic({ apiKey: options?.apiKey })\n }\n\n bindObserver(observer: Observer): void {\n this.observer = observer\n }\n\n setStepContext(ctx: StepContext): void {\n this.stepContext = ctx\n }\n\n async invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> {\n const translatedMessages = translateMessages(messages)\n const tools = options?.tools\n\n const requestEvent: LLMRequestEvent = { modelId: this.model, providerName: 'anthropic' }\n this.observer.onEvent?.(this.stepContext, 'llm.request', requestEvent)\n\n const response = await this.client.messages.create({\n model: this.model,\n messages: translatedMessages,\n max_tokens: this.options?.maxTokens ?? 4096,\n ...(this.options?.temperature !== undefined ? { temperature: this.options.temperature } : {}),\n ...(this.options?.topP !== undefined ? { top_p: this.options.topP } : {}),\n ...(this.options?.thinking !== undefined ? { thinking: { type: this.options.thinking.type, budget_tokens: this.options.thinking.budgetTokens } } : {}),\n ...(tools !== undefined ? { tools: translateTools(tools) } : {}),\n })\n\n const result = normalizeResponse(response)\n\n const event: LLMUsageEvent = {\n tokens: { input: response.usage.input_tokens, output: response.usage.output_tokens },\n modelId: this.model,\n stopReason: result.stopReason,\n providerName: 'anthropic',\n }\n this.observer.onEvent?.(this.stepContext, 'llm.response', event)\n\n return result\n }\n}\n","import type { LLM, Message, Tool, LLMResponse, LLMUsageEvent, LLMRequestEvent } from '@noetaris/harness-types'\nimport type { ObserverAware, Observer, StepContext } from '@noetaris/harness'\n\n/** Thrown by {@link MockClaude} when `invoke` is called with no responses queued. */\nexport class MockClaudeEmptyQueueError extends Error {\n constructor() {\n super('MockClaude has no responses configured — call new MockClaude(response) or enqueue(response) before invoke')\n this.name = 'MockClaudeEmptyQueueError'\n }\n}\n\nconst ZEROED_STEP_CONTEXT: StepContext = { agentId: '', sessionId: '', stepName: '' }\n\n/**\n * In-memory test double for {@link Claude}.\n *\n * Pre-load one or more {@link LLMResponse} objects; each `invoke()` call\n * dequeues the next response. The last response is **sticky** — once only\n * one remains it repeats indefinitely rather than throwing.\n *\n * `lastMessages` is populated after each `invoke()` call, allowing assertions\n * on the conversation history passed to the adapter.\n *\n * @example\n * ```ts\n * const llm = new MockClaude({ text: 'hello', toolCalls: [], stopReason: 'end' })\n * ```\n */\nexport class MockClaude implements LLM, ObserverAware {\n /** The message list from the most recent `invoke()` call. */\n lastMessages: Message[] = []\n\n private queue: LLMResponse[] = []\n private observer: Observer = {}\n private stepContext: StepContext = ZEROED_STEP_CONTEXT\n\n /**\n * @param responses - One or more responses to queue up front. May also be\n * added later via {@link enqueue}.\n */\n constructor(responses?: LLMResponse | LLMResponse[]) {\n if (responses !== undefined) {\n this.enqueue(responses)\n }\n }\n\n /** Add one or more responses to the end of the queue. */\n enqueue(response: LLMResponse | LLMResponse[]): void {\n const items = Array.isArray(response) ? response : [response]\n this.queue.push(...items)\n }\n\n bindObserver(observer: Observer): void {\n this.observer = observer\n }\n\n setStepContext(ctx: StepContext): void {\n this.stepContext = ctx\n }\n\n async invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> {\n void options\n const requestEvent: LLMRequestEvent = { modelId: 'mock', providerName: 'mock' }\n this.observer.onEvent?.(this.stepContext, 'llm.request', requestEvent)\n\n if (this.queue.length === 0) {\n throw new MockClaudeEmptyQueueError()\n }\n\n // sticky-last: dequeue only when more than one element remains\n const response: LLMResponse = this.queue.length > 1\n ? (this.queue.shift() as LLMResponse) // as: shift() on non-empty array is always defined; length > 1 is checked above\n : (this.queue[0] as LLMResponse) // as: queue.length === 1 guaranteed by the empty check; index 0 is always defined\n\n this.lastMessages = messages\n\n const event: LLMUsageEvent = {\n tokens: { input: 0, output: 0 },\n modelId: 'mock',\n stopReason: response.stopReason,\n providerName: 'mock',\n }\n this.observer.onEvent?.(this.stepContext, 'llm.response', event)\n\n return response\n }\n}\n"],"mappings":"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","names":["ZEROED_STEP_CONTEXT"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetaris/harness-anthropic",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Anthropic Claude adapter for @noetaris/harness",
5
5
  "type": "module",
6
6
  "repository": {
@@ -43,8 +43,8 @@
43
43
  "@noetaris/harness-types": ">=0.1.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@noetaris/harness": "^0.3.0",
47
- "@noetaris/harness-types": "^0.2.0",
46
+ "@noetaris/harness": "^0.3.2",
47
+ "@noetaris/harness-types": "^0.3.0",
48
48
  "@types/node": "^25.8.0",
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^6.0.3",