@animalabs/agent-framework 0.26.2 → 0.27.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 +56 -6
- package/dist/src/agent.d.ts +263 -22
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +531 -54
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +1 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +33 -0
- package/dist/src/auto-continue.d.ts.map +1 -0
- package/dist/src/auto-continue.js +35 -0
- package/dist/src/auto-continue.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -1
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +759 -129
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3482 -928
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +309 -46
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +2117 -395
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts +103 -0
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
- package/dist/src/gate/recipe-reconcile.js +325 -0
- package/dist/src/gate/recipe-reconcile.js.map +1 -0
- package/dist/src/gate/types.d.ts +68 -8
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +433 -113
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1152 -434
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +16 -8
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +29 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +14 -2
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +32 -7
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +4 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +14 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +73 -28
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +6 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +3 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +20 -2
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +33 -11
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +177 -145
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +380 -46
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +736 -77
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +82 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +132 -7
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +21 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +259 -0
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
- package/dist/src/mcpl/tool-presence.js +628 -0
- package/dist/src/mcpl/tool-presence.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js +14 -6
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +39 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
- package/dist/src/mcpl/tool-signature.js +292 -0
- package/dist/src/mcpl/tool-signature.js.map +1 -0
- package/dist/src/mcpl/toolset-host.d.ts +200 -0
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
- package/dist/src/mcpl/toolset-host.js +621 -0
- package/dist/src/mcpl/toolset-host.js.map +1 -0
- package/dist/src/mcpl/toolsets-module.d.ts +80 -0
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
- package/dist/src/mcpl/toolsets-module.js +431 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +2 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +3 -2
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +21 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +7 -9
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +21 -9
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +23 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -15
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +2 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +51 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +108 -6
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +273 -57
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +181 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
- package/dist/src/modules/workspace/direct-write.js +384 -0
- package/dist/src/modules/workspace/direct-write.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +26 -12
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +58 -28
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +10 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +1 -1
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +246 -27
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1216 -236
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts +41 -0
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
- package/dist/src/modules/workspace/read-page.js +213 -0
- package/dist/src/modules/workspace/read-page.js.map +1 -0
- package/dist/src/modules/workspace/result-text.d.ts +67 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/result-text.js +94 -0
- package/dist/src/modules/workspace/result-text.js.map +1 -0
- package/dist/src/modules/workspace/text.d.ts +29 -12
- package/dist/src/modules/workspace/text.d.ts.map +1 -1
- package/dist/src/modules/workspace/text.js +35 -33
- package/dist/src/modules/workspace/text.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +41 -17
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/sent-rows.d.ts +59 -0
- package/dist/src/sent-rows.d.ts.map +1 -0
- package/dist/src/sent-rows.js +145 -0
- package/dist/src/sent-rows.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +17 -10
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stable-hash.d.ts +3 -0
- package/dist/src/stable-hash.d.ts.map +1 -0
- package/dist/src/stable-hash.js +15 -0
- package/dist/src/stable-hash.js.map +1 -0
- package/dist/src/stored-injections.d.ts +99 -0
- package/dist/src/stored-injections.d.ts.map +1 -0
- package/dist/src/stored-injections.js +106 -0
- package/dist/src/stored-injections.js.map +1 -0
- package/dist/src/text-as-stored.d.ts +4 -3
- package/dist/src/text-as-stored.d.ts.map +1 -1
- package/dist/src/text-as-stored.js +4 -3
- package/dist/src/text-as-stored.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +81 -0
- package/dist/src/thinking-drops.d.ts.map +1 -0
- package/dist/src/thinking-drops.js +103 -0
- package/dist/src/thinking-drops.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +9 -8
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +9 -8
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +3 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +4 -2
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +21 -6
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +176 -24
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +411 -91
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +17 -9
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +84 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +186 -7
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +15 -1
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +24 -3
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +90 -7
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +57 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/agent.ts +593 -60
- package/src/api/server.ts +1 -2
- package/src/auto-continue.ts +42 -0
- package/src/code-execution/isolate/scripts.ts +11 -3
- package/src/framework.ts +3601 -967
- package/src/gate/event-gate.ts +2315 -389
- package/src/gate/index.ts +2 -1
- package/src/gate/recipe-reconcile.ts +355 -0
- package/src/gate/types.ts +70 -8
- package/src/index.ts +7 -4
- package/src/mcpl/channel-registry.ts +1220 -467
- package/src/mcpl/conversation-router.ts +34 -16
- package/src/mcpl/convert-block.ts +14 -1
- package/src/mcpl/hook-orchestrator.ts +33 -6
- package/src/mcpl/index.ts +6 -1
- package/src/mcpl/inference-router.ts +79 -34
- package/src/mcpl/prose-grammar.ts +7 -0
- package/src/mcpl/protocol-family.ts +2 -0
- package/src/mcpl/push-coalescer.ts +51 -13
- package/src/mcpl/reference-fetcher.ts +201 -162
- package/src/mcpl/references.ts +851 -77
- package/src/mcpl/server-connection.ts +145 -7
- package/src/mcpl/tool-lifecycle.ts +21 -0
- package/src/mcpl/tool-presence.ts +650 -0
- package/src/mcpl/tool-result-normalize.ts +18 -6
- package/src/mcpl/tool-signature.ts +285 -0
- package/src/mcpl/toolset-host.ts +637 -0
- package/src/mcpl/toolsets-module.ts +451 -0
- package/src/mcpl/transport.ts +3 -2
- package/src/mcpl/types.ts +22 -1
- package/src/mcpl/visible-content.ts +22 -9
- package/src/module-registry.ts +77 -24
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +48 -18
- package/src/modules/history/semantic.ts +308 -54
- package/src/modules/workspace/direct-write.ts +425 -0
- package/src/modules/workspace/edit-text.ts +82 -35
- package/src/modules/workspace/effects.ts +1 -1
- package/src/modules/workspace/index.ts +1274 -241
- package/src/modules/workspace/read-page.ts +222 -0
- package/src/modules/workspace/result-text.ts +119 -0
- package/src/modules/workspace/text.ts +58 -32
- package/src/modules/workspace/types.ts +41 -17
- package/src/sent-rows.ts +162 -0
- package/src/silent-heartbeat.ts +17 -10
- package/src/stable-hash.ts +14 -0
- package/src/stored-injections.ts +131 -0
- package/src/text-as-stored.ts +4 -3
- package/src/thinking-drops.ts +141 -0
- package/src/tool-image-ledger.ts +9 -8
- package/src/tool-presentation.ts +3 -1
- package/src/tool-result-guard.ts +19 -6
- package/src/tool-result-history.ts +471 -90
- package/src/tune-out/coordinator.ts +80 -37
- package/src/types/agent.ts +189 -7
- package/src/types/events.ts +15 -1
- package/src/types/framework.ts +21 -3
- package/src/types/module.ts +86 -7
- package/src/types/trace.ts +58 -2
|
@@ -25,15 +25,23 @@
|
|
|
25
25
|
* Autofetch policy (host default, not protocol): eager only below
|
|
26
26
|
* DEFAULT_EAGER_FETCH_MAX_BYTES from the connection origin — anything under
|
|
27
27
|
* that threshold the server could legitimately have inlined, so eager
|
|
28
|
-
* materialization is never worse than the status quo
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* that
|
|
28
|
+
* materialization is never worse than the status quo for a reference whose
|
|
29
|
+
* claim is true. A fetch the eager default starts reads no further than the
|
|
30
|
+
* first chunk past its server's eager limit (autofetch.maxBytes, this
|
|
31
|
+
* default when that's unset), whatever size the server claims; one it joins
|
|
32
|
+
* is its starter's, whose success answers it too. The eager default doesn't
|
|
33
|
+
* choose a reference again once its fetch of it failed. Everything larger
|
|
34
|
+
* is fetched on demand (the fetch_reference tool, or the code-execution
|
|
35
|
+
* result hook, where a script receiving a result is the strongest available
|
|
36
|
+
* signal that the bytes are wanted). Per-server budgets cap cumulative
|
|
37
|
+
* appetite.
|
|
32
38
|
*/
|
|
33
39
|
import { createHash } from 'node:crypto';
|
|
34
|
-
import type { ReferenceRecord } from './references.js';
|
|
40
|
+
import type { ReferenceRecord, Refusal, ServerAddress } from './references.js';
|
|
35
41
|
import { boundedForLog, describeCause } from './errors.js';
|
|
36
|
-
import {
|
|
42
|
+
import { dialCheck, dialServer, FRESH_REFERENCE, NOTHING_TO_DO, REFERENCE_LIMITS, sanitizeLabel, shownServerId } from './references.js';
|
|
43
|
+
|
|
44
|
+
export { normalizedOrigin, type Refusal } from './references.js';
|
|
37
45
|
|
|
38
46
|
/** Per-fetch hard ceiling on actual bytes. A call can lower it for itself
|
|
39
47
|
* (fetch_reference's max_bytes, which it clamps to this); nothing raises it. */
|
|
@@ -44,17 +52,18 @@ export const DEFAULT_EAGER_FETCH_MAX_BYTES = 256 * 1024;
|
|
|
44
52
|
* through a server reads counts against that server's, whether or not it's
|
|
45
53
|
* kept. */
|
|
46
54
|
export const DEFAULT_MAX_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
55
|
+
/** How long a caller waits for a fetch, its save included, unless it asks
|
|
56
|
+
* for another time. */
|
|
47
57
|
const FETCH_TIMEOUT_MS = 60_000;
|
|
58
|
+
/** How long the eager default waits for one reference. */
|
|
48
59
|
const EAGER_FETCH_TIMEOUT_MS = 10_000;
|
|
49
60
|
|
|
50
61
|
// What calling again can do, as each failure's remedy says it. The fetcher
|
|
51
62
|
// doesn't know where a reference came from (a tool result, a push, a channel
|
|
52
63
|
// message), so none names a tool, and fetch_reference's max_bytes is named
|
|
53
|
-
// only where a larger one can fetch it.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* uri arrives as this very id and refreshes its record. */
|
|
57
|
-
const FRESH_REFERENCE = "Once a fresh reference to it arrives, fetching may work; until then, fetching it again won't.";
|
|
64
|
+
// only where a larger one can fetch it. The refusals before dialing that a
|
|
65
|
+
// stub shares come from references.ts (dialServer, dialCheck) with their
|
|
66
|
+
// remedies (NOTHING_TO_DO, FRESH_REFERENCE, SERVER_BACK).
|
|
58
67
|
const TRY_LATER = 'Trying again later may work.';
|
|
59
68
|
/** A request that failed before an answer: a refused or reset connection
|
|
60
69
|
* can clear, and a cause that repeats on every try (a port fetch refuses, a
|
|
@@ -71,8 +80,6 @@ const BUDGET = "The budget doesn't renew on its own, so calling again won't chan
|
|
|
71
80
|
/** The host's own ceiling: fetch_reference clamps max_bytes to it, and no
|
|
72
81
|
* config sets another. */
|
|
73
82
|
const HOST_CEILING = "No max_bytes or setting lifts that limit, so calling again won't change it.";
|
|
74
|
-
/** The resolver reads the host's server configs as they are at each fetch. */
|
|
75
|
-
const SERVER_BACK = "Calling again won't change that unless whoever runs this host adds that server back.";
|
|
76
83
|
|
|
77
84
|
export interface ResolvedServerBinding {
|
|
78
85
|
/** The url the host dials the server at. The framework's resolver gives only
|
|
@@ -90,18 +97,6 @@ export interface ResolvedServerBinding {
|
|
|
90
97
|
* know resolves to undefined. */
|
|
91
98
|
export type ServerResolution = ResolvedServerBinding | 'no-address';
|
|
92
99
|
|
|
93
|
-
/** Why a fetch failed, or would be refused before dialing. */
|
|
94
|
-
export interface Refusal {
|
|
95
|
-
/** What happened, as a clause that completes "couldn't fetch <ref>: ",
|
|
96
|
-
* whatever it's about: fetch_reference's sentence, save_recent_image's,
|
|
97
|
-
* and the eager and script-demand fetches' log lines read it. */
|
|
98
|
-
error: string;
|
|
99
|
-
/** What calling again can do, as a sentence. fetch_reference and
|
|
100
|
-
* save_recent_image show it; the eager and script-demand fetches log the
|
|
101
|
-
* error alone. */
|
|
102
|
-
remedy: string;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
100
|
/** A fetch's result: the saved copy, or why it failed, which every failure
|
|
106
101
|
* says with both a clause and a remedy. The fields the other side lacks are
|
|
107
102
|
* declared undefined, so a caller can read them before checking ok. */
|
|
@@ -116,10 +111,49 @@ export type FetchOutcome =
|
|
|
116
111
|
error?: undefined;
|
|
117
112
|
remedy?: undefined;
|
|
118
113
|
}
|
|
119
|
-
| ({ ok: false; path?: undefined; bytes?: undefined; mimeType?: undefined; digestVerified?: undefined } & Refusal);
|
|
114
|
+
| ({ ok: false; path?: undefined; bytes?: undefined; mimeType?: undefined; digestVerified?: undefined } & Pick<Refusal, 'error' | 'remedy'>);
|
|
120
115
|
|
|
121
116
|
const refused = (error: string, remedy: string): { refusal: Refusal } => ({ refusal: { error, remedy } });
|
|
122
117
|
const fail = (error: string, remedy: string): FetchOutcome => ({ ok: false, error, remedy });
|
|
118
|
+
/** A timeout, naming the timeout that ran out: its starter's, when the
|
|
119
|
+
* fetch is aborted at its deadline, or a caller's own, when its wait ends
|
|
120
|
+
* while the fetch or its save goes on. */
|
|
121
|
+
function tookLonger(timeoutMs: number): FetchOutcome {
|
|
122
|
+
const seconds = timeoutMs / 1000;
|
|
123
|
+
return fail(`it took longer than ${seconds} second${seconds === 1 ? '' : 's'}`, TRY_LATER);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The two endings that name a call's own byte ceiling, as a function of it,
|
|
127
|
+
* so a caller that joined a fetch is told its own ceiling, not its
|
|
128
|
+
* starter's. */
|
|
129
|
+
const claimOver = (claimed: number) => (ceiling: number): string =>
|
|
130
|
+
`it claims ${claimed} bytes, over the ${ceiling} bytes this call allows`;
|
|
131
|
+
const readPast = (received: number) => (ceiling: number): string =>
|
|
132
|
+
`the download had reached ${received} bytes, past the ${ceiling} bytes this call allows, so it was stopped and nothing was kept`;
|
|
133
|
+
|
|
134
|
+
/** How a fetch ended, with the bound of its starter's it stopped on, if any:
|
|
135
|
+
* its byte ceiling, which a claim over it reaches before dialing too, or its
|
|
136
|
+
* deadline. A caller that takes a ceiling ending is told its own ceiling
|
|
137
|
+
* (atCeiling). A deadline ending names its starter's timeout, which ran out
|
|
138
|
+
* unanswered, so it holds for every caller that takes it. */
|
|
139
|
+
type Ended = FetchOutcome & { stoppedBy?: 'ceiling' | 'deadline'; atCeiling?: (ceiling: number) => string };
|
|
140
|
+
|
|
141
|
+
/** A fetch in flight, under the bounds of the caller that started it: its
|
|
142
|
+
* byte ceiling and its timeout. */
|
|
143
|
+
interface Flight {
|
|
144
|
+
ended: Promise<Ended>;
|
|
145
|
+
ceiling: number;
|
|
146
|
+
timeoutMs: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What the promise settles to, or undefined if the deadline, on the
|
|
150
|
+
* monotonic clock, comes first. */
|
|
151
|
+
function settledBy<T>(promise: Promise<T>, deadline: number): Promise<T | undefined> {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const timer = setTimeout(() => resolve(undefined), Math.max(0, deadline - performance.now()));
|
|
154
|
+
promise.finally(() => clearTimeout(timer)).then(resolve, reject);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
123
157
|
|
|
124
158
|
/** What calling again can do about a server's HTTP status. A later try can
|
|
125
159
|
* work after a timeout, a conflict or lock the server's state can clear, a
|
|
@@ -191,101 +225,107 @@ const EXT_BY_MIME = new Map<string, string>(Object.entries({
|
|
|
191
225
|
'application/pdf': 'pdf', 'application/zip': 'zip',
|
|
192
226
|
}));
|
|
193
227
|
|
|
194
|
-
/** A server's address as it's configured, its scheme and host, for a
|
|
195
|
-
* clause: never its userinfo, path or query. URL's own origin is 'null' for
|
|
196
|
-
* a scheme that isn't special, which normalizedOrigin accepts. The caller
|
|
197
|
-
* has already parsed it. */
|
|
198
|
-
function configuredOrigin(url: string): string {
|
|
199
|
-
const u = new URL(url);
|
|
200
|
-
return `${u.protocol}//${u.host}`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** ws→http / wss→https with default ports elided, for origin comparison. */
|
|
204
|
-
export function normalizedOrigin(url: string): string | null {
|
|
205
|
-
try {
|
|
206
|
-
const u = new URL(url);
|
|
207
|
-
const scheme = u.protocol === 'ws:' ? 'http:' : u.protocol === 'wss:' ? 'https:' : u.protocol;
|
|
208
|
-
const defaultPort = scheme === 'https:' ? '443' : '80';
|
|
209
|
-
const port = u.port === '' || u.port === defaultPort ? '' : `:${u.port}`;
|
|
210
|
-
return `${scheme}//${u.hostname.toLowerCase()}${port}`;
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
228
|
export class ReferenceFetcher {
|
|
217
229
|
/** Bytes read through each server in this process, charged to the server a
|
|
218
230
|
* fetch dials through as each chunk is read, kept or not. Concurrent
|
|
219
231
|
* fetches through one server share the total, so each checks what the
|
|
220
232
|
* others have taken. */
|
|
221
233
|
private totals = new Map<string, number>();
|
|
222
|
-
private inFlight = new Map<string,
|
|
234
|
+
private inFlight = new Map<string, Flight>();
|
|
223
235
|
|
|
224
236
|
constructor(
|
|
237
|
+
/** A server's address, credential and limits, by id, from the
|
|
238
|
+
* framework's own config: the origin a fetch is checked against and the
|
|
239
|
+
* token it carries come from one answer. */
|
|
225
240
|
private readonly resolveServer: (serverId: string) => ServerResolution | undefined,
|
|
226
241
|
/** Persist the payload; returns a mount-prefixed path or null when no
|
|
227
242
|
* writable mount without an include exists. File name is host-generated (§7.3). */
|
|
228
243
|
private readonly saveFile: (fileName: string, data: Buffer, mimeType: string) => Promise<string | null>,
|
|
229
244
|
) {}
|
|
230
245
|
|
|
231
|
-
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
/** The server a check (dialServer or dialCheck) chose, with its credential
|
|
247
|
+
* and limits from the same resolver answer whose address the check read:
|
|
248
|
+
* each server's resolution is read once here, so the origin the check
|
|
249
|
+
* passes and the token the fetch sends come from one answer, whatever a
|
|
250
|
+
* resolver answers the next time it's asked. */
|
|
251
|
+
private chosen(
|
|
252
|
+
check: (addressOf: (serverId: string) => ServerAddress | undefined) => { serverId: string } | { refusal: Refusal },
|
|
253
|
+
): { serverId: string; binding: ResolvedServerBinding } | { refusal: Refusal } {
|
|
254
|
+
const resolutions = new Map<string, ServerResolution | undefined>();
|
|
255
|
+
const addressOf = (serverId: string): ServerAddress | undefined => {
|
|
256
|
+
if (!resolutions.has(serverId)) resolutions.set(serverId, this.resolveServer(serverId));
|
|
257
|
+
const resolution = resolutions.get(serverId);
|
|
258
|
+
return resolution === undefined || resolution === 'no-address' ? resolution : { url: resolution.url };
|
|
259
|
+
};
|
|
260
|
+
const server = check(addressOf);
|
|
261
|
+
if ('refusal' in server) return server;
|
|
262
|
+
// The check chose only a server whose address it read as a url, so that
|
|
263
|
+
// server's resolution is a binding.
|
|
264
|
+
return { serverId: server.serverId, binding: resolutions.get(server.serverId) as ResolvedServerBinding };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The server a fetch of a record dials (§6.2), with its credential and
|
|
268
|
+
* limits, or why none can be. */
|
|
269
|
+
private serverFor(record: ReferenceRecord): { serverId: string; binding: ResolvedServerBinding } | { refusal: Refusal } {
|
|
270
|
+
return this.chosen((addressOf) => dialServer(record, addressOf));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Would the eager (autofetch) default fetch this record? Not one whose
|
|
274
|
+
* eager fetch already failed; not one the fetch would refuse before
|
|
275
|
+
* dialing, asked with the default ceiling, so such a record takes no eager
|
|
276
|
+
* slot; and only one whose claimed size is within the eager limit of the
|
|
277
|
+
* server it dials. For one whose fetch is already under way, which the
|
|
278
|
+
* eager fetch would join as fetch() does, only the server and its eager
|
|
279
|
+
* limit enter: the expiry, the budget and the ceiling were checked when
|
|
280
|
+
* that fetch began. */
|
|
238
281
|
isEagerEligible(record: ReferenceRecord): boolean {
|
|
239
|
-
if (record.fetchedPath) return false;
|
|
282
|
+
if (record.fetchedPath || record.eagerFailed) return false;
|
|
240
283
|
const check = this.inFlight.has(record.refId) ? this.serverFor(record) : this.beforeDial(record);
|
|
241
284
|
if ('refusal' in check) return false;
|
|
242
|
-
|
|
243
|
-
return record.testimony.sizeBytes !== undefined && record.testimony.sizeBytes <= cap;
|
|
285
|
+
return record.testimony.sizeBytes !== undefined && record.testimony.sizeBytes <= this.eagerLimit(check.binding);
|
|
244
286
|
}
|
|
245
287
|
|
|
246
|
-
/** The
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return refusal ?? refused(`it's ${UNBOUND_REASON}`, FRESH_REFERENCE);
|
|
288
|
+
/** The eager default's fetch. One it starts reads no further than the first
|
|
289
|
+
* chunk past the server's eager limit, whatever size the reference claims;
|
|
290
|
+
* one it joins reads under its starter's ceiling, and that fetch's success
|
|
291
|
+
* answers it too (fetch()). It waits no longer than
|
|
292
|
+
* EAGER_FETCH_TIMEOUT_MS. One that fails is remembered on the record, so
|
|
293
|
+
* the eager default doesn't choose that reference again; fetch_reference
|
|
294
|
+
* still fetches it. */
|
|
295
|
+
async fetchEager(record: ReferenceRecord): Promise<FetchOutcome> {
|
|
296
|
+
const server = this.serverFor(record);
|
|
297
|
+
const maxBytes = this.eagerLimit('refusal' in server ? undefined : server.binding);
|
|
298
|
+
const outcome = await this.fetch(record, { maxBytes, timeoutMs: EAGER_FETCH_TIMEOUT_MS });
|
|
299
|
+
if (!outcome.ok) record.eagerFailed = true;
|
|
300
|
+
return outcome;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** This server's eager limit: its autofetch.maxBytes,
|
|
304
|
+
* DEFAULT_EAGER_FETCH_MAX_BYTES when that's unset. Eligibility holds a
|
|
305
|
+
* claim to it, and a fetch the eager default starts stops reading at the
|
|
306
|
+
* first chunk past it. The host's own ceiling, which every fetch checks
|
|
307
|
+
* first, still holds past a larger one. */
|
|
308
|
+
private eagerLimit(binding: ResolvedServerBinding | undefined): number {
|
|
309
|
+
return binding?.autofetch?.maxBytes ?? DEFAULT_EAGER_FETCH_MAX_BYTES;
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
/** Every check the fetch makes before it dials, in its order: the server
|
|
272
313
|
* (none, unknown, no address, an address of its own or the reference's
|
|
273
314
|
* that doesn't parse, a reference address carrying a user name or
|
|
274
|
-
* password, a scheme other than http: or https:, off the uri's origin)
|
|
275
|
-
* the expiry,
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* refused. */
|
|
315
|
+
* password, a scheme other than http: or https:, off the uri's origin)
|
|
316
|
+
* and the expiry, which dialCheck makes as a reference's stub does; then
|
|
317
|
+
* a claimed size over the host's own ceiling, the server's spent budget,
|
|
318
|
+
* a claimed size over a call's ceiling. For a record with no saved copy
|
|
319
|
+
* and no fetch under way, the fetch, `refusal()` and the eager check all
|
|
320
|
+
* ask this, so what save_recent_image says is the reason the fetch would
|
|
321
|
+
* refuse with, and no eager slot goes to a fetch that would be refused. */
|
|
281
322
|
private beforeDial(record: ReferenceRecord, opts?: { maxBytes?: number }):
|
|
282
|
-
{ refusal: Refusal
|
|
283
|
-
|
|
323
|
+
{ refusal: Refusal; byCeiling?: true; atCeiling?: (ceiling: number) => string } |
|
|
324
|
+
{ serverId: string; binding: ResolvedServerBinding; ceiling: number; budget: number } {
|
|
325
|
+
const server = this.chosen((addressOf) => dialCheck(record, addressOf));
|
|
284
326
|
if ('refusal' in server) return server;
|
|
285
327
|
const { serverId, binding } = server;
|
|
286
328
|
const t = record.testimony;
|
|
287
|
-
const expiry = referenceExpiry(t);
|
|
288
|
-
if (expiry) return refused(`the reference is ${EXPIRY_REASON[expiry]}`, FRESH_REFERENCE);
|
|
289
329
|
|
|
290
330
|
const ceiling = opts?.maxBytes ?? DEFAULT_FETCH_MAX_BYTES;
|
|
291
331
|
// A claim past the host's own ceiling is refused first: nothing lifts
|
|
@@ -299,23 +339,29 @@ export class ReferenceFetcher {
|
|
|
299
339
|
const budget = binding.autofetch?.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
300
340
|
const used = this.totals.get(serverId) ?? 0;
|
|
301
341
|
if (used >= budget) {
|
|
302
|
-
return refused(`this host's fetch budget for the "${serverId}" server is used up (${used} of ${budget} bytes)`, BUDGET);
|
|
342
|
+
return refused(`this host's fetch budget for the "${shownServerId(serverId)}" server is used up (${used} of ${budget} bytes)`, BUDGET);
|
|
303
343
|
}
|
|
304
344
|
// Vector 4: a claim already over the ceiling refuses without dialing. One
|
|
305
345
|
// that's also over what's left of the budget is told about the budget,
|
|
306
346
|
// which no max_bytes lifts. A claim over what's left but within the
|
|
307
347
|
// ceiling still dials, and the budget stops it at its first chunk past.
|
|
348
|
+
// Both refusals here are reached only through the call's own ceiling, so
|
|
349
|
+
// each is that ceiling's ending: a caller whose ceiling holds the claim
|
|
350
|
+
// would dial, and one that joined this call's fetch does (fetch()).
|
|
308
351
|
if (t.sizeBytes !== undefined && t.sizeBytes > ceiling) {
|
|
309
352
|
if (t.sizeBytes > budget - used) {
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
353
|
+
return {
|
|
354
|
+
...refused(
|
|
355
|
+
`it claims ${t.sizeBytes} bytes, more than the ${budget - used} bytes left of this host's fetch budget for the "${shownServerId(serverId)}" server`,
|
|
356
|
+
BUDGET,
|
|
357
|
+
),
|
|
358
|
+
byCeiling: true,
|
|
359
|
+
};
|
|
314
360
|
}
|
|
315
361
|
// Within what's left of the budget and the host's ceiling, so a
|
|
316
362
|
// ceiling of the call's own is what a larger max_bytes lifts.
|
|
317
|
-
|
|
318
|
-
|
|
363
|
+
const atCeiling = claimOver(t.sizeBytes);
|
|
364
|
+
return { ...refused(atCeiling(ceiling), `Pass max_bytes of at least ${t.sizeBytes} to fetch it.`), byCeiling: true, atCeiling };
|
|
319
365
|
}
|
|
320
366
|
return { serverId, binding, ceiling, budget };
|
|
321
367
|
}
|
|
@@ -331,67 +377,63 @@ export class ReferenceFetcher {
|
|
|
331
377
|
return 'refusal' in check ? check.refusal : null;
|
|
332
378
|
}
|
|
333
379
|
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return
|
|
366
|
-
'and this host fetches only from the server itself', NOTHING_TO_DO);
|
|
380
|
+
/** Fetch a reference to workspace storage. Idempotent per record. Callers
|
|
381
|
+
* of one record share the fetch in flight, each under its own bounds: it
|
|
382
|
+
* waits no longer than its own timeout, counted from its call and covering
|
|
383
|
+
* the save. When the fetch it joined stopped on its starter's byte ceiling
|
|
384
|
+
* and its own is wider, it fetches again under its own. When that fetch
|
|
385
|
+
* stopped at its starter's deadline, it fetches again only if it has more
|
|
386
|
+
* time left than that starter's whole timeout, since a fresh fetch with no
|
|
387
|
+
* more time than one that ran out unanswered would likely be stopped too;
|
|
388
|
+
* otherwise it takes that ending. (A refetch's flight had less than its
|
|
389
|
+
* starter's timeout, so this errs toward not dialing.) A fetch that
|
|
390
|
+
* succeeds answers every caller waiting on it, whatever its own ceiling. A
|
|
391
|
+
* save still running when a caller stops waiting goes on, and the record
|
|
392
|
+
* is marked fetched when it lands. */
|
|
393
|
+
async fetch(record: ReferenceRecord, opts?: { maxBytes?: number; timeoutMs?: number }): Promise<FetchOutcome> {
|
|
394
|
+
const ceiling = opts?.maxBytes ?? DEFAULT_FETCH_MAX_BYTES;
|
|
395
|
+
const timeoutMs = opts?.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
396
|
+
const deadline = performance.now() + timeoutMs;
|
|
397
|
+
for (;;) {
|
|
398
|
+
if (record.fetchedPath) {
|
|
399
|
+
return { ok: true, path: record.fetchedPath, bytes: record.verifiedBytes,
|
|
400
|
+
mimeType: record.verifiedMimeType, digestVerified: record.digestVerified };
|
|
401
|
+
}
|
|
402
|
+
const flight = this.inFlight.get(record.refId) ?? this.start(record, ceiling, deadline, timeoutMs);
|
|
403
|
+
const ended = await settledBy(flight.ended, deadline);
|
|
404
|
+
if (ended === undefined) return tookLonger(timeoutMs);
|
|
405
|
+
const { stoppedBy, atCeiling, ...outcome } = ended;
|
|
406
|
+
if (stoppedBy === 'ceiling') {
|
|
407
|
+
if (ceiling > flight.ceiling) continue;
|
|
408
|
+
return (atCeiling ? { ...outcome, error: atCeiling(ceiling) } : outcome) as FetchOutcome;
|
|
409
|
+
}
|
|
410
|
+
if (stoppedBy === 'deadline' && deadline - performance.now() > flight.timeoutMs) continue;
|
|
411
|
+
return outcome as FetchOutcome;
|
|
367
412
|
}
|
|
368
|
-
return null;
|
|
369
413
|
}
|
|
370
414
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (existing) return existing;
|
|
380
|
-
const p = this.doFetch(record, opts).finally(() => this.inFlight.delete(record.refId));
|
|
381
|
-
this.inFlight.set(record.refId, p);
|
|
382
|
-
return p;
|
|
415
|
+
private start(record: ReferenceRecord, ceiling: number, deadline: number, timeoutMs: number): Flight {
|
|
416
|
+
const flight: Flight = {
|
|
417
|
+
ceiling,
|
|
418
|
+
timeoutMs,
|
|
419
|
+
ended: this.doFetch(record, ceiling, deadline, timeoutMs).finally(() => this.inFlight.delete(record.refId)),
|
|
420
|
+
};
|
|
421
|
+
this.inFlight.set(record.refId, flight);
|
|
422
|
+
return flight;
|
|
383
423
|
}
|
|
384
424
|
|
|
385
|
-
private async doFetch(record: ReferenceRecord,
|
|
425
|
+
private async doFetch(record: ReferenceRecord, ceiling: number, deadline: number, timeoutMs: number): Promise<Ended> {
|
|
386
426
|
const t = record.testimony;
|
|
387
|
-
const check = this.beforeDial(record,
|
|
388
|
-
if ('refusal' in check)
|
|
389
|
-
|
|
390
|
-
|
|
427
|
+
const check = this.beforeDial(record, { maxBytes: ceiling });
|
|
428
|
+
if ('refusal' in check) {
|
|
429
|
+
const refusal = fail(check.refusal.error, check.refusal.remedy);
|
|
430
|
+
return check.byCeiling ? { ...refusal, stoppedBy: 'ceiling', atCeiling: check.atCeiling } : refusal;
|
|
431
|
+
}
|
|
432
|
+
const { serverId, binding, budget } = check;
|
|
433
|
+
const server = `the "${shownServerId(serverId)}" server`;
|
|
391
434
|
|
|
392
|
-
const timeoutMs = opts?.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
393
435
|
const controller = new AbortController();
|
|
394
|
-
const timer = setTimeout(() => controller.abort(),
|
|
436
|
+
const timer = setTimeout(() => controller.abort(), Math.max(0, deadline - performance.now()));
|
|
395
437
|
try {
|
|
396
438
|
const headers: Record<string, string> = {};
|
|
397
439
|
if (binding.token) headers.authorization = `Bearer ${binding.token}`;
|
|
@@ -430,8 +472,8 @@ export class ReferenceFetcher {
|
|
|
430
472
|
}
|
|
431
473
|
if (received > ceiling) {
|
|
432
474
|
controller.abort();
|
|
433
|
-
|
|
434
|
-
|
|
475
|
+
const atCeiling = readPast(received);
|
|
476
|
+
return { ...fail(atCeiling(ceiling), largerMaxBytes(received, budget - total, server)), stoppedBy: 'ceiling', atCeiling };
|
|
435
477
|
}
|
|
436
478
|
const buf = Buffer.from(value);
|
|
437
479
|
hash.update(buf);
|
|
@@ -486,16 +528,13 @@ export class ReferenceFetcher {
|
|
|
486
528
|
return { ok: true, path, bytes: data.length, mimeType: observedMime, digestVerified };
|
|
487
529
|
} catch (e) {
|
|
488
530
|
// The timer is the only abort that throws: the budget and the ceiling
|
|
489
|
-
// abort the download and return at once.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
531
|
+
// abort the download and return at once. It fires at the starter's
|
|
532
|
+
// deadline, and a caller with more time left than the starter's whole
|
|
533
|
+
// timeout fetches again (fetch()).
|
|
534
|
+
if ((e as Error)?.name === 'AbortError') return { ...tookLonger(timeoutMs), stoppedBy: 'deadline' };
|
|
494
535
|
return fail(`the request to ${server} failed: ${boundedForLog(whyFetchFailed(e))}`, REQUEST_FAILED);
|
|
495
536
|
} finally {
|
|
496
537
|
clearTimeout(timer);
|
|
497
538
|
}
|
|
498
539
|
}
|
|
499
540
|
}
|
|
500
|
-
|
|
501
|
-
export { EAGER_FETCH_TIMEOUT_MS };
|