@animalabs/agent-framework 0.30.0 → 0.32.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/README.md +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +138 -102
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1133 -752
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +6 -12
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +65 -24
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1163 -770
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +6 -12
- package/src/types/framework.ts +66 -24
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- package/src/reasoning-loss.ts +0 -120
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
* rule with no name is never merged: the merge keys rules by name.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
import { quotedValue } from '../supplied-text.js';
|
|
44
|
+
|
|
43
45
|
/** What a start changed, by rule name, for its trace and its log line. */
|
|
44
46
|
export interface RecipeChanges {
|
|
45
47
|
added: string[];
|
|
@@ -173,11 +175,11 @@ export function reconcileWithRecipe(
|
|
|
173
175
|
const agree = (a: unknown, b: unknown): boolean => meaningOf(a) === meaningOf(b);
|
|
174
176
|
const liveDuplicate = duplicateName(live.rules);
|
|
175
177
|
if (liveDuplicate !== undefined) {
|
|
176
|
-
throw new Error(`gate.json names the rule
|
|
178
|
+
throw new Error(`gate.json names the rule ${quotedValue(liveDuplicate)} twice`);
|
|
177
179
|
}
|
|
178
180
|
const recipeDuplicate = duplicateName(recipe.rules);
|
|
179
181
|
if (recipeDuplicate !== undefined) {
|
|
180
|
-
throw new Error(`the recipe names the rule
|
|
182
|
+
throw new Error(`the recipe names the rule ${quotedValue(recipeDuplicate)} twice`);
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
const changes = noChanges();
|
|
@@ -322,7 +324,7 @@ export function reconcileLine(changes: RecipeChanges): string | null {
|
|
|
322
324
|
parts.push(`the recorded recipe couldn't be read (${changes.recordUnreadable}), so this start removed and redefined nothing`);
|
|
323
325
|
}
|
|
324
326
|
const list = (label: string, names: string[]) => {
|
|
325
|
-
if (names.length > 0) parts.push(`${label} ${names.join(', ')}`);
|
|
327
|
+
if (names.length > 0) parts.push(`${label} ${names.map((name) => quotedValue(name)).join(', ')}`);
|
|
326
328
|
};
|
|
327
329
|
list('added', changes.added);
|
|
328
330
|
list('removed', changes.removed);
|
|
@@ -332,13 +334,13 @@ export function reconcileLine(changes: RecipeChanges): string | null {
|
|
|
332
334
|
list('kept as edited, though the recipe changed or dropped it:', changes.keptEdited);
|
|
333
335
|
if (changes.differsFromRecipe.length > 0) {
|
|
334
336
|
parts.push(
|
|
335
|
-
`kept, though the recipe defines it otherwise: ${changes.differsFromRecipe.join(', ')} `
|
|
337
|
+
`kept, though the recipe defines it otherwise: ${changes.differsFromRecipe.map((name) => quotedValue(name)).join(', ')} `
|
|
336
338
|
+ '(remove such a rule, and the next start restores it from the recipe)',
|
|
337
339
|
);
|
|
338
340
|
}
|
|
339
341
|
if (changes.outsideRecipe.length > 0) {
|
|
340
342
|
parts.push(
|
|
341
|
-
`kept, not in the recipe: ${changes.outsideRecipe.join(', ')} `
|
|
343
|
+
`kept, not in the recipe: ${changes.outsideRecipe.map((name) => quotedValue(name)).join(', ')} `
|
|
342
344
|
+ "(a stale one can be removed, and no start brings it back)",
|
|
343
345
|
);
|
|
344
346
|
}
|
package/src/host-presentation.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
30
30
|
import { dirname } from 'node:path';
|
|
31
31
|
import type { ToolDefinition } from './types/events.js';
|
|
32
|
-
import { checkArgs,
|
|
32
|
+
import { checkArgs, renderToolDefinition, toolLine } from './mcpl/tool-signature.js';
|
|
33
33
|
import { META_TOOL_NAMES } from './mcpl/toolset-host.js';
|
|
34
34
|
|
|
35
35
|
/** The built-in toolset's name, as agents address it (`toolsets--open {toolset: "settings"}`). */
|
|
@@ -274,10 +274,7 @@ export class HostPresentation {
|
|
|
274
274
|
private announce(label: string, tools: ToolDefinition[], closeHint: string): string {
|
|
275
275
|
const lines = [`${label} open: call these with ${META_TOOL_NAMES.act} {tool, args}; they aren't in your tool list.`];
|
|
276
276
|
if (!tools.length) lines.push(' (none right now)');
|
|
277
|
-
for (const t of tools)
|
|
278
|
-
const sentence = firstSentence(t.description);
|
|
279
|
-
lines.push(` ${signature(t)}${sentence ? ` — ${sentence}` : ''}`);
|
|
280
|
-
}
|
|
277
|
+
for (const t of tools) lines.push(toolLine(t));
|
|
281
278
|
lines.push(`${closeHint} closes it.`);
|
|
282
279
|
return lines.join('\n');
|
|
283
280
|
}
|
|
@@ -318,7 +315,7 @@ export class HostPresentation {
|
|
|
318
315
|
const open = this.isOpen(agentName, BUILTIN_TOOLSET);
|
|
319
316
|
return [
|
|
320
317
|
`${this.builtinLabel} ${open ? 'open' : 'closed'}${open ? '' : ` — ${META_TOOL_NAMES.open} {toolset:"${BUILTIN_TOOLSET}"} opens it`}`,
|
|
321
|
-
...tools.map((t) =>
|
|
318
|
+
...tools.map((t) => toolLine(t)),
|
|
322
319
|
].join('\n');
|
|
323
320
|
}
|
|
324
321
|
const { serverId } = which;
|
|
@@ -329,7 +326,7 @@ export class HostPresentation {
|
|
|
329
326
|
const head = mode === 'full'
|
|
330
327
|
? `${this.tabLabel(serverId)} in your tool list${tools.length ? `; its hidden tools (${open ? 'open' : 'closed'}):` : '.'}`
|
|
331
328
|
: `${this.tabLabel(serverId)} a tab, connected, ${open ? 'open' : `hidden — ${META_TOOL_NAMES.open} {server:"${serverId}"} opens it`}:`;
|
|
332
|
-
return [head, ...tools.map((t) =>
|
|
329
|
+
return [head, ...tools.map((t) => toolLine(t))].join('\n');
|
|
333
330
|
}
|
|
334
331
|
|
|
335
332
|
/** `toolsets--look {tool}` for a host toolset's tool, or null when no host toolset has it. */
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './types/index.js';
|
|
|
5
5
|
export { AgentFramework, AgentStoppedError, BudgetPreflightError, DefaultErrorPolicy, ProviderWaitNotSleptError, QuiesceBlockedError, ResumeBlockedError } from './framework.js';
|
|
6
6
|
export type { RuntimeSettingsPreview, HostModeStatus, DeriveAgentOptions } from './framework.js';
|
|
7
7
|
export { Agent } from './agent.js';
|
|
8
|
+
export { FLAT_MINIMUM_MS, type InferenceBoundsOptions } from './inference-bounds.js';
|
|
8
9
|
export { storedInjectionMarkOf, type StoredInjectionMark } from './stored-injections.js';
|
|
9
10
|
export type { StartStreamResult } from './agent.js';
|
|
10
11
|
export { ProcessQueueImpl } from './queue.js';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { MembraneError } from '@animalabs/membrane';
|
|
2
|
+
import { PROGRESS_BOUNDS, resolveProgressBounds, type ProgressBounds } from '@animalabs/membrane/progress-bounds';
|
|
3
|
+
|
|
4
|
+
/** FrameworkConfig.inferenceBounds: how long the framework lets a model call
|
|
5
|
+
* go without progress. */
|
|
6
|
+
export interface InferenceBoundsOptions {
|
|
7
|
+
/** Membrane's phase bounds for a stream whose adapter reports content
|
|
8
|
+
* (Anthropic, Bedrock), and the values the flat and whole-call bounds
|
|
9
|
+
* derive from: the larger of `beforeContentMs` and `betweenEventsMs`,
|
|
10
|
+
* plus `max_tokens` at `thinkTokensPerSecond`. Each one given replaces
|
|
11
|
+
* membrane's default (PROGRESS_BOUNDS). */
|
|
12
|
+
progressBounds?: Partial<ProgressBounds>;
|
|
13
|
+
/** The least the flat bound on a stream can be, from its last provider
|
|
14
|
+
* event. Default FLAT_MINIMUM_MS. */
|
|
15
|
+
flatMinimumMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FLAT_MINIMUM_MS = 1_862_000;
|
|
19
|
+
|
|
20
|
+
export interface InferenceBounds {
|
|
21
|
+
readonly progressBounds: Readonly<ProgressBounds>;
|
|
22
|
+
readonly flatMinimumMs: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveInferenceBounds(option: false | InferenceBoundsOptions | undefined): InferenceBounds | null {
|
|
26
|
+
if (option === false) return null;
|
|
27
|
+
if (option !== undefined && (typeof option !== 'object' || option === null)) {
|
|
28
|
+
throw new TypeError(`[agent-framework] inferenceBounds must be false or an object, got ${option === null ? 'null' : typeof option}`);
|
|
29
|
+
}
|
|
30
|
+
const given = option?.progressBounds;
|
|
31
|
+
if (given !== undefined && (typeof given !== 'object' || given === null)) {
|
|
32
|
+
throw new TypeError(`[agent-framework] inferenceBounds.progressBounds must be an object of bounds, got ${given === null ? 'null' : typeof given}`);
|
|
33
|
+
}
|
|
34
|
+
let progressBounds: ProgressBounds;
|
|
35
|
+
try {
|
|
36
|
+
progressBounds = resolveProgressBounds(given ?? true) ?? { ...PROGRESS_BOUNDS };
|
|
37
|
+
} catch (refused) {
|
|
38
|
+
const why = refused instanceof Error ? refused.message.replace(/^\[membrane\] /, '') : String(refused);
|
|
39
|
+
throw new (refused instanceof TypeError ? TypeError : RangeError)(`[agent-framework] inferenceBounds.${why}`);
|
|
40
|
+
}
|
|
41
|
+
const flatMinimumMs: unknown = option?.flatMinimumMs === undefined ? FLAT_MINIMUM_MS : option.flatMinimumMs;
|
|
42
|
+
if (typeof flatMinimumMs !== 'number' || !Number.isFinite(flatMinimumMs) || flatMinimumMs <= 0) {
|
|
43
|
+
throw new RangeError(`[agent-framework] inferenceBounds.flatMinimumMs must be a finite number above 0, got ${String(flatMinimumMs)}`);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze({ progressBounds: Object.freeze(progressBounds), flatMinimumMs });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function allowanceMs(bounds: Readonly<ProgressBounds>, maxTokens: number | undefined): number {
|
|
49
|
+
return typeof maxTokens === 'number' && Number.isFinite(maxTokens) && maxTokens > 0
|
|
50
|
+
? Math.ceil(maxTokens / bounds.thinkTokensPerSecond) * 1000
|
|
51
|
+
: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setupMs(bounds: Readonly<ProgressBounds>): number {
|
|
55
|
+
return Math.max(bounds.beforeContentMs, bounds.betweenEventsMs);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function streamBounds(
|
|
59
|
+
bounds: InferenceBounds | null,
|
|
60
|
+
maxTokens: number | undefined,
|
|
61
|
+
): { progressBounds?: Readonly<ProgressBounds>; progressTimeoutMs?: number } {
|
|
62
|
+
if (bounds === null) return {};
|
|
63
|
+
const phases = bounds.progressBounds;
|
|
64
|
+
return {
|
|
65
|
+
progressBounds: phases,
|
|
66
|
+
progressTimeoutMs: Math.max(setupMs(phases) + allowanceMs(phases, maxTokens), bounds.flatMinimumMs),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function wholeCallMs(bounds: InferenceBounds, maxTokens: number | undefined): number {
|
|
71
|
+
if (!(typeof maxTokens === 'number' && Number.isFinite(maxTokens) && maxTokens > 0)) return Math.max(setupMs(bounds.progressBounds), bounds.flatMinimumMs);
|
|
72
|
+
return setupMs(bounds.progressBounds) + allowanceMs(bounds.progressBounds, maxTokens);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// membrane 0.8.0 marks a deadline it reports as run out with this registered
|
|
76
|
+
// symbol (on a sent request, or the Anthropic SDK's timeout, which covers its
|
|
77
|
+
// connect timeout too) and exports no reader or marker for it. The one reader:
|
|
78
|
+
// the deadline class here and the transient-failure check of an exhausted
|
|
79
|
+
// turn's re-arm both use it.
|
|
80
|
+
const DEADLINE_EXPIRY = Symbol.for('membrane.deadlineExpiry');
|
|
81
|
+
|
|
82
|
+
export function isDeadlineExpiry(error: unknown): boolean {
|
|
83
|
+
return typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[DEADLINE_EXPIRY] === true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Names the host setting for the bound a stream cut hit, read from the opening
|
|
88
|
+
* of membrane 0.8.0's message for it (its progress-bounds.ts and
|
|
89
|
+
* providers/utils.ts), so the reason an operator and the agent see names the
|
|
90
|
+
* host's setting for that bound, as the README's table maps them. A message it
|
|
91
|
+
* doesn't know is left as it is.
|
|
92
|
+
*/
|
|
93
|
+
export function withHostSetting(message: string): string {
|
|
94
|
+
const setting = message.includes('of a thinking block\'s start') ? 'progressBounds.thinkTokensPerSecond'
|
|
95
|
+
: message.startsWith('No content within ') ? 'progressBounds.beforeContentMs'
|
|
96
|
+
: message.startsWith('No provider event within ') ? 'progressBounds.betweenEventsMs'
|
|
97
|
+
: message.startsWith('No provider progress within ') ? 'flatMinimumMs'
|
|
98
|
+
: undefined;
|
|
99
|
+
return setting === undefined ? message : `${message} (the host's setting for this bound: \`inferenceBounds.${setting}\`)`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function deadlineCut(message: string | undefined): MembraneError {
|
|
103
|
+
const error = new MembraneError({
|
|
104
|
+
type: 'timeout',
|
|
105
|
+
retryable: false,
|
|
106
|
+
message: message === undefined ? 'The provider made no progress within the call\'s bound' : withHostSetting(message),
|
|
107
|
+
rawError: undefined,
|
|
108
|
+
});
|
|
109
|
+
Object.defineProperty(error, DEADLINE_EXPIRY, { value: true });
|
|
110
|
+
return error;
|
|
111
|
+
}
|
package/src/line-reader.ts
CHANGED
|
@@ -4,12 +4,26 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
4
4
|
const LF = 0x0a;
|
|
5
5
|
const CR = 0x0d;
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export const MAX_LINE_CHARS = 100 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export interface LineHandlers {
|
|
10
|
+
line: (line: string) => void;
|
|
11
|
+
end?: () => void;
|
|
12
|
+
tooLong: (maxChars: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readLines(input: Readable, on: LineHandlers, maxChars = MAX_LINE_CHARS): () => void {
|
|
8
16
|
const decoder = new StringDecoder('utf8');
|
|
9
17
|
let partial = '';
|
|
10
18
|
let lineEndedAtCr = false;
|
|
11
19
|
let stopped = false;
|
|
12
20
|
|
|
21
|
+
const refuse = () => {
|
|
22
|
+
stop();
|
|
23
|
+
partial = '';
|
|
24
|
+
on.tooLong(maxChars);
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
const take = (text: string) => {
|
|
14
28
|
let from = 0;
|
|
15
29
|
if (lineEndedAtCr && text.length > 0) {
|
|
@@ -19,6 +33,7 @@ export function readLines(input: Readable, onLine: (line: string) => void, onEnd
|
|
|
19
33
|
for (let i = from; i < text.length && !stopped; i++) {
|
|
20
34
|
const code = text.charCodeAt(i);
|
|
21
35
|
if (code !== LF && code !== CR) continue;
|
|
36
|
+
if (partial.length + (i - from) > maxChars) return refuse();
|
|
22
37
|
const line = partial + text.slice(from, i);
|
|
23
38
|
partial = '';
|
|
24
39
|
if (code === CR) {
|
|
@@ -26,9 +41,11 @@ export function readLines(input: Readable, onLine: (line: string) => void, onEnd
|
|
|
26
41
|
else if (text.charCodeAt(i + 1) === LF) i++;
|
|
27
42
|
}
|
|
28
43
|
from = i + 1;
|
|
29
|
-
|
|
44
|
+
on.line(line);
|
|
30
45
|
}
|
|
31
|
-
if (
|
|
46
|
+
if (stopped) return;
|
|
47
|
+
if (partial.length + (text.length - from) > maxChars) return refuse();
|
|
48
|
+
partial += text.slice(from);
|
|
32
49
|
};
|
|
33
50
|
|
|
34
51
|
const onData = (chunk: Buffer | string) => take(decoder.write(chunk));
|
|
@@ -37,11 +54,11 @@ export function readLines(input: Readable, onLine: (line: string) => void, onEnd
|
|
|
37
54
|
if (!stopped && partial.length > 0) {
|
|
38
55
|
const last = partial;
|
|
39
56
|
partial = '';
|
|
40
|
-
|
|
57
|
+
on.line(last);
|
|
41
58
|
}
|
|
42
59
|
if (stopped) return;
|
|
43
60
|
stop();
|
|
44
|
-
|
|
61
|
+
on.end?.();
|
|
45
62
|
};
|
|
46
63
|
const stop = () => {
|
|
47
64
|
stopped = true;
|
package/src/mcpl/errors.ts
CHANGED
|
@@ -68,6 +68,10 @@ export function boundedForLog(text: string, max = 200): string {
|
|
|
68
68
|
return points.length > max ? `${points.slice(0, max).join('')}…` : points.join('');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export function hostStoppedWhileHandling(): JsonRpcError {
|
|
72
|
+
return { code: -32000, message: 'the host stopped while handling this request' };
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
/**
|
|
72
76
|
* Create a "feature set not enabled" error.
|
|
73
77
|
* Returned when a server sends a message tagged with a feature set
|
|
@@ -167,8 +167,8 @@ export function conversationKey(source: InboundSource): string | undefined {
|
|
|
167
167
|
* well-formed is dropped (the store doesn't keep it). An `undefined` array
|
|
168
168
|
* element is written as `null`, as JSON writes it: the store's append
|
|
169
169
|
* refuses a body holding one, so no stored copy does. So a value
|
|
170
|
-
* hashes alike however its keys were ordered (
|
|
171
|
-
*
|
|
170
|
+
* hashes alike however its keys were ordered (a store on chronicle before
|
|
171
|
+
* 0.5.1 hands content back with its keys sorted), and alike
|
|
172
172
|
* before and after the store. editMessage writes JSON instead, so an edited
|
|
173
173
|
* copy can keep what an append wouldn't; it has changed anyway.
|
|
174
174
|
*
|
|
@@ -25,8 +25,10 @@ import type {
|
|
|
25
25
|
} from './types.js';
|
|
26
26
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
27
27
|
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
28
|
+
import { hostStoppedWhileHandling } from './errors.js';
|
|
28
29
|
import type { HookOrchestrator } from './hook-orchestrator.js';
|
|
29
30
|
import { logValue } from './log-value.js';
|
|
31
|
+
import { streamBounds, wholeCallMs, withHostSetting, type InferenceBounds } from '../inference-bounds.js';
|
|
30
32
|
|
|
31
33
|
// ============================================================================
|
|
32
34
|
// Responder interface
|
|
@@ -41,6 +43,7 @@ interface Responder {
|
|
|
41
43
|
/** Sends a streamed chunk on the transport the request came on, as its
|
|
42
44
|
* answer is sent. */
|
|
43
45
|
sendChunk?(params: InferenceChunkParams): void;
|
|
46
|
+
signal?: AbortSignal;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// ============================================================================
|
|
@@ -186,6 +189,7 @@ export class InferenceRouter {
|
|
|
186
189
|
* server with no policy, and per server and feature set with one, so a
|
|
187
190
|
* server that keeps asking doesn't flood the operator's log. */
|
|
188
191
|
private loggedRefusals = new Set<string>();
|
|
192
|
+
private running = new Set<{ serverId: string; stop: AbortController; answered: Promise<void> }>();
|
|
189
193
|
|
|
190
194
|
constructor(
|
|
191
195
|
membrane: Membrane,
|
|
@@ -193,6 +197,7 @@ export class InferenceRouter {
|
|
|
193
197
|
featureSetManager: FeatureSetManager,
|
|
194
198
|
routingPolicy: InferenceRoutingPolicy | null,
|
|
195
199
|
emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
|
|
200
|
+
private readonly inferenceBounds: () => InferenceBounds | null = () => null,
|
|
196
201
|
) {
|
|
197
202
|
this.membrane = membrane;
|
|
198
203
|
this.hookOrchestrator = hookOrchestrator;
|
|
@@ -216,6 +221,31 @@ export class InferenceRouter {
|
|
|
216
221
|
serverId: string,
|
|
217
222
|
params: McplInferenceRequestParams,
|
|
218
223
|
responder: Responder,
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
const stop = new AbortController();
|
|
226
|
+
let ended!: () => void;
|
|
227
|
+
const entry = { serverId, stop, answered: new Promise<void>((resolve) => { ended = resolve; }) };
|
|
228
|
+
this.running.add(entry);
|
|
229
|
+
try {
|
|
230
|
+
await this.answer(serverId, params, responder, stop.signal);
|
|
231
|
+
} finally {
|
|
232
|
+
this.running.delete(entry);
|
|
233
|
+
ended();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
stopAll(): Array<{ label: string; promise: Promise<void> }> {
|
|
238
|
+
return [...this.running].map(({ serverId, stop, answered }) => {
|
|
239
|
+
stop.abort();
|
|
240
|
+
return { label: `inference/request:${serverId}`, promise: answered };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async answer(
|
|
245
|
+
serverId: string,
|
|
246
|
+
params: McplInferenceRequestParams,
|
|
247
|
+
responder: Responder,
|
|
248
|
+
stopped: AbortSignal,
|
|
219
249
|
): Promise<void> {
|
|
220
250
|
// 1. Validate feature set
|
|
221
251
|
try {
|
|
@@ -295,6 +325,24 @@ export class InferenceRouter {
|
|
|
295
325
|
return;
|
|
296
326
|
}
|
|
297
327
|
|
|
328
|
+
const signal = responder.signal ? AbortSignal.any([stopped, responder.signal]) : stopped;
|
|
329
|
+
let stopAnswered = false;
|
|
330
|
+
const answerStop = () => {
|
|
331
|
+
if (responder.signal?.aborted) return;
|
|
332
|
+
stopAnswered = true;
|
|
333
|
+
const answer = hostStoppedWhileHandling();
|
|
334
|
+
this.emitTraceFn({ type: 'mcpl:inference_fail', serverId, model, error: answer.message, streamed });
|
|
335
|
+
// A throw from an abort listener would be uncaught.
|
|
336
|
+
try {
|
|
337
|
+
responder.respondError(answer.code, answer.message);
|
|
338
|
+
} catch {}
|
|
339
|
+
};
|
|
340
|
+
stopped.addEventListener('abort', answerStop, { once: true });
|
|
341
|
+
const settled = (): boolean => {
|
|
342
|
+
stopped.removeEventListener('abort', answerStop);
|
|
343
|
+
return stopAnswered;
|
|
344
|
+
};
|
|
345
|
+
|
|
298
346
|
// Emit trace: inference start
|
|
299
347
|
this.emitTraceFn({
|
|
300
348
|
type: 'mcpl:inference_start',
|
|
@@ -319,12 +367,14 @@ export class InferenceRouter {
|
|
|
319
367
|
},
|
|
320
368
|
};
|
|
321
369
|
|
|
370
|
+
if (stopAnswered) return;
|
|
322
371
|
try {
|
|
323
372
|
// 5. Run inference. Both modes end in membrane's response, and one
|
|
324
373
|
// builder makes the result from it.
|
|
325
374
|
const response = chunkRequestId != null
|
|
326
|
-
? await this.streamResponse(request, chunkRequestId, sendChunk
|
|
327
|
-
: await this.membrane.complete(request);
|
|
375
|
+
? await this.streamResponse(request, chunkRequestId, sendChunk!, signal)
|
|
376
|
+
: await this.membrane.complete(request, { ...this.wholeCallBound(request), signal });
|
|
377
|
+
if (settled()) return;
|
|
328
378
|
|
|
329
379
|
// 6. Answer
|
|
330
380
|
const result = resultFrom(response, model);
|
|
@@ -344,22 +394,23 @@ export class InferenceRouter {
|
|
|
344
394
|
|
|
345
395
|
responder.respond(result);
|
|
346
396
|
} catch (err) {
|
|
397
|
+
if (settled()) return;
|
|
347
398
|
// Every ending that isn't a completion lands here, in either mode:
|
|
348
399
|
// membrane's error, a stream that stopped early, a tool call, or a
|
|
349
400
|
// stop FinishReason can't name. The answer is the JSON-RPC error, never
|
|
350
401
|
// a result carrying what arrived.
|
|
402
|
+
const transportClosed = responder.signal?.aborted === true;
|
|
351
403
|
this.emitTraceFn({
|
|
352
404
|
type: 'mcpl:inference_fail',
|
|
353
405
|
serverId,
|
|
354
406
|
model,
|
|
355
|
-
error: err instanceof Error ? err.message : String(err),
|
|
407
|
+
error: transportClosed ? 'the transport the request came on closed' : err instanceof Error ? err.message : String(err),
|
|
356
408
|
streamed,
|
|
357
409
|
});
|
|
358
410
|
|
|
359
|
-
|
|
360
|
-
-32603,
|
|
361
|
-
|
|
362
|
-
);
|
|
411
|
+
if (!transportClosed) {
|
|
412
|
+
responder.respondError(-32603, err instanceof Error ? err.message : 'Inference failed');
|
|
413
|
+
}
|
|
363
414
|
}
|
|
364
415
|
}
|
|
365
416
|
|
|
@@ -407,6 +458,11 @@ export class InferenceRouter {
|
|
|
407
458
|
// Private: Streaming
|
|
408
459
|
// ==========================================================================
|
|
409
460
|
|
|
461
|
+
private wholeCallBound(request: NormalizedRequest): { progressTimeoutMs?: number } {
|
|
462
|
+
const bounds = this.inferenceBounds();
|
|
463
|
+
return bounds === null ? {} : { progressTimeoutMs: wholeCallMs(bounds, request.config.maxTokens) };
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
/**
|
|
411
467
|
* Run the inference as a stream, sending each visible text delta to the
|
|
412
468
|
* server as an inference/chunk, and return membrane's response once the
|
|
@@ -424,8 +480,9 @@ export class InferenceRouter {
|
|
|
424
480
|
request: NormalizedRequest,
|
|
425
481
|
requestId: string | number,
|
|
426
482
|
sendChunk: (params: InferenceChunkParams) => void,
|
|
483
|
+
signal: AbortSignal,
|
|
427
484
|
): Promise<NormalizedResponse> {
|
|
428
|
-
const stream = this.membrane.streamYielding(request);
|
|
485
|
+
const stream = this.membrane.streamYielding(request, { ...streamBounds(this.inferenceBounds(), request.config.maxTokens), signal });
|
|
429
486
|
let index = 0;
|
|
430
487
|
|
|
431
488
|
for await (const event of stream) {
|
|
@@ -437,7 +494,7 @@ export class InferenceRouter {
|
|
|
437
494
|
// XML tool mode the chunks can miss text the content holds: prose
|
|
438
495
|
// naming a tag the stream parser opens a block for, or a thinking
|
|
439
496
|
// block cut off before its close tag.
|
|
440
|
-
if (event.meta.visible) {
|
|
497
|
+
if (event.meta.visible && !signal.aborted) {
|
|
441
498
|
sendChunk({ requestId, index: index++, delta: event.content });
|
|
442
499
|
}
|
|
443
500
|
break;
|
|
@@ -448,7 +505,7 @@ export class InferenceRouter {
|
|
|
448
505
|
case 'error':
|
|
449
506
|
throw event.error;
|
|
450
507
|
case 'aborted':
|
|
451
|
-
throw new Error(`The inference stopped before it finished (${event.reason})`);
|
|
508
|
+
throw new Error(`The inference stopped before it finished (${event.reason}${event.message ? `: ${event.reason === 'timeout' ? withHostSetting(event.message) : event.message}` : ''})`);
|
|
452
509
|
}
|
|
453
510
|
}
|
|
454
511
|
throw new Error('The inference stream ended without a result');
|