@adonis-agora/agent 0.91.6 → 0.93.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/assets/spa/assets/index-DUq09Q_k.js +9 -0
  3. package/dist/assets/spa/index.html +1 -1
  4. package/dist/providers/agent_provider.d.ts.map +1 -1
  5. package/dist/providers/agent_provider.js +33 -0
  6. package/dist/providers/agent_provider.js.map +1 -1
  7. package/dist/src/agent-loop.d.ts +8 -0
  8. package/dist/src/agent-loop.d.ts.map +1 -1
  9. package/dist/src/agent-loop.js +196 -87
  10. package/dist/src/agent-loop.js.map +1 -1
  11. package/dist/src/agent-service.d.ts +73 -2
  12. package/dist/src/agent-service.d.ts.map +1 -1
  13. package/dist/src/agent-service.js +110 -2
  14. package/dist/src/agent-service.js.map +1 -1
  15. package/dist/src/diagnostics.d.ts +65 -0
  16. package/dist/src/diagnostics.d.ts.map +1 -1
  17. package/dist/src/diagnostics.js +18 -4
  18. package/dist/src/diagnostics.js.map +1 -1
  19. package/dist/src/durable/agent-run-workflow.d.ts.map +1 -1
  20. package/dist/src/durable/agent-run-workflow.js +11 -0
  21. package/dist/src/durable/agent-run-workflow.js.map +1 -1
  22. package/dist/src/index.d.ts +4 -3
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +3 -2
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/opencode/turn.d.ts.map +1 -1
  27. package/dist/src/opencode/turn.js +4 -1
  28. package/dist/src/opencode/turn.js.map +1 -1
  29. package/dist/src/run-timings.d.ts +16 -0
  30. package/dist/src/run-timings.d.ts.map +1 -0
  31. package/dist/src/run-timings.js +59 -0
  32. package/dist/src/run-timings.js.map +1 -0
  33. package/dist/src/runners/inline-agent-runner.d.ts.map +1 -1
  34. package/dist/src/runners/inline-agent-runner.js +5 -1
  35. package/dist/src/runners/inline-agent-runner.js.map +1 -1
  36. package/dist/src/spi/agent-store.d.ts +98 -2
  37. package/dist/src/spi/agent-store.d.ts.map +1 -1
  38. package/dist/src/spi/agent-store.js +7 -0
  39. package/dist/src/spi/agent-store.js.map +1 -1
  40. package/dist/src/spi/governance-queries.d.ts +81 -0
  41. package/dist/src/spi/governance-queries.d.ts.map +1 -1
  42. package/dist/src/stores/lucid-governance-queries.d.ts +18 -1
  43. package/dist/src/stores/lucid-governance-queries.d.ts.map +1 -1
  44. package/dist/src/stores/lucid-governance-queries.js +176 -2
  45. package/dist/src/stores/lucid-governance-queries.js.map +1 -1
  46. package/dist/src/stores/lucid-schema.d.ts +20 -0
  47. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  48. package/dist/src/stores/lucid-schema.js +172 -20
  49. package/dist/src/stores/lucid-schema.js.map +1 -1
  50. package/dist/src/stores/lucid.d.ts +9 -2
  51. package/dist/src/stores/lucid.d.ts.map +1 -1
  52. package/dist/src/stores/lucid.js +41 -5
  53. package/dist/src/stores/lucid.js.map +1 -1
  54. package/dist/src/testing/in-memory-governance-queries.d.ts +11 -1
  55. package/dist/src/testing/in-memory-governance-queries.d.ts.map +1 -1
  56. package/dist/src/testing/in-memory-governance-queries.js +86 -2
  57. package/dist/src/testing/in-memory-governance-queries.js.map +1 -1
  58. package/dist/src/testing/in-memory-store.d.ts +15 -4
  59. package/dist/src/testing/in-memory-store.d.ts.map +1 -1
  60. package/dist/src/testing/in-memory-store.js +21 -4
  61. package/dist/src/testing/in-memory-store.js.map +1 -1
  62. package/dist/src/types.d.ts +6 -1
  63. package/dist/src/types.d.ts.map +1 -1
  64. package/dist/src/usage-attribution.d.ts +10 -0
  65. package/dist/src/usage-attribution.d.ts.map +1 -0
  66. package/dist/src/usage-attribution.js +12 -0
  67. package/dist/src/usage-attribution.js.map +1 -0
  68. package/dist/src/usage-ledger.d.ts +9 -0
  69. package/dist/src/usage-ledger.d.ts.map +1 -0
  70. package/dist/src/usage-ledger.js +35 -0
  71. package/dist/src/usage-ledger.js.map +1 -0
  72. package/package.json +1 -1
  73. package/skills/agent-governance/SKILL.md +1 -1
  74. package/dist/assets/spa/assets/index-BsGunSCg.js +0 -9
