@bluecopa/harness 0.1.0-snapshot.21 → 0.1.0-snapshot.23

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.21",
3
+ "version": "0.1.0-snapshot.23",
4
4
  "description": "Provider-agnostic TypeScript agent framework",
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
@@ -366,6 +366,8 @@ export interface CreateProcessConfig {
366
366
  parentSignal: AbortSignal;
367
367
  /** Custom system prompt for this process (overrides PROCESS_SYSTEM_PROMPT). */
368
368
  processSystemPrompt?: string;
369
+ /** Async skill instructions to prepend to system prompt (resolved during process startup). */
370
+ skillPromptPromise?: Promise<string | null>;
369
371
 
370
372
  // Runtime extras
371
373
  hookRunner?: HookRunner;
@@ -421,12 +423,21 @@ export function createProcess(
421
423
  process.status = 'running';
422
424
  const seed = await seedPromise;
423
425
 
426
+ // Build system prompt: base + optional skill instructions
427
+ let systemPrompt = config.processSystemPrompt ?? PROCESS_SYSTEM_PROMPT;
428
+ if (config.skillPromptPromise) {
429
+ const skillInstructions = await config.skillPromptPromise;
430
+ if (skillInstructions) {
431
+ systemPrompt += '\n\n## Skill Instructions\n' + skillInstructions;
432
+ }
433
+ }
434
+
424
435
  const result = await Promise.race([
425
436
  runner.run({
426
437
  model,
427
438
  prompt: request.action,
428
439
  tools: config.processTools,
429
- systemPrompt: config.processSystemPrompt ?? PROCESS_SYSTEM_PROMPT,
440
+ systemPrompt,
430
441
  toolProvider: config.toolProvider,
431
442
  maxSteps,
432
443
  signal: ac.signal,
@@ -27,6 +27,9 @@ import { createProcess, firstEvent } from './agent-runner';
27
27
  import { EpisodeCompressor } from './episode-compressor';
28
28
  import { runConsolidation } from './consolidation';
29
29
  import { pickDefined } from './utils';
30
+ import { SkillRouter } from '../skills/skill-router';
31
+ import { loadSkillFromFile } from '../skills/skill-loader';
32
+ import type { SkillSummary } from '../skills/skill-types';
30
33
 
31
34
  // ── Default orchestrator prompt ──
32
35
 
@@ -75,6 +78,9 @@ export class ArcLoop {
75
78
  private readonly traceWriter: ((event: TraceEvent) => void) | undefined;
76
79
  private readonly tracedRunning = new Set<string>();
77
80
  private readonly processListeners: Promise<void>[] = [];
81
+ private readonly skillRouter: SkillRouter | undefined;
82
+ private skillSummaries: SkillSummary[] | null = null;
83
+ private skillSummariesPromise: Promise<SkillSummary[]> | null = null;
78
84
 
79
85
  constructor(config: ArcLoopConfig) {
80
86
  this.config = config;
@@ -114,6 +120,15 @@ export class ArcLoop {
114
120
 
115
121
  this.resilience = config.resilience;
116
122
  this.traceWriter = (config as ArcLoopConfig & { traceWriter?: (event: TraceEvent) => void }).traceWriter;
123
+
124
+ if (config.skillIndexPath) {
125
+ this.skillRouter = new SkillRouter();
126
+ // Lazy-load skill summaries on first dispatch
127
+ this.skillSummariesPromise = import('node:fs/promises')
128
+ .then(fs => fs.readFile(config.skillIndexPath!, 'utf-8'))
129
+ .then(raw => JSON.parse(raw) as SkillSummary[])
130
+ .catch(() => []);
131
+ }
117
132
  }
118
133
 
119
134
  private trace(kind: TraceEvent['kind']): void {
@@ -375,8 +390,10 @@ export class ArcLoop {
375
390
  }],
376
391
  });
377
392
  } else if (this.config.onOrchestratorTool) {
378
- await this.config.onOrchestratorTool(call.toolName, call.args);
379
- const resultText = `${call.toolName}: handled`;
393
+ const action = await this.config.onOrchestratorTool(call.toolName, call.args);
394
+ const resultText = (action && 'content' in action && typeof action.content === 'string')
395
+ ? action.content
396
+ : `${call.toolName}: handled`;
380
397
  toolResultMessages.push({
381
398
  role: 'tool',
382
399
  content: resultText,
@@ -457,6 +474,12 @@ export class ArcLoop {
457
474
  this.modelMap,
458
475
  this.modelMap.medium,
459
476
  );
477
+
478
+ // Resolve skill instructions only when skills are configured
479
+ const skillPromptPromise = this.skillRouter
480
+ ? this.resolveSkillPrompt(request.action)
481
+ : undefined;
482
+
460
483
  const proc = createProcess(request, {
461
484
  toolProvider: this.config.toolProvider,
462
485
  episodeStore: this.config.episodeStore,
@@ -469,6 +492,7 @@ export class ArcLoop {
469
492
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
470
493
  processTools: (profile?.tools ?? this.config.processTools ?? builtinTools) as any,
471
494
  processSystemPrompt: profile?.systemPrompt ?? this.config.processSystemPrompt,
495
+ skillPromptPromise,
472
496
  parentSignal,
473
497
  ...pickDefined(this.config, [
474
498
  'hookRunner',
@@ -483,6 +507,28 @@ export class ArcLoop {
483
507
  return proc;
484
508
  }
485
509
 
510
+ /** Resolve skill instructions for a process action. Returns null if no skill matched. */
511
+ private async resolveSkillPrompt(action: string): Promise<string | null> {
512
+ if (!this.skillRouter || !this.skillSummariesPromise) return null;
513
+
514
+ // Ensure summaries are loaded
515
+ if (!this.skillSummaries) {
516
+ this.skillSummaries = await this.skillSummariesPromise;
517
+ }
518
+ if (this.skillSummaries.length === 0) return null;
519
+
520
+ // Fast match only (keyword + alias, no LLM call)
521
+ const matched = await this.skillRouter.selectSkill(action, this.skillSummaries);
522
+ if (!matched) return null;
523
+
524
+ try {
525
+ const skill = await loadSkillFromFile(matched.path);
526
+ return skill.instructions || null;
527
+ } catch {
528
+ return null;
529
+ }
530
+ }
531
+
486
532
  private traceProcessRunning(procId: string): void {
487
533
  if (!this.tracedRunning.has(procId)) {
488
534
  this.tracedRunning.add(procId);