@noetic-tools/types 3.4.0 → 3.5.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.
@@ -120,6 +120,8 @@ export interface ContextHarness {
120
120
  detachedSpawn<I, O>(step: unknown, input: I, parentCtx: Context, overrides?: ContextDetachedSpawnOverrides): DetachedHandle<O>;
121
121
  createContext(opts?: {
122
122
  parent?: Context;
123
+ /** Charge this context's usage to `parent` as it is spent. Default `true`. */
124
+ rollUpUsage?: boolean;
123
125
  items?: Item[];
124
126
  state?: unknown;
125
127
  threadId?: string;
@@ -42,6 +42,8 @@ export interface CwdState {
42
42
  export interface RestoreContextOptions {
43
43
  /** Live parent context, when the restored execution hangs under one. */
44
44
  parent?: Context;
45
+ /** Charge the restored context's usage to `parent`, same as `createContext({ rollUpUsage })`. Default `true`. */
46
+ rollUpUsage?: boolean;
45
47
  /** Opaque per-execution state, same as `createContext({ state })`. */
46
48
  state?: unknown;
47
49
  /** Context layers for the restored context. Defaults to the harness's configured layers. */
@@ -56,6 +58,17 @@ export interface Context<TContext = ContextData, TState = unknown> {
56
58
  readonly cost: number;
57
59
  state: TState;
58
60
  readonly parent: Context<ContextData> | null;
61
+ /**
62
+ * Where this context's token/cost usage is also charged. Every charge
63
+ * recorded here is added to this context and then to each ancestor along
64
+ * this chain, so `tokens` / `cost` cover this context plus every descendant,
65
+ * live — whether the descendant succeeds, fails, or is abandoned.
66
+ *
67
+ * Equals `parent` unless the context was created with `rollUpUsage: false`,
68
+ * in which case it is `null` and the context keeps its spend to itself. When
69
+ * absent (a hand-rolled `Context` implementation), `parent` is used.
70
+ */
71
+ readonly usageParent?: Context<ContextData> | null;
59
72
  readonly depth: number;
60
73
  readonly span: Span;
61
74
  readonly threadId: string;
@@ -253,6 +253,12 @@ export interface AgentHarnessContract<TParams extends Record<string, unknown> =
253
253
  }): DetachedHandle<O>;
254
254
  createContext(opts?: {
255
255
  parent?: Context;
256
+ /**
257
+ * Charge this context's token/cost usage to `parent` as it is spent, so
258
+ * the parent's `tokens` / `cost` include it. Default `true`; pass `false`
259
+ * to keep the child's spend off the parent.
260
+ */
261
+ rollUpUsage?: boolean;
256
262
  items?: Item[];
257
263
  state?: unknown;
258
264
  threadId?: string;
@@ -33,5 +33,23 @@ export declare function createMessage(text: string, role: 'user' | 'developer'):
33
33
  export declare function extractAssistantText(items: ReadonlyArray<Item>): string;
34
34
  /** Naive token estimate: ~4 chars per token */
35
35
  export declare function estimateTokens(text: string): number;
36
- /** Accumulate token usage and cost from an LLM response onto a mutable context. */
36
+ /** @public One unit of spend to charge to a context: a model call, or any other billed work. */
37
+ export interface UsageCharge {
38
+ inputTokens: number;
39
+ outputTokens: number;
40
+ cachedTokens?: number;
41
+ /** USD. */
42
+ cost?: number;
43
+ }
44
+ /**
45
+ * @public Charge `charge` to `ctx` and to every ancestor on its `usageParent`
46
+ * chain. Charging at the moment of spend — rather than merging a child's
47
+ * totals into its parent when the child settles — is what keeps spend from a
48
+ * failed spawn, a fork path, or an abandoned `race` loser from being lost, and
49
+ * lets `until.maxCost` / `checkBudget` on an ancestor see siblings' spend live.
50
+ * Use this, not direct writes to `ctx.cost`, for any billed work outside a
51
+ * model call; a direct write stays on that one context.
52
+ */
53
+ export declare function recordUsage(ctx: Context, charge: UsageCharge): void;
54
+ /** Accumulate token usage and cost from an LLM response onto a context and its usage ancestors. */
37
55
  export declare function trackUsage(ctx: Context, response: LLMResponse): void;
@@ -90,23 +90,50 @@ export function extractAssistantText(items) {
90
90
  export function estimateTokens(text) {
91
91
  return Math.ceil(text.length / 4);
92
92
  }
93
- /** Accumulate token usage and cost from an LLM response onto a mutable context. */
94
- export function trackUsage(ctx, response) {
95
- if (!canTrackUsage(ctx)) {
96
- return;
97
- }
98
- ctx.tokens.input += response.usage.inputTokens;
99
- ctx.tokens.output += response.usage.outputTokens;
100
- ctx.tokens.total += response.usage.inputTokens + response.usage.outputTokens;
101
- // Only write `cached` when the response actually carried a figure. Defaulting
93
+ function addCharge(ctx, charge) {
94
+ ctx.tokens.input += charge.inputTokens;
95
+ ctx.tokens.output += charge.outputTokens;
96
+ ctx.tokens.total += charge.inputTokens + charge.outputTokens;
97
+ // Only write `cached` when the charge actually carried a figure. Defaulting
102
98
  // a missing report to 0 would erase the `RoundUsage` invariant that
103
99
  // `undefined` means "this provider says nothing about caching" — callers that
104
100
  // steer on cache behaviour could no longer tell that from "nothing was
105
- // cached". Mirrors the child→parent merge in execute-action.
106
- if (response.usage.cachedTokens !== undefined) {
107
- ctx.tokens.cached = (ctx.tokens.cached ?? 0) + response.usage.cachedTokens;
101
+ // cached".
102
+ if (charge.cachedTokens !== undefined) {
103
+ ctx.tokens.cached = (ctx.tokens.cached ?? 0) + charge.cachedTokens;
104
+ }
105
+ if (charge.cost) {
106
+ ctx.cost = ctx.cost + charge.cost;
108
107
  }
109
- if (response.cost) {
110
- ctx.cost = ctx.cost + response.cost;
108
+ }
109
+ function usageParentOf(ctx) {
110
+ return ctx.usageParent !== undefined ? ctx.usageParent : ctx.parent;
111
+ }
112
+ /**
113
+ * @public Charge `charge` to `ctx` and to every ancestor on its `usageParent`
114
+ * chain. Charging at the moment of spend — rather than merging a child's
115
+ * totals into its parent when the child settles — is what keeps spend from a
116
+ * failed spawn, a fork path, or an abandoned `race` loser from being lost, and
117
+ * lets `until.maxCost` / `checkBudget` on an ancestor see siblings' spend live.
118
+ * Use this, not direct writes to `ctx.cost`, for any billed work outside a
119
+ * model call; a direct write stays on that one context.
120
+ */
121
+ export function recordUsage(ctx, charge) {
122
+ // The chain is acyclic for every ContextImpl (parents are fixed at
123
+ // construction); the guard only protects against a hand-rolled Context.
124
+ const seen = new Set();
125
+ for (let current = ctx; current && !seen.has(current);) {
126
+ seen.add(current);
127
+ if (canTrackUsage(current)) {
128
+ addCharge(current, charge);
129
+ }
130
+ current = usageParentOf(current);
111
131
  }
112
132
  }
133
+ /** Accumulate token usage and cost from an LLM response onto a context and its usage ancestors. */
134
+ export function trackUsage(ctx, response) {
135
+ recordUsage(ctx, {
136
+ ...response.usage,
137
+ cost: response.cost,
138
+ });
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/types",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "Foundational type contracts and primitives shared across the Noetic framework (items, LLM config, adapters, steering, errors).",
6
6
  "license": "Apache-2.0",
@@ -60,8 +60,8 @@
60
60
  "zod": "^4.0.0"
61
61
  },
62
62
  "scripts": {
63
- "build": "bunx tsc -p tsconfig.build.json && bun scripts/add-js-extensions.ts",
64
- "verify:esm": "bun scripts/verify-node-esm.ts",
63
+ "build": "bunx tsc -p tsconfig.build.json && bun ../../scripts/add-js-extensions.ts",
64
+ "verify:esm": "bun ../../scripts/verify-node-esm.ts",
65
65
  "prepublishOnly": "rm -rf dist && bun run build && bun run verify:esm && bun scripts/strip-dev-conditions.ts",
66
66
  "test": "bun test",
67
67
  "test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov",