agentxchain 2.60.0 → 2.61.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.60.0",
3
+ "version": "2.61.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -575,7 +575,7 @@ function attachLegacyCurrentTurnAlias(state) {
575
575
  function formatBudgetRecoveryAction(isReadyToResume) {
576
576
  return isReadyToResume
577
577
  ? 'Run agentxchain resume to assign the next turn'
578
- : 'Increase per_run_max_usd in agentxchain.json, then run agentxchain resume';
578
+ : 'Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume';
579
579
  }
580
580
 
581
581
  function formatBudgetRecoveryDetail(spentUsd, limitUsd, remainingUsd, isReadyToResume) {
@@ -1938,7 +1938,7 @@ export function assignGovernedTurn(root, config, roleId) {
1938
1938
 
1939
1939
  // DEC-BUDGET-ENFORCE-001: Pre-assignment budget exhaustion guard
1940
1940
  if (state.budget_status?.remaining_usd != null && state.budget_status.remaining_usd <= 0) {
1941
- return { ok: false, error: `Cannot assign turn: run budget exhausted (spent $${(state.budget_status.spent_usd || 0).toFixed(2)} of $${((state.budget_status.spent_usd || 0) + state.budget_status.remaining_usd).toFixed(2)} limit). Increase per_run_max_usd in agentxchain.json, then run agentxchain resume` };
1941
+ return { ok: false, error: `Cannot assign turn: run budget exhausted (spent $${(state.budget_status.spent_usd || 0).toFixed(2)} of $${((state.budget_status.spent_usd || 0) + state.budget_status.remaining_usd).toFixed(2)} limit). Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume` };
1942
1942
  }
1943
1943
 
1944
1944
  // DEC-PARALLEL-011: Budget reservation
@@ -2675,7 +2675,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2675
2675
  recovery: {
2676
2676
  typed_reason: 'budget_exhausted',
2677
2677
  owner: 'human',
2678
- recovery_action: 'Increase per_run_max_usd in agentxchain.json, then run agentxchain resume',
2678
+ recovery_action: 'Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume',
2679
2679
  turn_retained: false,
2680
2680
  detail: `Run budget exhausted: spent $${updatedState.budget_status.spent_usd.toFixed(2)} of $${limit.toFixed(2)} limit ($${overBy.toFixed(2)} over)`,
2681
2681
  },
@@ -61,6 +61,7 @@ const VALID_API_PROXY_PREFLIGHT_FIELDS = [
61
61
  'tokenizer',
62
62
  'safety_margin_tokens',
63
63
  ];
64
+ const VALID_BUDGET_ON_EXCEED = ['pause_and_escalate'];
64
65
 
65
66
  function validateMcpRuntime(runtimeId, runtime, errors) {
66
67
  const transport = typeof runtime?.transport === 'string' && runtime.transport.trim()
@@ -537,6 +538,12 @@ export function validateV4Config(data, projectRoot) {
537
538
  errors.push(...policyValidation.errors);
538
539
  }
539
540
 
541
+ // Budget (optional but validated if present)
542
+ if (data.budget !== undefined) {
543
+ const budgetValidation = validateBudgetConfig(data.budget);
544
+ errors.push(...budgetValidation.errors);
545
+ }
546
+
540
547
  // Approval Policy (optional but validated if present)
541
548
  if (data.approval_policy !== undefined) {
542
549
  errors.push(...validateApprovalPolicy(data.approval_policy, data.routing));
@@ -551,6 +558,80 @@ export function validateV4Config(data, projectRoot) {
551
558
  return { ok: errors.length === 0, errors };
552
559
  }
553
560
 
561
+ export function validateBudgetConfig(budget) {
562
+ const errors = [];
563
+
564
+ if (budget === null) {
565
+ return { ok: true, errors };
566
+ }
567
+
568
+ if (!budget || typeof budget !== 'object' || Array.isArray(budget)) {
569
+ errors.push('budget must be an object');
570
+ return { ok: false, errors };
571
+ }
572
+
573
+ validateBudgetUsdLimit('budget.per_turn_max_usd', budget.per_turn_max_usd, errors);
574
+ validateBudgetUsdLimit('budget.per_run_max_usd', budget.per_run_max_usd, errors);
575
+
576
+ if (
577
+ Number.isFinite(budget.per_turn_max_usd) &&
578
+ Number.isFinite(budget.per_run_max_usd) &&
579
+ budget.per_turn_max_usd > budget.per_run_max_usd
580
+ ) {
581
+ errors.push('budget.per_turn_max_usd must be less than or equal to budget.per_run_max_usd when both are set');
582
+ }
583
+
584
+ if (budget.on_exceed !== undefined) {
585
+ if (typeof budget.on_exceed !== 'string' || !VALID_BUDGET_ON_EXCEED.includes(budget.on_exceed)) {
586
+ errors.push(`budget.on_exceed must be one of: ${VALID_BUDGET_ON_EXCEED.join(', ')} (warn is not implemented)`);
587
+ }
588
+ }
589
+
590
+ if (budget.cost_rates !== undefined) {
591
+ if (!budget.cost_rates || typeof budget.cost_rates !== 'object' || Array.isArray(budget.cost_rates)) {
592
+ errors.push('budget.cost_rates must be an object');
593
+ } else {
594
+ for (const [model, rates] of Object.entries(budget.cost_rates)) {
595
+ if (typeof model !== 'string' || !model.trim()) {
596
+ errors.push('budget.cost_rates model keys must be non-empty strings');
597
+ continue;
598
+ }
599
+ if (!rates || typeof rates !== 'object' || Array.isArray(rates)) {
600
+ errors.push(`budget.cost_rates.${model} must be an object`);
601
+ continue;
602
+ }
603
+ validateBudgetCostRate(`budget.cost_rates.${model}.input_per_1m`, rates.input_per_1m, errors);
604
+ validateBudgetCostRate(`budget.cost_rates.${model}.output_per_1m`, rates.output_per_1m, errors);
605
+ }
606
+ }
607
+ }
608
+
609
+ return { ok: errors.length === 0, errors };
610
+ }
611
+
612
+ function validateBudgetUsdLimit(path, value, errors) {
613
+ if (value === undefined || value === null) {
614
+ return;
615
+ }
616
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
617
+ errors.push(`${path} must be a finite number when provided`);
618
+ return;
619
+ }
620
+ if (value <= 0) {
621
+ errors.push(`${path} must be greater than 0 when provided`);
622
+ }
623
+ }
624
+
625
+ function validateBudgetCostRate(path, value, errors) {
626
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
627
+ errors.push(`${path} must be a finite number`);
628
+ return;
629
+ }
630
+ if (value < 0) {
631
+ errors.push(`${path} must be greater than or equal to 0`);
632
+ }
633
+ }
634
+
554
635
  export function validateSchedulesConfig(schedules, roles) {
555
636
  const errors = [];
556
637