@bluecopa/harness 0.1.0-snapshot.24 → 0.1.0-snapshot.26

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.24",
3
+ "version": "0.1.0-snapshot.26",
4
4
  "description": "Provider-agnostic TypeScript agent framework",
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
@@ -77,6 +77,7 @@ export class ArcLoop {
77
77
  private readonly resilience: ResiliencePolicy | undefined;
78
78
  private readonly traceWriter: ((event: TraceEvent) => void) | undefined;
79
79
  private readonly tracedRunning = new Set<string>();
80
+ private readonly reportedCompletions = new Set<string>();
80
81
  private readonly processListeners: Promise<void>[] = [];
81
82
  private readonly skillRouter: SkillRouter | undefined;
82
83
  private skillSummaries: SkillSummary[] | null = null;
@@ -421,9 +422,10 @@ export class ArcLoop {
421
422
  this.trace({ type: 'turn_end', turn });
422
423
  yield { type: 'turn_end', turn };
423
424
 
424
- // Wait for at least one process to reach a terminal state.
425
- // We poll process.status (set directly by createProcess's floating promise)
426
- // rather than consuming the outbox (which listenToProcess already reads).
425
+ // Wait for all processes dispatched this turn to reach a terminal state.
426
+ // When the orchestrator dispatches N parallel threads, there's no useful
427
+ // work it can do with partial results giving it a turn early just wastes
428
+ // an LLM call on empty Check loops.
427
429
  const active = [...this.processes.values()].filter(
428
430
  p => p.status === 'running' || p.status === 'pending',
429
431
  );
@@ -431,7 +433,7 @@ export class ArcLoop {
431
433
  const deadline = Date.now() + (this.config.processTimeout ?? 120_000);
432
434
  await new Promise<void>(resolve => {
433
435
  const check = () => {
434
- if (active.some(p => p.status !== 'running' && p.status !== 'pending') || Date.now() > deadline) {
436
+ if (active.every(p => p.status !== 'running' && p.status !== 'pending') || Date.now() > deadline) {
435
437
  resolve();
436
438
  } else {
437
439
  setTimeout(check, 10);
@@ -440,6 +442,32 @@ export class ArcLoop {
440
442
  check();
441
443
  });
442
444
  }
445
+
446
+ // Inject process completion messages into the orchestrator's conversation
447
+ // so it sees episodeIds and can pass them as contextEpisodeIds.
448
+ const completionMessages: AgentMessage[] = [];
449
+ for (const [id, proc] of this.processes) {
450
+ if (proc.result && !this.reportedCompletions.has(id)) {
451
+ this.reportedCompletions.add(id);
452
+ const ep = proc.result.episode;
453
+ const status = proc.result.success ? 'success' : `failed: ${proc.result.error ?? 'unknown'}`;
454
+ completionMessages.push({
455
+ role: 'user',
456
+ content: `[Thread ${id} completed] (episodeId: ${ep.id}) — ${status}\n${ep.summary}`,
457
+ });
458
+ }
459
+ }
460
+ if (completionMessages.length > 0) {
461
+ this.ctx.recordTurn({
462
+ role: 'process_result',
463
+ messages: completionMessages,
464
+ tokenEstimate: completionMessages.reduce((s, m) => {
465
+ const text = typeof m.content === 'string' ? m.content : getTextContent(m.content);
466
+ return s + estimateTokens(text);
467
+ }, 0),
468
+ timestamp: Date.now(),
469
+ });
470
+ }
443
471
  }
444
472
 
445
473
  await this.waitForProcessListeners();