agents-relay 2.0.100 → 2.0.102

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.
@@ -80,7 +80,7 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
80
80
  return;
81
81
  const attempt = fresh.attempt ?? 0;
82
82
  const maxAttempts = fresh.maxAttempts ?? (fresh.manualPlanning ? 1 : 3);
83
- retry = attempt < maxAttempts;
83
+ retry = attempt < maxAttempts && !message.startsWith('Planner JSON correction failed after one retry:');
84
84
  fresh.status = retry ? 'PLANNING' : 'FAILED';
85
85
  fresh.retryFeedback = message;
86
86
  fresh.error = message;
package/dist/planner.js CHANGED
@@ -502,17 +502,14 @@ export class AgentObjectivePlanner {
502
502
  this.agentFile = agentFile;
503
503
  }
504
504
  async plan(context, signal) {
505
- const resolvedPlanner = context.executionContext?.agents.find(agent => agent.name === 'planner')?.path;
506
505
  const effectivePlanner = context.executionContext ? await resolveAgentMarkdown('planner', BUNDLED_AGENTS_ROOT, context.executionContext.project.path) : undefined;
507
506
  const instructions = effectivePlanner?.content ?? await readFile(this.agentFile, 'utf8');
508
507
  const contract = await readFile(DEFAULT_TASK_JOB_CONTRACT, 'utf8');
509
508
  const resolvedContext = context.executionContext ? executionContextPrompt(context.executionContext) + '\n\n' : '';
510
509
  const prompt = (effectivePlanner ? `[Effective planner contract SHA-256: ${effectivePlanner.hash}]\n\n` : '') + instructions.trim() + '\n\n## Shared Task / Job Contract\n\n' + contract.trim() + '\n\n' + resolvedContext + '## Durable planner input\n\n' + plannerInput(context);
511
- return await new Promise((resolve, reject) => {
512
- const child = spawnOwnedProcess(this.command, ['exec', '--json', '--', prompt], { invocation: 'planner.agent', signal, correlation: { jobId: context.job.id }, spawn: { cwd: context.executionContext?.project.path, stdio: ['ignore', 'pipe', 'pipe'] } });
513
- let stdoutBuffer = '';
514
- let finalMessage = '';
515
- let stderrTail = '';
510
+ const runAgent = (input) => new Promise((resolve, reject) => {
511
+ const child = spawnOwnedProcess(this.command, ['exec', '--json', '--', input], { invocation: 'planner.agent', signal, correlation: { jobId: context.job.id }, spawn: { cwd: context.executionContext?.project.path, stdio: ['ignore', 'pipe', 'pipe'] } });
512
+ let stdoutBuffer = '', finalMessage = '', stderrTail = '';
516
513
  const consume = (line) => {
517
514
  const message = parsePlannerAgentMessage(line);
518
515
  if (message)
@@ -538,13 +535,23 @@ export class AgentObjectivePlanner {
538
535
  reject(new Error('Planner agent returned no final message'));
539
536
  return;
540
537
  }
541
- try {
542
- resolve(parsePlannerOutput(finalMessage));
543
- }
544
- catch (error) {
545
- reject(error);
546
- }
538
+ resolve(finalMessage);
547
539
  });
548
540
  });
541
+ const first = await runAgent(prompt);
542
+ try {
543
+ return parsePlannerOutput(first);
544
+ }
545
+ catch (error) {
546
+ if (signal.aborted)
547
+ throw error;
548
+ const correction = `${prompt}\n\n## Format correction (one retry only)\nYour previous final response was not valid Planner JSON. Error: ${error instanceof Error ? error.message : String(error)}\nPrevious response (possibly truncated):\n${first.slice(0, 12000)}\nReturn exactly one valid JSON object conforming to the Planner contract. No prose, no second JSON object.`;
549
+ try {
550
+ return parsePlannerOutput(await runAgent(correction));
551
+ }
552
+ catch (retryError) {
553
+ throw new Error(`Planner JSON correction failed after one retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
554
+ }
555
+ }
549
556
  }
550
557
  }
package/dist/pool.js CHANGED
@@ -5,6 +5,7 @@ import { scheduleBucket } from './scheduler.js';
5
5
  import { ReconcileCoordinator } from './reconcile-coordinator.js';
6
6
  import { errorMessage, observability, withObservabilityCorrelation } from './observability.js';
7
7
  import { jobHasActiveExecution } from './job-execution-slot.js';
8
+ import { unconsumedUnblockTrigger } from './unblock.js';
8
9
  const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
9
10
  export function compareJobPriority(a, b) { return RANK[a.priority ?? 'P2'] - RANK[b.priority ?? 'P2'] || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
10
11
  export function runnableManagedJobs(items) { return items.filter(item => item.job.state === 'ACTIVE').map(item => item.job).sort(compareJobPriority); }
@@ -12,6 +13,8 @@ export function needsLifecycleReconciliation(job, now = Date.now()) {
12
13
  // DRAFT create-only Jobs stay dormant until a caller explicitly creates work.
13
14
  if (job.tasks.length === 0)
14
15
  return false;
16
+ if (job.executionMode === 'autonomous' && unconsumedUnblockTrigger(job))
17
+ return true;
15
18
  if (job.tasks.some(task => (task.state === 'QUEUED' && task.executionId === null)
16
19
  || (task.state === 'RUNNING' && task.adapter === 'orchestrator' && task.executionId === null)
17
20
  || (task.state === 'DRAFT' && task.dependencies.length > 0)))
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { GraphRuntime } from './graph-v2.js';
3
3
  import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
4
- import { isEffectivelyBlocked } from '../unblock.js';
4
+ import { isEffectivelyBlocked, unconsumedUnblockTrigger } from '../unblock.js';
5
5
  const source = readFileSync(new URL('../../workflow/planner-orchestration.mmd', import.meta.url), 'utf8');
6
6
  const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
7
7
  export const plannerOrchestrationV2 = new GraphRuntime(source, {
@@ -51,7 +51,7 @@ export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
51
51
  recoverablePendingPlan,
52
52
  hasSeedWork: !((job.plans?.length ?? 0) === 0 && job.tasks.filter(task => task.kind !== 'planner').length === 0),
53
53
  noActivePlanner: !scopedPlans.some(plan => plan.status === 'PLANNING'),
54
- noBlockedWork: !scopedTasks.some(task => task.kind !== 'planner' && isEffectivelyBlocked(task)),
54
+ noBlockedWork: Boolean(unconsumedUnblockTrigger(job)) || !scopedTasks.some(task => task.kind !== 'planner' && isEffectivelyBlocked(task)),
55
55
  noActiveImplementation: !scopedTasks.some(task => task.kind !== 'planner' && ['DRAFT', 'QUEUED', 'RUNNING'].includes(task.state)),
56
56
  resolutionOwnedByTroubleshooter: scopedTasks.some(task => task.kind !== 'planner' && task.resolution?.owner === 'troubleshooter' && ['active', 'waiting'].includes(task.resolution.state)),
57
57
  continuationMaterialized: !(latestObjective === 'in_progress' && latestId && !job.tasks.some(task => task.kind !== 'planner' && task.planningDecisionId === latestId)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.100",
3
+ "version": "2.0.102",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {