@bluecopa/harness 0.1.0-snapshot.37 → 0.1.0-snapshot.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluecopa/harness",
3
- "version": "0.1.0-snapshot.37",
3
+ "version": "0.1.0-snapshot.38",
4
4
  "description": "Provider-agnostic TypeScript agent framework",
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { generateText, generateObject } from 'ai';
3
- import { anthropic } from '@ai-sdk/anthropic';
3
+ import { anthropic as defaultAnthropicProvider } from '@ai-sdk/anthropic';
4
+ import type { ModelFactory } from './types';
4
5
  import type { AgentMessage, ToolCallAction } from '../agent/types';
5
6
  import { getTextContent } from '../agent/types';
6
7
  import type { ToolProvider, ToolResult } from '../interfaces/tool-provider';
@@ -183,6 +184,7 @@ async function executeTool(
183
184
 
184
185
  export interface AgentRunnerConfig {
185
186
  model: string;
187
+ createModel?: ModelFactory;
186
188
  prompt: string;
187
189
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
190
  tools: Record<string, any>;
@@ -230,7 +232,6 @@ export class AgentRunner {
230
232
  const cachedSystem = [{
231
233
  role: 'system' as const,
232
234
  content: config.systemPrompt,
233
- providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
234
235
  }];
235
236
 
236
237
  for (let step = 0; step < config.maxSteps; step++) {
@@ -245,8 +246,7 @@ export class AgentRunner {
245
246
  const callLLM = async (effectiveSignal: AbortSignal) =>
246
247
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
248
  (generateText as any)({
248
- model: anthropic(config.model),
249
- thinking: { type: 'adaptive' },
249
+ model: (config.createModel ?? defaultAnthropicProvider)(config.model),
250
250
  tools: config.tools,
251
251
  toolChoice: 'auto',
252
252
  messages: toModelMessages(messages),
@@ -289,7 +289,7 @@ export class AgentRunner {
289
289
  ];
290
290
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
291
291
  const structured = await (generateObject as any)({
292
- model: anthropic(config.model),
292
+ model: (config.createModel ?? defaultAnthropicProvider)(config.model),
293
293
  schema: config.outputSchema,
294
294
  messages: toModelMessages(extractionMessages),
295
295
  system: config.systemPrompt,
@@ -400,6 +400,7 @@ export class AgentRunner {
400
400
  // ── createProcess factory ──
401
401
 
402
402
  export interface CreateProcessConfig {
403
+ createModel?: ModelFactory;
403
404
  toolProvider: ToolProvider;
404
405
  episodeStore: EpisodeStore;
405
406
  taskId: string;
@@ -449,7 +450,7 @@ export function createProcess(
449
450
 
450
451
  const outbox = createChannel<ProcessEvent>();
451
452
 
452
- const compressor = new EpisodeCompressor();
453
+ const compressor = new EpisodeCompressor(config.createModel);
453
454
  const runner = new AgentRunner();
454
455
 
455
456
  const process: Process = {
@@ -504,6 +505,7 @@ export function createProcess(
504
505
  outbox.push({ type: 'activity', activity });
505
506
  },
506
507
  ...pickDefined(config, [
508
+ 'createModel',
507
509
  'hookRunner',
508
510
  'permissionManager',
509
511
  'telemetry',
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { streamText } from 'ai';
3
- import { anthropic } from '@ai-sdk/anthropic';
3
+ import { anthropic as defaultAnthropicProvider } from '@ai-sdk/anthropic';
4
+ import type { ModelFactory } from './types';
4
5
  import type { AgentMessage, ToolCallAction } from '../agent/types';
5
6
  import { getTextContent } from '../agent/types';
6
7
  import type { Episode, AnyTool, ModelTier } from './arc-types';
@@ -83,9 +84,11 @@ export class ArcLoop {
83
84
  private readonly actionIndex = new Map<string, string>();
84
85
  private readonly processListeners: Promise<void>[] = [];
85
86
  private readonly skillResolver: SkillResolver | undefined;
87
+ private readonly createModel: ModelFactory;
86
88
 
87
89
  constructor(config: ArcLoopConfig) {
88
90
  this.config = config;
91
+ this.createModel = config.createModel ?? defaultAnthropicProvider;
89
92
  this.modelMap = { ...DEFAULT_MODEL_MAP, ...config.modelMap };
90
93
  this.orchestratorModel = resolveModel(config.model, this.modelMap, this.modelMap.strong);
91
94
  this.systemPrompt = config.systemPrompt ?? DEFAULT_ORCHESTRATOR_PROMPT;
@@ -93,7 +96,6 @@ export class ArcLoop {
93
96
  this.cachedSystem = [{
94
97
  role: 'system' as const,
95
98
  content: this.systemPrompt,
96
- providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
97
99
  }];
98
100
 
99
101
  this.tools = {
@@ -118,6 +120,7 @@ export class ArcLoop {
118
120
  episodeStore: config.episodeStore,
119
121
  memory: this.memory,
120
122
  taskId: config.taskId,
123
+ createModel: this.createModel,
121
124
  });
122
125
 
123
126
  this.resilience = config.resilience;
@@ -167,8 +170,7 @@ export class ArcLoop {
167
170
  const callLLM = async (effectiveSignal: AbortSignal) =>
168
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
172
  (streamText as any)({
170
- model: anthropic(this.orchestratorModel),
171
- thinking: { type: 'adaptive' },
173
+ model: this.createModel(this.orchestratorModel),
172
174
  tools: this.tools,
173
175
  toolChoice: 'auto',
174
176
  messages: toModelMessages(prepared.messages),
@@ -570,6 +572,7 @@ export class ArcLoop {
570
572
  episodeStore: this.config.episodeStore,
571
573
  taskId: this.config.taskId,
572
574
  sessionId: this.config.sessionId,
575
+ createModel: this.createModel,
573
576
  modelMap: this.modelMap,
574
577
  defaultModel,
575
578
  processMaxSteps: profile?.maxSteps ?? this.config.processMaxSteps ?? 20,
@@ -1,5 +1,6 @@
1
1
  import { generateText } from 'ai';
2
- import { anthropic } from '@ai-sdk/anthropic';
2
+ import { anthropic as defaultAnthropicProvider } from '@ai-sdk/anthropic';
3
+ import type { ModelFactory } from './types';
3
4
  import type { AgentMessage } from '../agent/types';
4
5
  import { getTextContent } from '../agent/types';
5
6
  import type { Episode, EpisodeStore } from './arc-types';
@@ -17,6 +18,7 @@ export interface ContextWindowConfig {
17
18
  episodeStore: EpisodeStore;
18
19
  memory: MemoryManager;
19
20
  taskId: string;
21
+ createModel?: ModelFactory;
20
22
  }
21
23
 
22
24
  export class ContextWindow {
@@ -249,7 +251,7 @@ export class ContextWindow {
249
251
 
250
252
  const fastModel = resolveModel('fast', DEFAULT_MODEL_MAP, DEFAULT_MODEL_MAP.fast);
251
253
  const result = await generateText({
252
- model: anthropic(fastModel),
254
+ model: (this.config.createModel ?? defaultAnthropicProvider)(fastModel),
253
255
  system: 'Summarize this conversation history into a concise context summary. Preserve key decisions, outcomes, and any information needed to continue the conversation. Keep under 500 words.',
254
256
  messages: [{ role: 'user', content: textToSummarize.slice(0, 8000) }],
255
257
  abortSignal: signal,
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { generateText } from 'ai';
3
- import { anthropic } from '@ai-sdk/anthropic';
3
+ import { anthropic as defaultAnthropicProvider } from '@ai-sdk/anthropic';
4
+ import type { ModelFactory } from './types';
4
5
  import type { AgentMessage } from '../agent/types';
5
6
  import { getTextContent } from '../agent/types';
6
7
  import type { Episode, EpisodeTrace } from './arc-types';
@@ -143,6 +144,12 @@ function extractArtifacts(messages: AgentMessage[]): EpisodeArtifact[] {
143
144
  // ── EpisodeCompressor class ──
144
145
 
145
146
  export class EpisodeCompressor {
147
+ private readonly createModel: ModelFactory;
148
+
149
+ constructor(createModel?: ModelFactory) {
150
+ this.createModel = createModel ?? defaultAnthropicProvider;
151
+ }
152
+
146
153
  compress(input: CompressInput): CompressOutput {
147
154
  const now = Date.now();
148
155
  const id = randomUUID();
@@ -200,7 +207,7 @@ export class EpisodeCompressor {
200
207
 
201
208
  const fastModel = resolveModel('fast', DEFAULT_MODEL_MAP, DEFAULT_MODEL_MAP.fast);
202
209
  const llmResult = await generateText({
203
- model: anthropic(fastModel),
210
+ model: this.createModel(fastModel),
204
211
  system: 'Summarize this agent conversation into a concise episode summary. Focus on: what was attempted, what tools were used, what files were changed, and the outcome. Keep it under 300 words.',
205
212
  messages: [{ role: 'user', content: conversationText.slice(0, 8000) }],
206
213
  abortSignal: signal,
package/src/arc/types.ts CHANGED
@@ -38,6 +38,12 @@ export interface EpisodeArtifact {
38
38
  content: string; // verbatim extracted content
39
39
  }
40
40
 
41
+ // ── Model factory ──
42
+
43
+ /** Creates an ai-sdk LanguageModel from a model ID string. Defaults to anthropic(). */
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ export type ModelFactory = (modelId: string) => any;
46
+
41
47
  // ── ArcLoop v2 Config ──
42
48
 
43
49
  export interface ArcLoopConfig {
@@ -46,7 +52,9 @@ export interface ArcLoopConfig {
46
52
  model?: string;
47
53
  /** Model tier mapping. Override to use different models for fast/medium/strong. */
48
54
  modelMap?: Record<import('./arc-types').ModelTier, string>;
49
- /** Anthropic API key */
55
+ /** Model factory creates an ai-sdk LanguageModel from a model ID. Defaults to anthropic(). */
56
+ createModel?: ModelFactory;
57
+ /** @deprecated Use createModel instead. Anthropic API key (set via ANTHROPIC_API_KEY env var). */
50
58
  apiKey?: string;
51
59
  /** Custom orchestrator system prompt */
52
60
  systemPrompt?: string;
@@ -1,5 +1,6 @@
1
1
  import { generateText, streamText, stepCountIs, tool, type Tool } from 'ai';
2
- import { anthropic } from '@ai-sdk/anthropic';
2
+ import { anthropic as defaultAnthropicProvider } from '@ai-sdk/anthropic';
3
+ import type { ModelFactory } from '../arc/types';
3
4
  import { z } from 'zod';
4
5
 
5
6
  import type { AgentAction, AgentMessage, AgentLoop, AgentStreamEvent, ToolCallAction, ToolBatchAction } from '../agent/types';
@@ -161,6 +162,7 @@ function toModelMessages(messages: AgentMessage[]): ModelMessage[] {
161
162
 
162
163
  export interface VercelAgentLoopConfig {
163
164
  model?: string;
165
+ createModel?: ModelFactory;
164
166
  systemPrompt?: string;
165
167
  apiKey?: string;
166
168
  /** Custom tool definitions. If provided, replaces built-in agentTools for LLM calls. */
@@ -169,8 +171,8 @@ export interface VercelAgentLoopConfig {
169
171
 
170
172
  export class VercelAgentLoop implements AgentLoop {
171
173
  private readonly model: string;
174
+ private readonly createModel: ModelFactory;
172
175
  private readonly systemPrompt: string;
173
- /** System prompt with Anthropic cache_control for prompt caching. */
174
176
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
177
  private readonly cachedSystem: any;
176
178
  private readonly tools: Record<string, AnyTool>;
@@ -178,6 +180,7 @@ export class VercelAgentLoop implements AgentLoop {
178
180
 
179
181
  constructor(config: VercelAgentLoopConfig = {}) {
180
182
  this.model = config.model ?? process.env.HARNESS_MODEL ?? 'claude-sonnet-4-5';
183
+ this.createModel = config.createModel ?? defaultAnthropicProvider;
181
184
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
185
  this.tools = config.tools ?? builtinTools as any;
183
186
  this.validToolNames = new Set(Object.keys(this.tools));
@@ -192,11 +195,9 @@ export class VercelAgentLoop implements AgentLoop {
192
195
  'When the task is fully complete, respond with a brief text summary (no tool call).',
193
196
  ].join(' ');
194
197
 
195
- // SystemModelMessage format with Anthropic cache_control
196
198
  this.cachedSystem = [{
197
199
  role: 'system' as const,
198
200
  content: this.systemPrompt,
199
- providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
200
201
  }];
201
202
 
202
203
  if (config.apiKey) {
@@ -205,13 +206,10 @@ export class VercelAgentLoop implements AgentLoop {
205
206
  }
206
207
 
207
208
  async nextAction(messages: AgentMessage[]): Promise<AgentAction> {
208
- if (!process.env.ANTHROPIC_API_KEY) {
209
- throw new Error('ANTHROPIC_API_KEY is required for default VercelAgentLoop');
210
- }
211
209
 
212
210
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
211
  const result = await (generateText as any)({
214
- model: anthropic(this.model),
212
+ model: this.createModel(this.model),
215
213
  tools: this.tools,
216
214
  toolChoice: 'auto',
217
215
  system: this.cachedSystem,
@@ -254,13 +252,10 @@ export class VercelAgentLoop implements AgentLoop {
254
252
  }
255
253
 
256
254
  async *streamAction(messages: AgentMessage[]): AsyncGenerator<AgentStreamEvent> {
257
- if (!process.env.ANTHROPIC_API_KEY) {
258
- throw new Error('ANTHROPIC_API_KEY is required for default VercelAgentLoop');
259
- }
260
255
 
261
256
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
257
  const result = (streamText as any)({
263
- model: anthropic(this.model),
258
+ model: this.createModel(this.model),
264
259
  tools: this.tools,
265
260
  toolChoice: 'auto',
266
261
  system: this.cachedSystem,
@@ -1,8 +1,9 @@
1
1
  import { generateObject } from 'ai';
2
- import { anthropic } from '@ai-sdk/anthropic';
2
+ import { anthropic as defaultAnthropicProvider } from '@ai-sdk/anthropic';
3
3
  import { z } from 'zod';
4
4
 
5
5
  import type { SkillSummary } from './skill-types';
6
+ import type { ModelFactory } from '../arc/types';
6
7
 
7
8
  const routeSchema = z.object({
8
9
  skillName: z.string().nullable(),
@@ -12,6 +13,7 @@ const routeSchema = z.object({
12
13
 
13
14
  export interface SkillRouterConfig {
14
15
  model?: string;
16
+ createModel?: ModelFactory;
15
17
  minConfidence?: number;
16
18
  aliases?: Record<string, string[]>;
17
19
  }
@@ -27,11 +29,13 @@ const DEFAULT_ALIASES: Record<string, string[]> = {
27
29
 
28
30
  export class SkillRouter {
29
31
  private readonly model: string;
32
+ private readonly createModel: ModelFactory;
30
33
  private readonly minConfidence: number;
31
34
  private readonly aliases: Record<string, string[]>;
32
35
 
33
36
  constructor(config: SkillRouterConfig = {}) {
34
37
  this.model = config.model ?? process.env.HARNESS_SKILL_ROUTER_MODEL ?? 'claude-3-5-haiku-latest';
38
+ this.createModel = config.createModel ?? defaultAnthropicProvider;
35
39
  this.minConfidence = config.minConfidence ?? Number(process.env.HARNESS_SKILL_ROUTER_THRESHOLD ?? '0.55');
36
40
  this.aliases = {
37
41
  ...DEFAULT_ALIASES,
@@ -53,9 +57,8 @@ export class SkillRouter {
53
57
  }
54
58
  }
55
59
 
56
- if (!process.env.ANTHROPIC_API_KEY) {
57
- return null;
58
- }
60
+ // LLM-based routing — if no API key is configured, the provider will fail
61
+ // and the catch block below will return null (graceful fallback).
59
62
 
60
63
  try {
61
64
  const skillList = summaries
@@ -63,7 +66,7 @@ export class SkillRouter {
63
66
  .join('\n');
64
67
 
65
68
  const { object } = await generateObject({
66
- model: anthropic(this.model),
69
+ model: this.createModel(this.model),
67
70
  schema: routeSchema,
68
71
  system: [
69
72
  'You are a skill router.',