@intx/inference 0.3.0 → 0.4.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/dist/assembly.d.ts +8 -0
- package/dist/assembly.js +3 -1
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +14 -5
- package/dist/harness.d.ts +2 -0
- package/dist/harness.js +9 -2
- package/dist/providers/anthropic.js +1 -6
- package/dist/reactor.d.ts +18 -0
- package/dist/reactor.js +94 -13
- package/package.json +4 -4
package/dist/assembly.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CredentialMaterialResolver } from "@intx/types";
|
|
1
2
|
import { type BlobReader, type AuditStore, type BeforeToolExtension, type Compactor, type ContextStore, type ContextTransform, type InferenceSource, type ReactorDirector, type ToolDefinition, type ToolResultTransform, type ToolRunner } from "@intx/types/runtime";
|
|
2
3
|
import { type AuditCollector } from "./audit-collector.js";
|
|
3
4
|
import { type AuthzExtensionOptions } from "./authz-extension.js";
|
|
@@ -27,6 +28,12 @@ export type ReactorAssemblyConfig = {
|
|
|
27
28
|
failOverToNextSource?: () => boolean;
|
|
28
29
|
/** Reset `source` to the most-preferred source, in place. */
|
|
29
30
|
resetToPreferredSource?: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the active source's credential secret by `credentialId` from the
|
|
33
|
+
* run's credential cell at send time. Threaded verbatim to the reactor;
|
|
34
|
+
* optional, defaulted fail-closed by the harness when omitted.
|
|
35
|
+
*/
|
|
36
|
+
readMaterial?: CredentialMaterialResolver;
|
|
30
37
|
toolRunner: ToolRunner;
|
|
31
38
|
contextStore: ContextStore;
|
|
32
39
|
onEvent: (event: ReactorEmittedEvent) => void;
|
|
@@ -51,6 +58,7 @@ export type ReactorAssemblyConfig = {
|
|
|
51
58
|
inferenceRunner?: ReactorConfig["inferenceRunner"];
|
|
52
59
|
gateTimeout?: number;
|
|
53
60
|
shutdownTimeoutMs?: number;
|
|
61
|
+
doomLoopThreshold?: number | false;
|
|
54
62
|
};
|
|
55
63
|
/**
|
|
56
64
|
* Output of `createReactorAssembly`. The `reactor` is started by the caller as
|
package/dist/assembly.js
CHANGED
|
@@ -24,7 +24,7 @@ const DEFAULT_SIZE_CAP_MAX_CHARS = 10_000;
|
|
|
24
24
|
* different composition.
|
|
25
25
|
*/
|
|
26
26
|
export function createReactorAssembly(config) {
|
|
27
|
-
const { sessionId, director, source, failOverToNextSource, resetToPreferredSource, toolRunner, contextStore, onEvent, authorize, toolDefinitions, auditStore, beforeToolExtensions: callerBeforeToolExtensions, toolResultTransforms: callerToolResultTransforms, contextTransforms, compactors, sizeCapMaxChars, afterCheckpoint: callerAfterCheckpoint, onShutdown: callerOnShutdown, deps, correlationValidator, inferenceRunner, gateTimeout, shutdownTimeoutMs, } = config;
|
|
27
|
+
const { sessionId, director, source, failOverToNextSource, resetToPreferredSource, readMaterial, toolRunner, contextStore, onEvent, authorize, toolDefinitions, auditStore, beforeToolExtensions: callerBeforeToolExtensions, toolResultTransforms: callerToolResultTransforms, contextTransforms, compactors, sizeCapMaxChars, afterCheckpoint: callerAfterCheckpoint, onShutdown: callerOnShutdown, deps, correlationValidator, inferenceRunner, gateTimeout, shutdownTimeoutMs, doomLoopThreshold, } = config;
|
|
28
28
|
// Audit collector is created up-front so the authz extension can route its
|
|
29
29
|
// decisions through `onDecision`. When no auditStore is supplied, no
|
|
30
30
|
// collector is created and authz runs without decision recording.
|
|
@@ -106,6 +106,7 @@ export function createReactorAssembly(config) {
|
|
|
106
106
|
source,
|
|
107
107
|
...(failOverToNextSource !== undefined ? { failOverToNextSource } : {}),
|
|
108
108
|
...(resetToPreferredSource !== undefined ? { resetToPreferredSource } : {}),
|
|
109
|
+
...(readMaterial !== undefined ? { readMaterial } : {}),
|
|
109
110
|
toolRunner,
|
|
110
111
|
contextStore,
|
|
111
112
|
onEvent: composedOnEvent,
|
|
@@ -126,6 +127,7 @@ export function createReactorAssembly(config) {
|
|
|
126
127
|
...(inferenceRunner !== undefined ? { inferenceRunner } : {}),
|
|
127
128
|
...(gateTimeout !== undefined ? { gateTimeout } : {}),
|
|
128
129
|
...(shutdownTimeoutMs !== undefined ? { shutdownTimeoutMs } : {}),
|
|
130
|
+
...(doomLoopThreshold !== undefined ? { doomLoopThreshold } : {}),
|
|
129
131
|
};
|
|
130
132
|
const reactor = createReactor(reactorConfig);
|
|
131
133
|
const blobReader = createBlobReader(contextStore);
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InferenceSource } from "@intx/types/runtime";
|
|
2
|
+
import type { CredentialMaterialResolver } from "@intx/types";
|
|
2
3
|
/**
|
|
3
4
|
* Sentinel for headers that carry the API key verbatim (no prefix).
|
|
4
5
|
* Used by providers like Anthropic (`x-api-key`) and Google
|
|
@@ -21,4 +22,4 @@ export declare const BEARER_CREDENTIAL_SENTINEL = "<inject:bearer-credential>";
|
|
|
21
22
|
* surprising, and no legitimate adapter constructs sentinel-bearing
|
|
22
23
|
* composite values.
|
|
23
24
|
*/
|
|
24
|
-
export declare function injectCredentials(headers: Record<string, string>, source: InferenceSource): Record<string, string>;
|
|
25
|
+
export declare function injectCredentials(headers: Record<string, string>, source: InferenceSource, readMaterial: CredentialMaterialResolver): Record<string, string>;
|
package/dist/auth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Sentinel placeholder strings adapters use in their built request
|
|
2
2
|
// headers to declare which credential the harness should fill at send
|
|
3
3
|
// time. The harness scans every header value and replaces exact-match
|
|
4
|
-
// sentinels with
|
|
5
|
-
// never see the API key.
|
|
4
|
+
// sentinels with the secret resolved from the source's `credentialId`
|
|
5
|
+
// against the run's credential cell. Adapters never see the API key.
|
|
6
6
|
//
|
|
7
7
|
// Each new provider adds a new header name + sentinel choice in its
|
|
8
8
|
// `buildRequest`; the harness needs no per-provider knowledge. The
|
|
@@ -38,14 +38,23 @@ export const BEARER_CREDENTIAL_SENTINEL = "<inject:bearer-credential>";
|
|
|
38
38
|
* surprising, and no legitimate adapter constructs sentinel-bearing
|
|
39
39
|
* composite values.
|
|
40
40
|
*/
|
|
41
|
-
export function injectCredentials(headers, source) {
|
|
41
|
+
export function injectCredentials(headers, source, readMaterial) {
|
|
42
|
+
// Resolve the source's secret lazily and once: only when a header actually
|
|
43
|
+
// carries a sentinel, so a request with no credential sentinel never touches
|
|
44
|
+
// the cell, and the fail-closed read (revoked/absent credential) surfaces only
|
|
45
|
+
// when the secret is genuinely needed.
|
|
46
|
+
let cachedSecret;
|
|
47
|
+
const secret = () => {
|
|
48
|
+
cachedSecret ??= readMaterial(source.credentialId).secret;
|
|
49
|
+
return cachedSecret;
|
|
50
|
+
};
|
|
42
51
|
const result = {};
|
|
43
52
|
for (const [name, value] of Object.entries(headers)) {
|
|
44
53
|
if (value === CREDENTIAL_SENTINEL) {
|
|
45
|
-
result[name] =
|
|
54
|
+
result[name] = secret();
|
|
46
55
|
}
|
|
47
56
|
else if (value === BEARER_CREDENTIAL_SENTINEL) {
|
|
48
|
-
result[name] = `Bearer ${
|
|
57
|
+
result[name] = `Bearer ${secret()}`;
|
|
49
58
|
}
|
|
50
59
|
else {
|
|
51
60
|
result[name] = value;
|
package/dist/harness.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConversationTurn, InferenceEvent, InferenceOptions, InferenceSource } from "@intx/types/runtime";
|
|
2
|
+
import type { CredentialMaterialResolver } from "@intx/types";
|
|
2
3
|
import type { AdapterRegistry } from "./adapter.js";
|
|
3
4
|
/**
|
|
4
5
|
* Default per-call inactivity timeout (ms). Two minutes is conservative
|
|
@@ -86,6 +87,7 @@ export type InferenceHarnessOptions = {
|
|
|
86
87
|
inferenceOptions?: InferenceOptions;
|
|
87
88
|
signal?: AbortSignal;
|
|
88
89
|
nextSeq: () => number;
|
|
90
|
+
readMaterial?: CredentialMaterialResolver;
|
|
89
91
|
deps: Dependencies;
|
|
90
92
|
};
|
|
91
93
|
/**
|
package/dist/harness.js
CHANGED
|
@@ -71,6 +71,13 @@ export function createDependencies(adapters) {
|
|
|
71
71
|
adapters,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
// Fail-closed default resolver, installed when a caller supplies no
|
|
75
|
+
// `readMaterial`. It throws only if a request actually reaches a credential
|
|
76
|
+
// sentinel, so a sentinel-free mock harness runs without a resolver while a
|
|
77
|
+
// real credentialed request surfaces the missing wiring loudly.
|
|
78
|
+
const unconfiguredCredentialResolver = (credentialId) => {
|
|
79
|
+
throw new Error(`no credential resolver supplied to the inference harness, but a request needs the secret for credential ${credentialId}`);
|
|
80
|
+
};
|
|
74
81
|
/**
|
|
75
82
|
* Run one fetch lifecycle and yield its events. Ends on the first
|
|
76
83
|
* `inference.error` or `inference.done`. The outer `runInference`
|
|
@@ -82,7 +89,7 @@ export function createDependencies(adapters) {
|
|
|
82
89
|
* directly would bypass retry handling.
|
|
83
90
|
*/
|
|
84
91
|
async function* runSingleAttempt(opts) {
|
|
85
|
-
const { turns, source, inferenceOptions, signal, nextSeq, deps } = opts;
|
|
92
|
+
const { turns, source, inferenceOptions, signal, nextSeq, readMaterial, deps, } = opts;
|
|
86
93
|
// Per-call options override source-bound defaults. The merge happens
|
|
87
94
|
// here, once, so the adapter and timeout-resolution paths below all
|
|
88
95
|
// see the effective option set without having to remember the
|
|
@@ -181,7 +188,7 @@ async function* runSingleAttempt(opts) {
|
|
|
181
188
|
}
|
|
182
189
|
// Resolve the full URL and inject credentials.
|
|
183
190
|
const url = resolveURL(builtRequest.url, source.baseURL);
|
|
184
|
-
const headers = injectCredentials(builtRequest.headers, source);
|
|
191
|
+
const headers = injectCredentials(builtRequest.headers, source, readMaterial ?? unconfiguredCredentialResolver);
|
|
185
192
|
// Per-call timeouts. The inactivity timer fires when the harness
|
|
186
193
|
// hasn't yielded an event for `inactivityTimeoutMs`; the total timer
|
|
187
194
|
// is a wall-clock cap from fetch onwards. We own one AbortController,
|
|
@@ -449,12 +449,7 @@ const MessageStart = type({
|
|
|
449
449
|
});
|
|
450
450
|
const MessageStop = type({ type: "'message_stop'" });
|
|
451
451
|
const Ping = type({ type: "'ping'" });
|
|
452
|
-
const AnthropicSSEEvent =
|
|
453
|
-
.or(ContentBlockStop)
|
|
454
|
-
.or(MessageDelta)
|
|
455
|
-
.or(MessageStart)
|
|
456
|
-
.or(MessageStop)
|
|
457
|
-
.or(Ping);
|
|
452
|
+
const AnthropicSSEEvent = type.or(ContentBlockDelta, ContentBlockStart, ContentBlockStop, MessageDelta, MessageStart, MessageStop, Ping);
|
|
458
453
|
// Maps Anthropic's wire usage object onto the internal TokenUsage. Anthropic
|
|
459
454
|
// never reports a distinct thinking-token count, so `thinking` is always 0.
|
|
460
455
|
// Shared by the streaming `message_start` path and the non-streaming
|
package/dist/reactor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InboundMessage, InferenceEvent, InferenceSource, ReactorDirector, ContextStore, ToolRunner, AbortReason, BeforeToolExtension, ToolResultTransform, ContextTransform, Compactor } from "@intx/types/runtime";
|
|
2
|
+
import type { CredentialMaterialResolver } from "@intx/types";
|
|
2
3
|
import type { Dependencies, InferenceHarnessOptions } from "./harness.js";
|
|
3
4
|
import type { CorrelationValidator } from "./correlation.js";
|
|
4
5
|
export type ReactorEmittedEvent = InferenceEvent | {
|
|
@@ -20,6 +21,13 @@ export type ReactorConfig = {
|
|
|
20
21
|
failOverToNextSource?: () => boolean;
|
|
21
22
|
/** Reset `source` to the most-preferred source, in place. */
|
|
22
23
|
resetToPreferredSource?: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the active source's credential secret by `credentialId` from the
|
|
26
|
+
* run's credential cell at send time. Read live per attempt, so a failover to
|
|
27
|
+
* a source with a different `credentialId` resolves that source's credential.
|
|
28
|
+
* Optional: the harness installs a fail-closed default when it is omitted.
|
|
29
|
+
*/
|
|
30
|
+
readMaterial?: CredentialMaterialResolver;
|
|
23
31
|
toolRunner: ToolRunner;
|
|
24
32
|
contextStore: ContextStore;
|
|
25
33
|
correlationValidator?: CorrelationValidator;
|
|
@@ -34,6 +42,16 @@ export type ReactorConfig = {
|
|
|
34
42
|
onShutdown?: () => Promise<void>;
|
|
35
43
|
gateTimeout?: number;
|
|
36
44
|
shutdownTimeoutMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Number of consecutive identical tool-call turns that trips doom-loop
|
|
47
|
+
* detection. A turn's identity is its batch of executed tool calls; a
|
|
48
|
+
* runaway model repeating the same call burns inference cost with no
|
|
49
|
+
* progress. On the Nth consecutive identical turn the reactor emits a fatal
|
|
50
|
+
* `reactor.error` and shuts the run down. Must be a positive integer.
|
|
51
|
+
* Pass `false` to disable doom-loop detection entirely. Defaults to
|
|
52
|
+
* `DEFAULT_DOOM_LOOP_THRESHOLD`.
|
|
53
|
+
*/
|
|
54
|
+
doomLoopThreshold?: number | false;
|
|
37
55
|
};
|
|
38
56
|
export type Reactor = {
|
|
39
57
|
/** Begin processing. Emits reactor.start. Must be called exactly once. */
|
package/dist/reactor.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// (INFERENCE.md § Agent Reactor)
|
|
15
15
|
import { getLogger } from "@intx/log";
|
|
16
16
|
import { ApprovalDecision, signalKindToGateType } from "@intx/types";
|
|
17
|
+
import { canonicalJsonStringify } from "@intx/types/wire-definition-hash";
|
|
17
18
|
import { type } from "arktype";
|
|
18
19
|
import { runInference } from "./harness.js";
|
|
19
20
|
import { createCapabilities } from "./director.js";
|
|
@@ -33,21 +34,54 @@ const SUSPENDED = Symbol("suspended");
|
|
|
33
34
|
function assertNever(x) {
|
|
34
35
|
throw new Error(`Unhandled resume case: ${JSON.stringify(x)}`);
|
|
35
36
|
}
|
|
36
|
-
function buildHarnessOpts(turns, source, options, signal, nextSeq, deps) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return { turns, source, signal, nextSeq, deps };
|
|
37
|
+
function buildHarnessOpts(turns, source, options, signal, nextSeq, readMaterial, deps) {
|
|
38
|
+
// exactOptionalPropertyTypes is on: only set the optional keys when defined.
|
|
39
|
+
return {
|
|
40
|
+
turns,
|
|
41
|
+
source,
|
|
42
|
+
...(options !== undefined ? { inferenceOptions: options } : {}),
|
|
43
|
+
signal,
|
|
44
|
+
nextSeq,
|
|
45
|
+
...(readMaterial !== undefined ? { readMaterial } : {}),
|
|
46
|
+
deps,
|
|
47
|
+
};
|
|
48
48
|
}
|
|
49
49
|
const DEFAULT_GATE_TIMEOUT_MS = 3_600_000;
|
|
50
50
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
51
|
+
const DEFAULT_DOOM_LOOP_THRESHOLD = 3;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the caller-facing `doomLoopThreshold` into the reactor's internal
|
|
54
|
+
* form: a positive integer when detection is active, or `null` when it is
|
|
55
|
+
* disabled. `undefined` (omitted) takes the default; `false` disables; a
|
|
56
|
+
* number is validated here — the construction edge is the one place that owns
|
|
57
|
+
* the default and rejects a malformed value loudly, so a stray `0`, negative,
|
|
58
|
+
* or non-integer throws rather than silently disarming the guard. Downstream
|
|
59
|
+
* code compares against the returned `number | null` and never sees the raw
|
|
60
|
+
* `false`, whose numeric coercion would otherwise trip the loop immediately.
|
|
61
|
+
*/
|
|
62
|
+
function resolveDoomLoopThreshold(raw) {
|
|
63
|
+
if (raw === false)
|
|
64
|
+
return null;
|
|
65
|
+
if (raw === undefined)
|
|
66
|
+
return DEFAULT_DOOM_LOOP_THRESHOLD;
|
|
67
|
+
if (!Number.isInteger(raw) || raw < 1) {
|
|
68
|
+
throw new Error(`doomLoopThreshold must be a positive integer or false, got ${String(raw)}`);
|
|
69
|
+
}
|
|
70
|
+
return raw;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Order-independent identity of a batch of executed tool calls. Each call
|
|
74
|
+
* canonicalizes to its name and arguments (the call `id` is excluded, since it
|
|
75
|
+
* differs on every request); sorting makes a parallel batch match regardless
|
|
76
|
+
* of the order the model emitted its calls. Two turns share a signature when
|
|
77
|
+
* they run the same multiset of `(name, arguments)` pairs.
|
|
78
|
+
*/
|
|
79
|
+
function toolBatchSignature(calls) {
|
|
80
|
+
return calls
|
|
81
|
+
.map((call) => canonicalJsonStringify({ name: call.name, arguments: call.arguments }))
|
|
82
|
+
.sort()
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
51
85
|
/**
|
|
52
86
|
* Creates a reactor instance bound to the given configuration.
|
|
53
87
|
* Call `start()` to begin the event loop.
|
|
@@ -61,6 +95,10 @@ export function createReactor(config) {
|
|
|
61
95
|
failOverToNextSource = () => false, resetToPreferredSource = () => {
|
|
62
96
|
/* single-source: nothing to reset */
|
|
63
97
|
}, } = config;
|
|
98
|
+
// Resolved once at the construction edge: a positive integer while detection
|
|
99
|
+
// is active, or `null` when the caller disabled it with `false`. Every
|
|
100
|
+
// downstream comparison reads this binding, never the raw config value.
|
|
101
|
+
const doomLoopThreshold = resolveDoomLoopThreshold(config.doomLoopThreshold);
|
|
64
102
|
// Monotonic sequence counter, scoped to this session.
|
|
65
103
|
let seq = 0;
|
|
66
104
|
function nextSeq() {
|
|
@@ -156,9 +194,22 @@ export function createReactor(config) {
|
|
|
156
194
|
// produces unambiguous start/end pairs downstream.
|
|
157
195
|
let currentMessageRunId = null;
|
|
158
196
|
let currentMessageId = null;
|
|
197
|
+
// Doom-loop detection state, scoped to the current message run. Each executed
|
|
198
|
+
// tool-call turn is reduced to a batch signature; consecutive identical
|
|
199
|
+
// signatures accumulate here, and the run is broken when the count reaches
|
|
200
|
+
// `doomLoopThreshold`. This is run-scoped, not cycle-scoped: it resets only in
|
|
201
|
+
// `openMessageRun`, never in `resetCycleAccumulators`. It is also deliberately
|
|
202
|
+
// ephemeral (closure state, not persisted) -- a mid-run restart resets it to
|
|
203
|
+
// zero and the loop simply re-accumulates and trips a few turns later.
|
|
204
|
+
let lastToolBatchSignature = null;
|
|
205
|
+
let toolBatchRepeatCount = 0;
|
|
206
|
+
let lastToolBatchNames = [];
|
|
159
207
|
function openMessageRun(messageId) {
|
|
160
208
|
currentMessageRunId = crypto.randomUUID();
|
|
161
209
|
currentMessageId = messageId;
|
|
210
|
+
lastToolBatchSignature = null;
|
|
211
|
+
toolBatchRepeatCount = 0;
|
|
212
|
+
lastToolBatchNames = [];
|
|
162
213
|
emit({
|
|
163
214
|
type: "message.run.started",
|
|
164
215
|
seq: nextSeq(),
|
|
@@ -448,7 +499,7 @@ export function createReactor(config) {
|
|
|
448
499
|
// prior cycle must not leave the agent permanently demoted.
|
|
449
500
|
resetToPreferredSource();
|
|
450
501
|
for (;;) {
|
|
451
|
-
const harnessOpts = buildHarnessOpts(prompt, config.source, options, signal, nextSeq, deps);
|
|
502
|
+
const harnessOpts = buildHarnessOpts(prompt, config.source, options, signal, nextSeq, config.readMaterial, deps);
|
|
452
503
|
let lastDone;
|
|
453
504
|
let lastError;
|
|
454
505
|
for await (const event of inferenceRunner(harnessOpts)) {
|
|
@@ -627,6 +678,25 @@ export function createReactor(config) {
|
|
|
627
678
|
// Suspended calls are parked, not answered: they contribute no tool
|
|
628
679
|
// result to history and no tool.done continuation event.
|
|
629
680
|
const results = outcomes.filter((o) => o !== SUSPENDED);
|
|
681
|
+
// Doom-loop accounting keys off the calls that actually ran, aligned to
|
|
682
|
+
// their outcome by index (both the parallel and serial paths above keep
|
|
683
|
+
// `outcomes` in `calls` order). A parked call contributes nothing, so a
|
|
684
|
+
// suspend-then-redispatch cycle counts its one real execution once. The
|
|
685
|
+
// loop reads `toolBatchRepeatCount` after this returns and breaks the run
|
|
686
|
+
// when it reaches the threshold. A `null` threshold means detection is
|
|
687
|
+
// disabled, so the accounting is skipped entirely.
|
|
688
|
+
const ranCalls = calls.filter((_call, i) => outcomes[i] !== SUSPENDED);
|
|
689
|
+
if (doomLoopThreshold !== null && ranCalls.length > 0) {
|
|
690
|
+
const signature = toolBatchSignature(ranCalls);
|
|
691
|
+
if (signature === lastToolBatchSignature) {
|
|
692
|
+
toolBatchRepeatCount += 1;
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
lastToolBatchSignature = signature;
|
|
696
|
+
toolBatchRepeatCount = 1;
|
|
697
|
+
}
|
|
698
|
+
lastToolBatchNames = ranCalls.map((call) => call.name);
|
|
699
|
+
}
|
|
630
700
|
cycleToolCallsExecuted += results.length;
|
|
631
701
|
if (addToHistory && stateManager !== null && results.length > 0) {
|
|
632
702
|
stateManager.appendTurn(createToolResultTurn(results));
|
|
@@ -1065,6 +1135,17 @@ export function createReactor(config) {
|
|
|
1065
1135
|
const parallel = toolsAction.parallel !== false;
|
|
1066
1136
|
const addToHistory = toolsAction.addToHistory !== false;
|
|
1067
1137
|
await executeTools(toolsAction.calls, parallel, addToHistory);
|
|
1138
|
+
if (doomLoopThreshold !== null &&
|
|
1139
|
+
toolBatchRepeatCount >= doomLoopThreshold) {
|
|
1140
|
+
const tools = lastToolBatchNames.join(", ");
|
|
1141
|
+
const message = `Doom loop detected: an identical tool batch (${tools}) executed ` +
|
|
1142
|
+
`${String(doomLoopThreshold)} times consecutively`;
|
|
1143
|
+
emitError(message, true);
|
|
1144
|
+
closeMessageRun("failed", { message, kind: "doom_loop" });
|
|
1145
|
+
done = true;
|
|
1146
|
+
await initiateShutdown();
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1068
1149
|
continue;
|
|
1069
1150
|
}
|
|
1070
1151
|
// No infer/tools/reply/suspend/wait/compact action — if a checkpoint
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intx/inference",
|
|
3
3
|
"description": "Provider-agnostic inference runtime with Anthropic, OpenAI, and Google GenAI adapters",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "LGPL-2.1-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@intx/log": "0.
|
|
21
|
-
"@intx/types": "0.
|
|
20
|
+
"@intx/log": "0.4.0",
|
|
21
|
+
"@intx/types": "0.4.0",
|
|
22
22
|
"arktype": "^2.1.29"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@intx/mime": "0.
|
|
25
|
+
"@intx/mime": "0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|