@juno-ai/bind 5.0.0 → 7.0.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.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Reading a tool call the model asked for.
3
+ *
4
+ * This is the dispatch half of a rule whose other half lives in
5
+ * `defects.ts`, and the two only work if they agree. `detectCompletionDefect`
6
+ * decides that a tool call with *empty* arguments is legitimate — the normal
7
+ * zero-argument shape, not a truncation — and passes it through to be run.
8
+ * Something then has to run it, and that something has to reach the same
9
+ * conclusion: empty arguments mean no arguments, not malformed JSON.
10
+ *
11
+ * Splitting those halves across a package boundary is how the original bug
12
+ * happened. Providers send `""` for a zero-arg call as readily as `"{}"`, the
13
+ * dispatcher fed `""` to `JSON.parse`, and a perfectly good call died as
14
+ * "Invalid JSON in tool arguments" — a whole recovery turn spent on a turn
15
+ * that was never broken. A host adopting only the detection half inherits the
16
+ * permissive decision and has to independently invent the matching parse.
17
+ *
18
+ * So both halves read {@link toolCallArgumentsAbsent}. One definition, and a
19
+ * change to what "absent" means cannot update one side and miss the other.
20
+ */
21
+ /**
22
+ * The shape this module reads. Structural rather than tied to an SDK's class,
23
+ * and `type` is optional because a host assembling calls by hand may not carry
24
+ * one — its absence is treated as the ordinary function call it almost always
25
+ * is, while a *present* non-function type is refused.
26
+ */
27
+ export interface DispatchableToolCall {
28
+ readonly type?: string | undefined;
29
+ readonly function?: {
30
+ readonly name?: string | undefined;
31
+ readonly arguments?: string | undefined;
32
+ } | undefined;
33
+ }
34
+ /**
35
+ * Whether a tool call carried no arguments at all.
36
+ *
37
+ * Whitespace counts as absent: a provider that pads its zero-arg payload is
38
+ * still saying "no arguments", and treating `" "` as content sends it to a
39
+ * JSON parse that can only fail.
40
+ */
41
+ export declare function toolCallArgumentsAbsent(rawArguments: string): boolean;
42
+ /**
43
+ * What a tool call's arguments turned out to be.
44
+ *
45
+ * A tagged union rather than a throw, because every outcome here has to end
46
+ * with the host emitting a `tool` message for this call's id. A transcript
47
+ * where an assistant asked for a tool and no `tool` message answers it is
48
+ * rejected by the provider on the *next* request, so "give up on this call"
49
+ * is never an option — only "dispatch it" or "answer it with an error".
50
+ */
51
+ export type ToolCallArguments =
52
+ /** Dispatch with these. `{}` for a legitimate zero-argument call. */
53
+ {
54
+ kind: "parsed";
55
+ arguments: Record<string, unknown>;
56
+ }
57
+ /**
58
+ * Not a function call, so there is nothing to dispatch. Present in the
59
+ * OpenAI union (`type: "custom"`) and in whatever a future provider adds.
60
+ */
61
+ | {
62
+ kind: "unsupported_type";
63
+ type: string;
64
+ }
65
+ /**
66
+ * Arguments that cannot be used: either not JSON at all, or JSON that is not
67
+ * an object.
68
+ *
69
+ * The two have different histories, which matters for how the host words the
70
+ * error. A *syntax* failure by this point is a real error rather than a
71
+ * truncation — a cut-off stream is caught upstream by
72
+ * `detectCompletionDefect` and retried, so anything still unparseable here
73
+ * survived that. A payload that parsed but is not an object was never
74
+ * eligible for that retry (it parses fine), so the model's next turn is its
75
+ * only correction — which is why {@link detail} names what was wrong, and
76
+ * why a host should put it in front of the model rather than only in a log.
77
+ */
78
+ | {
79
+ kind: "unparseable";
80
+ detail: string;
81
+ };
82
+ /**
83
+ * Read a tool call's arguments, or say why it cannot be dispatched.
84
+ *
85
+ * A parsed value that is not a JSON object — `"null"`, `"[]"`, `"42"`, all
86
+ * valid JSON — is refused rather than passed on. Tool arguments are a named
87
+ * parameter bag by definition, and handing a tool an array where it expects
88
+ * fields turns a clear failure here into a confusing one inside the tool.
89
+ */
90
+ export declare function parseToolCallArguments(toolCall: DispatchableToolCall): ToolCallArguments;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Reading a tool call the model asked for.
3
+ *
4
+ * This is the dispatch half of a rule whose other half lives in
5
+ * `defects.ts`, and the two only work if they agree. `detectCompletionDefect`
6
+ * decides that a tool call with *empty* arguments is legitimate — the normal
7
+ * zero-argument shape, not a truncation — and passes it through to be run.
8
+ * Something then has to run it, and that something has to reach the same
9
+ * conclusion: empty arguments mean no arguments, not malformed JSON.
10
+ *
11
+ * Splitting those halves across a package boundary is how the original bug
12
+ * happened. Providers send `""` for a zero-arg call as readily as `"{}"`, the
13
+ * dispatcher fed `""` to `JSON.parse`, and a perfectly good call died as
14
+ * "Invalid JSON in tool arguments" — a whole recovery turn spent on a turn
15
+ * that was never broken. A host adopting only the detection half inherits the
16
+ * permissive decision and has to independently invent the matching parse.
17
+ *
18
+ * So both halves read {@link toolCallArgumentsAbsent}. One definition, and a
19
+ * change to what "absent" means cannot update one side and miss the other.
20
+ */
21
+ /**
22
+ * Whether a tool call carried no arguments at all.
23
+ *
24
+ * Whitespace counts as absent: a provider that pads its zero-arg payload is
25
+ * still saying "no arguments", and treating `" "` as content sends it to a
26
+ * JSON parse that can only fail.
27
+ */
28
+ export function toolCallArgumentsAbsent(rawArguments) {
29
+ return rawArguments.trim().length === 0;
30
+ }
31
+ /**
32
+ * Read a tool call's arguments, or say why it cannot be dispatched.
33
+ *
34
+ * A parsed value that is not a JSON object — `"null"`, `"[]"`, `"42"`, all
35
+ * valid JSON — is refused rather than passed on. Tool arguments are a named
36
+ * parameter bag by definition, and handing a tool an array where it expects
37
+ * fields turns a clear failure here into a confusing one inside the tool.
38
+ */
39
+ export function parseToolCallArguments(toolCall) {
40
+ if (toolCall.type !== undefined && toolCall.type !== "function") {
41
+ return { kind: "unsupported_type", type: toolCall.type };
42
+ }
43
+ const raw = toolCall.function?.arguments;
44
+ if (typeof raw !== "string" || toolCallArgumentsAbsent(raw)) {
45
+ // The empty case and the missing case are the same call: the model named a
46
+ // tool and gave it nothing, which is what a zero-argument tool looks like.
47
+ return { kind: "parsed", arguments: {} };
48
+ }
49
+ let value;
50
+ try {
51
+ value = JSON.parse(raw);
52
+ }
53
+ catch (error) {
54
+ return {
55
+ kind: "unparseable",
56
+ detail: error instanceof Error ? error.message : String(error),
57
+ };
58
+ }
59
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
60
+ // `typeof` alone is not a usable name here: it reports both `null` and an
61
+ // array as "object", so the two payloads most likely to arrive would be
62
+ // described as the very thing they were rejected for not being.
63
+ const got = value === null ? "null" : Array.isArray(value) ? "an array" : typeof value;
64
+ return { kind: "unparseable", detail: `expected a JSON object, got ${got}` };
65
+ }
66
+ return { kind: "parsed", arguments: value };
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juno-ai/bind",
3
- "version": "5.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "Agent harness: the tool-calling turn kernel, deterministic LLM provider routing with transport-error classification, the streaming-completion watchdog, run mechanics, sub-agent lineage and admission, transcript healing, tool-schema sanitization, and the plugin/tool vocabulary. MIT-licensed; published to npm from the canonical repo via scripts/publish-bind.ts (docs/bind.md).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,6 +66,26 @@ export interface FailureDisposition {
66
66
  /** Return to the caller immediately; no further traversal. */
67
67
  readonly propagate: boolean;
68
68
  }
