broapp 0.4.4 → 0.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "broapp",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "Build tooling and runtime for local applications made of a Bun host, a browser UI, and a Brobridge connection between them",
6
6
  "license": "MIT",
@@ -39,6 +39,8 @@ export interface RunEndDetail {
39
39
  /** Tool round trips the turn made. */
40
40
  readonly steps: number;
41
41
  readonly ms: number;
42
+ /** The model the turn was sent to; absent when the turn ended before one was resolved. */
43
+ readonly modelId?: string;
42
44
  }
43
45
 
44
46
  /**
@@ -165,6 +167,13 @@ export interface CreateAiOptions {
165
167
  * reason to fail it.
166
168
  */
167
169
  readonly onContext?: (runId: string, delivered: DeliveredContext) => void;
170
+ /**
171
+ * Called after each completed model step of a turn, with the tokens its
172
+ * completed steps have used so far: what a running turn has cost, before
173
+ * `onRunEnd` says what it cost in all. A hook that throws is logged and
174
+ * ignored, like the others.
175
+ */
176
+ readonly onUsageSoFar?: (runId: string, soFar: { inputTokens: number; outputTokens: number }) => void;
168
177
  }
169
178
 
170
179
  /**
@@ -334,6 +343,7 @@ export function createAi(options: CreateAiOptions): Ai {
334
343
  approvals,
335
344
  ...(options.onRunEnd === undefined ? {} : { onRunEnd: options.onRunEnd }),
336
345
  ...(options.onContext === undefined ? {} : { onContext: options.onContext }),
346
+ ...(options.onUsageSoFar === undefined ? {} : { onUsageSoFar: options.onUsageSoFar }),
337
347
  transcripts: {
338
348
  save: (runId, messages) => {
339
349
  threadStore().saveTranscript(runId, messages);
@@ -75,6 +75,13 @@ export interface RunDeps {
75
75
  ) => void;
76
76
  /** Called once per turn with what the model is about to be given. */
77
77
  readonly onContext?: (runId: string, delivered: DeliveredContext) => void;
78
+ /**
79
+ * Called after each model step completes, with what the turn's completed
80
+ * steps have used so far. Not the SDK's `onStepEnd`, which this layer
81
+ * already hands to `streamText`: this is the turn's running subtotal, for
82
+ * somebody who wants to say what a turn has cost before it ends.
83
+ */
84
+ readonly onUsageSoFar?: (runId: string, soFar: { inputTokens: number; outputTokens: number }) => void;
78
85
  /**
79
86
  * The turn transcripts. Absent, every history turn is text and nothing is
80
87
  * written, which is exactly the layer before transcripts existed.
@@ -95,6 +102,8 @@ interface TurnTally {
95
102
  stepsEnded: number;
96
103
  stepInput: number;
97
104
  stepOutput: number;
105
+ /** The model the turn was sent to, once it is resolved. */
106
+ modelId?: string;
98
107
  }
99
108
 
100
109
  /**
@@ -798,6 +807,7 @@ export async function runChat(
798
807
  steps: tally.steps,
799
808
  ms: Date.now() - started,
800
809
  ...(tally.usage === undefined ? {} : { usage: tally.usage }),
810
+ ...(tally.modelId === undefined ? {} : { modelId: tally.modelId }),
801
811
  };
802
812
  safely(deps.logger, 'onRunEnd', () =>
803
813
  onRunEnd(params.runId, status, params.message.slice(0, SUMMARY_CHARS), detail),
@@ -860,6 +870,9 @@ async function runTurn(
860
870
  // after the provider and key checks, so the vision check below and the model
861
871
  // instance built later both follow it without a second code path.
862
872
  const resolved = await deps.registry.resolve({ modelId: params.modelId });
873
+ // Known here and nowhere later: a listener writing down what the turn used
874
+ // is told which model used it, not left to guess from settings since changed.
875
+ tally.modelId = resolved.modelId;
863
876
 
864
877
  // Both checks come before anything is emitted, so a turn that cannot carry
865
878
  // its images fails as a whole rather than half-answering.
@@ -930,6 +943,11 @@ async function runTurn(
930
943
  tally.stepsEnded += 1;
931
944
  tally.stepInput += step.usage.inputTokens ?? 0;
932
945
  tally.stepOutput += step.usage.outputTokens ?? 0;
946
+ const onUsageSoFar = deps.onUsageSoFar;
947
+ if (onUsageSoFar !== undefined) {
948
+ const soFar = { inputTokens: tally.stepInput, outputTokens: tally.stepOutput };
949
+ safely(deps.logger, 'onUsageSoFar', () => onUsageSoFar(params.runId, soFar));
950
+ }
933
951
  },
934
952
  });
935
953