agentxchain 2.80.0 → 2.81.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 +2 -0
- package/bin/agentxchain.js +12 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +121 -0
- package/src/commands/replay.js +120 -0
- package/src/lib/accepted-turn-history.js +84 -0
package/README.md
CHANGED
|
@@ -167,6 +167,8 @@ agentxchain step
|
|
|
167
167
|
| `approve-completion` | Approve a pending human-gated run completion |
|
|
168
168
|
| `validate` | Validate governed kickoff wiring, a staged turn, or both |
|
|
169
169
|
| `template validate` | Prove the template registry, workflow-kit scaffold contract, and planning artifact completeness (`--json` exposes a `workflow_kit` block) |
|
|
170
|
+
| `verify turn` | Replay a staged turn's declared machine-evidence commands to confirm reproducibility before acceptance |
|
|
171
|
+
| `replay turn` | Replay an accepted turn's machine-evidence commands from history for audit and drift detection |
|
|
170
172
|
| `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
|
|
171
173
|
| `dashboard` | Open the local governance dashboard in your browser for repo-local runs or multi-repo coordinator initiatives, including pending gate approvals |
|
|
172
174
|
| `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
|
package/bin/agentxchain.js
CHANGED
|
@@ -60,6 +60,7 @@ import { doctorCommand } from '../src/commands/doctor.js';
|
|
|
60
60
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
61
61
|
import { validateCommand } from '../src/commands/validate.js';
|
|
62
62
|
import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
63
|
+
import { replayTurnCommand } from '../src/commands/replay.js';
|
|
63
64
|
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
64
65
|
import { rebindCommand } from '../src/commands/rebind.js';
|
|
65
66
|
import { branchCommand } from '../src/commands/branch.js';
|
|
@@ -388,6 +389,17 @@ verifyCmd
|
|
|
388
389
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
389
390
|
.action(verifyExportCommand);
|
|
390
391
|
|
|
392
|
+
const replayCmd = program
|
|
393
|
+
.command('replay')
|
|
394
|
+
.description('Replay accepted governed evidence against the current workspace');
|
|
395
|
+
|
|
396
|
+
replayCmd
|
|
397
|
+
.command('turn [turn_id]')
|
|
398
|
+
.description('Replay an accepted turn\'s declared machine-evidence commands from history')
|
|
399
|
+
.option('-j, --json', 'Output as JSON')
|
|
400
|
+
.option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
|
|
401
|
+
.action(replayTurnCommand);
|
|
402
|
+
|
|
391
403
|
program
|
|
392
404
|
.command('migrate')
|
|
393
405
|
.description('Migrate a legacy v3 project to governed format')
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getWatchPid } from './watch.js';
|
|
|
8
8
|
import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
|
|
9
9
|
import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
|
|
10
10
|
import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
|
|
11
|
+
import { PLUGIN_MANIFEST_FILE } from '../lib/plugins.js';
|
|
11
12
|
|
|
12
13
|
export async function doctorCommand(opts = {}) {
|
|
13
14
|
const root = findProjectRoot(process.cwd());
|
|
@@ -74,6 +75,7 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
74
75
|
const check = checkRuntimeReachable(rtId, rt);
|
|
75
76
|
checks.push(check);
|
|
76
77
|
}
|
|
78
|
+
const connectorProbe = getConnectorProbeRecommendation(runtimes);
|
|
77
79
|
|
|
78
80
|
// 4. State directory
|
|
79
81
|
const stateDir = join(root, '.agentxchain');
|
|
@@ -130,6 +132,90 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
// 8. Installed plugin health (only when plugins are installed)
|
|
136
|
+
const installedPlugins = rawConfig.plugins || {};
|
|
137
|
+
const pluginNames = Object.keys(installedPlugins);
|
|
138
|
+
if (pluginNames.length > 0) {
|
|
139
|
+
for (const pluginName of pluginNames) {
|
|
140
|
+
const meta = installedPlugins[pluginName];
|
|
141
|
+
const checkId = `plugin_${pluginName.replace(/[^a-z0-9_-]/gi, '_')}`;
|
|
142
|
+
|
|
143
|
+
// Check install path exists
|
|
144
|
+
if (!meta.install_path) {
|
|
145
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: 'No install_path recorded', plugin_name: pluginName });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const installAbsPath = join(root, meta.install_path);
|
|
149
|
+
if (!existsSync(installAbsPath)) {
|
|
150
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Install path missing: ${meta.install_path}`, plugin_name: pluginName });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check manifest exists and is valid
|
|
155
|
+
const manifestPath = join(installAbsPath, PLUGIN_MANIFEST_FILE);
|
|
156
|
+
if (!existsSync(manifestPath)) {
|
|
157
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: 'Manifest file missing', plugin_name: pluginName });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
let manifest;
|
|
161
|
+
try {
|
|
162
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Manifest is corrupt JSON: ${err.message}`, plugin_name: pluginName });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check hook files exist
|
|
169
|
+
const hookErrors = [];
|
|
170
|
+
if (manifest.hooks && typeof manifest.hooks === 'object') {
|
|
171
|
+
for (const [hookName, hookDef] of Object.entries(manifest.hooks)) {
|
|
172
|
+
if (!hookDef) continue;
|
|
173
|
+
const commands = Array.isArray(hookDef) ? hookDef : (hookDef.command ? [hookDef] : []);
|
|
174
|
+
for (const cmd of commands) {
|
|
175
|
+
const cmdArgs = cmd.command || cmd;
|
|
176
|
+
if (Array.isArray(cmdArgs) && cmdArgs.length > 0) {
|
|
177
|
+
const firstArg = cmdArgs[0];
|
|
178
|
+
if (typeof firstArg === 'string' && (firstArg.startsWith('./') || firstArg.startsWith('../'))) {
|
|
179
|
+
const hookFilePath = join(installAbsPath, firstArg);
|
|
180
|
+
if (!existsSync(hookFilePath)) {
|
|
181
|
+
hookErrors.push(`${hookName}: ${firstArg}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (hookErrors.length > 0) {
|
|
189
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Missing hook files: ${hookErrors.join(', ')}`, plugin_name: pluginName });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check config env vars (warn only)
|
|
194
|
+
const envWarnings = [];
|
|
195
|
+
const pluginConfig = meta.config || {};
|
|
196
|
+
for (const [key, value] of Object.entries(pluginConfig)) {
|
|
197
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
198
|
+
const envVar = value.slice(1);
|
|
199
|
+
if (!process.env[envVar]) {
|
|
200
|
+
envWarnings.push(envVar);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Also check webhook_env pattern from config
|
|
205
|
+
if (pluginConfig.webhook_env && !process.env[pluginConfig.webhook_env]) {
|
|
206
|
+
if (!envWarnings.includes(pluginConfig.webhook_env)) {
|
|
207
|
+
envWarnings.push(pluginConfig.webhook_env);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (envWarnings.length > 0) {
|
|
212
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'warn', detail: `Env var(s) not set: ${envWarnings.join(', ')}`, plugin_name: pluginName });
|
|
213
|
+
} else {
|
|
214
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'pass', detail: `v${manifest.version || '?'}, ${Object.keys(manifest.hooks || {}).length} hooks`, plugin_name: pluginName });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
133
219
|
// Compute summary
|
|
134
220
|
const failCount = checks.filter(c => c.level === 'fail').length;
|
|
135
221
|
const warnCount = checks.filter(c => c.level === 'warn').length;
|
|
@@ -143,6 +229,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
143
229
|
...versionSurface,
|
|
144
230
|
config_version: versionSurface.config_generation,
|
|
145
231
|
overall,
|
|
232
|
+
connector_probe_recommended: connectorProbe.recommended,
|
|
233
|
+
connector_probe_runtime_ids: connectorProbe.runtimeIds,
|
|
234
|
+
connector_probe_detail: connectorProbe.detail,
|
|
146
235
|
checks,
|
|
147
236
|
fail_count: failCount,
|
|
148
237
|
warn_count: warnCount,
|
|
@@ -173,6 +262,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
173
262
|
} else {
|
|
174
263
|
console.log(chalk.red(` Not ready: ${failCount} failure${failCount > 1 ? 's' : ''}, ${warnCount} warning${warnCount > 1 ? 's' : ''}.`));
|
|
175
264
|
}
|
|
265
|
+
if (failCount === 0 && connectorProbe.recommended) {
|
|
266
|
+
console.log(chalk.dim(` Next: ${connectorProbe.detail}`));
|
|
267
|
+
}
|
|
176
268
|
console.log('');
|
|
177
269
|
}
|
|
178
270
|
|
|
@@ -236,6 +328,35 @@ function checkRuntimeReachable(rtId, rt) {
|
|
|
236
328
|
}
|
|
237
329
|
}
|
|
238
330
|
|
|
331
|
+
function getConnectorProbeRecommendation(runtimes) {
|
|
332
|
+
const runtimeIds = [];
|
|
333
|
+
|
|
334
|
+
for (const [rtId, rt] of Object.entries(runtimes || {})) {
|
|
335
|
+
if (!rt || typeof rt !== 'object') continue;
|
|
336
|
+
if (rt.type === 'api_proxy' || rt.type === 'remote_agent') {
|
|
337
|
+
runtimeIds.push(rtId);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (rt.type === 'mcp' && (rt.transport || 'stdio') === 'streamable_http') {
|
|
341
|
+
runtimeIds.push(rtId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (runtimeIds.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
recommended: false,
|
|
348
|
+
runtimeIds: [],
|
|
349
|
+
detail: null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
recommended: true,
|
|
355
|
+
runtimeIds,
|
|
356
|
+
detail: 'run `agentxchain connector check` to live-probe api / remote HTTP runtimes before the first governed turn.',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
239
360
|
function getCurrentPhase(root) {
|
|
240
361
|
const statePath = join(root, '.agentxchain', 'state.json');
|
|
241
362
|
if (!existsSync(statePath)) return null;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { loadProjectContext } from '../lib/config.js';
|
|
4
|
+
import { normalizeVerification } from '../lib/repo-observer.js';
|
|
5
|
+
import { resolveAcceptedTurnHistoryReference } from '../lib/accepted-turn-history.js';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
|
|
8
|
+
replayVerificationMachineEvidence,
|
|
9
|
+
} from '../lib/verification-replay.js';
|
|
10
|
+
|
|
11
|
+
export async function replayTurnCommand(turnId, opts = {}) {
|
|
12
|
+
const context = loadProjectContext();
|
|
13
|
+
if (!context) {
|
|
14
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (context.config.protocol_mode !== 'governed' || context.version !== 4) {
|
|
19
|
+
console.log(chalk.red('replay turn is only available in governed v4 projects.'));
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const timeoutMs = Number.parseInt(String(opts.timeout || String(DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS)), 10);
|
|
24
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
25
|
+
console.log(chalk.red('replay turn requires a positive integer --timeout in milliseconds.'));
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { root, config } = context;
|
|
30
|
+
const resolved = resolveAcceptedTurnHistoryReference(root, turnId);
|
|
31
|
+
if (!resolved.ok) {
|
|
32
|
+
console.log(chalk.red(resolved.error));
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const entry = resolved.entry;
|
|
37
|
+
const runtimeType = config.runtimes?.[entry.runtime_id]?.type || 'unknown';
|
|
38
|
+
const payload = {
|
|
39
|
+
source: 'history',
|
|
40
|
+
match_kind: resolved.match_kind,
|
|
41
|
+
turn_id: entry.turn_id,
|
|
42
|
+
resolved_turn_id: resolved.resolved_ref,
|
|
43
|
+
run_id: entry.run_id || null,
|
|
44
|
+
role: entry.role || null,
|
|
45
|
+
phase: entry.phase || null,
|
|
46
|
+
runtime_id: entry.runtime_id || null,
|
|
47
|
+
runtime_type: runtimeType,
|
|
48
|
+
accepted_at: entry.accepted_at || null,
|
|
49
|
+
declared_status: entry.verification?.status || 'skipped',
|
|
50
|
+
normalized_status: normalizeVerification(entry.verification, runtimeType).status,
|
|
51
|
+
timeout_ms: timeoutMs,
|
|
52
|
+
prior_verification_replay: entry.verification_replay || null,
|
|
53
|
+
...replayVerificationMachineEvidence({
|
|
54
|
+
root,
|
|
55
|
+
verification: entry.verification,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
emitReplayTurn(payload, opts.json);
|
|
61
|
+
process.exit(payload.overall === 'match' ? 0 : 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function emitReplayTurn(payload, jsonMode) {
|
|
65
|
+
if (jsonMode) {
|
|
66
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(chalk.bold(` Replay Turn: ${chalk.cyan(payload.turn_id)}`));
|
|
72
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
73
|
+
console.log(` ${chalk.dim('Source:')} accepted history (${payload.match_kind})`);
|
|
74
|
+
console.log(` ${chalk.dim('Run:')} ${payload.run_id || '—'}`);
|
|
75
|
+
console.log(` ${chalk.dim('Role:')} ${payload.role || '—'}`);
|
|
76
|
+
console.log(` ${chalk.dim('Phase:')} ${payload.phase || '—'}`);
|
|
77
|
+
console.log(` ${chalk.dim('Runtime:')} ${payload.runtime_id || '—'} (${payload.runtime_type})`);
|
|
78
|
+
console.log(` ${chalk.dim('Accepted:')} ${payload.accepted_at || '—'}`);
|
|
79
|
+
console.log(` ${chalk.dim('Declared:')} ${payload.declared_status}`);
|
|
80
|
+
console.log(` ${chalk.dim('Normalized:')} ${payload.normalized_status}`);
|
|
81
|
+
if (payload.prior_verification_replay) {
|
|
82
|
+
const prior = payload.prior_verification_replay;
|
|
83
|
+
const verifiedAt = prior.verified_at ? ` at ${prior.verified_at}` : '';
|
|
84
|
+
console.log(` ${chalk.dim('Prior replay:')} ${prior.overall} (${prior.matched_commands || 0}/${prior.replayed_commands || 0})${verifiedAt}`);
|
|
85
|
+
}
|
|
86
|
+
console.log(` ${chalk.dim('Outcome:')} ${formatOutcome(payload.overall)}`);
|
|
87
|
+
|
|
88
|
+
if (payload.reason) {
|
|
89
|
+
console.log(` ${chalk.dim('Reason:')} ${payload.reason}`);
|
|
90
|
+
console.log('');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('');
|
|
95
|
+
for (const command of payload.commands || []) {
|
|
96
|
+
const marker = command.matched ? chalk.green('match') : chalk.red('mismatch');
|
|
97
|
+
console.log(` [${marker}] ${command.command}`);
|
|
98
|
+
console.log(` declared=${command.declared_exit_code} actual=${command.actual_exit_code == null ? 'null' : command.actual_exit_code}`);
|
|
99
|
+
if (command.signal) {
|
|
100
|
+
console.log(` signal=${command.signal}`);
|
|
101
|
+
}
|
|
102
|
+
if (command.timed_out) {
|
|
103
|
+
console.log(' timed_out=true');
|
|
104
|
+
}
|
|
105
|
+
if (command.error) {
|
|
106
|
+
console.log(` error=${command.error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(chalk.dim(' Replay uses the current workspace and shell environment. It verifies declared exit-code reproducibility, not historical stdout/stderr identity.'));
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatOutcome(outcome) {
|
|
116
|
+
if (outcome === 'match') return chalk.green('match');
|
|
117
|
+
if (outcome === 'mismatch') return chalk.red('mismatch');
|
|
118
|
+
return chalk.yellow('not_reproducible');
|
|
119
|
+
}
|
|
120
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
5
|
+
|
|
6
|
+
export function queryAcceptedTurnHistory(root) {
|
|
7
|
+
const filePath = join(root, HISTORY_PATH);
|
|
8
|
+
if (!existsSync(filePath)) return [];
|
|
9
|
+
|
|
10
|
+
let content;
|
|
11
|
+
try {
|
|
12
|
+
content = readFileSync(filePath, 'utf8').trim();
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!content) return [];
|
|
18
|
+
|
|
19
|
+
return content
|
|
20
|
+
.split('\n')
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((line) => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(line);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.filter((entry) => entry && typeof entry.turn_id === 'string')
|
|
30
|
+
.reverse();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveAcceptedTurnHistoryReference(root, ref) {
|
|
34
|
+
const entries = queryAcceptedTurnHistory(root);
|
|
35
|
+
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: 'No accepted turn history found. Accept at least one governed turn first.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!ref) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
entry: entries[0],
|
|
47
|
+
resolved_ref: entries[0].turn_id,
|
|
48
|
+
match_kind: 'latest',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const exact = entries.find((entry) => entry.turn_id === ref);
|
|
53
|
+
if (exact) {
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
entry: exact,
|
|
57
|
+
resolved_ref: exact.turn_id,
|
|
58
|
+
match_kind: 'exact',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prefixMatches = entries.filter((entry) => entry.turn_id.startsWith(ref));
|
|
63
|
+
if (prefixMatches.length === 1) {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
entry: prefixMatches[0],
|
|
67
|
+
resolved_ref: prefixMatches[0].turn_id,
|
|
68
|
+
match_kind: 'prefix',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (prefixMatches.length > 1) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: `Turn reference "${ref}" is ambiguous. Matches: ${prefixMatches.map((entry) => entry.turn_id).join(', ')}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `Accepted turn ${ref} not found in history.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|