69
+ /**
70
+ * The disposition for a failure whose attempt **already produced output the
71
+ * caller cannot take back** — tokens streamed to a UI, or a provider-side
72
+ * effect a replay would repeat.
73
+ *
74
+ * Every other input to `failureDisposition` is a property of the *error*. This
75
+ * one is a property of the *attempt*, and only the host knows it: a transport
76
+ * that buffers the whole completion before returning can always replay, and one
77
+ * that forwards deltas to a live view cannot. So it arrives as a fact on the
78
+ * failure outcome rather than as another arm of the taxonomy.
79
+ *
80
+ * Traversal is withheld entirely — not just the same-endpoint retry. Trying the
81
+ * next provider re-renders the same turn, which is the same duplication with a
82
+ * different label on it.
83
+ *
84
+ * The **breaker effect is preserved**: the endpoint really did fail, and that is
85
+ * true regardless of how far the response got. Suppressing it here would hide a
86
+ * dying endpoint from every later call precisely because it dies late.
87
+ */
88
+ export declare function propagateOnly(disposition: FailureDisposition): FailureDisposition;
69
89
  /**
70
90
  * The exhaustive failure → routing-behavior matrix (PRD §7.1). Pure; the
71
91
  * executor applies it, the circuit breaker consumes its `breaker` effect.
package/routing/errors.js CHANGED
@@ -39,6 +39,36 @@ const CREDENTIAL_FAILURE = Object.freeze({
39
39
  breaker: "open_immediately",
40
40
  propagate: false,
41
41
  });
42
+ /**
43
+ * The disposition for a failure whose attempt **already produced output the
44
+ * caller cannot take back** — tokens streamed to a UI, or a provider-side
45
+ * effect a replay would repeat.
46
+ *
47
+ * Every other input to `failureDisposition` is a property of the *error*. This
48
+ * one is a property of the *attempt*, and only the host knows it: a transport
49
+ * that buffers the whole completion before returning can always replay, and one
50
+ * that forwards deltas to a live view cannot. So it arrives as a fact on the
51
+ * failure outcome rather than as another arm of the taxonomy.
52
+ *
53
+ * Traversal is withheld entirely — not just the same-endpoint retry. Trying the
54
+ * next provider re-renders the same turn, which is the same duplication with a
55
+ * different label on it.
56
+ *
57
+ * The **breaker effect is preserved**: the endpoint really did fail, and that is
58
+ * true regardless of how far the response got. Suppressing it here would hide a
59
+ * dying endpoint from every later call precisely because it dies late.
60
+ */
61
+ export function propagateOnly(disposition) {
62
+ if (disposition.propagate)
63
+ return disposition;
64
+ return Object.freeze({
65
+ sameEndpointRetry: false,
66
+ nextProvider: false,
67
+ fallbackModel: false,
68
+ breaker: disposition.breaker,
69
+ propagate: true,
70
+ });
71
+ }
42
72
  /**
43
73
  * The exhaustive failure → routing-behavior matrix (PRD §7.1). Pure; the
44
74
  * executor applies it, the circuit breaker consumes its `breaker` effect.
@@ -7,6 +7,18 @@ export type AttemptOutcome<T> = Readonly<{
7
7
  }> | Readonly<{
8
8
  kind: "failure";
9
9
  error: InferenceAttemptError;
10
+ /**
11
+ * This attempt already put output somewhere the caller cannot take it
12
+ * back — tokens forwarded to a live view, or a provider-side effect a
13
+ * replay would repeat. The executor then **withholds all traversal** and
14
+ * propagates, because retrying re-renders a turn the user has partly seen.
15
+ *
16
+ * Omitted means replayable, which is right for a transport that buffers
17
+ * the whole completion before returning — it has shown nobody anything
18
+ * yet. Set it from the transport, at the point the first byte leaves:
19
+ * a boolean the transport flips when it forwards its first delta.
20
+ */
21
+ producedOutput?: boolean;
10
22
  }>;
