agentxchain 2.35.0 → 2.37.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/dashboard/app.js +2 -1
- package/dashboard/components/timeline.js +37 -1
- package/package.json +1 -1
- package/scripts/release-bump.sh +47 -9
- package/src/commands/status.js +1 -27
- package/src/lib/adapters/api-proxy-adapter.js +130 -3
- package/src/lib/continuity-status.js +27 -0
- package/src/lib/dashboard/state-reader.js +11 -1
- package/src/lib/normalized-config.js +1 -1
package/dashboard/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { render as renderBlockers } from './components/blockers.js';
|
|
|
16
16
|
import { render as renderArtifacts } from './components/artifacts.js';
|
|
17
17
|
|
|
18
18
|
const VIEWS = {
|
|
19
|
-
timeline: { fetch: ['state', 'history', 'audit', 'annotations'], render: renderTimeline },
|
|
19
|
+
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations'], render: renderTimeline },
|
|
20
20
|
ledger: { fetch: ['ledger'], render: renderLedger },
|
|
21
21
|
hooks: { fetch: ['audit', 'annotations'], render: renderHooks },
|
|
22
22
|
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit'], render: renderBlocked },
|
|
@@ -29,6 +29,7 @@ const VIEWS = {
|
|
|
29
29
|
|
|
30
30
|
const API_MAP = {
|
|
31
31
|
state: '/api/state',
|
|
32
|
+
continuity: '/api/continuity',
|
|
32
33
|
history: '/api/history',
|
|
33
34
|
ledger: '/api/ledger',
|
|
34
35
|
audit: '/api/hooks/audit',
|
|
@@ -132,7 +132,41 @@ function renderTurnDetailPanel(turnId, annotations, audit) {
|
|
|
132
132
|
return html;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
function renderContinuityPanel(continuity) {
|
|
136
|
+
if (!continuity) return '';
|
|
137
|
+
|
|
138
|
+
const checkpoint = continuity.checkpoint;
|
|
139
|
+
const checkpointSummary = checkpoint?.last_checkpoint_at
|
|
140
|
+
? `${checkpoint.checkpoint_reason || 'unknown'} at ${checkpoint.last_checkpoint_at}`
|
|
141
|
+
: (checkpoint?.checkpoint_reason || 'No session checkpoint recorded');
|
|
142
|
+
|
|
143
|
+
let html = `<div class="section continuity-section"><h3>Continuity</h3><div class="turn-card">`;
|
|
144
|
+
|
|
145
|
+
if (checkpoint) {
|
|
146
|
+
html += `<div class="turn-detail"><span class="detail-label">Session:</span> <span class="mono">${esc(checkpoint.session_id || 'unknown')}</span></div>`;
|
|
147
|
+
html += `<div class="turn-detail"><span class="detail-label">Checkpoint:</span> ${esc(checkpointSummary)}</div>`;
|
|
148
|
+
html += `<div class="turn-detail"><span class="detail-label">Last turn:</span> <span class="mono">${esc(checkpoint.last_turn_id || 'none')}</span></div>`;
|
|
149
|
+
html += `<div class="turn-detail"><span class="detail-label">Last role:</span> ${esc(checkpoint.last_role || 'unknown')}</div>`;
|
|
150
|
+
if (continuity.stale_checkpoint) {
|
|
151
|
+
html += `<div class="turn-detail risks"><span class="detail-label">Warning:</span> checkpoint tracks <span class="mono">${esc(checkpoint.run_id || 'unknown')}</span>, but state.json remains source of truth.</div>`;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
html += `<div class="turn-detail"><span class="detail-label">Checkpoint:</span> No session checkpoint recorded</div>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (continuity.restart_recommended) {
|
|
158
|
+
html += `<div class="turn-detail"><span class="detail-label">Restart:</span> <span class="mono">agentxchain restart</span></div>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (continuity.recovery_report_path) {
|
|
162
|
+
html += `<div class="turn-detail"><span class="detail-label">Report:</span> <span class="mono">${esc(continuity.recovery_report_path)}</span></div>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
html += `</div></div>`;
|
|
166
|
+
return html;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function render({ state, continuity, history, annotations, audit }) {
|
|
136
170
|
if (!state) {
|
|
137
171
|
return `<div class="placeholder"><h2>No Run</h2><p>No governed run found. Start one with <code class="mono">agentxchain init --governed</code></p></div>`;
|
|
138
172
|
}
|
|
@@ -152,6 +186,8 @@ export function render({ state, history, annotations, audit }) {
|
|
|
152
186
|
</div>
|
|
153
187
|
</div>`;
|
|
154
188
|
|
|
189
|
+
html += renderContinuityPanel(continuity);
|
|
190
|
+
|
|
155
191
|
// Active turns
|
|
156
192
|
if (activeTurns.length > 0) {
|
|
157
193
|
html += `<div class="section"><h3>Active Turns</h3><div class="turn-list">`;
|
package/package.json
CHANGED
package/scripts/release-bump.sh
CHANGED
|
@@ -56,6 +56,8 @@ ALLOWED_RELEASE_PATHS=(
|
|
|
56
56
|
".agentxchain-conformance/capabilities.json"
|
|
57
57
|
"website-v2/docs/protocol-implementor-guide.mdx"
|
|
58
58
|
".planning/LAUNCH_EVIDENCE_REPORT.md"
|
|
59
|
+
"cli/homebrew/agentxchain.rb"
|
|
60
|
+
"cli/homebrew/README.md"
|
|
59
61
|
)
|
|
60
62
|
|
|
61
63
|
is_allowed_release_path() {
|
|
@@ -119,7 +121,11 @@ echo " OK: tag v${TARGET_VERSION} does not exist"
|
|
|
119
121
|
# Ensures all governed version surfaces already reference the target version
|
|
120
122
|
# BEFORE the bump commit is created. This catches stale drift that would
|
|
121
123
|
# otherwise only be discovered after minting local release identities.
|
|
122
|
-
|
|
124
|
+
#
|
|
125
|
+
# NOTE: Homebrew mirror formula and README are NOT checked here. They are
|
|
126
|
+
# auto-aligned in step 5 because the registry SHA256 is inherently a
|
|
127
|
+
# post-publish artifact. See DEC-HOMEBREW-SHA-SPLIT-001.
|
|
128
|
+
echo "[4/9] Verifying version-surface alignment for ${TARGET_VERSION}..."
|
|
123
129
|
SURFACE_ERRORS=()
|
|
124
130
|
|
|
125
131
|
# 4a. CHANGELOG top heading
|
|
@@ -172,13 +178,45 @@ if [[ "${#SURFACE_ERRORS[@]}" -gt 0 ]]; then
|
|
|
172
178
|
fi
|
|
173
179
|
echo " OK: all 7 governed version surfaces reference ${TARGET_VERSION}"
|
|
174
180
|
|
|
175
|
-
# 5.
|
|
176
|
-
|
|
181
|
+
# 5. Auto-align Homebrew mirror to target version
|
|
182
|
+
# The formula URL and README version/tarball are updated automatically.
|
|
183
|
+
# The SHA256 is carried from the previous version — it is inherently a
|
|
184
|
+
# post-publish artifact (npm registry tarballs are not byte-identical to
|
|
185
|
+
# local npm-pack output). sync-homebrew.sh corrects the SHA after publish.
|
|
186
|
+
echo "[5/9] Auto-aligning Homebrew mirror to ${TARGET_VERSION}..."
|
|
187
|
+
HOMEBREW_MIRROR="${REPO_ROOT}/cli/homebrew/agentxchain.rb"
|
|
188
|
+
HOMEBREW_MIRROR_README="${REPO_ROOT}/cli/homebrew/README.md"
|
|
189
|
+
TARBALL_URL="https://registry.npmjs.org/agentxchain/-/agentxchain-${TARGET_VERSION}.tgz"
|
|
190
|
+
HOMEBREW_ALIGNED=false
|
|
191
|
+
|
|
192
|
+
if [[ -f "$HOMEBREW_MIRROR" ]]; then
|
|
193
|
+
ESCAPED_URL="$(printf '%s' "$TARBALL_URL" | sed 's/[&/\]/\\&/g')"
|
|
194
|
+
sed -i.bak -E "s|^([[:space:]]*url \").*(\")|\1${ESCAPED_URL}\2|" "$HOMEBREW_MIRROR"
|
|
195
|
+
rm -f "${HOMEBREW_MIRROR}.bak"
|
|
196
|
+
HOMEBREW_ALIGNED=true
|
|
197
|
+
echo " OK: formula URL -> ${TARBALL_URL}"
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [[ -f "$HOMEBREW_MIRROR_README" ]]; then
|
|
201
|
+
sed -i.bak -E "s|^(- version: \`).*(\`)|\1${TARGET_VERSION}\2|" "$HOMEBREW_MIRROR_README"
|
|
202
|
+
sed -i.bak -E "s|^(- source tarball: \`).*(\`)|\1${TARBALL_URL}\2|" "$HOMEBREW_MIRROR_README"
|
|
203
|
+
rm -f "${HOMEBREW_MIRROR_README}.bak"
|
|
204
|
+
echo " OK: README version and tarball -> ${TARGET_VERSION}"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if $HOMEBREW_ALIGNED; then
|
|
208
|
+
echo " Note: SHA carried from previous version; sync-homebrew.sh will set the real registry SHA post-publish"
|
|
209
|
+
else
|
|
210
|
+
echo " Skipped: no Homebrew mirror files found"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# 6. Update version files (no git operations)
|
|
214
|
+
echo "[6/9] Updating version files..."
|
|
177
215
|
npm version "$TARGET_VERSION" --no-git-tag-version
|
|
178
216
|
echo " OK: package.json updated to ${TARGET_VERSION}"
|
|
179
217
|
|
|
180
|
-
#
|
|
181
|
-
echo "[
|
|
218
|
+
# 7. Stage version files
|
|
219
|
+
echo "[7/9] Staging version files..."
|
|
182
220
|
git add -- package.json
|
|
183
221
|
if [[ -f package-lock.json ]]; then
|
|
184
222
|
git add -- package-lock.json
|
|
@@ -188,8 +226,8 @@ for rel_path in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
|
188
226
|
done
|
|
189
227
|
echo " OK: version files and allowed release surfaces staged"
|
|
190
228
|
|
|
191
|
-
#
|
|
192
|
-
echo "[
|
|
229
|
+
# 8. Create release commit
|
|
230
|
+
echo "[8/9] Creating release commit..."
|
|
193
231
|
git commit -m "${TARGET_VERSION}"
|
|
194
232
|
RELEASE_SHA=$(git rev-parse HEAD)
|
|
195
233
|
COMMIT_MSG=$(git log -1 --format=%s)
|
|
@@ -199,8 +237,8 @@ if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
|
|
|
199
237
|
fi
|
|
200
238
|
echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
|
|
201
239
|
|
|
202
|
-
#
|
|
203
|
-
echo "[
|
|
240
|
+
# 9. Create annotated tag
|
|
241
|
+
echo "[9/9] Creating annotated tag..."
|
|
204
242
|
git tag -a "v${TARGET_VERSION}" -m "v${TARGET_VERSION}"
|
|
205
243
|
TAG_SHA=$(git rev-parse "v${TARGET_VERSION}")
|
|
206
244
|
if [[ -z "$TAG_SHA" ]]; then
|
package/src/commands/status.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
1
|
import chalk from 'chalk';
|
|
4
2
|
import { loadConfig, loadLock, loadProjectContext, loadProjectState, loadState } from '../lib/config.js';
|
|
5
3
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
6
4
|
import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const SESSION_RECOVERY_PATH = '.agentxchain/SESSION_RECOVERY.md';
|
|
5
|
+
import { getContinuityStatus } from '../lib/continuity-status.js';
|
|
10
6
|
|
|
11
7
|
export async function statusCommand(opts) {
|
|
12
8
|
const context = loadProjectContext();
|
|
@@ -247,28 +243,6 @@ function renderGovernedStatus(context, opts) {
|
|
|
247
243
|
console.log('');
|
|
248
244
|
}
|
|
249
245
|
|
|
250
|
-
function getContinuityStatus(root, state) {
|
|
251
|
-
const checkpoint = readSessionCheckpoint(root);
|
|
252
|
-
const recoveryReportPath = existsSync(join(root, SESSION_RECOVERY_PATH))
|
|
253
|
-
? SESSION_RECOVERY_PATH
|
|
254
|
-
: null;
|
|
255
|
-
|
|
256
|
-
if (!checkpoint && !recoveryReportPath) return null;
|
|
257
|
-
|
|
258
|
-
const staleCheckpoint = !!(
|
|
259
|
-
checkpoint?.run_id
|
|
260
|
-
&& state?.run_id
|
|
261
|
-
&& checkpoint.run_id !== state.run_id
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
checkpoint,
|
|
266
|
-
stale_checkpoint: staleCheckpoint,
|
|
267
|
-
recovery_report_path: recoveryReportPath,
|
|
268
|
-
restart_recommended: !!state && !['blocked', 'completed', 'failed'].includes(state.status),
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
246
|
function renderContinuityStatus(continuity, state) {
|
|
273
247
|
if (!continuity) return;
|
|
274
248
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* All error returns include a `classified` ApiProxyError object with
|
|
24
24
|
* error_class, recovery instructions, and retryable flag.
|
|
25
25
|
*
|
|
26
|
-
* Supported providers: "anthropic", "openai"
|
|
26
|
+
* Supported providers: "anthropic", "openai", "google"
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
|
@@ -45,9 +45,12 @@ import {
|
|
|
45
45
|
import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
|
|
46
46
|
|
|
47
47
|
// Provider endpoint registry
|
|
48
|
+
// Google Gemini endpoint requires the model name interpolated at call time;
|
|
49
|
+
// the registry stores a template with {model} as a placeholder.
|
|
48
50
|
const PROVIDER_ENDPOINTS = {
|
|
49
51
|
anthropic: 'https://api.anthropic.com/v1/messages',
|
|
50
52
|
openai: 'https://api.openai.com/v1/chat/completions',
|
|
53
|
+
google: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent',
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
// Bundled cost rates per million tokens (USD).
|
|
@@ -67,6 +70,10 @@ const BUNDLED_COST_RATES = {
|
|
|
67
70
|
'o3': { input_per_1m: 2.00, output_per_1m: 8.00 },
|
|
68
71
|
'o3-mini': { input_per_1m: 1.10, output_per_1m: 4.40 },
|
|
69
72
|
'o4-mini': { input_per_1m: 1.10, output_per_1m: 4.40 },
|
|
73
|
+
// Google Gemini — verified 2026-04-09 (training knowledge)
|
|
74
|
+
'gemini-2.5-pro': { input_per_1m: 1.25, output_per_1m: 10.00 },
|
|
75
|
+
'gemini-2.5-flash': { input_per_1m: 0.15, output_per_1m: 0.60 },
|
|
76
|
+
'gemini-2.0-flash': { input_per_1m: 0.10, output_per_1m: 0.40 },
|
|
70
77
|
};
|
|
71
78
|
|
|
72
79
|
// Resolve cost rates: operator-supplied cost_rates override bundled defaults
|
|
@@ -135,6 +142,25 @@ const PROVIDER_ERROR_MAPS = {
|
|
|
135
142
|
{ provider_error_type: 'rate_limit_error', http_status: 429, error_class: 'rate_limited', retryable: true },
|
|
136
143
|
],
|
|
137
144
|
},
|
|
145
|
+
google: {
|
|
146
|
+
extractErrorType(body) {
|
|
147
|
+
// Google errors use { error: { status: "INVALID_ARGUMENT", ... } }
|
|
148
|
+
return typeof body?.error?.status === 'string' ? body.error.status : null;
|
|
149
|
+
},
|
|
150
|
+
extractErrorCode(body) {
|
|
151
|
+
return typeof body?.error?.code === 'number' ? String(body.error.code) : null;
|
|
152
|
+
},
|
|
153
|
+
mappings: [
|
|
154
|
+
{ provider_error_type: 'UNAUTHENTICATED', http_status: 401, error_class: 'auth_failure', retryable: false },
|
|
155
|
+
{ provider_error_type: 'PERMISSION_DENIED', http_status: 403, error_class: 'auth_failure', retryable: false },
|
|
156
|
+
{ provider_error_type: 'NOT_FOUND', http_status: 404, error_class: 'model_not_found', retryable: false },
|
|
157
|
+
{ provider_error_type: 'RESOURCE_EXHAUSTED', http_status: 429, error_class: 'rate_limited', retryable: true },
|
|
158
|
+
{ provider_error_type: 'INVALID_ARGUMENT', http_status: 400, body_pattern: /token.*limit|context|too.long/i, error_class: 'context_overflow', retryable: false },
|
|
159
|
+
{ provider_error_type: 'INVALID_ARGUMENT', http_status: 400, error_class: 'invalid_request', retryable: false },
|
|
160
|
+
{ provider_error_type: 'UNAVAILABLE', http_status: 503, error_class: 'provider_overloaded', retryable: true },
|
|
161
|
+
{ provider_error_type: 'INTERNAL', http_status: 500, error_class: 'unknown_api_error', retryable: true },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
138
164
|
};
|
|
139
165
|
|
|
140
166
|
// ── Error classification ──────────────────────────────────────────────────────
|
|
@@ -442,6 +468,10 @@ function usageFromTelemetry(provider, model, usage, config) {
|
|
|
442
468
|
if (provider === 'openai') {
|
|
443
469
|
inputTokens = Number.isFinite(usage.prompt_tokens) ? usage.prompt_tokens : 0;
|
|
444
470
|
outputTokens = Number.isFinite(usage.completion_tokens) ? usage.completion_tokens : 0;
|
|
471
|
+
} else if (provider === 'google') {
|
|
472
|
+
// Google Gemini returns usageMetadata at root level with promptTokenCount / candidatesTokenCount
|
|
473
|
+
inputTokens = Number.isFinite(usage.promptTokenCount) ? usage.promptTokenCount : 0;
|
|
474
|
+
outputTokens = Number.isFinite(usage.candidatesTokenCount) ? usage.candidatesTokenCount : 0;
|
|
445
475
|
} else {
|
|
446
476
|
inputTokens = Number.isFinite(usage.input_tokens) ? usage.input_tokens : 0;
|
|
447
477
|
outputTokens = Number.isFinite(usage.output_tokens) ? usage.output_tokens : 0;
|
|
@@ -691,7 +721,9 @@ async function executeApiCall({
|
|
|
691
721
|
};
|
|
692
722
|
}
|
|
693
723
|
|
|
694
|
-
|
|
724
|
+
// Google Gemini returns usage at responseData.usageMetadata; others at responseData.usage
|
|
725
|
+
const usageSource = provider === 'google' ? responseData.usageMetadata : responseData.usage;
|
|
726
|
+
const usage = usageFromTelemetry(provider, model, usageSource, config);
|
|
695
727
|
const extraction = extractTurnResult(responseData, provider);
|
|
696
728
|
|
|
697
729
|
if (!extraction.ok) {
|
|
@@ -791,7 +823,7 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
791
823
|
return errorReturn(root, turn.turn_id, classified);
|
|
792
824
|
}
|
|
793
825
|
|
|
794
|
-
|
|
826
|
+
let endpoint = runtime.base_url || PROVIDER_ENDPOINTS[provider];
|
|
795
827
|
if (!endpoint) {
|
|
796
828
|
const classified = classifyError(
|
|
797
829
|
'unsupported_provider',
|
|
@@ -802,6 +834,12 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
802
834
|
return errorReturn(root, turn.turn_id, classified);
|
|
803
835
|
}
|
|
804
836
|
|
|
837
|
+
// Google Gemini: interpolate model into endpoint URL and append API key as query param
|
|
838
|
+
if (provider === 'google') {
|
|
839
|
+
endpoint = endpoint.replace('{model}', encodeURIComponent(model));
|
|
840
|
+
endpoint += (endpoint.includes('?') ? '&' : '?') + `key=${encodeURIComponent(apiKey)}`;
|
|
841
|
+
}
|
|
842
|
+
|
|
805
843
|
// Build request
|
|
806
844
|
const maxOutputTokens = runtime.max_output_tokens || 4096;
|
|
807
845
|
const timeoutSeconds = runtime.timeout_seconds || 120;
|
|
@@ -1086,10 +1124,92 @@ function buildOpenAiRequest(promptMd, contextMd, model, maxOutputTokens) {
|
|
|
1086
1124
|
};
|
|
1087
1125
|
}
|
|
1088
1126
|
|
|
1127
|
+
function buildGoogleHeaders(_apiKey) {
|
|
1128
|
+
// Google Gemini uses API key as a query parameter, not a header
|
|
1129
|
+
return {
|
|
1130
|
+
'Content-Type': 'application/json',
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function buildGoogleRequest(promptMd, contextMd, model, maxOutputTokens) {
|
|
1135
|
+
const userContent = contextMd
|
|
1136
|
+
? `${promptMd}${SEPARATOR}${contextMd}`
|
|
1137
|
+
: promptMd;
|
|
1138
|
+
|
|
1139
|
+
return {
|
|
1140
|
+
systemInstruction: {
|
|
1141
|
+
parts: [{ text: SYSTEM_PROMPT }],
|
|
1142
|
+
},
|
|
1143
|
+
contents: [
|
|
1144
|
+
{
|
|
1145
|
+
role: 'user',
|
|
1146
|
+
parts: [{ text: userContent }],
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
generationConfig: {
|
|
1150
|
+
maxOutputTokens,
|
|
1151
|
+
responseMimeType: 'application/json',
|
|
1152
|
+
},
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function extractGoogleTurnResult(responseData) {
|
|
1157
|
+
const promptBlockReason = responseData?.promptFeedback?.blockReason;
|
|
1158
|
+
if (typeof promptBlockReason === 'string' && promptBlockReason.trim()) {
|
|
1159
|
+
return {
|
|
1160
|
+
ok: false,
|
|
1161
|
+
error: `Google Gemini blocked the prompt before generation (blockReason: ${promptBlockReason})`,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (!Array.isArray(responseData?.candidates) || responseData.candidates.length === 0) {
|
|
1166
|
+
return { ok: false, error: 'API response has no candidates' };
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const candidate = responseData.candidates[0];
|
|
1170
|
+
const finishReason = typeof candidate?.finishReason === 'string'
|
|
1171
|
+
? candidate.finishReason
|
|
1172
|
+
: null;
|
|
1173
|
+
const parts = candidate?.content?.parts;
|
|
1174
|
+
if (!Array.isArray(parts) || parts.length === 0) {
|
|
1175
|
+
if (finishReason && finishReason !== 'STOP') {
|
|
1176
|
+
return {
|
|
1177
|
+
ok: false,
|
|
1178
|
+
error: `Google Gemini candidate has no content parts (finishReason: ${finishReason})`,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
return { ok: false, error: 'API response candidate has no content parts' };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const textPart = parts.find(p => typeof p.text === 'string');
|
|
1185
|
+
if (!textPart?.text?.trim()) {
|
|
1186
|
+
if (finishReason && finishReason !== 'STOP') {
|
|
1187
|
+
return {
|
|
1188
|
+
ok: false,
|
|
1189
|
+
error: `Google Gemini returned no extractable text (finishReason: ${finishReason})`,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
return { ok: false, error: 'API response has no text content part' };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const extraction = extractTurnResultFromText(textPart.text);
|
|
1196
|
+
if (!extraction.ok && finishReason && finishReason !== 'STOP') {
|
|
1197
|
+
return {
|
|
1198
|
+
ok: false,
|
|
1199
|
+
error: `Google Gemini returned non-extractable turn JSON (finishReason: ${finishReason})`,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return extraction;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1089
1206
|
function buildProviderHeaders(provider, apiKey) {
|
|
1090
1207
|
if (provider === 'openai') {
|
|
1091
1208
|
return buildOpenAiHeaders(apiKey);
|
|
1092
1209
|
}
|
|
1210
|
+
if (provider === 'google') {
|
|
1211
|
+
return buildGoogleHeaders(apiKey);
|
|
1212
|
+
}
|
|
1093
1213
|
return buildAnthropicHeaders(apiKey);
|
|
1094
1214
|
}
|
|
1095
1215
|
|
|
@@ -1097,6 +1217,9 @@ function buildProviderRequest(provider, promptMd, contextMd, model, maxOutputTok
|
|
|
1097
1217
|
if (provider === 'openai') {
|
|
1098
1218
|
return buildOpenAiRequest(promptMd, contextMd, model, maxOutputTokens);
|
|
1099
1219
|
}
|
|
1220
|
+
if (provider === 'google') {
|
|
1221
|
+
return buildGoogleRequest(promptMd, contextMd, model, maxOutputTokens);
|
|
1222
|
+
}
|
|
1100
1223
|
return buildAnthropicRequest(promptMd, contextMd, model, maxOutputTokens);
|
|
1101
1224
|
}
|
|
1102
1225
|
|
|
@@ -1184,6 +1307,9 @@ function extractTurnResult(responseData, provider = 'anthropic') {
|
|
|
1184
1307
|
if (provider === 'openai') {
|
|
1185
1308
|
return extractOpenAiTurnResult(responseData);
|
|
1186
1309
|
}
|
|
1310
|
+
if (provider === 'google') {
|
|
1311
|
+
return extractGoogleTurnResult(responseData);
|
|
1312
|
+
}
|
|
1187
1313
|
return extractAnthropicTurnResult(responseData);
|
|
1188
1314
|
}
|
|
1189
1315
|
|
|
@@ -1198,6 +1324,7 @@ export {
|
|
|
1198
1324
|
extractTurnResult,
|
|
1199
1325
|
buildAnthropicRequest,
|
|
1200
1326
|
buildOpenAiRequest,
|
|
1327
|
+
buildGoogleRequest,
|
|
1201
1328
|
classifyError,
|
|
1202
1329
|
classifyHttpError,
|
|
1203
1330
|
BUNDLED_COST_RATES,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readSessionCheckpoint } from './session-checkpoint.js';
|
|
4
|
+
|
|
5
|
+
export const SESSION_RECOVERY_PATH = '.agentxchain/SESSION_RECOVERY.md';
|
|
6
|
+
|
|
7
|
+
export function getContinuityStatus(root, state) {
|
|
8
|
+
const checkpoint = readSessionCheckpoint(root);
|
|
9
|
+
const recoveryReportPath = existsSync(join(root, SESSION_RECOVERY_PATH))
|
|
10
|
+
? SESSION_RECOVERY_PATH
|
|
11
|
+
: null;
|
|
12
|
+
|
|
13
|
+
if (!checkpoint && !recoveryReportPath) return null;
|
|
14
|
+
|
|
15
|
+
const staleCheckpoint = !!(
|
|
16
|
+
checkpoint?.run_id
|
|
17
|
+
&& state?.run_id
|
|
18
|
+
&& checkpoint.run_id !== state.run_id
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
checkpoint,
|
|
23
|
+
stale_checkpoint: staleCheckpoint,
|
|
24
|
+
recovery_report_path: recoveryReportPath,
|
|
25
|
+
restart_recommended: !!state && !['blocked', 'completed', 'failed'].includes(state.status),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFileSync, existsSync } from 'fs';
|
|
10
|
-
import { join, normalize } from 'path';
|
|
10
|
+
import { join, normalize, resolve } from 'path';
|
|
11
|
+
import { getContinuityStatus } from '../continuity-status.js';
|
|
11
12
|
|
|
12
13
|
const STATE_FILE = 'state.json';
|
|
13
14
|
const SESSION_FILE = 'session.json';
|
|
15
|
+
const SESSION_RECOVERY_FILE = 'SESSION_RECOVERY.md';
|
|
14
16
|
const HISTORY_FILE = 'history.jsonl';
|
|
15
17
|
const LEDGER_FILE = 'decision-ledger.jsonl';
|
|
16
18
|
const HOOK_AUDIT_FILE = 'hook-audit.jsonl';
|
|
@@ -44,6 +46,7 @@ export const RESOURCE_MAP = {
|
|
|
44
46
|
export const FILE_TO_RESOURCE = Object.fromEntries(
|
|
45
47
|
Object.entries(RESOURCE_MAP).map(([resource, file]) => [normalizeRelativePath(file), resource])
|
|
46
48
|
);
|
|
49
|
+
FILE_TO_RESOURCE[normalizeRelativePath(SESSION_RECOVERY_FILE)] = '/api/continuity';
|
|
47
50
|
|
|
48
51
|
export const WATCH_DIRECTORIES = [
|
|
49
52
|
'',
|
|
@@ -86,6 +89,13 @@ export function readJsonlFile(agentxchainDir, filename) {
|
|
|
86
89
|
* Read a resource by its API path. Returns { data, format } or null.
|
|
87
90
|
*/
|
|
88
91
|
export function readResource(agentxchainDir, resourcePath) {
|
|
92
|
+
if (resourcePath === '/api/continuity') {
|
|
93
|
+
const root = resolve(agentxchainDir, '..');
|
|
94
|
+
const state = readJsonFile(agentxchainDir, STATE_FILE);
|
|
95
|
+
const data = getContinuityStatus(root, state);
|
|
96
|
+
return { data, format: 'json' };
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
const filename = RESOURCE_MAP[resourcePath];
|
|
90
100
|
if (!filename) return null;
|
|
91
101
|
|
|
@@ -18,7 +18,7 @@ import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
|
|
|
18
18
|
|
|
19
19
|
const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
20
20
|
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp', 'remote_agent'];
|
|
21
|
-
const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
|
|
21
|
+
const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai', 'google'];
|
|
22
22
|
export const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
23
23
|
const VALID_MCP_TRANSPORTS = ['stdio', 'streamable_http'];
|
|
24
24
|
const DEFAULT_PHASES = ['planning', 'implementation', 'qa'];
|