@@ -1,7 +1,10 @@
1
1
  import { turnChannel } from './genui/channels.js';
2
2
  import { GenuiTurnTelemetry, observeGenuiFrames } from './genui-telemetry.js';
3
3
  import { DEFAULT_REFUSAL_REASON } from './refusal.js';
4
+ import { rollupRunTimings, toolsWallClockMs } from './run-timings.js';
4
5
  import { observeFirstText } from './turn-timing.js';
6
+ import { usageAttribution } from './usage-attribution.js';
7
+ import { recordUsageRow } from './usage-ledger.js';
5
8
  export { DEFAULT_REFUSAL_REASON } from './refusal.js';
6
9
  import { createHash } from 'node:crypto';
7
10
  import { resolveActionProposalApproval } from './action-proposal-approval.js';
@@ -9,7 +12,7 @@ import { prepareActionProposal } from './action-proposal-preparation.js';
9
12
  import { actionProposalReceipt } from './action-proposal-receipt.js';
10
13
  import { danglingToolCallIds, settleDanglingToolCalls } from './dangling-tool-calls.js';
11
14
  import { detachedDelivered, detachedStarted, } from './delegation.js';
12
- import { publishAgentDelegated, publishAgentMessage, publishAgentRetrieved, publishAgentRunCancelled, publishAgentRunFailed, publishAgentRunFinished, publishAgentRunStarted, publishAgentToolCall, publishAgentToolRetry, spannedAgent, } from './diagnostics.js';
15
+ import { publishAgentDelegated, publishAgentMessage, publishAgentRetrieved, publishAgentRunCancelled, publishAgentRunFailed, publishAgentRunFinished, publishAgentRunStarted, publishAgentToolCall, publishAgentToolRetry, spannedAgent, timedSpan, } from './diagnostics.js';
13
16
  import { ASK_TOOL_NAME, askInputSchema, askToolDefinition, DEFAULT_INTAKE_PREAMBLE, isHumanDecision, normalizeElicitationReply, settleElicitation, } from './elicitation.js';
14
17
  import { renderHistorySummary } from './history-window.js';
15
18
  import { buildMemoryBlock, offerMemories, REMEMBER_TOOL_NAME, rememberInputSchema, rememberToolDefinition, writeMemory, } from './memory.js';
