agentxchain 2.21.0 → 2.23.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/README.md +1 -1
- package/bin/agentxchain.js +32 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +55 -8
- package/src/commands/escalate.js +1 -1
- package/src/commands/proposal.js +144 -0
- package/src/commands/resume.js +9 -3
- package/src/commands/status.js +2 -2
- package/src/commands/step.js +10 -5
- package/src/lib/adapters/api-proxy-adapter.js +47 -15
- package/src/lib/blocked-state.js +78 -8
- package/src/lib/config.js +11 -2
- package/src/lib/dispatch-bundle.js +176 -1
- package/src/lib/governed-state.js +481 -17
- package/src/lib/normalized-config.js +3 -3
- package/src/lib/proposal-ops.js +451 -0
- package/src/lib/repo-observer.js +1 -0
- package/src/lib/schemas/turn-result.schema.json +28 -0
- package/src/lib/turn-result-validator.js +35 -0
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ The first-party governed workflow kit includes `.planning/SYSTEM_SPEC.md` alongs
|
|
|
214
214
|
- `manual`: implemented
|
|
215
215
|
- `local_cli`: implemented
|
|
216
216
|
- `mcp`: implemented for stdio and streamable HTTP tool-contract dispatch
|
|
217
|
-
- `api_proxy`: implemented for synchronous
|
|
217
|
+
- `api_proxy`: implemented for synchronous `review_only` and `proposed` write-authority turns; stages a provider-backed result during `step`
|
|
218
218
|
|
|
219
219
|
## Legacy IDE Mode
|
|
220
220
|
|
package/bin/agentxchain.js
CHANGED
|
@@ -68,6 +68,7 @@ import { resumeCommand } from '../src/commands/resume.js';
|
|
|
68
68
|
import { escalateCommand } from '../src/commands/escalate.js';
|
|
69
69
|
import { acceptTurnCommand } from '../src/commands/accept-turn.js';
|
|
70
70
|
import { rejectTurnCommand } from '../src/commands/reject-turn.js';
|
|
71
|
+
import { proposalListCommand, proposalDiffCommand, proposalApplyCommand, proposalRejectCommand } from '../src/commands/proposal.js';
|
|
71
72
|
import { stepCommand } from '../src/commands/step.js';
|
|
72
73
|
import { runCommand } from '../src/commands/run.js';
|
|
73
74
|
import { approveTransitionCommand } from '../src/commands/approve-transition.js';
|
|
@@ -535,4 +536,35 @@ intakeCmd
|
|
|
535
536
|
.option('-j, --json', 'Output as JSON')
|
|
536
537
|
.action(intakeStatusCommand);
|
|
537
538
|
|
|
539
|
+
// --- Proposal operations ----------------------------------------------------
|
|
540
|
+
|
|
541
|
+
const proposalCmd = program
|
|
542
|
+
.command('proposal')
|
|
543
|
+
.description('Manage proposed changes from api_proxy agents');
|
|
544
|
+
|
|
545
|
+
proposalCmd
|
|
546
|
+
.command('list')
|
|
547
|
+
.description('List all proposals and their status')
|
|
548
|
+
.action(proposalListCommand);
|
|
549
|
+
|
|
550
|
+
proposalCmd
|
|
551
|
+
.command('diff <turn_id>')
|
|
552
|
+
.description('Show diff between proposed files and current workspace')
|
|
553
|
+
.option('--file <path>', 'Show diff for a single file only')
|
|
554
|
+
.action(proposalDiffCommand);
|
|
555
|
+
|
|
556
|
+
proposalCmd
|
|
557
|
+
.command('apply <turn_id>')
|
|
558
|
+
.description('Apply proposed changes to the workspace')
|
|
559
|
+
.option('--file <path>', 'Apply only a specific file')
|
|
560
|
+
.option('--dry-run', 'Show what would change without writing')
|
|
561
|
+
.option('--force', 'Override proposal conflicts or unverifiable legacy proposals')
|
|
562
|
+
.action(proposalApplyCommand);
|
|
563
|
+
|
|
564
|
+
proposalCmd
|
|
565
|
+
.command('reject <turn_id>')
|
|
566
|
+
.description('Reject a proposal without applying changes')
|
|
567
|
+
.option('--reason <reason>', 'Reason for rejection (required)')
|
|
568
|
+
.action(proposalRejectCommand);
|
|
569
|
+
|
|
538
570
|
program.parse();
|
package/package.json
CHANGED
package/scripts/release-bump.sh
CHANGED
|
@@ -6,6 +6,7 @@ set -euo pipefail
|
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
8
|
CLI_DIR="${SCRIPT_DIR}/.."
|
|
9
|
+
REPO_ROOT="$(cd "${CLI_DIR}/.." && pwd)"
|
|
9
10
|
cd "$CLI_DIR"
|
|
10
11
|
|
|
11
12
|
TARGET_VERSION=""
|
|
@@ -46,13 +47,56 @@ fi
|
|
|
46
47
|
echo "AgentXchain Release Identity: ${TARGET_VERSION}"
|
|
47
48
|
echo "============================================="
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
TARGET_RELEASE_DOC="website-v2/docs/releases/v${TARGET_VERSION//./-}.mdx"
|
|
51
|
+
ALLOWED_RELEASE_PATHS=(
|
|
52
|
+
"cli/CHANGELOG.md"
|
|
53
|
+
"${TARGET_RELEASE_DOC}"
|
|
54
|
+
"website-v2/sidebars.ts"
|
|
55
|
+
"website-v2/src/pages/index.tsx"
|
|
56
|
+
".agentxchain-conformance/capabilities.json"
|
|
57
|
+
"website-v2/docs/protocol-implementor-guide.mdx"
|
|
58
|
+
".planning/LAUNCH_EVIDENCE_REPORT.md"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
is_allowed_release_path() {
|
|
62
|
+
local candidate="$1"
|
|
63
|
+
local allowed
|
|
64
|
+
for allowed in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
65
|
+
if [[ "$candidate" == "$allowed" ]]; then
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stage_if_present() {
|
|
73
|
+
local rel_path="$1"
|
|
74
|
+
if [[ -e "${REPO_ROOT}/${rel_path}" ]]; then
|
|
75
|
+
git -C "$REPO_ROOT" add -- "$rel_path"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
if git -C "$REPO_ROOT" ls-files --error-unmatch "$rel_path" >/dev/null 2>&1; then
|
|
79
|
+
git -C "$REPO_ROOT" add -- "$rel_path"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# 1. Assert only allowed release-surface dirt is present
|
|
84
|
+
echo "[1/7] Checking release-prep tree state..."
|
|
85
|
+
DISALLOWED_DIRTY=()
|
|
86
|
+
while IFS= read -r status_line; do
|
|
87
|
+
[[ -z "$status_line" ]] && continue
|
|
88
|
+
path="${status_line#?? }"
|
|
89
|
+
if ! is_allowed_release_path "$path"; then
|
|
90
|
+
DISALLOWED_DIRTY+=("$path")
|
|
91
|
+
fi
|
|
92
|
+
done < <(git -C "$REPO_ROOT" status --porcelain)
|
|
93
|
+
|
|
94
|
+
if [[ "${#DISALLOWED_DIRTY[@]}" -gt 0 ]]; then
|
|
95
|
+
echo "FAIL: Working tree contains changes outside the allowed release surfaces:" >&2
|
|
96
|
+
printf ' - %s\n' "${DISALLOWED_DIRTY[@]}" >&2
|
|
53
97
|
exit 1
|
|
54
98
|
fi
|
|
55
|
-
echo " OK: tree
|
|
99
|
+
echo " OK: tree contains only allowed release-prep changes"
|
|
56
100
|
|
|
57
101
|
# 2. Assert not already at target version
|
|
58
102
|
echo "[2/7] Checking current version..."
|
|
@@ -78,11 +122,14 @@ echo " OK: package.json updated to ${TARGET_VERSION}"
|
|
|
78
122
|
|
|
79
123
|
# 5. Stage version files
|
|
80
124
|
echo "[5/7] Staging version files..."
|
|
81
|
-
git add package.json
|
|
125
|
+
git add -- package.json
|
|
82
126
|
if [[ -f package-lock.json ]]; then
|
|
83
|
-
git add package-lock.json
|
|
127
|
+
git add -- package-lock.json
|
|
84
128
|
fi
|
|
85
|
-
|
|
129
|
+
for rel_path in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
130
|
+
stage_if_present "$rel_path"
|
|
131
|
+
done
|
|
132
|
+
echo " OK: version files and allowed release surfaces staged"
|
|
86
133
|
|
|
87
134
|
# 6. Create release commit
|
|
88
135
|
echo "[6/7] Creating release commit..."
|
package/src/commands/escalate.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function escalateCommand(opts) {
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const recovery = deriveRecoveryDescriptor(result.state);
|
|
38
|
+
const recovery = deriveRecoveryDescriptor(result.state, config);
|
|
39
39
|
|
|
40
40
|
console.log('');
|
|
41
41
|
console.log(chalk.yellow(' Run Escalated'));
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadProjectContext } from '../lib/config.js';
|
|
3
|
+
import { listProposals, diffProposal, applyProposal, rejectProposal } from '../lib/proposal-ops.js';
|
|
4
|
+
|
|
5
|
+
export async function proposalListCommand() {
|
|
6
|
+
const context = requireGovernedContext();
|
|
7
|
+
const result = listProposals(context.root);
|
|
8
|
+
if (!result.ok) {
|
|
9
|
+
console.log(chalk.red(result.error));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (result.proposals.length === 0) {
|
|
14
|
+
console.log(chalk.dim(' No proposals found.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(chalk.bold(' Proposals'));
|
|
20
|
+
console.log(chalk.dim(' ' + '─'.repeat(60)));
|
|
21
|
+
for (const p of result.proposals) {
|
|
22
|
+
const statusColor = p.status === 'applied' ? chalk.green : p.status === 'rejected' ? chalk.red : chalk.yellow;
|
|
23
|
+
console.log(` ${chalk.dim(p.turn_id)} ${p.role} ${p.file_count} files ${statusColor(p.status)}`);
|
|
24
|
+
}
|
|
25
|
+
console.log('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function proposalDiffCommand(turnId, opts) {
|
|
29
|
+
const context = requireGovernedContext();
|
|
30
|
+
if (!turnId) {
|
|
31
|
+
console.log(chalk.red('Usage: agentxchain proposal diff <turn_id>'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = diffProposal(context.root, turnId, opts.file);
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
console.log(chalk.red(result.error));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const d of result.diffs) {
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.bold(` ${d.path}`) + chalk.dim(` (${d.action})`));
|
|
44
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
45
|
+
for (const line of d.preview.split('\n')) {
|
|
46
|
+
if (line.startsWith('+')) console.log(chalk.green(` ${line}`));
|
|
47
|
+
else if (line.startsWith('-')) console.log(chalk.red(` ${line}`));
|
|
48
|
+
else console.log(chalk.dim(` ${line}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function proposalApplyCommand(turnId, opts) {
|
|
55
|
+
const context = requireGovernedContext();
|
|
56
|
+
if (!turnId) {
|
|
57
|
+
console.log(chalk.red('Usage: agentxchain proposal apply <turn_id>'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = applyProposal(context.root, turnId, {
|
|
62
|
+
file: opts.file,
|
|
63
|
+
dryRun: opts.dryRun,
|
|
64
|
+
force: opts.force,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
if (Array.isArray(result.conflicts) && result.conflicts.length > 0) {
|
|
69
|
+
console.log(chalk.red(` ${result.error}`));
|
|
70
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
71
|
+
for (const conflict of result.conflicts) {
|
|
72
|
+
console.log(` ${chalk.red('•')} ${conflict.path} ${chalk.dim(`(${conflict.reason})`)}`);
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.red(result.error));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('');
|
|
82
|
+
if (result.dry_run) {
|
|
83
|
+
console.log(chalk.yellow(' Dry Run — No Changes Written'));
|
|
84
|
+
} else if (result.forced) {
|
|
85
|
+
console.log(chalk.yellow(' Proposal Applied With Force'));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.green(' Proposal Applied'));
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
90
|
+
console.log(` ${chalk.dim('Turn:')} ${turnId}`);
|
|
91
|
+
console.log(` ${chalk.dim('Applied:')} ${result.applied_files.length} files`);
|
|
92
|
+
if (result.applied_files.length > 0) {
|
|
93
|
+
for (const f of result.applied_files) {
|
|
94
|
+
console.log(` ${chalk.dim('•')} ${f}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (result.skipped_files.length > 0) {
|
|
98
|
+
console.log(` ${chalk.dim('Skipped:')} ${result.skipped_files.length} files`);
|
|
99
|
+
for (const f of result.skipped_files) {
|
|
100
|
+
console.log(` ${chalk.dim('•')} ${f}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (result.forced && result.overridden_conflicts?.length > 0) {
|
|
104
|
+
console.log(` ${chalk.dim('Forced:')} ${result.overridden_conflicts.length} conflicts overridden`);
|
|
105
|
+
for (const conflict of result.overridden_conflicts) {
|
|
106
|
+
console.log(` ${chalk.dim('•')} ${conflict.path}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function proposalRejectCommand(turnId, opts) {
|
|
113
|
+
const context = requireGovernedContext();
|
|
114
|
+
if (!turnId) {
|
|
115
|
+
console.log(chalk.red('Usage: agentxchain proposal reject <turn_id> --reason "..."'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = rejectProposal(context.root, turnId, opts.reason);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
console.log(chalk.red(result.error));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(chalk.yellow(' Proposal Rejected'));
|
|
127
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
128
|
+
console.log(` ${chalk.dim('Turn:')} ${turnId}`);
|
|
129
|
+
console.log(` ${chalk.dim('Reason:')} ${opts.reason}`);
|
|
130
|
+
console.log('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function requireGovernedContext() {
|
|
134
|
+
const context = loadProjectContext();
|
|
135
|
+
if (!context) {
|
|
136
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (context.config.protocol_mode !== 'governed') {
|
|
140
|
+
console.log(chalk.red('The proposal command is only available for governed projects.'));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
return context;
|
|
144
|
+
}
|
package/src/commands/resume.js
CHANGED
|
@@ -21,6 +21,7 @@ import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
|
21
21
|
import {
|
|
22
22
|
initializeGovernedRun,
|
|
23
23
|
assignGovernedTurn,
|
|
24
|
+
deriveAfterDispatchHookRecoveryAction,
|
|
24
25
|
markRunBlocked,
|
|
25
26
|
getActiveTurns,
|
|
26
27
|
getActiveTurnCount,
|
|
@@ -358,13 +359,17 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
|
358
359
|
|| `after_dispatch hook blocked dispatch for turn ${turnId}`;
|
|
359
360
|
const errorCode = afterDispatchHooks.tamper?.error_code || 'hook_blocked';
|
|
360
361
|
|
|
362
|
+
const recoveryAction = deriveAfterDispatchHookRecoveryAction(state, config, {
|
|
363
|
+
turnRetained: true,
|
|
364
|
+
turnId,
|
|
365
|
+
});
|
|
361
366
|
markRunBlocked(root, {
|
|
362
367
|
blockedOn: `hook:after_dispatch:${hookName}`,
|
|
363
368
|
category: 'dispatch_error',
|
|
364
369
|
recovery: {
|
|
365
370
|
typed_reason: afterDispatchHooks.tamper ? 'hook_tamper' : 'hook_block',
|
|
366
371
|
owner: 'human',
|
|
367
|
-
recovery_action:
|
|
372
|
+
recovery_action: recoveryAction,
|
|
368
373
|
turn_retained: true,
|
|
369
374
|
detail,
|
|
370
375
|
},
|
|
@@ -378,6 +383,7 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
|
378
383
|
hookName,
|
|
379
384
|
error: detail,
|
|
380
385
|
errorCode,
|
|
386
|
+
recoveryAction,
|
|
381
387
|
hookResults: afterDispatchHooks,
|
|
382
388
|
});
|
|
383
389
|
|
|
@@ -387,7 +393,7 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
|
387
393
|
return { ok: true };
|
|
388
394
|
}
|
|
389
395
|
|
|
390
|
-
function printDispatchHookFailure({ turnId, roleId, hookName, error, hookResults }) {
|
|
396
|
+
function printDispatchHookFailure({ turnId, roleId, hookName, error, hookResults, recoveryAction }) {
|
|
391
397
|
const isTamper = hookResults?.tamper;
|
|
392
398
|
console.log('');
|
|
393
399
|
console.log(chalk.yellow(' Dispatch Blocked By Hook'));
|
|
@@ -399,7 +405,7 @@ function printDispatchHookFailure({ turnId, roleId, hookName, error, hookResults
|
|
|
399
405
|
console.log(` ${chalk.dim('Error:')} ${error}`);
|
|
400
406
|
console.log(` ${chalk.dim('Reason:')} ${isTamper ? 'hook_tamper' : 'hook_block'}`);
|
|
401
407
|
console.log(` ${chalk.dim('Owner:')} human`);
|
|
402
|
-
console.log(` ${chalk.dim('Action:')}
|
|
408
|
+
console.log(` ${chalk.dim('Action:')} ${recoveryAction}`);
|
|
403
409
|
console.log('');
|
|
404
410
|
}
|
|
405
411
|
|
package/src/commands/status.js
CHANGED
|
@@ -168,7 +168,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
168
168
|
if (state?.blocked_on) {
|
|
169
169
|
console.log('');
|
|
170
170
|
if (state.status === 'blocked') {
|
|
171
|
-
const recovery = deriveRecoveryDescriptor(state);
|
|
171
|
+
const recovery = deriveRecoveryDescriptor(state, config);
|
|
172
172
|
const detail = recovery?.detail || state.blocked_on;
|
|
173
173
|
console.log(` ${chalk.dim('Blocked:')} ${chalk.red.bold('BLOCKED')} — ${detail}`);
|
|
174
174
|
} else if (state.blocked_on.startsWith('human_approval:')) {
|
|
@@ -179,7 +179,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
const recovery = deriveRecoveryDescriptor(state);
|
|
182
|
+
const recovery = deriveRecoveryDescriptor(state, config);
|
|
183
183
|
if (recovery) {
|
|
184
184
|
console.log('');
|
|
185
185
|
console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
|
package/src/commands/step.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
initializeGovernedRun,
|
|
29
29
|
assignGovernedTurn,
|
|
30
30
|
acceptGovernedTurn,
|
|
31
|
+
deriveAfterDispatchHookRecoveryAction,
|
|
31
32
|
rejectGovernedTurn,
|
|
32
33
|
markRunBlocked,
|
|
33
34
|
getActiveTurnCount,
|
|
@@ -338,7 +339,7 @@ export async function stepCommand(opts) {
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
if (!afterDispatchHooks.ok) {
|
|
341
|
-
const blocked = blockStepForHookIssue(root, turn, {
|
|
342
|
+
const blocked = blockStepForHookIssue(root, state, turn, {
|
|
342
343
|
hookResults: afterDispatchHooks,
|
|
343
344
|
phase: 'after_dispatch',
|
|
344
345
|
defaultDetail: `after_dispatch hook blocked dispatch for turn ${turn.turn_id}`,
|
|
@@ -654,7 +655,7 @@ export async function stepCommand(opts) {
|
|
|
654
655
|
});
|
|
655
656
|
|
|
656
657
|
if (!beforeValidationHooks.ok) {
|
|
657
|
-
const blocked = blockStepForHookIssue(root, turn, {
|
|
658
|
+
const blocked = blockStepForHookIssue(root, state, turn, {
|
|
658
659
|
hookResults: beforeValidationHooks,
|
|
659
660
|
phase: 'before_validation',
|
|
660
661
|
defaultDetail: `before_validation hook blocked validation for turn ${turn.turn_id}`,
|
|
@@ -686,7 +687,7 @@ export async function stepCommand(opts) {
|
|
|
686
687
|
});
|
|
687
688
|
|
|
688
689
|
if (!afterValidationHooks.ok) {
|
|
689
|
-
const blocked = blockStepForHookIssue(root, turn, {
|
|
690
|
+
const blocked = blockStepForHookIssue(root, state, turn, {
|
|
690
691
|
hookResults: afterValidationHooks,
|
|
691
692
|
phase: 'after_validation',
|
|
692
693
|
defaultDetail: `after_validation hook blocked acceptance for turn ${turn.turn_id}`,
|
|
@@ -775,7 +776,7 @@ function loadHookStagedTurn(root, stagingRel) {
|
|
|
775
776
|
}
|
|
776
777
|
}
|
|
777
778
|
|
|
778
|
-
function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail, config }) {
|
|
779
|
+
function blockStepForHookIssue(root, state, turn, { hookResults, phase, defaultDetail, config }) {
|
|
779
780
|
const hookName = hookResults.blocker?.hook_name
|
|
780
781
|
|| hookResults.results?.find((entry) => entry.hook_name)?.hook_name
|
|
781
782
|
|| 'unknown';
|
|
@@ -783,13 +784,17 @@ function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail,
|
|
|
783
784
|
|| hookResults.tamper?.message
|
|
784
785
|
|| defaultDetail;
|
|
785
786
|
const errorCode = hookResults.tamper?.error_code || 'hook_blocked';
|
|
787
|
+
const recoveryAction = deriveAfterDispatchHookRecoveryAction(state, config, {
|
|
788
|
+
turnRetained: true,
|
|
789
|
+
turnId: turn.turn_id,
|
|
790
|
+
});
|
|
786
791
|
const blocked = markRunBlocked(root, {
|
|
787
792
|
blockedOn: `hook:${phase}:${hookName}`,
|
|
788
793
|
category: phase === 'after_dispatch' ? 'dispatch_error' : 'validation_error',
|
|
789
794
|
recovery: {
|
|
790
795
|
typed_reason: hookResults.tamper ? 'hook_tamper' : 'hook_block',
|
|
791
796
|
owner: 'human',
|
|
792
|
-
recovery_action:
|
|
797
|
+
recovery_action: recoveryAction,
|
|
793
798
|
turn_retained: true,
|
|
794
799
|
detail,
|
|
795
800
|
},
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API proxy adapter —
|
|
2
|
+
* API proxy adapter — synchronous provider calls for review and proposed authoring.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - review_only
|
|
4
|
+
* Supported write authorities:
|
|
5
|
+
* - review_only: single request/response, structured review JSON
|
|
6
|
+
* - proposed: single request/response, structured JSON with proposed_changes[]
|
|
7
|
+
* (orchestrator materializes proposals to .agentxchain/proposed/<turn_id>/)
|
|
8
|
+
*
|
|
9
|
+
* Constraints:
|
|
6
10
|
* - single request / single response (synchronous within `step`)
|
|
7
|
-
* - no tool use, no
|
|
11
|
+
* - no tool use, no direct repo writes
|
|
8
12
|
* - turn result must arrive as structured JSON
|
|
9
13
|
* - provider telemetry is authoritative for cost
|
|
10
14
|
*
|
|
@@ -46,13 +50,37 @@ const PROVIDER_ENDPOINTS = {
|
|
|
46
50
|
openai: 'https://api.openai.com/v1/chat/completions',
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
//
|
|
50
|
-
|
|
53
|
+
// Bundled cost rates per million tokens (USD).
|
|
54
|
+
// These are convenience defaults — operators can override via budget.cost_rates in agentxchain.json.
|
|
55
|
+
// Verified: 2026-04-07 (Anthropic via docs.anthropic.com; OpenAI from training knowledge)
|
|
56
|
+
const BUNDLED_COST_RATES = {
|
|
57
|
+
// Anthropic — verified 2026-04-07
|
|
51
58
|
'claude-sonnet-4-6': { input_per_1m: 3.00, output_per_1m: 15.00 },
|
|
52
|
-
'claude-opus-4-6': { input_per_1m:
|
|
53
|
-
'claude-haiku-4-5-20251001': { input_per_1m:
|
|
59
|
+
'claude-opus-4-6': { input_per_1m: 5.00, output_per_1m: 25.00 },
|
|
60
|
+
'claude-haiku-4-5-20251001': { input_per_1m: 1.00, output_per_1m: 5.00 },
|
|
61
|
+
// OpenAI — verified 2026-04-07 (training knowledge, could not live-verify openai.com)
|
|
62
|
+
'gpt-4o': { input_per_1m: 2.50, output_per_1m: 10.00 },
|
|
63
|
+
'gpt-4o-mini': { input_per_1m: 0.15, output_per_1m: 0.60 },
|
|
64
|
+
'gpt-4.1': { input_per_1m: 2.00, output_per_1m: 8.00 },
|
|
65
|
+
'gpt-4.1-mini': { input_per_1m: 0.40, output_per_1m: 1.60 },
|
|
66
|
+
'gpt-4.1-nano': { input_per_1m: 0.10, output_per_1m: 0.40 },
|
|
67
|
+
'o3': { input_per_1m: 2.00, output_per_1m: 8.00 },
|
|
68
|
+
'o3-mini': { input_per_1m: 1.10, output_per_1m: 4.40 },
|
|
69
|
+
'o4-mini': { input_per_1m: 1.10, output_per_1m: 4.40 },
|
|
54
70
|
};
|
|
55
71
|
|
|
72
|
+
// Resolve cost rates: operator-supplied cost_rates override bundled defaults
|
|
73
|
+
function getCostRates(model, config) {
|
|
74
|
+
const operatorRates = config?.budget?.cost_rates;
|
|
75
|
+
if (operatorRates && typeof operatorRates === 'object' && operatorRates[model]) {
|
|
76
|
+
const r = operatorRates[model];
|
|
77
|
+
if (Number.isFinite(r.input_per_1m) && Number.isFinite(r.output_per_1m)) {
|
|
78
|
+
return r;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return BUNDLED_COST_RATES[model] || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
const RETRYABLE_ERROR_CLASSES = [
|
|
57
85
|
'rate_limited',
|
|
58
86
|
'network_failure',
|
|
@@ -405,7 +433,7 @@ function emptyUsageTotals() {
|
|
|
405
433
|
};
|
|
406
434
|
}
|
|
407
435
|
|
|
408
|
-
function usageFromTelemetry(provider, model, usage) {
|
|
436
|
+
function usageFromTelemetry(provider, model, usage, config) {
|
|
409
437
|
if (!usage || typeof usage !== 'object') return null;
|
|
410
438
|
|
|
411
439
|
let inputTokens = 0;
|
|
@@ -419,7 +447,7 @@ function usageFromTelemetry(provider, model, usage) {
|
|
|
419
447
|
outputTokens = Number.isFinite(usage.output_tokens) ? usage.output_tokens : 0;
|
|
420
448
|
}
|
|
421
449
|
|
|
422
|
-
const rates =
|
|
450
|
+
const rates = getCostRates(model, config);
|
|
423
451
|
const usd = rates
|
|
424
452
|
? (inputTokens / 1_000_000) * rates.input_per_1m + (outputTokens / 1_000_000) * rates.output_per_1m
|
|
425
453
|
: 0;
|
|
@@ -566,6 +594,7 @@ async function executeApiCall({
|
|
|
566
594
|
requestBody,
|
|
567
595
|
timeoutSeconds,
|
|
568
596
|
signal,
|
|
597
|
+
config,
|
|
569
598
|
}) {
|
|
570
599
|
const timeoutMs = timeoutSeconds * 1000;
|
|
571
600
|
const controller = new AbortController();
|
|
@@ -662,7 +691,7 @@ async function executeApiCall({
|
|
|
662
691
|
};
|
|
663
692
|
}
|
|
664
693
|
|
|
665
|
-
const usage = usageFromTelemetry(provider, model, responseData.usage);
|
|
694
|
+
const usage = usageFromTelemetry(provider, model, responseData.usage, config);
|
|
666
695
|
const extraction = extractTurnResult(responseData, provider);
|
|
667
696
|
|
|
668
697
|
if (!extraction.ok) {
|
|
@@ -730,9 +759,9 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
730
759
|
return { ok: false, error: `Runtime "${runtimeId}" is not an api_proxy runtime` };
|
|
731
760
|
}
|
|
732
761
|
|
|
733
|
-
// Enforce
|
|
734
|
-
if (role?.write_authority !== 'review_only') {
|
|
735
|
-
return { ok: false, error: `
|
|
762
|
+
// Enforce api_proxy restriction: review_only or proposed only
|
|
763
|
+
if (role?.write_authority !== 'review_only' && role?.write_authority !== 'proposed') {
|
|
764
|
+
return { ok: false, error: `api_proxy only supports review_only and proposed roles (got "${role?.write_authority}")` };
|
|
736
765
|
}
|
|
737
766
|
|
|
738
767
|
// Read dispatch bundle
|
|
@@ -869,6 +898,7 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
869
898
|
requestBody,
|
|
870
899
|
timeoutSeconds,
|
|
871
900
|
signal,
|
|
901
|
+
config,
|
|
872
902
|
});
|
|
873
903
|
|
|
874
904
|
const attemptCompletedAt = new Date().toISOString();
|
|
@@ -1170,5 +1200,7 @@ export {
|
|
|
1170
1200
|
buildOpenAiRequest,
|
|
1171
1201
|
classifyError,
|
|
1172
1202
|
classifyHttpError,
|
|
1173
|
-
|
|
1203
|
+
BUNDLED_COST_RATES,
|
|
1204
|
+
BUNDLED_COST_RATES as COST_RATES, // backward compat alias
|
|
1205
|
+
getCostRates,
|
|
1174
1206
|
};
|