agentxchain 2.1.1 → 2.3.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 +41 -9
- package/bin/agentxchain.js +89 -0
- package/package.json +7 -2
- package/scripts/publish-from-tag.sh +14 -9
- package/scripts/release-postflight.sh +42 -2
- package/src/commands/intake-approve.js +44 -0
- package/src/commands/intake-plan.js +62 -0
- package/src/commands/intake-record.js +86 -0
- package/src/commands/intake-resolve.js +45 -0
- package/src/commands/intake-scan.js +87 -0
- package/src/commands/intake-start.js +53 -0
- package/src/commands/intake-status.js +113 -0
- package/src/commands/intake-triage.js +54 -0
- package/src/commands/verify.js +8 -3
- package/src/lib/adapters/api-proxy-adapter.js +125 -27
- package/src/lib/intake.js +924 -0
- package/src/lib/normalized-config.js +10 -0
- package/src/lib/protocol-conformance.js +28 -4
- package/src/lib/reference-conformance-adapter.js +141 -0
- package/src/lib/repo-observer.js +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { findProjectRoot } from '../lib/config.js';
|
|
3
|
+
import { startIntent } from '../lib/intake.js';
|
|
4
|
+
|
|
5
|
+
export async function intakeStartCommand(opts) {
|
|
6
|
+
const root = findProjectRoot(process.cwd());
|
|
7
|
+
if (!root) {
|
|
8
|
+
if (opts.json) {
|
|
9
|
+
console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
|
|
10
|
+
} else {
|
|
11
|
+
console.log(chalk.red('agentxchain.json not found'));
|
|
12
|
+
}
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!opts.intent) {
|
|
17
|
+
const msg = '--intent <id> is required';
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
|
|
20
|
+
} else {
|
|
21
|
+
console.log(chalk.red(msg));
|
|
22
|
+
}
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = startIntent(root, opts.intent, {
|
|
27
|
+
role: opts.role || undefined,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (opts.json) {
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
} else if (result.ok) {
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.green(` Started intent ${result.intent.intent_id}`));
|
|
35
|
+
console.log(` Run: ${result.run_id}`);
|
|
36
|
+
console.log(` Turn: ${result.turn_id}`);
|
|
37
|
+
console.log(` Role: ${result.role}`);
|
|
38
|
+
console.log(chalk.dim(` Status: planned \u2192 executing`));
|
|
39
|
+
console.log('');
|
|
40
|
+
} else if (result.missing) {
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(chalk.red(` Cannot start intent ${opts.intent}`));
|
|
43
|
+
console.log(chalk.red(' Recorded planning artifacts are missing on disk:'));
|
|
44
|
+
for (const m of result.missing) {
|
|
45
|
+
console.log(chalk.red(` ${m}`));
|
|
46
|
+
}
|
|
47
|
+
console.log('');
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.red(` ${result.error}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.exit(result.exitCode);
|
|
53
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { findProjectRoot } from '../lib/config.js';
|
|
3
|
+
import { intakeStatus } from '../lib/intake.js';
|
|
4
|
+
|
|
5
|
+
export async function intakeStatusCommand(opts) {
|
|
6
|
+
const root = findProjectRoot(process.cwd());
|
|
7
|
+
if (!root) {
|
|
8
|
+
if (opts.json) {
|
|
9
|
+
console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
|
|
10
|
+
} else {
|
|
11
|
+
console.log(chalk.red('agentxchain.json not found'));
|
|
12
|
+
}
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = intakeStatus(root, opts.intent || null);
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify(result, null, 2));
|
|
20
|
+
process.exit(result.exitCode);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
console.log(chalk.red(` ${result.error}`));
|
|
25
|
+
process.exit(result.exitCode);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Detail mode: single intent
|
|
29
|
+
if (result.intent) {
|
|
30
|
+
printIntentDetail(result.intent, result.event);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// List mode: summary
|
|
35
|
+
printSummary(result.summary);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printSummary(summary) {
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(chalk.bold(' AgentXchain Intake Status'));
|
|
42
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
43
|
+
console.log(` ${chalk.dim('Events:')} ${summary.total_events}`);
|
|
44
|
+
|
|
45
|
+
const statusParts = Object.entries(summary.by_status)
|
|
46
|
+
.map(([s, c]) => `${s}: ${c}`)
|
|
47
|
+
.join(', ');
|
|
48
|
+
console.log(` ${chalk.dim('Intents:')} ${summary.total_intents} (${statusParts || 'none'})`);
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
if (summary.intents.length > 0) {
|
|
52
|
+
console.log(chalk.dim(' Recent Intents:'));
|
|
53
|
+
for (const i of summary.intents.slice(0, 20)) {
|
|
54
|
+
const pri = i.priority ? i.priority.padEnd(3) : '---';
|
|
55
|
+
const tpl = (i.template || '---').padEnd(12);
|
|
56
|
+
const st = statusColor(i.status);
|
|
57
|
+
console.log(` ${chalk.dim(i.intent_id)} ${pri} ${tpl} ${st} ${chalk.dim(i.updated_at)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printIntentDetail(intent, event) {
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(chalk.bold(` Intent: ${intent.intent_id}`));
|
|
67
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
68
|
+
console.log(` ${chalk.dim('Status:')} ${statusColor(intent.status)}`);
|
|
69
|
+
console.log(` ${chalk.dim('Event:')} ${intent.event_id}`);
|
|
70
|
+
console.log(` ${chalk.dim('Priority:')} ${intent.priority || '—'}`);
|
|
71
|
+
console.log(` ${chalk.dim('Template:')} ${intent.template || '—'}`);
|
|
72
|
+
console.log(` ${chalk.dim('Charter:')} ${intent.charter || '—'}`);
|
|
73
|
+
console.log(` ${chalk.dim('Created:')} ${intent.created_at}`);
|
|
74
|
+
console.log(` ${chalk.dim('Updated:')} ${intent.updated_at}`);
|
|
75
|
+
|
|
76
|
+
if (intent.acceptance_contract && intent.acceptance_contract.length > 0) {
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(chalk.dim(' Acceptance Contract:'));
|
|
79
|
+
for (const a of intent.acceptance_contract) {
|
|
80
|
+
console.log(` - ${a}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (intent.history && intent.history.length > 0) {
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(chalk.dim(' History:'));
|
|
87
|
+
for (const h of intent.history) {
|
|
88
|
+
const from = h.from || '(new)';
|
|
89
|
+
console.log(` ${chalk.dim(h.at)} ${from} → ${h.to} ${chalk.dim(h.reason)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (event) {
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(chalk.dim(' Source Event:'));
|
|
96
|
+
console.log(` ${chalk.dim('Source:')} ${event.source}`);
|
|
97
|
+
console.log(` ${chalk.dim('Signal:')} ${JSON.stringify(event.signal)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function statusColor(status) {
|
|
104
|
+
switch (status) {
|
|
105
|
+
case 'detected': return chalk.yellow(status);
|
|
106
|
+
case 'triaged': return chalk.cyan(status);
|
|
107
|
+
case 'approved': return chalk.green(status);
|
|
108
|
+
case 'planned': return chalk.green(status);
|
|
109
|
+
case 'suppressed': return chalk.dim(status);
|
|
110
|
+
case 'rejected': return chalk.red(status);
|
|
111
|
+
default: return status;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { findProjectRoot } from '../lib/config.js';
|
|
3
|
+
import { triageIntent } from '../lib/intake.js';
|
|
4
|
+
|
|
5
|
+
export async function intakeTriageCommand(opts) {
|
|
6
|
+
const root = findProjectRoot(process.cwd());
|
|
7
|
+
if (!root) {
|
|
8
|
+
if (opts.json) {
|
|
9
|
+
console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
|
|
10
|
+
} else {
|
|
11
|
+
console.log(chalk.red('agentxchain.json not found'));
|
|
12
|
+
}
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!opts.intent) {
|
|
17
|
+
const msg = '--intent <id> is required';
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
|
|
20
|
+
} else {
|
|
21
|
+
console.log(chalk.red(msg));
|
|
22
|
+
}
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fields = {
|
|
27
|
+
suppress: opts.suppress || false,
|
|
28
|
+
reject: opts.reject || false,
|
|
29
|
+
reason: opts.reason || null,
|
|
30
|
+
priority: opts.priority || null,
|
|
31
|
+
template: opts.template || null,
|
|
32
|
+
charter: opts.charter || null,
|
|
33
|
+
acceptance_contract: opts.acceptance
|
|
34
|
+
? opts.acceptance.split(',').map(s => s.trim()).filter(Boolean)
|
|
35
|
+
: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = triageIntent(root, opts.intent, fields);
|
|
39
|
+
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
console.log(JSON.stringify(result, null, 2));
|
|
42
|
+
} else if (result.ok) {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.green(` Intent ${result.intent.intent_id} → ${result.intent.status}`));
|
|
45
|
+
if (result.intent.priority) {
|
|
46
|
+
console.log(chalk.dim(` Priority: ${result.intent.priority} Template: ${result.intent.template}`));
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.red(` ${result.error}`));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.exit(result.exitCode);
|
|
54
|
+
}
|
package/src/commands/verify.js
CHANGED
|
@@ -57,13 +57,18 @@ function printProtocolReport(report) {
|
|
|
57
57
|
: tier.status === 'skipped'
|
|
58
58
|
? chalk.yellow('skipped')
|
|
59
59
|
: chalk.red(tier.status);
|
|
60
|
-
|
|
60
|
+
const niCount = tier.fixtures_not_implemented || 0;
|
|
61
|
+
const niSuffix = niCount > 0 ? chalk.yellow(`, ${niCount} not implemented`) : '';
|
|
62
|
+
console.log(` ${tierKey}: ${label} (${tier.fixtures_passed}/${tier.fixtures_run} passed${niSuffix})`);
|
|
61
63
|
|
|
64
|
+
for (const ni of tier.not_implemented || []) {
|
|
65
|
+
console.log(chalk.yellow(` ○ ${ni.fixture_id}: ${ni.message}`));
|
|
66
|
+
}
|
|
62
67
|
for (const failure of tier.failures || []) {
|
|
63
|
-
console.log(chalk.red(`
|
|
68
|
+
console.log(chalk.red(` ✗ ${failure.fixture_id}: ${failure.message}`));
|
|
64
69
|
}
|
|
65
70
|
for (const error of tier.errors || []) {
|
|
66
|
-
console.log(chalk.red(`
|
|
71
|
+
console.log(chalk.red(` ✗ ${error.fixture_id}: ${error.message}`));
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* All error returns include a `classified` ApiProxyError object with
|
|
20
20
|
* error_class, recovery instructions, and retryable flag.
|
|
21
21
|
*
|
|
22
|
-
* Supported providers: "anthropic"
|
|
22
|
+
* Supported providers: "anthropic", "openai"
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
|
@@ -43,6 +43,7 @@ import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
|
|
|
43
43
|
// Provider endpoint registry
|
|
44
44
|
const PROVIDER_ENDPOINTS = {
|
|
45
45
|
anthropic: 'https://api.anthropic.com/v1/messages',
|
|
46
|
+
openai: 'https://api.openai.com/v1/chat/completions',
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
// Cost rates per million tokens (USD)
|
|
@@ -91,6 +92,21 @@ const PROVIDER_ERROR_MAPS = {
|
|
|
91
92
|
{ provider_error_type: 'api_error', http_status: 500, error_class: 'unknown_api_error', retryable: true },
|
|
92
93
|
],
|
|
93
94
|
},
|
|
95
|
+
openai: {
|
|
96
|
+
extractErrorType(body) {
|
|
97
|
+
return typeof body?.error?.type === 'string' ? body.error.type : null;
|
|
98
|
+
},
|
|
99
|
+
extractErrorCode(body) {
|
|
100
|
+
return typeof body?.error?.code === 'string' ? body.error.code : null;
|
|
101
|
+
},
|
|
102
|
+
mappings: [
|
|
103
|
+
{ provider_error_code: 'invalid_api_key', http_status: 401, error_class: 'auth_failure', retryable: false },
|
|
104
|
+
{ provider_error_code: 'model_not_found', http_status: 404, error_class: 'model_not_found', retryable: false },
|
|
105
|
+
{ provider_error_type: 'invalid_request_error', http_status: 400, body_pattern: /context|token.*limit|too.many.tokens/i, error_class: 'context_overflow', retryable: false },
|
|
106
|
+
{ provider_error_type: 'invalid_request_error', http_status: 400, error_class: 'invalid_request', retryable: false },
|
|
107
|
+
{ provider_error_type: 'rate_limit_error', http_status: 429, error_class: 'rate_limited', retryable: true },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
94
110
|
};
|
|
95
111
|
|
|
96
112
|
// ── Error classification ──────────────────────────────────────────────────────
|
|
@@ -261,7 +277,8 @@ function classifyProviderHttpError(status, body, provider, model, authEnv) {
|
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
for (const mapping of providerMap.mappings) {
|
|
264
|
-
if (mapping.provider_error_type !== providerErrorType) continue;
|
|
280
|
+
if (mapping.provider_error_type && mapping.provider_error_type !== providerErrorType) continue;
|
|
281
|
+
if (mapping.provider_error_code && mapping.provider_error_code !== providerErrorCode) continue;
|
|
265
282
|
if (!httpStatusMatches(mapping.http_status, status)) continue;
|
|
266
283
|
if (mapping.body_pattern && !mapping.body_pattern.test(body)) continue;
|
|
267
284
|
return {
|
|
@@ -388,11 +405,20 @@ function emptyUsageTotals() {
|
|
|
388
405
|
};
|
|
389
406
|
}
|
|
390
407
|
|
|
391
|
-
function usageFromTelemetry(model, usage) {
|
|
408
|
+
function usageFromTelemetry(provider, model, usage) {
|
|
392
409
|
if (!usage || typeof usage !== 'object') return null;
|
|
393
410
|
|
|
394
|
-
|
|
395
|
-
|
|
411
|
+
let inputTokens = 0;
|
|
412
|
+
let outputTokens = 0;
|
|
413
|
+
|
|
414
|
+
if (provider === 'openai') {
|
|
415
|
+
inputTokens = Number.isFinite(usage.prompt_tokens) ? usage.prompt_tokens : 0;
|
|
416
|
+
outputTokens = Number.isFinite(usage.completion_tokens) ? usage.completion_tokens : 0;
|
|
417
|
+
} else {
|
|
418
|
+
inputTokens = Number.isFinite(usage.input_tokens) ? usage.input_tokens : 0;
|
|
419
|
+
outputTokens = Number.isFinite(usage.output_tokens) ? usage.output_tokens : 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
396
422
|
const rates = COST_RATES[model];
|
|
397
423
|
const usd = rates
|
|
398
424
|
? (inputTokens / 1_000_000) * rates.input_per_1m + (outputTokens / 1_000_000) * rates.output_per_1m
|
|
@@ -567,7 +593,7 @@ async function executeApiCall({
|
|
|
567
593
|
try {
|
|
568
594
|
response = await fetch(endpoint, {
|
|
569
595
|
method: 'POST',
|
|
570
|
-
headers:
|
|
596
|
+
headers: buildProviderHeaders(provider, apiKey),
|
|
571
597
|
body: JSON.stringify(requestBody),
|
|
572
598
|
signal: controller.signal,
|
|
573
599
|
});
|
|
@@ -636,8 +662,8 @@ async function executeApiCall({
|
|
|
636
662
|
};
|
|
637
663
|
}
|
|
638
664
|
|
|
639
|
-
const usage = usageFromTelemetry(model, responseData.usage);
|
|
640
|
-
const extraction = extractTurnResult(responseData);
|
|
665
|
+
const usage = usageFromTelemetry(provider, model, responseData.usage);
|
|
666
|
+
const extraction = extractTurnResult(responseData, provider);
|
|
641
667
|
|
|
642
668
|
if (!extraction.ok) {
|
|
643
669
|
return {
|
|
@@ -788,7 +814,7 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
788
814
|
}
|
|
789
815
|
}
|
|
790
816
|
|
|
791
|
-
const requestBody =
|
|
817
|
+
const requestBody = buildProviderRequest(provider, promptMd, effectiveContextMd, model, maxOutputTokens);
|
|
792
818
|
|
|
793
819
|
// Persist request metadata for auditability
|
|
794
820
|
const dispatchDir = join(root, getDispatchTurnDir(turn.turn_id));
|
|
@@ -1007,25 +1033,59 @@ function buildAnthropicRequest(promptMd, contextMd, model, maxOutputTokens) {
|
|
|
1007
1033
|
};
|
|
1008
1034
|
}
|
|
1009
1035
|
|
|
1036
|
+
function buildOpenAiHeaders(apiKey) {
|
|
1037
|
+
return {
|
|
1038
|
+
'Content-Type': 'application/json',
|
|
1039
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function buildOpenAiRequest(promptMd, contextMd, model, maxOutputTokens) {
|
|
1044
|
+
const userContent = contextMd
|
|
1045
|
+
? `${promptMd}${SEPARATOR}${contextMd}`
|
|
1046
|
+
: promptMd;
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
model,
|
|
1050
|
+
max_completion_tokens: maxOutputTokens,
|
|
1051
|
+
response_format: { type: 'json_object' },
|
|
1052
|
+
messages: [
|
|
1053
|
+
{ role: 'developer', content: SYSTEM_PROMPT },
|
|
1054
|
+
{ role: 'user', content: userContent },
|
|
1055
|
+
],
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function buildProviderHeaders(provider, apiKey) {
|
|
1060
|
+
if (provider === 'openai') {
|
|
1061
|
+
return buildOpenAiHeaders(apiKey);
|
|
1062
|
+
}
|
|
1063
|
+
return buildAnthropicHeaders(apiKey);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function buildProviderRequest(provider, promptMd, contextMd, model, maxOutputTokens) {
|
|
1067
|
+
if (provider === 'openai') {
|
|
1068
|
+
return buildOpenAiRequest(promptMd, contextMd, model, maxOutputTokens);
|
|
1069
|
+
}
|
|
1070
|
+
return buildAnthropicRequest(promptMd, contextMd, model, maxOutputTokens);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1010
1073
|
/**
|
|
1011
1074
|
* Extract structured turn result JSON from an Anthropic API response.
|
|
1012
1075
|
* Looks for JSON in the first text content block.
|
|
1013
1076
|
*/
|
|
1014
|
-
function
|
|
1015
|
-
if (
|
|
1016
|
-
return {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
if (!textBlock?.text) {
|
|
1021
|
-
return { ok: false, error: 'API response has no text content block' };
|
|
1077
|
+
function extractTurnResultFromText(text) {
|
|
1078
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
1079
|
+
return {
|
|
1080
|
+
ok: false,
|
|
1081
|
+
error: 'Could not extract structured turn result JSON from API response. The model did not return valid turn result JSON.',
|
|
1082
|
+
};
|
|
1022
1083
|
}
|
|
1023
1084
|
|
|
1024
|
-
const
|
|
1085
|
+
const trimmed = text.trim();
|
|
1025
1086
|
|
|
1026
|
-
// Try parsing the entire response as JSON first
|
|
1027
1087
|
try {
|
|
1028
|
-
const parsed = JSON.parse(
|
|
1088
|
+
const parsed = JSON.parse(trimmed);
|
|
1029
1089
|
if (parsed && typeof parsed === 'object' && parsed.schema_version) {
|
|
1030
1090
|
return { ok: true, turnResult: parsed };
|
|
1031
1091
|
}
|
|
@@ -1033,8 +1093,7 @@ function extractTurnResult(responseData) {
|
|
|
1033
1093
|
// Not pure JSON — try extracting from markdown fences
|
|
1034
1094
|
}
|
|
1035
1095
|
|
|
1036
|
-
|
|
1037
|
-
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
1096
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
1038
1097
|
if (fenceMatch) {
|
|
1039
1098
|
try {
|
|
1040
1099
|
const parsed = JSON.parse(fenceMatch[1].trim());
|
|
@@ -1046,12 +1105,11 @@ function extractTurnResult(responseData) {
|
|
|
1046
1105
|
}
|
|
1047
1106
|
}
|
|
1048
1107
|
|
|
1049
|
-
|
|
1050
|
-
const
|
|
1051
|
-
const jsonEnd = text.lastIndexOf('}');
|
|
1108
|
+
const jsonStart = trimmed.indexOf('{');
|
|
1109
|
+
const jsonEnd = trimmed.lastIndexOf('}');
|
|
1052
1110
|
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
|
1053
1111
|
try {
|
|
1054
|
-
const parsed = JSON.parse(
|
|
1112
|
+
const parsed = JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1));
|
|
1055
1113
|
if (parsed && typeof parsed === 'object' && parsed.schema_version) {
|
|
1056
1114
|
return { ok: true, turnResult: parsed };
|
|
1057
1115
|
}
|
|
@@ -1066,6 +1124,39 @@ function extractTurnResult(responseData) {
|
|
|
1066
1124
|
};
|
|
1067
1125
|
}
|
|
1068
1126
|
|
|
1127
|
+
function extractAnthropicTurnResult(responseData) {
|
|
1128
|
+
if (!responseData?.content || !Array.isArray(responseData.content)) {
|
|
1129
|
+
return { ok: false, error: 'API response has no content blocks' };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const textBlock = responseData.content.find(b => b.type === 'text');
|
|
1133
|
+
if (!textBlock?.text) {
|
|
1134
|
+
return { ok: false, error: 'API response has no text content block' };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return extractTurnResultFromText(textBlock.text);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function extractOpenAiTurnResult(responseData) {
|
|
1141
|
+
if (!Array.isArray(responseData?.choices) || responseData.choices.length === 0) {
|
|
1142
|
+
return { ok: false, error: 'API response has no choices' };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const content = responseData.choices[0]?.message?.content;
|
|
1146
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
1147
|
+
return { ok: false, error: 'API response has no message content' };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return extractTurnResultFromText(content);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function extractTurnResult(responseData, provider = 'anthropic') {
|
|
1154
|
+
if (provider === 'openai') {
|
|
1155
|
+
return extractOpenAiTurnResult(responseData);
|
|
1156
|
+
}
|
|
1157
|
+
return extractAnthropicTurnResult(responseData);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1069
1160
|
function resolveTargetTurn(state, turnId) {
|
|
1070
1161
|
if (turnId && state?.active_turns?.[turnId]) {
|
|
1071
1162
|
return state.active_turns[turnId];
|
|
@@ -1073,4 +1164,11 @@ function resolveTargetTurn(state, turnId) {
|
|
|
1073
1164
|
return state?.current_turn || Object.values(state?.active_turns || {})[0];
|
|
1074
1165
|
}
|
|
1075
1166
|
|
|
1076
|
-
export {
|
|
1167
|
+
export {
|
|
1168
|
+
extractTurnResult,
|
|
1169
|
+
buildAnthropicRequest,
|
|
1170
|
+
buildOpenAiRequest,
|
|
1171
|
+
classifyError,
|
|
1172
|
+
classifyHttpError,
|
|
1173
|
+
COST_RATES,
|
|
1174
|
+
};
|