@@ -512,8 +515,9 @@ async function applyHistoryWindow(window, deps, input, hooks, messages, step) {
512
515
  const summary = await step('history:summarize', () => summarize(drop, ctx));
513
516
  if (summary.usage !== undefined) {
514
517
  const usage = summary.usage;
515
- await step('persist:usage:history', () => deps.store.recordUsage({
518
+ await step('persist:usage:history', () => recordUsageRow(deps.store, {
516
519
  threadId: input.threadId,
520
+ ...usageAttribution(input),
517
521
  actorRef: input.actor.id,
518
522
  runId: hooks.runId,
519
523
  modelId: summary.modelId ?? deps.modelId ?? 'unknown',
@@ -727,6 +731,26 @@ const PARALLEL_TOOLS_PATCH = 'agent:parallel-tools';
727
731
  * supply.
728
732
  */
729
733
  const MESSAGE_TOOL_RESULTS_PATCH = 'agent:message-tool-results';
734
+ /** Note a settled call's journaled execution window on its step. */
735
+ function noteToolTiming(turn, call, journaled) {
736
+ if (turn.toolTimings === undefined || journaled === undefined || journaled === null)
737
+ return;
738
+ if (typeof journaled.ms !== 'number')
739
+ return;
740
+ turn.toolTimings.push({
741
+ toolCallId: call.id,
742
+ toolName: call.name,
743
+ ms: journaled.ms,
744
+ ...(typeof journaled.at === 'number' ? { at: journaled.at } : {}),
745
+ });
746
+ }
747
+ /** The execution window a persist checkpoint journals, from the outcome measured in this process. */
748
+ function journalTiming(outcome) {
749
+ return {
750
+ ...(outcome.durationMs !== undefined ? { ms: outcome.durationMs } : {}),
751
+ ...(outcome.startedAt !== undefined ? { at: outcome.startedAt } : {}),
752
+ };
753
+ }
730
754
  /**
731
755
  * Record the call and settle its KIND, which decides this call's control flow: an `action` suspends
732
756
  * the run on an approval signal (`tool:<runId>:<callId>`), an `agent` delegates to a child run,
@@ -1065,7 +1089,7 @@ async function invokeClaimedTool(turn, claimed, presentationOnly) {
1065
1089
  const invokeOptions = allowedTools === undefined ? {} : { allowedTools };
1066
1090
  // Set only when the step's body runs here: a checkpoint read back from the journal has no timing.
1067
1091
  let ranAt;
1068
- const timing = () => ranAt === undefined ? { replayed: true } : { durationMs: Date.now() - ranAt };
1092
+ const timing = () => ranAt === undefined ? { replayed: true } : { durationMs: Date.now() - ranAt, startedAt: ranAt };
1069
1093
  try {
1070
1094
  const raw = await hooks.step(`tool:${call.id}`, async () => {
1071
1095
  ranAt = Date.now();
@@ -1226,7 +1250,7 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef = turn.input
1226
1250
  });
1227
1251
  };
1228
1252
  if (outcome.status === 'failed') {
1229
- await hooks.step(`persist:toolfail:${call.id}`, async () => {
1253
+ const journaled = await hooks.step(`persist:toolfail:${call.id}`, async () => {
1230
1254
  await deps.store.updateToolCall({
1231
1255
  toolCallId: call.id,
1232
1256
  runId: hooks.runId,
@@ -1236,7 +1260,10 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef = turn.input
1236
1260
  ...decided,
1237
1261
  });
1238
1262
  await settle();
1263
+ // Journaled with the checkpoint that records it, so a replay counts the call's time once.
1264
+ return journalTiming(outcome);
1239
1265
  });
1266
+ noteToolTiming(turn, call, journaled);
1240
1267
  publishAgentToolCall({
1241
1268
  runId: hooks.runId,
1242
1269
  toolCallId: call.id,
@@ -1253,7 +1280,7 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef = turn.input
1253
1280
  ...(outcome.denied ? { denied: true } : {}),
1254
1281
  };
1255
1282
  }
1256
- await hooks.step(`persist:toolexec:${call.id}`, async () => {
1283
+ const journaled = await hooks.step(`persist:toolexec:${call.id}`, async () => {
1257
1284
  await deps.store.updateToolCall({
1258
1285
  toolCallId: call.id,
1259
1286
  runId: hooks.runId,
@@ -1264,7 +1291,10 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef = turn.input
1264
1291
  ...decided,
1265
1292
  });
1266
1293
  await settle();
1294
+ // Journaled with the checkpoint that records it, so a replay counts the call's time once.
1295
+ return journalTiming(outcome);
1267
1296
  });
1297
+ noteToolTiming(turn, call, journaled);
1268
1298
  publishAgentToolCall({
1269
1299
  runId: hooks.runId,
1270
1300
  toolCallId: call.id,
@@ -1906,8 +1936,9 @@ async function structureAnswer(args) {
1906
1936
  ...(turn.modelId !== undefined ? { modelId: turn.modelId } : {}),
1907
1937
  };
1908
1938
  });
1909
- await hooks.step(`persist:usage:structured:${step}:${attempt}`, () => deps.store.recordUsage({
1939
+ await hooks.step(`persist:usage:structured:${step}:${attempt}`, () => recordUsageRow(deps.store, {
1910
1940
  threadId: input.threadId,
1941
+ ...usageAttribution(input),
1911
1942
  actorRef: input.actor.id,
1912
1943
  runId: hooks.runId,
1913
1944
  modelId: reply.modelId ?? deps.modelId ?? 'unknown',
@@ -2077,14 +2108,29 @@ export function streamErrorFrame(error, runId) {
2077
2108
  */
2078
2109
  export async function runAgentLoop(boundDeps, requested, hooks) {
2079
2110
  const startedAt = Date.now();
2111
+ const clock = {};
2080
2112
  try {
2081
- return await runAgentTurn(boundDeps, requested, hooks);
2113
+ return await runAgentTurn(boundDeps, requested, hooks, clock);
2082
2114
  }
2083
2115
  catch (error) {
2084
- publishRunEnded(error, requested, hooks, startedAt);
2116
+ publishRunEnded(error, requested, hooks, startedAt, clock);
2085
2117
  throw error;
2086
2118
  }
2087
2119
  }
2120
+ /** The queue time the runner reports, rounded; `undefined` when it has none (or it fails). */
2121
+ async function readQueueMs(hooks) {
2122
+ if (hooks.queueMs === undefined)
2123
+ return undefined;
2124
+ try {
2125
+ const value = await hooks.queueMs();
2126
+ return typeof value === 'number' && Number.isFinite(value)
2127
+ ? Math.max(0, Math.round(value))
2128
+ : undefined;
2129
+ }
2130
+ catch {
2131
+ return undefined;
2132
+ }
2133
+ }
2088
2134
  /** Who and what the run is, for its terminal diagnostics. */
2089
2135
  function runContext(input) {
2090
2136
  return {
@@ -2102,22 +2148,36 @@ function runContext(input) {
2102
2148
  * nothing; a run whose abort signal fired was stopped; anything else failed, under the same code the
2103
2149
  * stream's `error` frame carries.
2104
2150
  */
2105
- function publishRunEnded(error, input, hooks, startedAt) {
2151
+ function publishRunEnded(error, input, hooks, startedAt, clock) {
2106
2152
  if (!isReplayIntegrityError(error) && hooks.isControlFlowError?.(error) === true)
2107
2153
  return;
2108
2154
  const base = {
2109
2155
  runId: hooks.runId,
2110
2156
  threadId: input.threadId,
2111
2157
  durationMs: Date.now() - startedAt,
2158
+ ...(clock.queueMs !== undefined ? { queueMs: clock.queueMs } : {}),
2112
2159
  ...runContext(input),
2113
2160
  };
2114
2161
  if (hooks.abortSignal?.aborted === true) {
2115
2162
  publishAgentRunCancelled(base);
2116
2163
  return;
2117
2164
  }
2118
- publishAgentRunFailed({ ...base, code: streamErrorCode(error) });
2165
+ const timings = clock.steps !== undefined ? rollupRunTimings(clock.steps) : undefined;
2166
+ publishAgentRunFailed({
2167
+ ...base,
2168
+ code: streamErrorCode(error),
2169
+ ...(timings !== undefined
2170
+ ? {
2171
+ modelMs: timings.modelMs,
2172
+ toolsMs: timings.toolsMs,
2173
+ modelCalls: timings.modelCalls,
2174
+ toolCalls: timings.toolCalls,
2175
+ stepTimings: timings.steps,
2176
+ }
2177
+ : {}),
2178
+ });
2119
2179
  }
2120
- async function runAgentTurn(boundDeps, requested, hooks) {
2180
+ async function runAgentTurn(boundDeps, requested, hooks, clock) {
2121
2181
  // The persona, FIRST and only when the run names one by id: its definition is read from config
2122
2182
  // once and frozen in this checkpoint, so a run resumed after the persona was edited or removed
2123
2183
  // keeps the prompt and allow-list it started with. A run that names none spends no position here
@@ -2177,14 +2237,28 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2177
2237
  // Open the run (turn) row FIRST — before the quota gate — so even a quota-rejected run is tracked
2178
2238
  // (the runner settles it `failed`). A checkpointed step so a durable replay reuses the ONE row (no
2179
2239
  // duplicate on resume); `run_id` is then stamped onto every message / tool call / usage row below.
2180
- await hooks.step('persist:run:start', () => deps.store.recordRunStart({
2181
- runId: hooks.runId,
2182
- threadId: input.threadId,
2183
- actor: input.actor,
2184
- durable: hooks.durable ?? false,
2185
- ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
2186
- ...(input.parentRunId !== undefined ? { parentRunId: input.parentRunId } : {}),
2187
- }));
2240
+ //
2241
+ // The run's queue time is read INSIDE this checkpoint and journaled with it: the body runs once per
2242
+ // slice of a durable run, and only the first execution of this step is the moment it left the queue.
2243
+ const opened = await hooks.step('persist:run:start', async () => {
2244
+ const queueMs = await readQueueMs(hooks);
2245
+ await deps.store.recordRunStart({
2246
+ runId: hooks.runId,
2247
+ threadId: input.threadId,
2248
+ actor: input.actor,
2249
+ durable: hooks.durable ?? false,
2250
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
2251
+ ...(input.parentRunId !== undefined ? { parentRunId: input.parentRunId } : {}),
2252
+ ...(queueMs !== undefined ? { queueMs } : {}),
2253
+ });
2254
+ return queueMs !== undefined ? { queueMs } : {};
2255
+ });
2256
+ if (typeof opened === 'object' && opened !== null && typeof opened.queueMs === 'number') {
2257
+ clock.queueMs = opened.queueMs;
2258
+ }
2259
+ // Where each step's time went, filled as the steps run (or replay from their checkpoints).
2260
+ const stepTimings = [];
2261
+ clock.steps = stepTimings;
2188
2262
  if (deps.quota !== undefined) {
2189
2263
  const quota = deps.quota;
2190
2264
  const state = await hooks.step('quota:check', () => quota.check(input.actor.id, deps.day));
@@ -2351,8 +2425,9 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2351
2425
  // placeholder faria a linha parecer precificável e sair como $0.00 — pior do que a ausência.
2352
2426
  if (retrieved.usage !== undefined && retrieved.usage.inputTokens > 0) {
2353
2427
  const embeddingUsage = retrieved.usage;
2354
- await hooks.step('persist:usage:embedding', () => deps.store.recordUsage({
2428
+ await hooks.step('persist:usage:embedding', () => recordUsageRow(deps.store, {
2355
2429
  threadId: input.threadId,
2430
+ ...usageAttribution(input),
2356
2431
  actorRef: input.actor.id,
2357
2432
  runId: hooks.runId,
2358
2433
  modelId: embeddingUsage.modelId ?? 'unknown',
@@ -2457,72 +2532,86 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2457
2532
  // Stamped from THIS process's registry, which is the one that built `tools` for the call above —
2458
2533
  // and stamped INSIDE the checkpoint, so the kinds are journaled with the calls they describe
2459
2534
  // rather than re-derived by whatever process replays this turn.
2460
- let turn = await hooks.step(`llm:${i}`, async () => stampToolKinds(await spannedAgent('llm.turn', hooks.runId, { runId: hooks.runId, step: i }, async () => {
2461
- const incremental = gateMode === 'incremental'
2462
- ? createIncrementalGate({
2463
- processors: outputProcessors,
2464
- ctx: processorContext(i),
2465
- lookbackChars: gateLookback,
2466
- writer,
2467
- })
2468
- : undefined;
2469
- const buffer = gateMode === 'whole' ? createFrameBuffer() : undefined;
2470
- // Written from inside this checkpoint, like every frame the model streams, so a replay
2471
- // that returns the journaled turn never opens the step twice.
2472
- await writer.write({ t: 'event', event: { kind: 'step-start' } });
2473
- const announced = announcedToolCalls(incremental?.writer ?? buffer?.writer ?? writer);
2474
- // A tool that previews its input (genui's streaming tree) gets partial `ui` frames
2475
- // while the model writes its arguments. Added BELOW the observer: a preview is shown,
2476
- // never part of what the turn persists.
2477
- const previews = previewToolInputs(announced.writer, async (name, toolCallId) => tools.some((definition) => definition.name === name)
2478
- ? deps.registry.previewInput(name, {
2479
- actor: input.actor,
2480
- threadId: input.threadId,
2481
- toolCallId,
2482
- ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
2483
- ...(input.uiCapabilities !== undefined
2484
- ? { uiCapabilities: input.uiCapabilities }
2485
- : {}),
2486
- channel: turnChannel(input.pageContext),
2487
- })
2488
- : undefined);
2489
- // Reasoning and pushed UI are read off the frames INSIDE this checkpoint, so they are
2490
- // journaled with the turn and a replay persists the same thinking instead of none.
2491
- const frames = observeTurnFrames(previews.writer);
2492
- const result = withTurnFrames(await abortableModelCall(hooks, (signal) => deps.model.runTurn({
2493
- system: prompt.system,
2494
- messages: prompt.messages,
2495
- tools,
2496
- sink: frames.writer,
2497
- ...signal,
2498
- })), frames.summary());
2499
- // A provider that streams no tool frames still gets its calls announced — an outcome
2500
- // for a call the client never saw announced drops the whole message there.
2501
- await announced.announceRest(result.toolCalls, tools);
2502
- // Journaled with the turn, so whichever process settles the calls withdraws the same
2503
- // previews the stream showed.
2504
- const shownPreviews = previews.shown();
2505
- const previewed = shownPreviews.length > 0 ? { previews: shownPreviews } : {};
2506
- if (incremental !== undefined) {
2507
- await incremental.settled();
2508
- const refusal = incremental.rejection();
2509
- return {
2510
- ...result,
2511
- ...previewed,
2512
- releasedText: incremental.released(),
2513
- ...(refusal !== undefined ? { gateRejection: refusal } : {}),
2514
- };
2515
- }
2516
- return buffer === undefined
2517
- ? { ...result, ...previewed }
2518
- : { ...result, ...previewed, bufferedFrames: buffer.frames() };
2519
- }, (result) => ({
2520
- ...(result.modelId !== undefined ? { modelId: result.modelId } : {}),
2521
- inputTokens: result.usage.inputTokens,
2522
- outputTokens: result.usage.outputTokens,
2523
- textLength: result.text.length,
2524
- toolCalls: result.toolCalls.length,
2525
- })), { ...deps, actionApprovalMode: thread.actionApprovalMode ?? 'blocking' }, input, hooks));
2535
+ let turn = await hooks.step(`llm:${i}`, async () => {
2536
+ const timed = await timedSpan('llm.turn', hooks.runId, { runId: hooks.runId, step: i }, async () => {
2537
+ const incremental = gateMode === 'incremental'
2538
+ ? createIncrementalGate({
2539
+ processors: outputProcessors,
2540
+ ctx: processorContext(i),
2541
+ lookbackChars: gateLookback,
2542
+ writer,
2543
+ })
2544
+ : undefined;
2545
+ const buffer = gateMode === 'whole' ? createFrameBuffer() : undefined;
2546
+ // Written from inside this checkpoint, like every frame the model streams, so a replay
2547
+ // that returns the journaled turn never opens the step twice.
2548
+ await writer.write({ t: 'event', event: { kind: 'step-start' } });
2549
+ const announced = announcedToolCalls(incremental?.writer ?? buffer?.writer ?? writer);
2550
+ // A tool that previews its input (genui's streaming tree) gets partial `ui` frames
2551
+ // while the model writes its arguments. Added BELOW the observer: a preview is shown,
2552
+ // never part of what the turn persists.
2553
+ const previews = previewToolInputs(announced.writer, async (name, toolCallId) => tools.some((definition) => definition.name === name)
2554
+ ? deps.registry.previewInput(name, {
2555
+ actor: input.actor,
2556
+ threadId: input.threadId,
2557
+ toolCallId,
2558
+ ...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
2559
+ ...(input.uiCapabilities !== undefined
2560
+ ? { uiCapabilities: input.uiCapabilities }
2561
+ : {}),
2562
+ channel: turnChannel(input.pageContext),
2563
+ })
2564
+ : undefined);
2565
+ // Reasoning and pushed UI are read off the frames INSIDE this checkpoint, so they are
2566
+ // journaled with the turn and a replay persists the same thinking instead of none.
2567
+ const frames = observeTurnFrames(previews.writer);
2568
+ const result = withTurnFrames(await abortableModelCall(hooks, (signal) => deps.model.runTurn({
2569
+ system: prompt.system,
2570
+ messages: prompt.messages,
2571
+ tools,
2572
+ sink: frames.writer,
2573
+ ...signal,
2574
+ })), frames.summary());
2575
+ // A provider that streams no tool frames still gets its calls announced — an outcome
2576
+ // for a call the client never saw announced drops the whole message there.
2577
+ await announced.announceRest(result.toolCalls, tools);
2578
+ // Journaled with the turn, so whichever process settles the calls withdraws the same
2579
+ // previews the stream showed.
2580
+ const shownPreviews = previews.shown();
2581
+ const previewed = shownPreviews.length > 0 ? { previews: shownPreviews } : {};
2582
+ if (incremental !== undefined) {
2583
+ await incremental.settled();
2584
+ const refusal = incremental.rejection();
2585
+ return {
2586
+ ...result,
2587
+ ...previewed,
2588
+ releasedText: incremental.released(),
2589
+ ...(refusal !== undefined ? { gateRejection: refusal } : {}),
2590
+ };
2591
+ }
2592
+ return buffer === undefined
2593
+ ? { ...result, ...previewed }
2594
+ : { ...result, ...previewed, bufferedFrames: buffer.frames() };
2595
+ }, (result) => ({
2596
+ ...(result.modelId !== undefined ? { modelId: result.modelId } : {}),
2597
+ inputTokens: result.usage.inputTokens,
2598
+ outputTokens: result.usage.outputTokens,
2599
+ textLength: result.text.length,
2600
+ toolCalls: result.toolCalls.length,
2601
+ }));
2602
+ // The span's own duration rides the journaled turn, so a replay counts the call once.
2603
+ return {
2604
+ ...(await stampToolKinds(timed.value, { ...deps, actionApprovalMode: thread.actionApprovalMode ?? 'blocking' }, input, hooks)),
2605
+ modelMs: Math.round(timed.durationMs),
2606
+ };
2607
+ });
2608
+ const stepTiming = {
2609
+ step: i,
2610
+ ...(typeof turn.modelMs === 'number' ? { modelMs: turn.modelMs } : {}),
2611
+ tools: [],
2612
+ toolsMs: 0,
2613
+ };
2614
+ stepTimings.push(stepTiming);
2526
2615
  // The gate runs before ANYTHING downstream: before the held frames reach the subscriber, before
2527
2616
  // the assistant message is persisted, before this text becomes the next step's context.
2528
2617
  let rejection;
@@ -2576,8 +2665,9 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2576
2665
  // `resolveModelPrice` e não `priceByModel.get`: o provider responde com o snapshot datado
2577
2666
  // (`gpt-4o-mini-2024-07-18`) e o operador precifica o alias (`gpt-4o-mini`). Ver o helper.
2578
2667
  const costUsd = resolveCostUsd(turn.usage, turn.costUsd, resolveModelPrice(priceByModel, resolvedModelId));
2579
- await hooks.step(`persist:usage:${i}`, () => deps.store.recordUsage({
2668
+ await hooks.step(`persist:usage:${i}`, () => recordUsageRow(deps.store, {
2580
2669
  threadId: input.threadId,
2670
+ ...usageAttribution(input),
2581
2671
  actorRef: input.actor.id,
2582
2672
  runId: hooks.runId,
2583
2673
  modelId: resolvedModelId,
@@ -2772,6 +2862,7 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2772
2862
  actionApprovalMode: thread.actionApprovalMode ?? 'blocking',
2773
2863
  ...(skillOffer !== undefined ? { skills: skillOffer } : {}),
2774
2864
  ...(memoryDigest !== undefined ? { memory: memoryDigest } : {}),
2865
+ toolTimings: [],
2775
2866
  };
2776
2867
  // A model routinely asks for several tools at once, and running them back to back makes the
2777
2868
  // turn cost their sum. Overlapping them is safe here because a checkpoint position is handed
@@ -2817,6 +2908,16 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2817
2908
  results.push(await runClaimedToolCall(turnCalls, await claimToolCall(turnCalls, call)));
2818
2909
  }
2819
2910
  }
2911
+ // Settled, in call order: the step's tool time, parallel calls counted once.
2912
+ const ranTools = turnCalls.toolTimings ?? [];
2913
+ const order = new Map(turn.toolCalls.map((call, index) => [call.id, index]));
2914
+ ranTools.sort((left, right) => (order.get(left.toolCallId) ?? 0) - (order.get(right.toolCallId) ?? 0));
2915
+ stepTiming.tools = ranTools.map((entry) => ({
2916
+ toolCallId: entry.toolCallId,
2917
+ toolName: entry.toolName,
2918
+ ms: entry.ms,
2919
+ }));
2920
+ stepTiming.toolsMs = toolsWallClockMs(ranTools);
2820
2921
  // The outputs land on the assistant message that made the calls. A reader reopening the thread
2821
2922
  // pairs a call with its result off THAT message, so results that only reached the tool-call
2822
2923
  // table leave every tool in the turn rendering as one still running.
@@ -2956,6 +3057,7 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2956
3057
  }
2957
3058
  // Every text frame of the turn has gone out by now, so its time to first token is known.
2958
3059
  const firstTextAt = firstText.firstTextAt();
3060
+ const timings = rollupRunTimings(stepTimings);
2959
3061
  // Settle the run `completed` with its rollup. Normal completion only — the loop never records a
2960
3062
  // failure (it doesn't catch its own crash; that's the runner's job). A checkpointed step so a
2961
3063
  // resumed durable replay settles the ONE row exactly once, and `recordRunEnd` is first-terminal so
@@ -2970,6 +3072,7 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2970
3072
  ...(hasCost ? { costUsd: totalCost } : {}),
2971
3073
  ...(firstTextAt !== undefined ? { ttftMs: firstTextAt - turnStartedAt } : {}),
2972
3074
  ...(lastModelId !== undefined ? { modelId: lastModelId } : {}),
3075
+ timings,
2973
3076
  }));
2974
3077
  await writer.end();
2975
3078
  publishAgentRunFinished({
@@ -2982,6 +3085,12 @@ async function runAgentTurn(boundDeps, requested, hooks) {
2982
3085
  ...(firstTextAt !== undefined ? { ttftMs: firstTextAt - turnStartedAt } : {}),
2983
3086
  ...(hasCost ? { costUsd: totalCost } : {}),
2984
3087
  ...(lastModelId !== undefined ? { modelId: lastModelId } : {}),
3088
+ ...(clock.queueMs !== undefined ? { queueMs: clock.queueMs } : {}),
3089
+ modelMs: timings.modelMs,
3090
+ toolsMs: timings.toolsMs,
3091
+ modelCalls: timings.modelCalls,
3092
+ toolCalls: timings.toolCalls,
3093
+ stepTimings: timings.steps,
2985
3094
  ...runContext(input),
2986
3095
  });
2987
3096
  return { text: lastText, ...(structured !== undefined ? { object: structured } : {}) };