@agent-native/core 0.94.2 → 0.95.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/pr-visual-recap.mdx +20 -12
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +154 -1
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +3 -0
- package/corpus/core/src/agent/engine/registry.ts +6 -1
- package/corpus/core/src/agent/engine/types.ts +2 -0
- package/corpus/core/src/cli/code-agent-executor.ts +61 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/cli/recap.ts +262 -25
- package/corpus/core/src/client/settings/UsageSection.tsx +26 -5
- package/corpus/core/src/index.ts +2 -0
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
- package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
- package/corpus/core/src/integrations/adapters/slack.ts +77 -7
- package/corpus/core/src/integrations/types.ts +30 -0
- package/corpus/core/src/integrations/webhook-handler.ts +54 -18
- package/corpus/core/src/scripts/call-agent.ts +1 -0
- package/corpus/core/src/server/request-context.ts +2 -0
- package/corpus/core/src/usage/store.ts +82 -20
- package/corpus/templates/plan/actions/record-recap-usage-cost.ts +49 -0
- package/corpus/templates/plan/actions/record-recap-usage.ts +15 -22
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +127 -1
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +3 -0
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +1 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +2 -0
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/engine/types.d.ts +2 -0
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/engine/types.js.map +1 -1
- package/dist/cli/code-agent-executor.d.ts +2 -0
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +52 -3
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +22 -2
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +199 -22
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/settings/UsageSection.d.ts.map +1 -1
- package/dist/client/settings/UsageSection.js +12 -3
- package/dist/client/settings/UsageSection.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +76 -21
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +4 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +130 -6
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +56 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +54 -16
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +1 -0
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/usage/store.d.ts +21 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +54 -19
- package/dist/usage/store.js.map +1 -1
- package/docs/content/pr-visual-recap.mdx +20 -12
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ import fs from "node:fs";
|
|
|
40
40
|
import os from "node:os";
|
|
41
41
|
import path from "node:path";
|
|
42
42
|
|
|
43
|
+
import { normalizeOpenAiBaseUrl } from "../agent/engine/openai-compatible-endpoint.js";
|
|
43
44
|
import {
|
|
44
45
|
DEFAULT_PLAN_APP_URL,
|
|
45
46
|
fetchPlanBlockCatalog,
|
|
@@ -99,7 +100,9 @@ export const PR_VISUAL_RECAP_SETUP: string[] = [
|
|
|
99
100
|
" ANTHROPIC_API_KEY — the LLM key for the default Claude Code backend",
|
|
100
101
|
"Optional (only if you change defaults):",
|
|
101
102
|
" OPENAI_API_KEY (secret) + VISUAL_RECAP_AGENT=codex (variable) — use Codex instead of Claude",
|
|
102
|
-
"
|
|
103
|
+
" VISUAL_RECAP_API_KEY (secret) + VISUAL_RECAP_AGENT=openai-compatible + VISUAL_RECAP_BASE_URL (variable) — use DeepSeek, Kimi, or any OpenAI-compatible API",
|
|
104
|
+
" VISUAL_RECAP_MODEL (variable, required for openai-compatible) — provider model id; optional override for Claude/Codex",
|
|
105
|
+
" VISUAL_RECAP_REASONING (variable) — reasoning depth (none|minimal|low|medium|high|xhigh; Codex only)",
|
|
103
106
|
" VISUAL_RECAP_SKILL_SOURCE=repo (variable) — pin CI to the repo-local visual-recap skill instead of latest bundled guidance",
|
|
104
107
|
" VISUAL_RECAP_SECRET_SCAN=off|high-confidence|strict (variable) — default high-confidence; strict restores generic TOKEN/SECRET assignment suppression",
|
|
105
108
|
" PLAN_RECAP_APP_URL (secret) — only when self-hosting the plan app (defaults to https://plan.agent-native.com)",
|
|
@@ -198,10 +201,12 @@ export function buildReusableCallerWorkflow(
|
|
|
198
201
|
` PLAN_RECAP_TOKEN: \${{ secrets.PLAN_RECAP_TOKEN }}\n` +
|
|
199
202
|
` ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}\n` +
|
|
200
203
|
` OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}\n` +
|
|
204
|
+
` VISUAL_RECAP_API_KEY: \${{ secrets.VISUAL_RECAP_API_KEY }}\n` +
|
|
201
205
|
` PLAN_RECAP_APP_URL: \${{ secrets.PLAN_RECAP_APP_URL }}\n` +
|
|
202
206
|
` with:\n` +
|
|
203
207
|
` agent: ${agentValue}\n` +
|
|
204
208
|
` model: ${modelValue}\n` +
|
|
209
|
+
` base-url: \${{ vars.VISUAL_RECAP_BASE_URL || '' }}\n` +
|
|
205
210
|
` reasoning: \${{ vars.VISUAL_RECAP_REASONING || '' }}\n` +
|
|
206
211
|
` skill-source: \${{ vars.VISUAL_RECAP_SKILL_SOURCE || 'auto' }}\n` +
|
|
207
212
|
` secret-scan: \${{ vars.VISUAL_RECAP_SECRET_SCAN || 'high-confidence' }}\n` +
|
|
@@ -253,9 +258,9 @@ export function writePrVisualRecapReusableCallerWorkflow(
|
|
|
253
258
|
|
|
254
259
|
// Narrow type used only where it's needed (avoids importing the full
|
|
255
260
|
// RecapAgent type before it is defined below).
|
|
256
|
-
type RecapAgentValue = "claude" | "codex";
|
|
261
|
+
type RecapAgentValue = "claude" | "codex" | "openai-compatible";
|
|
257
262
|
|
|
258
|
-
export type RecapAgent = "claude" | "codex";
|
|
263
|
+
export type RecapAgent = "claude" | "codex" | "openai-compatible";
|
|
259
264
|
|
|
260
265
|
const DEFAULT_RECAP_APP_URL = DEFAULT_PLAN_APP_URL;
|
|
261
266
|
|
|
@@ -263,15 +268,26 @@ export function normalizeRecapAgent(value: string | undefined): RecapAgent {
|
|
|
263
268
|
const agent = (value || "claude").toLowerCase();
|
|
264
269
|
if (agent === "codex") return "codex";
|
|
265
270
|
if (agent === "claude") return "claude";
|
|
271
|
+
if (
|
|
272
|
+
["openai-compatible", "deepseek", "kimi", "moonshot", "custom"].includes(
|
|
273
|
+
agent,
|
|
274
|
+
)
|
|
275
|
+
) {
|
|
276
|
+
return "openai-compatible";
|
|
277
|
+
}
|
|
266
278
|
throw new Error(
|
|
267
|
-
`Unsupported recap agent "${value}" (expected "claude" or "
|
|
279
|
+
`Unsupported recap agent "${value}" (expected "claude", "codex", or "openai-compatible").`,
|
|
268
280
|
);
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
export function recapRequiredSecrets(agent: RecapAgent): string[] {
|
|
272
284
|
return [
|
|
273
285
|
"PLAN_RECAP_TOKEN",
|
|
274
|
-
agent === "codex"
|
|
286
|
+
agent === "codex"
|
|
287
|
+
? "OPENAI_API_KEY"
|
|
288
|
+
: agent === "openai-compatible"
|
|
289
|
+
? "VISUAL_RECAP_API_KEY"
|
|
290
|
+
: "ANTHROPIC_API_KEY",
|
|
275
291
|
];
|
|
276
292
|
}
|
|
277
293
|
|
|
@@ -431,10 +447,71 @@ export interface RecapSetupPlan {
|
|
|
431
447
|
workflowPath: string;
|
|
432
448
|
workflowExists: boolean;
|
|
433
449
|
requiredSecrets: string[];
|
|
450
|
+
requiredVariables: readonly RecapVariableRequirement[];
|
|
451
|
+
variableProblems: RecapVariableProblem[];
|
|
434
452
|
variableValues: Record<string, string>;
|
|
435
453
|
secretValues: Record<string, string | undefined>;
|
|
436
454
|
}
|
|
437
455
|
|
|
456
|
+
export interface RecapVariableRequirement {
|
|
457
|
+
name: "VISUAL_RECAP_BASE_URL" | "VISUAL_RECAP_MODEL";
|
|
458
|
+
example: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface RecapVariableProblem {
|
|
462
|
+
requirement: RecapVariableRequirement;
|
|
463
|
+
reason: string;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const OPENAI_COMPATIBLE_VARIABLE_REQUIREMENTS = [
|
|
467
|
+
{
|
|
468
|
+
name: "VISUAL_RECAP_BASE_URL",
|
|
469
|
+
example: "https://provider.example/v1",
|
|
470
|
+
},
|
|
471
|
+
{ name: "VISUAL_RECAP_MODEL", example: "provider-model-id" },
|
|
472
|
+
] as const satisfies readonly RecapVariableRequirement[];
|
|
473
|
+
|
|
474
|
+
const RECAP_MODEL_PATTERN = /^[a-zA-Z0-9._-]{1,80}$/;
|
|
475
|
+
const OPENAI_COMPATIBLE_RECAP_MODEL_PATTERN = /^[^\p{C}\p{Z}]{1,200}$/u;
|
|
476
|
+
|
|
477
|
+
export function validateOpenAiCompatibleRecapVariables(input: {
|
|
478
|
+
baseUrl?: string;
|
|
479
|
+
model?: string;
|
|
480
|
+
}): RecapVariableProblem[] {
|
|
481
|
+
const [baseUrlRequirement, modelRequirement] =
|
|
482
|
+
OPENAI_COMPATIBLE_VARIABLE_REQUIREMENTS;
|
|
483
|
+
const problems: RecapVariableProblem[] = [];
|
|
484
|
+
const baseUrl = input.baseUrl?.trim() || "";
|
|
485
|
+
const rawModel = input.model || "";
|
|
486
|
+
const model = rawModel.trim();
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const parsed = normalizeOpenAiBaseUrl(baseUrl);
|
|
490
|
+
if (!parsed) throw new Error("empty");
|
|
491
|
+
} catch {
|
|
492
|
+
problems.push({
|
|
493
|
+
requirement: baseUrlRequirement,
|
|
494
|
+
reason:
|
|
495
|
+
"VISUAL_RECAP_BASE_URL must be a valid http(s) URL without credentials",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!model) {
|
|
500
|
+
problems.push({
|
|
501
|
+
requirement: modelRequirement,
|
|
502
|
+
reason: "VISUAL_RECAP_MODEL is required (openai-compatible backend)",
|
|
503
|
+
});
|
|
504
|
+
} else if (!OPENAI_COMPATIBLE_RECAP_MODEL_PATTERN.test(rawModel)) {
|
|
505
|
+
problems.push({
|
|
506
|
+
requirement: modelRequirement,
|
|
507
|
+
reason:
|
|
508
|
+
"invalid VISUAL_RECAP_MODEL value (must be 1-200 characters without whitespace or controls)",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return problems;
|
|
513
|
+
}
|
|
514
|
+
|
|
438
515
|
export function buildRecapSetupPlan(input: {
|
|
439
516
|
baseDir: string;
|
|
440
517
|
appUrl?: string;
|
|
@@ -448,10 +525,18 @@ export function buildRecapSetupPlan(input: {
|
|
|
448
525
|
);
|
|
449
526
|
const agent = normalizeRecapAgent(input.agent || env.VISUAL_RECAP_AGENT);
|
|
450
527
|
const requiredSecrets = recapRequiredSecrets(agent);
|
|
528
|
+
const requiredVariables =
|
|
529
|
+
agent === "openai-compatible"
|
|
530
|
+
? OPENAI_COMPATIBLE_VARIABLE_REQUIREMENTS
|
|
531
|
+
: [];
|
|
451
532
|
const planToken =
|
|
452
533
|
envValue(env, "PLAN_RECAP_TOKEN") ?? planTokenFromLocalStore(appUrl);
|
|
453
534
|
const llmSecretName =
|
|
454
|
-
agent === "codex"
|
|
535
|
+
agent === "codex"
|
|
536
|
+
? "OPENAI_API_KEY"
|
|
537
|
+
: agent === "openai-compatible"
|
|
538
|
+
? "VISUAL_RECAP_API_KEY"
|
|
539
|
+
: "ANTHROPIC_API_KEY";
|
|
455
540
|
const variableValues: Record<string, string> = {};
|
|
456
541
|
if (agent !== "claude") variableValues.VISUAL_RECAP_AGENT = agent;
|
|
457
542
|
for (const key of [
|
|
@@ -462,6 +547,17 @@ export function buildRecapSetupPlan(input: {
|
|
|
462
547
|
const value = envValue(env, key);
|
|
463
548
|
if (value) variableValues[key] = value;
|
|
464
549
|
}
|
|
550
|
+
if (agent === "openai-compatible") {
|
|
551
|
+
const baseUrl = envValue(env, "VISUAL_RECAP_BASE_URL");
|
|
552
|
+
if (baseUrl) variableValues.VISUAL_RECAP_BASE_URL = baseUrl;
|
|
553
|
+
}
|
|
554
|
+
const variableProblems =
|
|
555
|
+
agent === "openai-compatible"
|
|
556
|
+
? validateOpenAiCompatibleRecapVariables({
|
|
557
|
+
baseUrl: variableValues.VISUAL_RECAP_BASE_URL,
|
|
558
|
+
model: variableValues.VISUAL_RECAP_MODEL,
|
|
559
|
+
})
|
|
560
|
+
: [];
|
|
465
561
|
return {
|
|
466
562
|
agent,
|
|
467
563
|
appUrl,
|
|
@@ -472,6 +568,8 @@ export function buildRecapSetupPlan(input: {
|
|
|
472
568
|
),
|
|
473
569
|
workflowExists: fs.existsSync(recapWorkflowFile(input.baseDir)),
|
|
474
570
|
requiredSecrets,
|
|
571
|
+
requiredVariables,
|
|
572
|
+
variableProblems,
|
|
475
573
|
variableValues,
|
|
476
574
|
secretValues: {
|
|
477
575
|
PLAN_RECAP_TOKEN: planToken,
|
|
@@ -592,7 +690,12 @@ function runSetup(args: Record<string, string | boolean>): void {
|
|
|
592
690
|
}
|
|
593
691
|
}
|
|
594
692
|
|
|
693
|
+
const invalidVariableNames = new Set(
|
|
694
|
+
plan.variableProblems.map((problem) => problem.requirement.name),
|
|
695
|
+
);
|
|
595
696
|
for (const [name, value] of Object.entries(plan.variableValues)) {
|
|
697
|
+
if (invalidVariableNames.has(name as RecapVariableRequirement["name"]))
|
|
698
|
+
continue;
|
|
596
699
|
const status = setGithubVariable(name, value, repo, dryRun);
|
|
597
700
|
if (status === "set") {
|
|
598
701
|
lines.push(` ${name}: set to ${value}.`);
|
|
@@ -605,6 +708,21 @@ function runSetup(args: Record<string, string | boolean>): void {
|
|
|
605
708
|
);
|
|
606
709
|
}
|
|
607
710
|
}
|
|
711
|
+
for (const problem of plan.variableProblems) {
|
|
712
|
+
const { requirement } = problem;
|
|
713
|
+
const hasValue = Boolean(plan.variableValues[requirement.name]);
|
|
714
|
+
lines.push(
|
|
715
|
+
` ${requirement.name}: ${hasValue ? "invalid value" : "missing value"}.`,
|
|
716
|
+
);
|
|
717
|
+
lines.push(` ${problem.reason}.`);
|
|
718
|
+
lines.push(
|
|
719
|
+
` Set manually: ${commandForMissingVariable(
|
|
720
|
+
requirement.name,
|
|
721
|
+
requirement.example,
|
|
722
|
+
repo,
|
|
723
|
+
)}`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
608
726
|
}
|
|
609
727
|
|
|
610
728
|
lines.push("");
|
|
@@ -695,6 +813,36 @@ function runDoctor(args: Record<string, string | boolean>): void {
|
|
|
695
813
|
} else {
|
|
696
814
|
const configuredAgent = variables.get("VISUAL_RECAP_AGENT") || "claude";
|
|
697
815
|
lines.push(`[ok] Recap backend variable: ${configuredAgent}.`);
|
|
816
|
+
const remoteVariableProblems =
|
|
817
|
+
plan.agent === "openai-compatible"
|
|
818
|
+
? validateOpenAiCompatibleRecapVariables({
|
|
819
|
+
baseUrl: variables.get("VISUAL_RECAP_BASE_URL"),
|
|
820
|
+
model: variables.get("VISUAL_RECAP_MODEL"),
|
|
821
|
+
})
|
|
822
|
+
: [];
|
|
823
|
+
const problemsByName = new Map(
|
|
824
|
+
remoteVariableProblems.map((problem) => [
|
|
825
|
+
problem.requirement.name,
|
|
826
|
+
problem,
|
|
827
|
+
]),
|
|
828
|
+
);
|
|
829
|
+
for (const requirement of plan.requiredVariables) {
|
|
830
|
+
const problem = problemsByName.get(requirement.name);
|
|
831
|
+
if (!problem) {
|
|
832
|
+
lines.push(`[ok] GitHub variable configured: ${requirement.name}.`);
|
|
833
|
+
} else {
|
|
834
|
+
ok = false;
|
|
835
|
+
const hasValue = Boolean(variables.get(requirement.name)?.trim());
|
|
836
|
+
lines.push(`[${hasValue ? "invalid" : "missing"}] ${problem.reason}.`);
|
|
837
|
+
lines.push(
|
|
838
|
+
` Set it with: ${commandForMissingVariable(
|
|
839
|
+
requirement.name,
|
|
840
|
+
requirement.example,
|
|
841
|
+
repo,
|
|
842
|
+
)}`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
698
846
|
}
|
|
699
847
|
|
|
700
848
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
@@ -988,6 +1136,7 @@ function agentLabel(agent: string): string {
|
|
|
988
1136
|
const normalized = agent.toLowerCase();
|
|
989
1137
|
if (normalized === "codex") return "Codex";
|
|
990
1138
|
if (normalized === "claude") return "Claude";
|
|
1139
|
+
if (normalized === "openai-compatible") return "OpenAI-compatible";
|
|
991
1140
|
return agent || "Agent";
|
|
992
1141
|
}
|
|
993
1142
|
|
|
@@ -1028,7 +1177,7 @@ function readTextIfExists(file: string): string | null {
|
|
|
1028
1177
|
}
|
|
1029
1178
|
|
|
1030
1179
|
function localAgentResultCandidates(agent: string): Array<{
|
|
1031
|
-
agent:
|
|
1180
|
+
agent: string;
|
|
1032
1181
|
resultFile: string;
|
|
1033
1182
|
stderrFile: string;
|
|
1034
1183
|
exitCodeFile: string;
|
|
@@ -1046,9 +1195,16 @@ function localAgentResultCandidates(agent: string): Array<{
|
|
|
1046
1195
|
stderrFile: "codex-stderr.log",
|
|
1047
1196
|
exitCodeFile: "codex-exit-code.txt",
|
|
1048
1197
|
},
|
|
1198
|
+
{
|
|
1199
|
+
agent: "openai-compatible",
|
|
1200
|
+
resultFile: "openai-compatible-result.txt",
|
|
1201
|
+
stderrFile: "openai-compatible-stderr.log",
|
|
1202
|
+
exitCodeFile: "openai-compatible-exit-code.txt",
|
|
1203
|
+
},
|
|
1049
1204
|
];
|
|
1050
1205
|
const normalized = agent.toLowerCase();
|
|
1051
|
-
if (normalized === "codex") return [all[1], all[0]];
|
|
1206
|
+
if (normalized === "codex") return [all[1], all[0], all[2]];
|
|
1207
|
+
if (normalized === "openai-compatible") return [all[2], all[0], all[1]];
|
|
1052
1208
|
return all;
|
|
1053
1209
|
}
|
|
1054
1210
|
|
|
@@ -3255,10 +3411,14 @@ export interface RecapGateInput {
|
|
|
3255
3411
|
hasAnthropic: boolean;
|
|
3256
3412
|
/** OPENAI_API_KEY present. */
|
|
3257
3413
|
hasOpenai: boolean;
|
|
3414
|
+
/** VISUAL_RECAP_API_KEY present for OpenAI-compatible backends. */
|
|
3415
|
+
hasOpenaiCompatible?: boolean;
|
|
3258
3416
|
/** Raw VISUAL_RECAP_AGENT value (may be undefined / mis-cased). */
|
|
3259
3417
|
agentRaw: string | undefined;
|
|
3260
3418
|
/** Raw VISUAL_RECAP_MODEL value (may be undefined). */
|
|
3261
3419
|
model: string | undefined;
|
|
3420
|
+
/** Raw VISUAL_RECAP_BASE_URL value for OpenAI-compatible backends. */
|
|
3421
|
+
baseUrl?: string;
|
|
3262
3422
|
/** Raw VISUAL_RECAP_SKILL_SOURCE value (auto/latest/repo; may be undefined). */
|
|
3263
3423
|
skillSource: string | undefined;
|
|
3264
3424
|
/** Filenames changed by the PR (for the self-modifying guard). */
|
|
@@ -3360,23 +3520,41 @@ export function evaluateRecapGate(input: RecapGateInput): {
|
|
|
3360
3520
|
// The chosen backend's API key must be present. Normalize the agent value once
|
|
3361
3521
|
// here and validate it: an unknown or mis-cased value (e.g. "Claude", "gpt")
|
|
3362
3522
|
// must NOT silently pass the gate and then match neither agent step.
|
|
3363
|
-
const
|
|
3364
|
-
|
|
3523
|
+
const rawAgent = (input.agentRaw || "claude").toLowerCase();
|
|
3524
|
+
const agent = ["deepseek", "kimi", "moonshot", "custom"].includes(rawAgent)
|
|
3525
|
+
? "openai-compatible"
|
|
3526
|
+
: rawAgent;
|
|
3527
|
+
if (!["claude", "codex", "openai-compatible"].includes(agent)) {
|
|
3365
3528
|
reasons.push(
|
|
3366
|
-
`unsupported VISUAL_RECAP_AGENT "${input.agentRaw}" (expected "claude" or "
|
|
3529
|
+
`unsupported VISUAL_RECAP_AGENT "${input.agentRaw}" (expected "claude", "codex", or "openai-compatible")`,
|
|
3367
3530
|
);
|
|
3368
3531
|
} else if (agent === "codex") {
|
|
3369
3532
|
if (!input.hasOpenai)
|
|
3370
3533
|
reasons.push("OPENAI_API_KEY not configured (codex backend)");
|
|
3371
|
-
} else {
|
|
3534
|
+
} else if (agent === "claude") {
|
|
3372
3535
|
if (!input.hasAnthropic)
|
|
3373
3536
|
reasons.push("ANTHROPIC_API_KEY not configured (claude backend)");
|
|
3537
|
+
} else {
|
|
3538
|
+
if (!input.hasOpenaiCompatible)
|
|
3539
|
+
reasons.push(
|
|
3540
|
+
"VISUAL_RECAP_API_KEY not configured (openai-compatible backend)",
|
|
3541
|
+
);
|
|
3542
|
+
reasons.push(
|
|
3543
|
+
...validateOpenAiCompatibleRecapVariables({
|
|
3544
|
+
baseUrl: input.baseUrl,
|
|
3545
|
+
model: input.model,
|
|
3546
|
+
}).map((problem) => problem.reason),
|
|
3547
|
+
);
|
|
3374
3548
|
}
|
|
3375
3549
|
|
|
3376
3550
|
// Validate VISUAL_RECAP_MODEL if set — an unchecked value could be injected by
|
|
3377
3551
|
// a repo settings writer and passed straight to the agent CLI.
|
|
3378
3552
|
const model = input.model || "";
|
|
3379
|
-
if (
|
|
3553
|
+
if (
|
|
3554
|
+
agent !== "openai-compatible" &&
|
|
3555
|
+
model &&
|
|
3556
|
+
!RECAP_MODEL_PATTERN.test(model)
|
|
3557
|
+
) {
|
|
3380
3558
|
reasons.push(
|
|
3381
3559
|
"invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})",
|
|
3382
3560
|
);
|
|
@@ -3514,8 +3692,10 @@ async function runGate(): Promise<void> {
|
|
|
3514
3692
|
hasPlan: process.env.HAS_PLAN === "true",
|
|
3515
3693
|
hasAnthropic: process.env.HAS_ANTHROPIC === "true",
|
|
3516
3694
|
hasOpenai: process.env.HAS_OPENAI === "true",
|
|
3695
|
+
hasOpenaiCompatible: process.env.HAS_COMPATIBLE === "true",
|
|
3517
3696
|
agentRaw: process.env.AGENT,
|
|
3518
3697
|
model: process.env.VISUAL_RECAP_MODEL,
|
|
3698
|
+
baseUrl: process.env.VISUAL_RECAP_BASE_URL,
|
|
3519
3699
|
skillSource: process.env.VISUAL_RECAP_SKILL_SOURCE,
|
|
3520
3700
|
changedFiles,
|
|
3521
3701
|
});
|
|
@@ -4103,6 +4283,49 @@ export function parseCodexUsage(jsonl: string): ParsedUsage | null {
|
|
|
4103
4283
|
};
|
|
4104
4284
|
}
|
|
4105
4285
|
|
|
4286
|
+
/** Parse the usage sidecar emitted by an Agent-Native Code run. */
|
|
4287
|
+
export function parseOpenAiCompatibleUsage(json: string): ParsedUsage | null {
|
|
4288
|
+
const obj = parseLastJsonObject(json);
|
|
4289
|
+
const usage = obj?.usage ?? obj;
|
|
4290
|
+
if (!usage || typeof usage !== "object") return null;
|
|
4291
|
+
|
|
4292
|
+
const input =
|
|
4293
|
+
usage.inputTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? undefined;
|
|
4294
|
+
const output =
|
|
4295
|
+
usage.outputTokens ??
|
|
4296
|
+
usage.output_tokens ??
|
|
4297
|
+
usage.completion_tokens ??
|
|
4298
|
+
undefined;
|
|
4299
|
+
if (input == null && output == null) return null;
|
|
4300
|
+
|
|
4301
|
+
const asCount = (value: unknown): number => {
|
|
4302
|
+
const parsed = Number(value ?? 0);
|
|
4303
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
|
4304
|
+
};
|
|
4305
|
+
const inputDetails = usage.inputTokenDetails;
|
|
4306
|
+
return {
|
|
4307
|
+
inputTokens: asCount(input),
|
|
4308
|
+
outputTokens: asCount(output),
|
|
4309
|
+
cacheReadTokens: asCount(
|
|
4310
|
+
usage.cacheReadTokens ??
|
|
4311
|
+
usage.cachedInputTokens ??
|
|
4312
|
+
usage.cache_read_input_tokens ??
|
|
4313
|
+
inputDetails?.cacheReadTokens,
|
|
4314
|
+
),
|
|
4315
|
+
cacheWriteTokens: asCount(
|
|
4316
|
+
usage.cacheWriteTokens ??
|
|
4317
|
+
usage.cache_write_tokens ??
|
|
4318
|
+
inputDetails?.cacheWriteTokens,
|
|
4319
|
+
),
|
|
4320
|
+
model:
|
|
4321
|
+
typeof obj?.model === "string"
|
|
4322
|
+
? obj.model
|
|
4323
|
+
: typeof usage.model === "string"
|
|
4324
|
+
? usage.model
|
|
4325
|
+
: undefined,
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4106
4329
|
/**
|
|
4107
4330
|
* `recap usage` — parse the agent's run output for token usage and POST it to
|
|
4108
4331
|
* the plan app's record-recap-usage action so the recap row carries cost. The
|
|
@@ -4134,7 +4357,12 @@ async function runUsage(args: Record<string, string | boolean>): Promise<void> {
|
|
|
4134
4357
|
path.resolve(stringArg(args, "result-file")),
|
|
4135
4358
|
"utf8",
|
|
4136
4359
|
);
|
|
4137
|
-
parsed =
|
|
4360
|
+
parsed =
|
|
4361
|
+
agent === "codex"
|
|
4362
|
+
? parseCodexUsage(raw)
|
|
4363
|
+
: agent === "openai-compatible"
|
|
4364
|
+
? parseOpenAiCompatibleUsage(raw)
|
|
4365
|
+
: parseClaudeUsage(raw);
|
|
4138
4366
|
} catch (err) {
|
|
4139
4367
|
done({ ok: false, reason: `could not read/parse usage: ${String(err)}` });
|
|
4140
4368
|
return;
|
|
@@ -4149,10 +4377,18 @@ async function runUsage(args: Record<string, string | boolean>): Promise<void> {
|
|
|
4149
4377
|
const model =
|
|
4150
4378
|
parsed.model ??
|
|
4151
4379
|
optionalArg(args, "model") ??
|
|
4152
|
-
(agent === "codex"
|
|
4380
|
+
(agent === "codex"
|
|
4381
|
+
? "gpt-5.6-sol"
|
|
4382
|
+
: agent === "openai-compatible"
|
|
4383
|
+
? "openai-compatible"
|
|
4384
|
+
: "claude");
|
|
4385
|
+
const usageAgent =
|
|
4386
|
+
agent === "claude" || agent === "codex" || agent === "openai-compatible"
|
|
4387
|
+
? agent
|
|
4388
|
+
: undefined;
|
|
4153
4389
|
const body: Record<string, unknown> = {
|
|
4154
4390
|
planId,
|
|
4155
|
-
...(
|
|
4391
|
+
...(usageAgent ? { agent: usageAgent } : {}),
|
|
4156
4392
|
model,
|
|
4157
4393
|
inputTokens: parsed.inputTokens,
|
|
4158
4394
|
outputTokens: parsed.outputTokens,
|
|
@@ -4230,16 +4466,16 @@ function runAgentSummary(args: Record<string, string | boolean>): void {
|
|
|
4230
4466
|
const HELP = `npx @agent-native/core@latest recap — PR visual recap helpers (used by the GitHub Action)
|
|
4231
4467
|
|
|
4232
4468
|
Usage:
|
|
4233
|
-
npx @agent-native/core@latest recap setup [--repo owner/name] [--agent claude|codex] [--app-url <url>] [--skip-secrets] [--dry-run] [--force]
|
|
4234
|
-
npx @agent-native/core@latest recap doctor [--repo owner/name] [--agent claude|codex] [--app-url <url>]
|
|
4469
|
+
npx @agent-native/core@latest recap setup [--repo owner/name] [--agent claude|codex|openai-compatible] [--app-url <url>] [--skip-secrets] [--dry-run] [--force]
|
|
4470
|
+
npx @agent-native/core@latest recap doctor [--repo owner/name] [--agent claude|codex|openai-compatible] [--app-url <url>]
|
|
4235
4471
|
npx @agent-native/core@latest recap collect-diff --base <baseSha> --head <headSha> [--out recap.diff] [--stat recap.stat]
|
|
4236
4472
|
npx @agent-native/core@latest recap block-reference [--app-url <url>] [--out recap-blocks.md]
|
|
4237
4473
|
npx @agent-native/core@latest recap scan --diff <path> [--mode off|high-confidence|strict]
|
|
4238
4474
|
npx @agent-native/core@latest recap build-prompt --pr <n> [--repo owner/name] [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--block-reference recap-blocks.md] [--prev-plan-id <id>] [--huge] [--local-files] [--local-dir <folder>] [--skill-source auto|latest|repo] [--out <path>]
|
|
4239
4475
|
npx @agent-native/core@latest recap publish [--source recap-source.json] [--out recap-url.txt] [--repo owner/name] [--pr <n>] [--prev-plan-id <id>] [--source-pr-state open|closed|merged] [--source-pr-merged-at <iso>] [--source-author-email <email>] [--source-author-name <name>] [--source-author-login <login>] [--app-url <url>] [--token <planToken>] [--github-token <ghToken>]
|
|
4240
4476
|
npx @agent-native/core@latest recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png] [--theme light|dark] [--image-cache-key <key>]
|
|
4241
|
-
npx @agent-native/core@latest recap usage --plan-url <planUrl> --result-file <path> --app-url <url> --token <planToken> [--agent claude|codex] [--model <id>]
|
|
4242
|
-
npx @agent-native/core@latest recap agent-summary --result-file <path> [--stderr-file <path>] [--exit-code-file <path>] [--agent claude|codex]
|
|
4477
|
+
npx @agent-native/core@latest recap usage --plan-url <planUrl> --result-file <path> --app-url <url> --token <planToken> [--agent claude|codex|openai-compatible] [--model <id>]
|
|
4478
|
+
npx @agent-native/core@latest recap agent-summary --result-file <path> [--stderr-file <path>] [--exit-code-file <path>] [--agent claude|codex|openai-compatible]
|
|
4243
4479
|
npx @agent-native/core@latest recap comment <find-plan-id|upsert> --repo owner/name --issue <n> --token <github-token>
|
|
4244
4480
|
npx @agent-native/core@latest recap check start [--repo owner/name] [--sha <headSha>] [--token <github-token>] [--workflow-url <url>]
|
|
4245
4481
|
Create the in-progress "Visual Recap" GitHub check run and write its id to
|
|
@@ -4256,16 +4492,16 @@ Usage:
|
|
|
4256
4492
|
The PR Visual Recap security gate. Decides whether to run the recap at all
|
|
4257
4493
|
and which (normalized) backend agent to use. Reads the pull_request payload
|
|
4258
4494
|
from $GITHUB_EVENT_PATH, the secret-presence/agent/model signals from the
|
|
4259
|
-
environment (HAS_PLAN / HAS_ANTHROPIC / HAS_OPENAI === 'true', AGENT,
|
|
4260
|
-
VISUAL_RECAP_MODEL), the repo from $GITHUB_REPOSITORY, and the PR's changed
|
|
4495
|
+
environment (HAS_PLAN / HAS_ANTHROPIC / HAS_OPENAI / HAS_COMPATIBLE === 'true', AGENT,
|
|
4496
|
+
VISUAL_RECAP_MODEL / VISUAL_RECAP_BASE_URL), the repo from $GITHUB_REPOSITORY, and the PR's changed
|
|
4261
4497
|
files from the GitHub REST API (paged, with GH_TOKEN/GITHUB_TOKEN). Skips
|
|
4262
4498
|
drafts, forks without secret access, bot authors, the missing-secret case, an
|
|
4263
4499
|
invalid agent/model, and any untrusted PR that touches recap-control files
|
|
4264
4500
|
(repo-pinned skill instructions, .claude/**, root CLAUDE.md, root AGENTS.md,
|
|
4265
4501
|
root .mcp.json) — failing CLOSED on any file-list error. Writes
|
|
4266
|
-
run=<true|false> and agent=<claude|codex> to $GITHUB_OUTPUT.
|
|
4502
|
+
run=<true|false> and agent=<claude|codex|openai-compatible> to $GITHUB_OUTPUT.
|
|
4267
4503
|
npx @agent-native/core@latest recap agent-summary
|
|
4268
|
-
Read the captured
|
|
4504
|
+
Read the captured agent result file and write a sanitized one-line
|
|
4269
4505
|
summary to stdout and $GITHUB_OUTPUT (summary). Used only when no plan URL
|
|
4270
4506
|
was produced, so PR comments/checks explain the actual failure.
|
|
4271
4507
|
npx @agent-native/core@latest recap scan
|
|
@@ -4286,7 +4522,8 @@ Usage:
|
|
|
4286
4522
|
commands; secret values are sent to gh through stdin, never argv.
|
|
4287
4523
|
npx @agent-native/core@latest recap doctor
|
|
4288
4524
|
Check workflow presence/drift, local Plans publish-token availability, gh
|
|
4289
|
-
repo access, and required GitHub Actions secrets for the
|
|
4525
|
+
repo access, and required GitHub Actions secrets and variables for the
|
|
4526
|
+
selected backend, including provider-variable validity.
|
|
4290
4527
|
`;
|
|
4291
4528
|
|
|
4292
4529
|
export async function runRecap(argv: string[]): Promise<void> {
|
|
@@ -6,6 +6,7 @@ import { agentNativePath } from "../api-path.js";
|
|
|
6
6
|
interface UsageBucket {
|
|
7
7
|
key: string;
|
|
8
8
|
cents: number;
|
|
9
|
+
cost: UsageCostAggregate;
|
|
9
10
|
calls: number;
|
|
10
11
|
inputTokens: number;
|
|
11
12
|
outputTokens: number;
|
|
@@ -16,6 +17,7 @@ interface UsageBucket {
|
|
|
16
17
|
interface DailyBucket {
|
|
17
18
|
date: string;
|
|
18
19
|
cents: number;
|
|
20
|
+
cost: UsageCostAggregate;
|
|
19
21
|
calls: number;
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -30,8 +32,14 @@ interface UsageRecentEntry {
|
|
|
30
32
|
cacheReadTokens: number;
|
|
31
33
|
cacheWriteTokens: number;
|
|
32
34
|
cents: number;
|
|
35
|
+
costSource: "reported" | "estimated" | "unavailable";
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
type UsageCostAggregate =
|
|
39
|
+
| { status: "known"; knownCents: number; unavailableCalls: 0 }
|
|
40
|
+
| { status: "partial"; knownCents: number; unavailableCalls: number }
|
|
41
|
+
| { status: "unavailable"; knownCents: 0; unavailableCalls: number };
|
|
42
|
+
|
|
35
43
|
interface UsageBillingMode {
|
|
36
44
|
unit: "usd" | "builder-credits";
|
|
37
45
|
label: string;
|
|
@@ -44,6 +52,7 @@ interface UsageBillingMode {
|
|
|
44
52
|
interface UsageSummary {
|
|
45
53
|
billing?: UsageBillingMode;
|
|
46
54
|
totalCents: number;
|
|
55
|
+
totalCost: UsageCostAggregate;
|
|
47
56
|
totalCalls: number;
|
|
48
57
|
totalInputTokens: number;
|
|
49
58
|
totalOutputTokens: number;
|
|
@@ -103,6 +112,15 @@ function formatSpend(cents: number, billing: UsageBillingMode): string {
|
|
|
103
112
|
return `$${(cents / 100).toFixed(2)}`;
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
function formatAggregateCost(
|
|
116
|
+
cost: UsageCostAggregate,
|
|
117
|
+
billing: UsageBillingMode,
|
|
118
|
+
): string {
|
|
119
|
+
if (cost.status === "known") return formatSpend(cost.knownCents, billing);
|
|
120
|
+
if (cost.status === "unavailable") return "Unknown";
|
|
121
|
+
return `${formatSpend(cost.knownCents, billing)} + ${cost.unavailableCalls} unpriced`;
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
function formatTokens(n: number): string {
|
|
107
125
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
108
126
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
@@ -139,7 +157,7 @@ function BucketBars({
|
|
|
139
157
|
{b.key || "(none)"}
|
|
140
158
|
</span>
|
|
141
159
|
<span className="shrink-0 text-muted-foreground tabular-nums">
|
|
142
|
-
{
|
|
160
|
+
{formatAggregateCost(b.cost, billing)}
|
|
143
161
|
<span className="ms-1 opacity-60">
|
|
144
162
|
· {formatTokens(b.inputTokens + b.outputTokens)} tok
|
|
145
163
|
</span>
|
|
@@ -183,7 +201,7 @@ function DailySparkline({
|
|
|
183
201
|
(displayAmountFromCostCents(d.cents, billing) / max) * 100,
|
|
184
202
|
)}%`,
|
|
185
203
|
}}
|
|
186
|
-
title={`${d.date}: ${
|
|
204
|
+
title={`${d.date}: ${formatAggregateCost(d.cost, billing)} (${d.calls} calls)`}
|
|
187
205
|
/>
|
|
188
206
|
))}
|
|
189
207
|
</div>
|
|
@@ -266,7 +284,7 @@ export function UsageSection() {
|
|
|
266
284
|
: "Total spend"}
|
|
267
285
|
</div>
|
|
268
286
|
<div className="text-[18px] font-semibold tabular-nums">
|
|
269
|
-
{
|
|
287
|
+
{formatAggregateCost(data.totalCost, billing)}
|
|
270
288
|
</div>
|
|
271
289
|
</div>
|
|
272
290
|
<div className="text-end">
|
|
@@ -352,7 +370,9 @@ export function UsageSection() {
|
|
|
352
370
|
</div>
|
|
353
371
|
</div>
|
|
354
372
|
<div className="shrink-0 text-end tabular-nums text-muted-foreground">
|
|
355
|
-
{
|
|
373
|
+
{r.costSource === "unavailable"
|
|
374
|
+
? "Unknown"
|
|
375
|
+
: formatSpend(r.cents, billing)}
|
|
356
376
|
</div>
|
|
357
377
|
</div>
|
|
358
378
|
))}
|
|
@@ -370,7 +390,8 @@ export function UsageSection() {
|
|
|
370
390
|
<p className="text-[10px] text-muted-foreground">
|
|
371
391
|
Spend is estimated from published Anthropic pricing and your own
|
|
372
392
|
recorded token counts. Cached input is priced at ~10% of regular
|
|
373
|
-
input.
|
|
393
|
+
input. Calls without provider pricing are marked unknown and
|
|
394
|
+
excluded from known-cost subtotals.
|
|
374
395
|
</p>
|
|
375
396
|
)}
|
|
376
397
|
</>
|
package/corpus/core/src/index.ts
CHANGED