11
23
  /**
12
24
  * One provider request. The host's transport adapter performs the network
@@ -1,4 +1,4 @@
1
- import { failureDisposition, isRetriableAttemptError, } from "./errors.js";
1
+ import { failureDisposition, propagateOnly, isRetriableAttemptError, } from "./errors.js";
2
2
  /**
3
3
  * Drive one structured-output attempt over a frozen route plan with the
4
4
  * normative loop nesting (PRD §5.2): model stage → provider candidate →
@@ -71,7 +71,12 @@ export async function executeRoutePlan(options) {
71
71
  };
72
72
  }
73
73
  failures.push(outcome.error);
74
- const disposition = failureDisposition(outcome.error);
74
+ // The clamp is applied to the taxonomy's answer rather than folded into
75
+ // it, so `failureDisposition` stays total over the error kinds and this
76
+ // stays one testable step. See `propagateOnly`.
77
+ const disposition = outcome.producedOutput === true
78
+ ? propagateOnly(failureDisposition(outcome.error))
79
+ : failureDisposition(outcome.error);
75
80
  switch (disposition.breaker) {
76
81
  case "none":
77
82
  // A breaker-invisible failure (abort, propagated client error)
@@ -6,7 +6,7 @@ export { buildRoutePlan, type RoutePlanRequest, type RoutePlanResult } from "./p
6
6
  export { buildRoutePlanWithConfigDegradation, type DegradedStage, type DegradedPlanResult, } from "./plan-degradation.js";
7
7
  export { computeConfiguredRatesCostCents, type BillingBasisUsage, type BillingBasisResult, } from "./billing-basis.js";
8
8
  export { fallbackKindOfCursor } from "./executor.js";
9
- export { failureDisposition, categorizeHttpStatus, isRetriableAttemptError, type RouteAttemptCursor, type AttemptTarget, type HttpFailureCategory, type InferenceAttemptError, type FailureDisposition, type BreakerEffect, } from "./errors.js";
9
+ export { failureDisposition, propagateOnly, categorizeHttpStatus, isRetriableAttemptError, type RouteAttemptCursor, type AttemptTarget, type HttpFailureCategory, type InferenceAttemptError, type FailureDisposition, type BreakerEffect, } from "./errors.js";
10
10
  export { classifyAttemptError, retryAfterMsFromHeaders, isAbortByName, type AttemptClassification, type TransportFailure, type TransportFailureKind, } from "./attempt-errors.js";
11
11
  export { createCircuitBreaker, type BreakerKey, type CircuitBreakerOptions, type EndpointAdmission, type RecordFailureOptions, type RouteCircuitBreaker, } from "./circuit-breaker.js";
12
12
  export { executeRoutePlan, type AttemptOutcome, type AttemptFn, type FallbackKind, type ExecutePlanOptions, type RouteExecutionResult, } from "./executor.js";
package/routing/index.js CHANGED
@@ -6,7 +6,7 @@ export { buildRoutePlan } from "./planner.js";
6
6
  export { buildRoutePlanWithConfigDegradation, } from "./plan-degradation.js";
7
7
  export { computeConfiguredRatesCostCents, } from "./billing-basis.js";
8
8
  export { fallbackKindOfCursor } from "./executor.js";
9
- export { failureDisposition, categorizeHttpStatus, isRetriableAttemptError, } from "./errors.js";
9
+ export { failureDisposition, propagateOnly, categorizeHttpStatus, isRetriableAttemptError, } from "./errors.js";
10
10
  export { classifyAttemptError, retryAfterMsFromHeaders, isAbortByName, } from "./attempt-errors.js";
11
11
  export { createCircuitBreaker, } from "./circuit-breaker.js";
12
12
  export { executeRoutePlan, } from "./executor.js";
package/run/children.d.ts CHANGED
@@ -86,6 +86,26 @@ export type ChainRule =
86
86
  msSinceLastSpawn: number | null;
87
87
  cooldownMs: number;
88
88
  }
89
+ /**
90
+ * Whether this parent→child pair has *already* run, at all, within whatever
91
+ * scope the host measured over. The stronger sibling of
92
+ * `pair_cooldown`: a cooldown lets a pair repeat once enough time passes,
93
+ * while this permits the pair exactly once and never again.
94
+ *
95
+ * Reach for it where a repeat is not slow but wrong — two agents that answer
96
+ * each other are a loop whose every hop looks individually reasonable, and a
97
+ * time-based bound only makes such a loop cheaper per hour rather than
98
+ * ending it.
99
+ *
100
+ * The scope is the host's to choose and is deliberately not modelled here:
101
+ * "already paired in this chain" and "already paired in this conversation"
102
+ * are the same rule over different measurements, and naming either one would
103
+ * put a product's containment vocabulary into the harness.
104
+ */
105
+ | {
106
+ kind: "pair_seen";
107
+ alreadyPaired: boolean;
108
+ }
89
109
  /**
90
110
  * A ceiling on concurrent runs for the whole tenant. Worth having alongside
91
111
  * the chain rules: depth and chain budgets constrain one lineage, and neither
@@ -95,6 +115,26 @@ export type ChainRule =
95
115
  kind: "tenant_ceiling";
96
116
  activeRuns: number;
97
117
  maxActiveRuns: number;
118
+ }
119
+ /**
120
+ * How many runs a tenant may *start* within a rolling window, as opposed to
121
+ * how many may be in flight at once (`tenant_ceiling`).
122
+ *
123
+ * The two bound different abuses and neither implies the other. A ceiling
124
+ * caps concurrency, so a caller that starts and finishes runs quickly slips
125
+ * under it indefinitely — a message flood, or one fan-out across a large
126
+ * group, is exactly that shape. A rate caps total starts, so it bounds spend
127
+ * where the ceiling bounds load.
128
+ *
129
+ * `windowMs` is carried only so the refusal can say how long the wait is;
130
+ * the rule does not roll the window itself. The host measures over whatever
131
+ * window it chose and passes both.
132
+ */
133
+ | {
134
+ kind: "tenant_rate";
135
+ runsInWindow: number;
136
+ maxRuns: number;
137
+ windowMs: number;
98
138
  };
