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.
@@ -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
+ }
@@ -57,13 +57,18 @@ function printProtocolReport(report) {
57
57
  : tier.status === 'skipped'
58
58
  ? chalk.yellow('skipped')
59
59
  : chalk.red(tier.status);
60
- console.log(` ${tierKey}: ${label} (${tier.fixtures_passed}/${tier.fixtures_run} passed)`);
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(` - ${failure.fixture_id}: ${failure.message}`));
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(` - ${error.fixture_id}: ${error.message}`));
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" (others can be added behind the same interface)
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
- const inputTokens = Number.isFinite(usage.input_tokens) ? usage.input_tokens : 0;
395
- const outputTokens = Number.isFinite(usage.output_tokens) ? usage.output_tokens : 0;
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: buildAnthropicHeaders(apiKey),
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 = buildAnthropicRequest(promptMd, effectiveContextMd, model, maxOutputTokens);
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 extractTurnResult(responseData) {
1015
- if (!responseData?.content || !Array.isArray(responseData.content)) {
1016
- return { ok: false, error: 'API response has no content blocks' };
1017
- }
1018
-
1019
- const textBlock = responseData.content.find(b => b.type === 'text');
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 text = textBlock.text.trim();
1085
+ const trimmed = text.trim();
1025
1086
 
1026
- // Try parsing the entire response as JSON first
1027
1087
  try {
1028
- const parsed = JSON.parse(text);
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
- // Try extracting JSON from markdown code fences
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
- // Try finding JSON object boundaries
1050
- const jsonStart = text.indexOf('{');
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(text.slice(jsonStart, jsonEnd + 1));
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 { extractTurnResult, buildAnthropicRequest, classifyError, classifyHttpError, COST_RATES };
1167
+ export {
1168
+ extractTurnResult,
1169
+ buildAnthropicRequest,
1170
+ buildOpenAiRequest,
1171
+ classifyError,
1172
+ classifyHttpError,
1173
+ COST_RATES,
1174
+ };