@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
|
@@ -181,6 +181,12 @@ export interface PlatformAdapterCapabilities {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
export interface PlatformRunProgress {
|
|
184
|
+
/**
|
|
185
|
+
* Opaque, provider-owned reference for resuming this progress surface from a
|
|
186
|
+
* durable continuation. It deliberately contains no user content,
|
|
187
|
+
* credentials, or provider payload.
|
|
188
|
+
*/
|
|
189
|
+
ref?: PlatformRunProgressRef;
|
|
184
190
|
/** Receive normalized agent events. Implementations should throttle writes. */
|
|
185
191
|
onEvent(event: AgentChatEvent): Promise<void> | void;
|
|
186
192
|
/** Finalize the provider-native progress surface with the answer. */
|
|
@@ -189,6 +195,19 @@ export interface PlatformRunProgress {
|
|
|
189
195
|
fail?(message: string): Promise<void>;
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Safe, minimal reference to a provider-native run-progress surface.
|
|
200
|
+
*
|
|
201
|
+
* The field values are opaque to the framework. Adapters may use `kind` to
|
|
202
|
+
* distinguish their own resume strategy and `streamTs` to identify the
|
|
203
|
+
* provider-side stream. No incoming message text, platform payload, or
|
|
204
|
+
* credential belongs here.
|
|
205
|
+
*/
|
|
206
|
+
export interface PlatformRunProgressRef {
|
|
207
|
+
kind: string;
|
|
208
|
+
streamTs: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
192
211
|
export interface ImmediateWebhookResponse {
|
|
193
212
|
status: number;
|
|
194
213
|
body: unknown;
|
|
@@ -304,6 +323,17 @@ export interface PlatformAdapter {
|
|
|
304
323
|
incoming: IncomingMessage,
|
|
305
324
|
): Promise<PlatformRunProgress | null>;
|
|
306
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Reattach a durable continuation to a provider-native progress surface
|
|
328
|
+
* previously started by this adapter. Adapters that cannot resume a native
|
|
329
|
+
* surface should omit this and the continuation will use its normal reply
|
|
330
|
+
* path instead.
|
|
331
|
+
*/
|
|
332
|
+
resumeRunProgress?(
|
|
333
|
+
incoming: IncomingMessage,
|
|
334
|
+
ref: PlatformRunProgressRef,
|
|
335
|
+
): Promise<PlatformRunProgress | null>;
|
|
336
|
+
|
|
307
337
|
/**
|
|
308
338
|
* Send a proactive outbound message to a platform destination. Adapters that
|
|
309
339
|
* only support direct replies can omit this.
|
|
@@ -796,6 +796,7 @@ async function processIncomingMessage(
|
|
|
796
796
|
attempts: opts.attempts,
|
|
797
797
|
incoming,
|
|
798
798
|
placeholderRef: opts.placeholderRef,
|
|
799
|
+
progressRef: progress?.ref,
|
|
799
800
|
scopeId: incoming.integrationScopeId,
|
|
800
801
|
principalType: opts.principalType ?? "user",
|
|
801
802
|
lineage: {
|
|
@@ -872,8 +873,9 @@ async function processIncomingMessage(
|
|
|
872
873
|
},
|
|
873
874
|
async (completedRun: ActiveRun) => {
|
|
874
875
|
let keepSlackInputWindow = false;
|
|
876
|
+
let queuedA2AContinuation = false;
|
|
875
877
|
try {
|
|
876
|
-
|
|
878
|
+
queuedA2AContinuation = hasQueuedA2AContinuation(completedRun);
|
|
877
879
|
const slackInputRequest =
|
|
878
880
|
incoming.platform === "slack"
|
|
879
881
|
? extractSlackInputRequest(completedRun)
|
|
@@ -963,7 +965,15 @@ async function processIncomingMessage(
|
|
|
963
965
|
threadDeepLinkUrl,
|
|
964
966
|
});
|
|
965
967
|
let delivered = false;
|
|
966
|
-
if (progress) {
|
|
968
|
+
if (queuedA2AContinuation && progress?.ref) {
|
|
969
|
+
// Post substantive parent results as a normal thread reply while
|
|
970
|
+
// the one continuation that claimed this resumable stream keeps
|
|
971
|
+
// it open for its eventual terminal result.
|
|
972
|
+
await adapter.sendResponse(outgoing, incoming, {
|
|
973
|
+
placeholderRef: opts.placeholderRef,
|
|
974
|
+
});
|
|
975
|
+
delivered = true;
|
|
976
|
+
} else if (progress) {
|
|
967
977
|
try {
|
|
968
978
|
await progress.complete(outgoing);
|
|
969
979
|
delivered = true;
|
|
@@ -988,24 +998,35 @@ async function processIncomingMessage(
|
|
|
988
998
|
keepSlackInputWindow = true;
|
|
989
999
|
}
|
|
990
1000
|
} else if (progress) {
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
//
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1001
|
+
// A continuation owns the eventual final response. If the adapter
|
|
1002
|
+
// supplied a durable progress reference, leave the same native
|
|
1003
|
+
// stream open for the continuation processor to update and close;
|
|
1004
|
+
// ending it here discards the plan/task UI before the delegated
|
|
1005
|
+
// work has actually finished.
|
|
1006
|
+
if (progress.ref) {
|
|
1007
|
+
await progress.onEvent({
|
|
1008
|
+
type: "agent_call_progress",
|
|
1009
|
+
agent:
|
|
1010
|
+
getQueuedA2AContinuationAgent(completedRun) ??
|
|
1011
|
+
"delegated agent",
|
|
1012
|
+
state: "working",
|
|
1013
|
+
elapsedSeconds: 0,
|
|
1014
|
+
detail: "Continuing in the background",
|
|
1015
|
+
});
|
|
1016
|
+
} else {
|
|
1017
|
+
// Older adapters have no resumable native surface. Close their
|
|
1018
|
+
// stream cleanly; the continuation will deliver one standard
|
|
1019
|
+
// final reply when the downstream task is terminal.
|
|
1020
|
+
const deferred = adapter.formatAgentResponse(
|
|
1007
1021
|
"The delegated agent is still working. I’ll post its final result in this thread automatically.",
|
|
1008
1022
|
);
|
|
1023
|
+
try {
|
|
1024
|
+
await progress.complete(deferred);
|
|
1025
|
+
} catch {
|
|
1026
|
+
await progress.fail?.(
|
|
1027
|
+
"The delegated agent is still working. I’ll post its final result in this thread automatically.",
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1009
1030
|
}
|
|
1010
1031
|
}
|
|
1011
1032
|
|
|
@@ -1036,6 +1057,10 @@ async function processIncomingMessage(
|
|
|
1036
1057
|
`[integrations] Error sending response to ${incoming.platform}:`,
|
|
1037
1058
|
err,
|
|
1038
1059
|
);
|
|
1060
|
+
// A queued continuation owns the final platform response. Later
|
|
1061
|
+
// bookkeeping failures (for example, persisting this parent run)
|
|
1062
|
+
// must not close its resumable native stream with a false failure.
|
|
1063
|
+
if (queuedA2AContinuation) return;
|
|
1039
1064
|
// Last-ditch: try to post a brief apology so the thread isn't silent.
|
|
1040
1065
|
try {
|
|
1041
1066
|
await progress?.fail?.(
|
|
@@ -1322,6 +1347,17 @@ function hasQueuedA2AContinuation(completedRun: ActiveRun): boolean {
|
|
|
1322
1347
|
});
|
|
1323
1348
|
}
|
|
1324
1349
|
|
|
1350
|
+
function getQueuedA2AContinuationAgent(completedRun: ActiveRun): string | null {
|
|
1351
|
+
for (let i = completedRun.events.length - 1; i >= 0; i--) {
|
|
1352
|
+
const event = completedRun.events[i]!.event;
|
|
1353
|
+
if (event.type !== "agent_call") continue;
|
|
1354
|
+
if (typeof event.agent === "string" && event.agent.trim()) {
|
|
1355
|
+
return event.agent;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1325
1361
|
function extractSlackInputRequest(
|
|
1326
1362
|
completedRun: ActiveRun,
|
|
1327
1363
|
): { text: string } | null {
|
|
@@ -427,6 +427,7 @@ async function enqueueIntegrationContinuationIfPossible(
|
|
|
427
427
|
externalThreadId: integration.incoming.externalThreadId,
|
|
428
428
|
incoming: integration.incoming,
|
|
429
429
|
placeholderRef: integration.placeholderRef,
|
|
430
|
+
progressRef: integration.progressRef,
|
|
430
431
|
ownerEmail,
|
|
431
432
|
orgId: getRequestOrgId() ?? null,
|
|
432
433
|
agentName: agent.name,
|
|
@@ -164,6 +164,8 @@ export interface RequestContext {
|
|
|
164
164
|
attempts?: number;
|
|
165
165
|
incoming: import("../integrations/types.js").IncomingMessage;
|
|
166
166
|
placeholderRef?: string;
|
|
167
|
+
/** Opaque provider-native progress surface for a durable continuation. */
|
|
168
|
+
progressRef?: import("../integrations/types.js").PlatformRunProgressRef;
|
|
167
169
|
installationId?: string;
|
|
168
170
|
scopeId?: string;
|
|
169
171
|
principalType?: "user" | "service";
|
|
@@ -197,6 +197,8 @@ export interface UsageRecord {
|
|
|
197
197
|
* provider-reported dollar cost so two surfaces agree exactly.
|
|
198
198
|
*/
|
|
199
199
|
costCentsX100?: number;
|
|
200
|
+
/** Whether cost is provider-reported, estimated, or unavailable. */
|
|
201
|
+
costSource?: UsageCostSource;
|
|
200
202
|
orgId?: string;
|
|
201
203
|
runId?: string;
|
|
202
204
|
threadId?: string;
|
|
@@ -206,6 +208,8 @@ export interface UsageRecord {
|
|
|
206
208
|
sourceId?: string;
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
export type UsageCostSource = "reported" | "estimated" | "unavailable";
|
|
212
|
+
|
|
209
213
|
let _initPromise: Promise<void> | undefined;
|
|
210
214
|
|
|
211
215
|
async function ensureUsageTable(): Promise<void> {
|
|
@@ -221,6 +225,7 @@ async function ensureUsageTable(): Promise<void> {
|
|
|
221
225
|
cache_read_tokens ${intType()} NOT NULL DEFAULT 0,
|
|
222
226
|
cache_write_tokens ${intType()} NOT NULL DEFAULT 0,
|
|
223
227
|
cost_cents_x100 ${intType()} NOT NULL DEFAULT 0,
|
|
228
|
+
cost_source TEXT NOT NULL DEFAULT 'estimated',
|
|
224
229
|
model TEXT NOT NULL DEFAULT '',
|
|
225
230
|
label TEXT NOT NULL DEFAULT 'chat',
|
|
226
231
|
app TEXT NOT NULL DEFAULT '',
|
|
@@ -240,6 +245,7 @@ async function ensureUsageTable(): Promise<void> {
|
|
|
240
245
|
const additions: Array<[string, string]> = [
|
|
241
246
|
["cache_read_tokens", `${intType()} NOT NULL DEFAULT 0`],
|
|
242
247
|
["cache_write_tokens", `${intType()} NOT NULL DEFAULT 0`],
|
|
248
|
+
["cost_source", `TEXT NOT NULL DEFAULT 'estimated'`],
|
|
243
249
|
["label", `TEXT NOT NULL DEFAULT 'chat'`],
|
|
244
250
|
["app", `TEXT NOT NULL DEFAULT ''`],
|
|
245
251
|
["ref_id", `TEXT NOT NULL DEFAULT ''`],
|
|
@@ -379,6 +385,7 @@ export async function recordUsage(
|
|
|
379
385
|
app,
|
|
380
386
|
refId,
|
|
381
387
|
costCentsX100,
|
|
388
|
+
costSource,
|
|
382
389
|
orgId,
|
|
383
390
|
runId,
|
|
384
391
|
threadId,
|
|
@@ -410,14 +417,24 @@ export async function recordUsage(
|
|
|
410
417
|
|
|
411
418
|
// Prefer an explicit precomputed cost (e.g. a provider-reported dollar cost);
|
|
412
419
|
// otherwise derive it from tokens via the pricing table.
|
|
420
|
+
const resolvedCostSource =
|
|
421
|
+
costSource ?? (costCentsX100 == null ? "estimated" : "reported");
|
|
413
422
|
const costX100 =
|
|
414
|
-
|
|
415
|
-
|
|
423
|
+
resolvedCostSource === "unavailable"
|
|
424
|
+
? 0
|
|
425
|
+
: (costCentsX100 ??
|
|
426
|
+
calculateCost(
|
|
427
|
+
inTok,
|
|
428
|
+
outTok,
|
|
429
|
+
modelName,
|
|
430
|
+
cacheReadTokens,
|
|
431
|
+
cacheWriteTokens,
|
|
432
|
+
));
|
|
416
433
|
const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
417
434
|
await client.execute({
|
|
418
435
|
sql: `INSERT INTO token_usage
|
|
419
|
-
(id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, ref_id, org_id, run_id, thread_id, task_id, integration_scope_id, source_platform, source_id, created_at)
|
|
420
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
436
|
+
(id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, cost_source, model, label, app, ref_id, org_id, run_id, thread_id, task_id, integration_scope_id, source_platform, source_id, created_at)
|
|
437
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
421
438
|
args: [
|
|
422
439
|
id,
|
|
423
440
|
ownerEmail,
|
|
@@ -426,6 +443,7 @@ export async function recordUsage(
|
|
|
426
443
|
cacheReadTokens,
|
|
427
444
|
cacheWriteTokens,
|
|
428
445
|
costX100,
|
|
446
|
+
resolvedCostSource,
|
|
429
447
|
modelName,
|
|
430
448
|
resolvedLabel,
|
|
431
449
|
resolvedApp,
|
|
@@ -469,6 +487,7 @@ export interface UsageBucket {
|
|
|
469
487
|
cacheReadTokens: number;
|
|
470
488
|
cacheWriteTokens: number;
|
|
471
489
|
cents: number;
|
|
490
|
+
cost: UsageCostAggregate;
|
|
472
491
|
calls: number;
|
|
473
492
|
}
|
|
474
493
|
|
|
@@ -476,9 +495,15 @@ export interface DailyBucket {
|
|
|
476
495
|
/** YYYY-MM-DD (UTC) */
|
|
477
496
|
date: string;
|
|
478
497
|
cents: number;
|
|
498
|
+
cost: UsageCostAggregate;
|
|
479
499
|
calls: number;
|
|
480
500
|
}
|
|
481
501
|
|
|
502
|
+
export type UsageCostAggregate =
|
|
503
|
+
| { status: "known"; knownCents: number; unavailableCalls: 0 }
|
|
504
|
+
| { status: "partial"; knownCents: number; unavailableCalls: number }
|
|
505
|
+
| { status: "unavailable"; knownCents: 0; unavailableCalls: number };
|
|
506
|
+
|
|
482
507
|
export interface UsageRecentEntry {
|
|
483
508
|
id: number;
|
|
484
509
|
createdAt: number;
|
|
@@ -490,11 +515,14 @@ export interface UsageRecentEntry {
|
|
|
490
515
|
cacheReadTokens: number;
|
|
491
516
|
cacheWriteTokens: number;
|
|
492
517
|
cents: number;
|
|
518
|
+
costSource: UsageCostSource;
|
|
493
519
|
}
|
|
494
520
|
|
|
495
521
|
export interface UsageSummary {
|
|
496
522
|
billing?: UsageBillingMode;
|
|
523
|
+
/** Legacy known-cost subtotal. Use totalCost to preserve unavailable spend. */
|
|
497
524
|
totalCents: number;
|
|
525
|
+
totalCost: UsageCostAggregate;
|
|
498
526
|
totalCalls: number;
|
|
499
527
|
totalInputTokens: number;
|
|
500
528
|
totalOutputTokens: number;
|
|
@@ -523,7 +551,8 @@ export async function getUsageSummary(
|
|
|
523
551
|
|
|
524
552
|
const totalRow = await client.execute({
|
|
525
553
|
sql: `SELECT
|
|
526
|
-
COALESCE(SUM(cost_cents_x100), 0) AS
|
|
554
|
+
COALESCE(SUM(CASE WHEN cost_source = 'unavailable' THEN 0 ELSE cost_cents_x100 END), 0) AS known_cents,
|
|
555
|
+
COALESCE(SUM(CASE WHEN cost_source = 'unavailable' THEN 1 ELSE 0 END), 0) AS unavailable_calls,
|
|
527
556
|
COUNT(*) AS calls,
|
|
528
557
|
COALESCE(SUM(input_tokens), 0) AS in_tok,
|
|
529
558
|
COALESCE(SUM(output_tokens), 0) AS out_tok,
|
|
@@ -536,7 +565,8 @@ export async function getUsageSummary(
|
|
|
536
565
|
|
|
537
566
|
const bucketSql = (col: string) => ({
|
|
538
567
|
sql: `SELECT ${col} AS k,
|
|
539
|
-
COALESCE(SUM(cost_cents_x100), 0) AS
|
|
568
|
+
COALESCE(SUM(CASE WHEN cost_source = 'unavailable' THEN 0 ELSE cost_cents_x100 END), 0) AS known_cents,
|
|
569
|
+
COALESCE(SUM(CASE WHEN cost_source = 'unavailable' THEN 1 ELSE 0 END), 0) AS unavailable_calls,
|
|
540
570
|
COUNT(*) AS calls,
|
|
541
571
|
COALESCE(SUM(input_tokens), 0) AS in_tok,
|
|
542
572
|
COALESCE(SUM(output_tokens), 0) AS out_tok,
|
|
@@ -545,16 +575,19 @@ export async function getUsageSummary(
|
|
|
545
575
|
FROM token_usage
|
|
546
576
|
WHERE owner_email = ? AND created_at >= ?
|
|
547
577
|
GROUP BY ${col}
|
|
548
|
-
ORDER BY
|
|
578
|
+
ORDER BY known_cents DESC`,
|
|
549
579
|
args: [options.ownerEmail, sinceMs],
|
|
550
580
|
});
|
|
551
581
|
|
|
552
582
|
const mapBuckets = (rows: unknown[]): UsageBucket[] =>
|
|
553
583
|
rows.map((r) => {
|
|
554
584
|
const row = r as Record<string, number | string | null>;
|
|
585
|
+
const knownCents = Number(row.known_cents ?? 0) / 100;
|
|
586
|
+
const unavailableCalls = Number(row.unavailable_calls ?? 0);
|
|
555
587
|
return {
|
|
556
588
|
key: String(row.k ?? ""),
|
|
557
|
-
cents:
|
|
589
|
+
cents: knownCents,
|
|
590
|
+
cost: buildUsageCostAggregate(knownCents, unavailableCalls),
|
|
558
591
|
calls: Number(row.calls ?? 0),
|
|
559
592
|
inputTokens: Number(row.in_tok ?? 0),
|
|
560
593
|
outputTokens: Number(row.out_tok ?? 0),
|
|
@@ -573,30 +606,42 @@ export async function getUsageSummary(
|
|
|
573
606
|
// date functions (SQLite `strftime`, Postgres `to_char`). Cheap enough
|
|
574
607
|
// for a 30-day window; if this grows, swap for a dialect-aware query.
|
|
575
608
|
const dayRows = await client.execute({
|
|
576
|
-
sql: `SELECT created_at, cost_cents_x100 FROM token_usage
|
|
609
|
+
sql: `SELECT created_at, cost_cents_x100, cost_source FROM token_usage
|
|
577
610
|
WHERE owner_email = ? AND created_at >= ?`,
|
|
578
611
|
args: [options.ownerEmail, sinceMs],
|
|
579
612
|
});
|
|
580
|
-
const dayMap = new Map<
|
|
581
|
-
|
|
613
|
+
const dayMap = new Map<
|
|
614
|
+
string,
|
|
615
|
+
{ knownCentsX100: number; unavailableCalls: number; calls: number }
|
|
616
|
+
>();
|
|
617
|
+
for (const row of dayRows.rows as Array<Record<string, number | string>>) {
|
|
582
618
|
const date = new Date(Number(row.created_at)).toISOString().slice(0, 10);
|
|
583
|
-
const prev = dayMap.get(date) ?? {
|
|
584
|
-
|
|
619
|
+
const prev = dayMap.get(date) ?? {
|
|
620
|
+
knownCentsX100: 0,
|
|
621
|
+
unavailableCalls: 0,
|
|
622
|
+
calls: 0,
|
|
623
|
+
};
|
|
624
|
+
if (row.cost_source === "unavailable") prev.unavailableCalls += 1;
|
|
625
|
+
else prev.knownCentsX100 += Number(row.cost_cents_x100 ?? 0);
|
|
585
626
|
prev.calls += 1;
|
|
586
627
|
dayMap.set(date, prev);
|
|
587
628
|
}
|
|
588
629
|
const byDay: DailyBucket[] = [...dayMap.entries()]
|
|
589
|
-
.map(([date, v]) =>
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
630
|
+
.map(([date, v]) => {
|
|
631
|
+
const knownCents = v.knownCentsX100 / 100;
|
|
632
|
+
return {
|
|
633
|
+
date,
|
|
634
|
+
cents: knownCents,
|
|
635
|
+
cost: buildUsageCostAggregate(knownCents, v.unavailableCalls),
|
|
636
|
+
calls: v.calls,
|
|
637
|
+
};
|
|
638
|
+
})
|
|
594
639
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
595
640
|
|
|
596
641
|
const recentRows = await client.execute({
|
|
597
642
|
sql: `SELECT id, created_at, label, app, model,
|
|
598
643
|
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
599
|
-
cost_cents_x100
|
|
644
|
+
cost_cents_x100, cost_source
|
|
600
645
|
FROM token_usage
|
|
601
646
|
WHERE owner_email = ?
|
|
602
647
|
ORDER BY created_at DESC
|
|
@@ -616,11 +661,15 @@ export async function getUsageSummary(
|
|
|
616
661
|
cacheReadTokens: Number(row.cache_read_tokens ?? 0),
|
|
617
662
|
cacheWriteTokens: Number(row.cache_write_tokens ?? 0),
|
|
618
663
|
cents: Number(row.cost_cents_x100 ?? 0) / 100,
|
|
664
|
+
costSource: String(row.cost_source ?? "estimated") as UsageCostSource,
|
|
619
665
|
}));
|
|
620
666
|
|
|
667
|
+
const knownCents = Number(t.known_cents ?? 0) / 100;
|
|
668
|
+
const unavailableCalls = Number(t.unavailable_calls ?? 0);
|
|
621
669
|
return {
|
|
622
670
|
billing: USD_USAGE_BILLING,
|
|
623
|
-
totalCents:
|
|
671
|
+
totalCents: knownCents,
|
|
672
|
+
totalCost: buildUsageCostAggregate(knownCents, unavailableCalls),
|
|
624
673
|
totalCalls: Number(t.calls ?? 0),
|
|
625
674
|
totalInputTokens: Number(t.in_tok ?? 0),
|
|
626
675
|
totalOutputTokens: Number(t.out_tok ?? 0),
|
|
@@ -634,3 +683,16 @@ export async function getUsageSummary(
|
|
|
634
683
|
recent,
|
|
635
684
|
};
|
|
636
685
|
}
|
|
686
|
+
|
|
687
|
+
function buildUsageCostAggregate(
|
|
688
|
+
knownCents: number,
|
|
689
|
+
unavailableCalls: number,
|
|
690
|
+
): UsageCostAggregate {
|
|
691
|
+
if (unavailableCalls === 0) {
|
|
692
|
+
return { status: "known", knownCents, unavailableCalls: 0 };
|
|
693
|
+
}
|
|
694
|
+
if (knownCents === 0) {
|
|
695
|
+
return { status: "unavailable", knownCents: 0, unavailableCalls };
|
|
696
|
+
}
|
|
697
|
+
return { status: "partial", knownCents, unavailableCalls };
|
|
698
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type RecapUsageCostSource = "reported" | "estimated" | "unavailable";
|
|
2
|
+
|
|
3
|
+
export interface RecapUsageCostInput {
|
|
4
|
+
agent?: "claude" | "codex" | "openai-compatible";
|
|
5
|
+
model: string;
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
cacheReadTokens: number;
|
|
9
|
+
cacheWriteTokens: number;
|
|
10
|
+
reportedCostUsd?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type CostEstimator = (
|
|
14
|
+
inputTokens: number,
|
|
15
|
+
outputTokens: number,
|
|
16
|
+
model: string,
|
|
17
|
+
cacheReadTokens: number,
|
|
18
|
+
cacheWriteTokens: number,
|
|
19
|
+
) => number;
|
|
20
|
+
|
|
21
|
+
export function resolveRecapUsageCost(
|
|
22
|
+
input: RecapUsageCostInput,
|
|
23
|
+
estimateCost?: CostEstimator,
|
|
24
|
+
): { costCentsX100: number | null; costSource: RecapUsageCostSource } {
|
|
25
|
+
if (input.reportedCostUsd != null) {
|
|
26
|
+
return {
|
|
27
|
+
costCentsX100: Math.max(1, Math.round(input.reportedCostUsd * 10_000)),
|
|
28
|
+
costSource: "reported",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (input.agent === "openai-compatible") {
|
|
32
|
+
return { costCentsX100: null, costSource: "unavailable" };
|
|
33
|
+
}
|
|
34
|
+
if (!estimateCost) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"A cost estimator is required for standard recap backends.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
costCentsX100: estimateCost(
|
|
41
|
+
input.inputTokens,
|
|
42
|
+
input.outputTokens,
|
|
43
|
+
input.model,
|
|
44
|
+
input.cacheReadTokens,
|
|
45
|
+
input.cacheWriteTokens,
|
|
46
|
+
),
|
|
47
|
+
costSource: "estimated",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -5,11 +5,12 @@ import { z } from "zod";
|
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
7
|
import { assertPlanEditor, nowIso } from "../server/plans.js";
|
|
8
|
+
import { resolveRecapUsageCost } from "./record-recap-usage-cost.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Attach token usage
|
|
11
|
+
* Attach token usage and an honest cost state to an existing recap, keyed by
|
|
11
12
|
* plan id. The PR Visual Recap workflow calls this AFTER the coding agent
|
|
12
|
-
* (Claude Code / Codex) exits — the recap is created mid-run, before the run's
|
|
13
|
+
* (Claude Code / Codex / OpenAI-compatible Agent-Native Code) exits — the recap is created mid-run, before the run's
|
|
13
14
|
* own token total exists, so usage can't ride along with create-visual-recap.
|
|
14
15
|
*
|
|
15
16
|
* Internal by design: no `publicAgent`, so it is NOT advertised as an MCP tool
|
|
@@ -24,11 +25,11 @@ import { assertPlanEditor, nowIso } from "../server/plans.js";
|
|
|
24
25
|
*/
|
|
25
26
|
export default defineAction({
|
|
26
27
|
description:
|
|
27
|
-
"Record
|
|
28
|
+
"Record LLM token usage and reported or estimated cost when available for a recap, keyed by plan id. Internal mechanic used by the PR Visual Recap workflow after the agent run finishes; not an agent-facing tool.",
|
|
28
29
|
agentTool: false,
|
|
29
30
|
schema: z.object({
|
|
30
31
|
planId: z.string().min(1),
|
|
31
|
-
agent: z.enum(["claude", "codex"]).optional(),
|
|
32
|
+
agent: z.enum(["claude", "codex", "openai-compatible"]).optional(),
|
|
32
33
|
model: z.string().min(1),
|
|
33
34
|
// Token counts must already be normalized to the cache-exclusive shape
|
|
34
35
|
// calculateCost expects (the recap CLI strips Codex's cached_input_tokens
|
|
@@ -37,7 +38,8 @@ export default defineAction({
|
|
|
37
38
|
outputTokens: z.number().int().nonnegative(),
|
|
38
39
|
cacheReadTokens: z.number().int().nonnegative().default(0),
|
|
39
40
|
cacheWriteTokens: z.number().int().nonnegative().default(0),
|
|
40
|
-
// Claude Code reports a real dollar cost
|
|
41
|
+
// Claude Code reports a real dollar cost; Codex and compatible providers
|
|
42
|
+
// generally provide tokens only.
|
|
41
43
|
reportedCostUsd: z.number().nonnegative().optional(),
|
|
42
44
|
}),
|
|
43
45
|
run: async (args) => {
|
|
@@ -54,18 +56,10 @@ export default defineAction({
|
|
|
54
56
|
.limit(1);
|
|
55
57
|
if (!row) throw new Error(`Plan ${args.planId} not found`);
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: calculateCost(
|
|
62
|
-
args.inputTokens,
|
|
63
|
-
args.outputTokens,
|
|
64
|
-
args.model,
|
|
65
|
-
args.cacheReadTokens,
|
|
66
|
-
args.cacheWriteTokens,
|
|
67
|
-
);
|
|
68
|
-
const costSource = reported ? "reported" : "estimated";
|
|
59
|
+
const { costCentsX100, costSource } = resolveRecapUsageCost(
|
|
60
|
+
args,
|
|
61
|
+
calculateCost,
|
|
62
|
+
);
|
|
69
63
|
const recordedAt = nowIso();
|
|
70
64
|
|
|
71
65
|
await db
|
|
@@ -84,10 +78,8 @@ export default defineAction({
|
|
|
84
78
|
})
|
|
85
79
|
.where(eq(schema.plans.id, args.planId));
|
|
86
80
|
|
|
87
|
-
// Mirror into the shared
|
|
88
|
-
//
|
|
89
|
-
// it idempotent on re-push; costCentsX100 carries the exact figure stored on
|
|
90
|
-
// the plan row (reported when available) so the two surfaces agree.
|
|
81
|
+
// Mirror tokens into the shared usage table. Unknown compatible-provider
|
|
82
|
+
// prices remain explicitly unavailable instead of inheriting a generic rate.
|
|
91
83
|
await recordUsage({
|
|
92
84
|
ownerEmail: row.ownerEmail,
|
|
93
85
|
inputTokens: args.inputTokens,
|
|
@@ -98,7 +90,8 @@ export default defineAction({
|
|
|
98
90
|
label: "visual-recap",
|
|
99
91
|
app: "plan",
|
|
100
92
|
refId: args.planId,
|
|
101
|
-
costCentsX100,
|
|
93
|
+
costCentsX100: costCentsX100 ?? undefined,
|
|
94
|
+
costSource,
|
|
102
95
|
});
|
|
103
96
|
|
|
104
97
|
return { planId: args.planId, costCentsX100, costSource };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;
|
|
1
|
+
{"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AA6lCD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,CAsIR;AAED,wBAAgB,kCAAkC,CAChD,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,IAAI,CA8Bf"}
|