99
139
  export type ChildAdmission = {
100
140
  admitted: true;
@@ -105,9 +145,9 @@ export type ChildAdmission = {
105
145
  /**
106
146
  * Could waiting change this answer?
107
147
  *
108
- * The four rules are not the same kind of refusal, and prose alone does
109
- * not separate them: a cooldown clears on its own, a spent chain budget
110
- * never does. Without this a model reads "refused" and has to guess
148
+ * The rules are not all the same kind of refusal, and prose alone does
149
+ * not separate them: a cooldown or a rolling window clears on its own, a
150
+ * spent chain budget or a repeated pair never does. Without this a model reads "refused" and has to guess
111
151
  * between waiting and giving up — and guessing wrong either wastes the
112
152
  * run on retries into the same wall or abandons work it could have done
113
153
  * a moment later.
@@ -127,6 +167,18 @@ export type ChildAdmission = {
127
167
  * reason the model sees when several apply. An empty rule list admits — this
128
168
  * function bounds what it is given and claims nothing about what it is not.
129
169
  *
170
+ * **When a measurement fails, omit the rule — do not pass a sentinel.** The
171
+ * two failure postures here answer different questions and are easy to
172
+ * conflate. A rule whose number arrived broken (`NaN`, negative) refuses,
173
+ * because a nonsense count is not evidence of safety. A rule you could not
174
+ * measure at all — the count query threw, the store was unreachable — is
175
+ * absent, and absence admits. That is a deliberate seam, not an oversight:
176
+ * whether a transient infrastructure failure should stop all work or let it
177
+ * through is an availability judgement about a specific product, and this
178
+ * function has no standing to make it. Decide it at the call site, in a
179
+ * `catch`, by choosing whether to append the rule. Passing `NaN` or `-1` to
180
+ * mean "unknown" inverts the answer you almost certainly want.
181
+ *
130
182
  * **Call before enqueuing, never after.** A chain that is bounded only once its
131
183
  * runs are already queued is not bounded; it is billed.
132
184
  *
package/run/children.js CHANGED
@@ -48,6 +48,18 @@ parent) {
48
48
  * reason the model sees when several apply. An empty rule list admits — this
49
49
  * function bounds what it is given and claims nothing about what it is not.
50
50
  *
51
+ * **When a measurement fails, omit the rule — do not pass a sentinel.** The
52
+ * two failure postures here answer different questions and are easy to
53
+ * conflate. A rule whose number arrived broken (`NaN`, negative) refuses,
54
+ * because a nonsense count is not evidence of safety. A rule you could not
55
+ * measure at all — the count query threw, the store was unreachable — is
56
+ * absent, and absence admits. That is a deliberate seam, not an oversight:
57
+ * whether a transient infrastructure failure should stop all work or let it
58
+ * through is an availability judgement about a specific product, and this
59
+ * function has no standing to make it. Decide it at the call site, in a
60
+ * `catch`, by choosing whether to append the rule. Passing `NaN` or `-1` to
61
+ * mean "unknown" inverts the answer you almost certainly want.
62
+ *
51
63
  * **Call before enqueuing, never after.** A chain that is bounded only once its
52
64
  * runs are already queued is not bounded; it is billed.
53
65
  *
@@ -83,8 +95,8 @@ function evaluateRule(rule) {
83
95
  const childDepth = rule.parentDepth + 1;
84
96
  return childDepth >= rule.maxDepth
85
97
  ? refuse(rule, false, `chain depth limit reached (${childDepth}/${rule.maxDepth}) — ` +
86
- `this is as deep as the chain may go, so do the work in this run ` +
87
- `instead of spawning`)
98
+ `as far as work may be handed on, and waiting will not change ` +
99
+ `that; do this work here rather than delegating again`)
88
100
  : null;
89
101
  }
90
102
  case "chain_budget": {
@@ -94,8 +106,8 @@ function evaluateRule(rule) {
94
106
  return broken;
95
107
  return rule.runsInChain >= rule.maxRuns
96
108
  ? refuse(rule, false, `chain run budget exhausted (${rule.runsInChain}/${rule.maxRuns}) — ` +
97
- `the budget is spent for this whole chain and does not refill, so ` +
98
- `do the work in this run instead of spawning`)
109
+ `spent for this whole chain and it does not refill, so do this ` +
110
+ `work here rather than delegating again`)
99
111
  : null;
100
112
  }
101
113
  case "pair_cooldown": {
@@ -106,10 +118,19 @@ function evaluateRule(rule) {
106
118
  if (broken)
107
119
  return broken;
108
120
  return rule.msSinceLastSpawn < rule.cooldownMs
109
- ? refuse(rule, true, `spawn cooldown active (${rule.msSinceLastSpawn}ms since the last ` +
110
- `spawn, ${rule.cooldownMs}ms required) retry in ` +
111
- `${rule.cooldownMs - rule.msSinceLastSpawn}ms, or do the work in ` +
112
- `this run`)
121
+ ? refuse(rule, true, `handoff cooldown active retry in ` +
122
+ `${rule.cooldownMs - rule.msSinceLastSpawn}ms, or do this work ` +
123
+ `here (${rule.msSinceLastSpawn}ms since the last handoff, ` +
124
+ `${rule.cooldownMs}ms required)`)
125
+ : null;
126
+ }
127
+ case "pair_seen": {
128
+ // A boolean carries no measurement to validate — the host either
129
+ // observed the prior pairing or it did not.
130
+ return rule.alreadyPaired
131
+ ? refuse(rule, false, `repeat handoff refused — these two have already worked together ` +
132
+ `here and are allowed to only once, so waiting will not change ` +
133
+ `that; do this work here instead`)
113
134
  : null;
114
135
  }
115
136
  case "tenant_ceiling": {
@@ -119,8 +140,20 @@ function evaluateRule(rule) {
119
140
  return broken;
120
141
  return rule.activeRuns >= rule.maxActiveRuns
121
142
  ? refuse(rule, true, `concurrent run ceiling reached (${rule.activeRuns}/` +
122
- `${rule.maxActiveRuns}) — this clears as other runs finish, so ` +
123
- `retry shortly or do the work in this run`)
143
+ `${rule.maxActiveRuns}) — clears as other runs finish, so retry ` +
144
+ `shortly or do this work here`)
145
+ : null;
146
+ }
147
+ case "tenant_rate": {
148
+ const broken = unmeasurable(rule, rule.runsInWindow, "runsInWindow") ??
149
+ unmeasurable(rule, rule.maxRuns, "maxRuns") ??
150
+ unmeasurable(rule, rule.windowMs, "windowMs");
151
+ if (broken)
152
+ return broken;
153
+ return rule.runsInWindow >= rule.maxRuns
154
+ ? refuse(rule, true, `start-rate limit reached (${rule.runsInWindow}/${rule.maxRuns} ` +
155
+ `runs in the last ${rule.windowMs}ms) — the window rolls, so ` +
156
+ `wait up to ${rule.windowMs}ms and retry, or do this work here`)
124
157
  : null;
125
158
  }
126
159
  default: {
package/run/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { RunTimeoutError, createRunDeadline, classifyRunFailure, createCoalescedHeartbeat, unrefTimer, type RunDeadline, type CoalescedHeartbeat, } from "./harness.js";
2
2
  export { runToolCallsPooledByTool } from "./tool-batch.js";
3
3
  export { rootChain, descendChain, admitChildRun, createPollSchedule, type ChainRef, type ChainRule, type ChildAdmission, type PollStep, type PollSchedule, type PollScheduleOptions, } from "./children.js";
4
+ export { toolCallReceiptKeyString, canonicalJson, toolCallArgsHash, decideToolCallReceipt, type ToolCallReceiptKey, type DigestFn, type ReceiptState, type EffectResumability, type ReceiptDecision, } from "./receipts.js";
package/run/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { RunTimeoutError, createRunDeadline, classifyRunFailure, createCoalescedHeartbeat, unrefTimer, } from "./harness.js";
2
2
  export { runToolCallsPooledByTool } from "./tool-batch.js";
3
3
  export { rootChain, descendChain, admitChildRun, createPollSchedule, } from "./children.js";
4
+ export { toolCallReceiptKeyString, canonicalJson, toolCallArgsHash, decideToolCallReceipt, } from "./receipts.js";