@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
package/src/agent.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, YieldingStream } from '@animalabs/membrane';
|
|
1
|
+
import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, ThinkingBindingEntry, YieldingStream } from '@animalabs/membrane';
|
|
2
2
|
import { isAbortedResponse } from '@animalabs/membrane';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
3
|
import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
|
|
5
4
|
import { ToolResultGuard } from './tool-result-guard.js';
|
|
6
5
|
import { withoutTokenEstimates } from './thinking-token-stamp.js';
|
|
6
|
+
import { placeThinkingDrops, type CompiledForRequest, type PlacedDrop } from './thinking-drops.js';
|
|
7
|
+
import { carriedBlock, type SentRow } from './sent-rows.js';
|
|
7
8
|
import { injectionsAsStored } from './text-as-stored.js';
|
|
8
|
-
import {
|
|
9
|
+
import { stableHash } from './stable-hash.js';
|
|
10
|
+
import { hasRenderedContent, storedInjectionMarkOf } from './stored-injections.js';
|
|
11
|
+
import { assertBooleanSetting, holdsSettings, kindOf } from './config-checks.js';
|
|
9
12
|
import {
|
|
10
13
|
CONTINUE_ROW,
|
|
11
14
|
continueTurn,
|
|
@@ -33,6 +36,45 @@ export interface ActivationRequestOptions {
|
|
|
33
36
|
/** A request that will never be sent (a preview): compile it as a dry
|
|
34
37
|
* run (see `compileWithInjections` for what that leaves alone). */
|
|
35
38
|
dryRun?: boolean;
|
|
39
|
+
/** Messages added to the compile's: a preview's stand-in for the context
|
|
40
|
+
* injections a start would store before its compile
|
|
41
|
+
* (stored-injections.ts), put where that compile would render them, as
|
|
42
|
+
* the window's newest rows. A preview stores nothing, so the compile
|
|
43
|
+
* doesn't weigh them against the budget or shape them as it would the
|
|
44
|
+
* stored rows. */
|
|
45
|
+
appendedMessages?: NormalizedMessage[];
|
|
46
|
+
/** `false`: the caller retries this stream's failures itself, so membrane
|
|
47
|
+
* sends a failed round once and hands the error up. The framework's turns
|
|
48
|
+
* pass it (its errorPolicy is their retry layer); a direct caller of
|
|
49
|
+
* startStream keeps membrane's retries. */
|
|
50
|
+
membraneRetries?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A compile's render stats, as context-manager's `getRenderStats()` types
|
|
54
|
+
* them; each compile carries its own, where the strategy records them, in
|
|
55
|
+
* `CompileResult.renderStats`. */
|
|
56
|
+
export type RenderStats = NonNullable<ReturnType<ContextManager['getRenderStats']>>;
|
|
57
|
+
|
|
58
|
+
/** The render stats of the dry compile behind each request a dry run
|
|
59
|
+
* assembled (`assembleActivationRequest` with `dryRun`), by that request.
|
|
60
|
+
* A request that is sent has none here. Read through the framework's
|
|
61
|
+
* `getRequestCompileStats`. */
|
|
62
|
+
const dryCompileStats = new WeakMap<object, RenderStats | undefined>();
|
|
63
|
+
|
|
64
|
+
/** The render stats of the dry compile that built `request`, or null: see
|
|
65
|
+
* `AgentFramework.getRequestCompileStats`. */
|
|
66
|
+
export function dryCompileStatsOf(request: object): RenderStats | null {
|
|
67
|
+
return dryCompileStats.get(request) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What a compile is told when the agent's model binds its thinking: `compile()`'s options for a context manager
|
|
72
|
+
* that keeps a thinking-binding record, as agent-framework holds the binding (with the participant whose messages
|
|
73
|
+
* are the agent's own replies) and as digests of the request's head (bindingCompileOptions).
|
|
74
|
+
*/
|
|
75
|
+
export interface BindingCompileOptions {
|
|
76
|
+
thinkingBinding?: { binds: boolean; dropBlock: boolean; participant: string };
|
|
77
|
+
identity?: RequestIdentity;
|
|
36
78
|
}
|
|
37
79
|
|
|
38
80
|
export interface StartStreamResult {
|
|
@@ -40,13 +82,25 @@ export interface StartStreamResult {
|
|
|
40
82
|
request: NormalizedRequest;
|
|
41
83
|
/** The request-only `[Continue]` prompt this request's reply follows, if
|
|
42
84
|
* any, read from the request: a new one when it ends on `[Continue]`;
|
|
43
|
-
* else, when it ends on tool results,
|
|
85
|
+
* else, when it ends on tool results, or on tool results and then
|
|
86
|
+
* stored context injections alone, the prompt of the reply that called
|
|
44
87
|
* those tools (`toolLoopPrompt`); else none. The framework stamps the
|
|
45
88
|
* reply's rows with it (`metadata.promptedBy`), for later compiles to
|
|
46
89
|
* render it before them. */
|
|
47
90
|
continuePrompt?: RequestOnlyPrompt;
|
|
48
91
|
takeKvSubmission?: () => { submissionId: string; wireReceipt: CacheWireReceipt } | undefined;
|
|
49
92
|
drainKvSubmissionIds?: () => string[];
|
|
93
|
+
/** Where each block a call of this stream reports the provider dropped came from: a stored reply this
|
|
94
|
+
* request's compile rendered, a round the stream appended, or neither (placeThinkingDrops). Optional, so a
|
|
95
|
+
* stand-in for the stream start needn't supply it; without it, no drop can be placed. */
|
|
96
|
+
placeDrops?: (entries: readonly ThinkingBindingEntry[]) => PlacedDrop[];
|
|
97
|
+
/** The compile's `thinkingRecordedAfter`, when it gave one: a reply stored at or below it predates the branch's
|
|
98
|
+
* thinking-binding record, so the provider may drop its thinking with no strip having missed. */
|
|
99
|
+
thinkingRecordedAfter?: number;
|
|
100
|
+
/** Tells the context manager what the stream's later rounds carried after the compiled messages, every round so
|
|
101
|
+
* far in sent order, with this request's head (context-manager's `noteRequestSent`). Present only where the
|
|
102
|
+
* model binds its thinking and the context manager keeps a record it can be told of. */
|
|
103
|
+
noteRequestSent?: (rows: readonly SentRow[]) => boolean;
|
|
50
104
|
}
|
|
51
105
|
import type {
|
|
52
106
|
ContextManager,
|
|
@@ -55,10 +109,13 @@ import type {
|
|
|
55
109
|
CompileResult,
|
|
56
110
|
HotContextSettingsStatus,
|
|
57
111
|
HotContextSettingsUpdate,
|
|
112
|
+
MessageId,
|
|
58
113
|
MessageMetadata,
|
|
114
|
+
RequestIdentity,
|
|
59
115
|
} from '@animalabs/context-manager';
|
|
60
116
|
import type {
|
|
61
117
|
AgentConfig,
|
|
118
|
+
AutoContinueConfig,
|
|
62
119
|
AgentState,
|
|
63
120
|
SameRoundThinkTextPolicy,
|
|
64
121
|
SameRoundThinkTextPolicySource,
|
|
@@ -76,16 +133,22 @@ import type {
|
|
|
76
133
|
AgentRuntimeSettingsOverrides,
|
|
77
134
|
} from './types/index.js';
|
|
78
135
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether a request's messages, as sanitized, end on `agentName`'s own
|
|
139
|
+
* message, apart from `injectionTail` stored context injections after it
|
|
140
|
+
* (counted over the messages the sanitizer keeps).
|
|
141
|
+
*/
|
|
142
|
+
function endsOnOwnMessage(messages: NormalizedMessage[], agentName: string, injectionTail: number): boolean {
|
|
143
|
+
const own = messages[messages.length - 1 - injectionTail];
|
|
144
|
+
return own !== undefined && own.participant === agentName
|
|
145
|
+
&& messages.slice(messages.length - injectionTail).every((m) => m.participant.startsWith('system_context:'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A digest of a value as a request's JSON carries it (a field whose value is undefined left out, as the wire leaves
|
|
149
|
+
* it out), key order aside: the head digests the thinking binding compares, so a change on the wire changes one. */
|
|
150
|
+
function wireDigest(value: unknown): string {
|
|
151
|
+
return stableHash(JSON.parse(JSON.stringify(value) ?? 'null'));
|
|
89
152
|
}
|
|
90
153
|
|
|
91
154
|
const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
|
|
@@ -94,14 +157,16 @@ const DEFAULT_SAME_ROUND_THINK_TEXT_POLICY: SameRoundThinkTextPolicy = 'public';
|
|
|
94
157
|
|
|
95
158
|
/**
|
|
96
159
|
* Refuse an agent config whose `promptCaching`, `toolResultGuard`,
|
|
97
|
-
* `toolWrapperProseGuard`, `thinking.enabled
|
|
98
|
-
* holds anything but true or false
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* the
|
|
160
|
+
* `toolWrapperProseGuard`, `thinking.enabled`, `thinkingBinding.dropBlock`,
|
|
161
|
+
* `thinkingBinding.binds` or `refusalHandling.autoRewind` holds anything but true or false
|
|
162
|
+
* (assertBooleanSetting, where null counts as left out), or whose `thinking`, `thinkingBinding` or
|
|
163
|
+
* `refusalHandling` is anything but an object, null or false (holdsSettings),
|
|
164
|
+
* or whose `autoContinue` resolveAutoContinue refuses. The constructor runs it
|
|
165
|
+
* first, so the rule holds wherever an Agent is built. `create()` also runs it
|
|
166
|
+
* for each of its agents before anything opens, and `createEphemeralAgent`
|
|
167
|
+
* before it reserves the name, so a refusal there leaves nothing behind.
|
|
168
|
+
* Membrane reads `thinking.enabled`, and the refusal is here because the
|
|
169
|
+
* setting is AgentConfig's.
|
|
105
170
|
*/
|
|
106
171
|
export function assertAgentSwitches(config: AgentConfig): void {
|
|
107
172
|
const agent = `Agent "${config.name}"`;
|
|
@@ -117,6 +182,20 @@ export function assertAgentSwitches(config: AgentConfig): void {
|
|
|
117
182
|
'true asks the provider for extended thinking, and false or leaving it out doesn\'t',
|
|
118
183
|
);
|
|
119
184
|
}
|
|
185
|
+
if (holdsSettings(`${agent} thinkingBinding`, config.thinkingBinding, '{ binds: true, dropBlock: true }')) {
|
|
186
|
+
assertBooleanSetting(
|
|
187
|
+
`${agent} thinkingBinding.dropBlock`,
|
|
188
|
+
config.thinkingBinding.dropBlock,
|
|
189
|
+
'true asks the provider to drop a signed thinking block whose earlier conversation changed rather than refuse the request, ' +
|
|
190
|
+
'and false or leaving it out sends nothing, so the account\'s default applies',
|
|
191
|
+
);
|
|
192
|
+
assertBooleanSetting(
|
|
193
|
+
`${agent} thinkingBinding.binds`,
|
|
194
|
+
config.thinkingBinding.binds,
|
|
195
|
+
'true says the model binds its signed thinking to the conversation before it, so each compile tells the context manager so, ' +
|
|
196
|
+
'and false or leaving it out compiles as before',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
120
199
|
if (holdsSettings(`${agent} refusalHandling`, config.refusalHandling, '{ autoRewind: true }')) {
|
|
121
200
|
assertBooleanSetting(
|
|
122
201
|
`${agent} refusalHandling.autoRewind`,
|
|
@@ -137,6 +216,50 @@ export function assertAgentSwitches(config: AgentConfig): void {
|
|
|
137
216
|
'true keeps a response whose whole prose is a tool-call-shaped wrapper from running or entering the agent\'s history, ' +
|
|
138
217
|
'and false or leaving it out doesn\'t',
|
|
139
218
|
);
|
|
219
|
+
resolveAutoContinue(config);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** `AutoContinueConfig.maxConsecutive` when left out. */
|
|
223
|
+
export const DEFAULT_AUTO_CONTINUE_MAX = 3;
|
|
224
|
+
/** The most continuations in a row a config may ask for. */
|
|
225
|
+
const MAX_AUTO_CONTINUE_MAX = 20;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `AgentConfig.autoContinue` resolved: undefined when off (left out, null or
|
|
229
|
+
* false), else its cap. `true` takes the default cap; an object takes
|
|
230
|
+
* `maxConsecutive`, a whole number from 1 to 20 (the default when left out).
|
|
231
|
+
* Any other value is refused.
|
|
232
|
+
*/
|
|
233
|
+
export function resolveAutoContinue(config: AgentConfig): { maxConsecutive: number } | undefined {
|
|
234
|
+
const setting = `Agent "${config.name}" autoContinue`;
|
|
235
|
+
const value: unknown = config.autoContinue;
|
|
236
|
+
if (value === true) return { maxConsecutive: DEFAULT_AUTO_CONTINUE_MAX };
|
|
237
|
+
if (value !== undefined && value !== null && value !== false && (typeof value !== 'object' || Array.isArray(value))) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`${setting} must be true, false or an object such as { maxConsecutive: 3 }, not ${kindOf(value)}` +
|
|
240
|
+
(typeof value === 'string' ? ' (true and false go unquoted)' : ''),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (!holdsSettings(setting, value as AgentConfig['autoContinue'], '{ maxConsecutive: 3 }')) return undefined;
|
|
244
|
+
const max = (value as AutoContinueConfig).maxConsecutive;
|
|
245
|
+
if (max === undefined) return { maxConsecutive: DEFAULT_AUTO_CONTINUE_MAX };
|
|
246
|
+
if (typeof max !== 'number' || !Number.isInteger(max) || max < 1 || max > MAX_AUTO_CONTINUE_MAX) {
|
|
247
|
+
// The value's kind, never the value (config-checks): a `${VAR}` that
|
|
248
|
+
// lands here could be a credential.
|
|
249
|
+
const kind = typeof max !== 'number' ? kindOf(max)
|
|
250
|
+
: !Number.isInteger(max) ? 'a number that isn\'t whole'
|
|
251
|
+
: max < 1 ? 'a number below 1' : `a number above ${MAX_AUTO_CONTINUE_MAX}`;
|
|
252
|
+
throw new Error(
|
|
253
|
+
`${setting}.maxConsecutive must be a whole number from 1 to ${MAX_AUTO_CONTINUE_MAX}, not ${kind}: ` +
|
|
254
|
+
'the most turns in a row that continue without skip_reply before the agent waits for the next event',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return { maxConsecutive: max };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** A branch's key in Agent.unsentSince: its id and when it was created. */
|
|
261
|
+
function branchKey(branch: { readonly id: string; readonly created: number }): string {
|
|
262
|
+
return `${branch.id}\u0000${branch.created}`;
|
|
140
263
|
}
|
|
141
264
|
|
|
142
265
|
/**
|
|
@@ -162,6 +285,12 @@ export class Agent {
|
|
|
162
285
|
* blocks are preserved through Chronicle.
|
|
163
286
|
*/
|
|
164
287
|
readonly thinking: AgentConfig['thinking'];
|
|
288
|
+
/** Whether requests ask the provider to drop signed thinking whose earlier
|
|
289
|
+
* conversation changed (see AgentConfig.thinkingBinding.dropBlock). */
|
|
290
|
+
private readonly thinkingDropBlock: boolean;
|
|
291
|
+
/** Whether the model binds its signed thinking to the conversation before
|
|
292
|
+
* it (see AgentConfig.thinkingBinding.binds). */
|
|
293
|
+
private readonly thinkingBinds: boolean;
|
|
165
294
|
/** Refusal auto-rewind policy (see AgentConfig.refusalHandling). */
|
|
166
295
|
readonly refusalHandling: AgentConfig['refusalHandling'];
|
|
167
296
|
readonly toolResultGuard: ToolResultGuard;
|
|
@@ -169,6 +298,9 @@ export class Agent {
|
|
|
169
298
|
readonly proseRouting: 'locus' | 'explicit' | 'hybrid' | 'disabled';
|
|
170
299
|
/** Exact whole-response known-tool wrapper containment (default off). */
|
|
171
300
|
readonly toolWrapperProseGuard: boolean;
|
|
301
|
+
/** Continuation after a turn the model ends itself (see
|
|
302
|
+
* AgentConfig.autoContinue); undefined when off. */
|
|
303
|
+
readonly autoContinue: { maxConsecutive: number } | undefined;
|
|
172
304
|
/** Clock rows' interval and precision (see AgentConfig.clock); undefined
|
|
173
305
|
* when the agent writes none of its own (a resident still reads those
|
|
174
306
|
* another resident writes into the shared slot). */
|
|
@@ -298,10 +430,13 @@ export class Agent {
|
|
|
298
430
|
this.maxTokens = config.maxTokens ?? 4096;
|
|
299
431
|
this.temperature = config.temperature;
|
|
300
432
|
this.thinking = config.thinking;
|
|
433
|
+
this.thinkingDropBlock = config.thinkingBinding?.dropBlock === true;
|
|
434
|
+
this.thinkingBinds = config.thinkingBinding?.binds === true;
|
|
301
435
|
this.refusalHandling = config.refusalHandling;
|
|
302
436
|
this.toolResultGuard = new ToolResultGuard(config.name, contextManager, config.toolResultGuard === true);
|
|
303
437
|
this.proseRouting = config.proseRouting ?? 'locus';
|
|
304
438
|
this.toolWrapperProseGuard = config.toolWrapperProseGuard === true;
|
|
439
|
+
this.autoContinue = resolveAutoContinue(config);
|
|
305
440
|
this.clock = resolveClockConfig(config.name, config.clock);
|
|
306
441
|
this.cacheTtl = config.cacheTtl ?? '1h';
|
|
307
442
|
this.promptCaching = config.promptCaching !== false;
|
|
@@ -309,7 +444,12 @@ export class Agent {
|
|
|
309
444
|
this.providerParams = config.providerParams;
|
|
310
445
|
this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
|
|
311
446
|
this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
|
|
312
|
-
|
|
447
|
+
// The window size converted as the projection's comparison (`projected > window`) converts it, so a string such
|
|
448
|
+
// as "200000" from a JSON or YAML config is its number, for every rule that reads it. A value whose number isn't
|
|
449
|
+
// finite is no window: null, NaN and Infinity, which the comparison never passed, and -Infinity, which it always
|
|
450
|
+
// did, a restart at every boundary.
|
|
451
|
+
const window = config.physicalWindowTokens == null ? NaN : Number(config.physicalWindowTokens);
|
|
452
|
+
this.physicalWindowTokens = Number.isFinite(window) ? window : undefined;
|
|
313
453
|
this.contextBudgetTokens = config.contextBudgetTokens;
|
|
314
454
|
this.configuredContextBudgetTokens = config.contextBudgetTokens;
|
|
315
455
|
this.contextManager = contextManager;
|
|
@@ -978,6 +1118,57 @@ export class Agent {
|
|
|
978
1118
|
return this.shownWatermark;
|
|
979
1119
|
}
|
|
980
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* The newest consumed watermark a request carried that was answered: its
|
|
1123
|
+
* round's tool calls, or its completion, came back. A message a boundary
|
|
1124
|
+
* injected counts as heard only once a request carrying it is answered;
|
|
1125
|
+
* the consumed watermark moves when the stream is handed it, before its
|
|
1126
|
+
* request runs, so a request that fails, ending its stream with an error,
|
|
1127
|
+
* leaves this as it was.
|
|
1128
|
+
*/
|
|
1129
|
+
private answeredWatermark: { branch: string; sequence: number } | null = null;
|
|
1130
|
+
|
|
1131
|
+
getAnsweredWatermark(): { branch: string; sequence: number } | null {
|
|
1132
|
+
return this.answeredWatermark;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** A request that carried `watermark` was answered. Requests are answered
|
|
1136
|
+
* in the order they're sent, and the consumed watermark only advances
|
|
1137
|
+
* within a branch, so the latest is the newest. */
|
|
1138
|
+
markAnswered(watermark: { branch: string; sequence: number } | null): void {
|
|
1139
|
+
if (watermark) this.answeredWatermark = watermark;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Per destination (a server's channel, by its key), the newest consumed
|
|
1144
|
+
* watermark the agent had read when it published there by a route the
|
|
1145
|
+
* framework sees (its prose, a sleep's announcement, a channel_publish, a
|
|
1146
|
+
* draft resend, or a send tool whose channel the framework records), on
|
|
1147
|
+
* the branch it was taken on. A wake that carries a channel, unless it's
|
|
1148
|
+
* marked ambient (InferenceRequest.ambient), keeps its turn unless the
|
|
1149
|
+
* agent spoke there after reading the write the wake is for. It
|
|
1150
|
+
* lives as long as this Agent: an agent made again starts it empty, and
|
|
1151
|
+
* such a wake then stays.
|
|
1152
|
+
*/
|
|
1153
|
+
private publishedThrough = new Map<string, { branch: string; sequence: number }>();
|
|
1154
|
+
|
|
1155
|
+
/** Note a publish into `destination` (its key), made having read through
|
|
1156
|
+
* `readThrough`: the consumed watermark when its words were written, or
|
|
1157
|
+
* its call made. */
|
|
1158
|
+
notePublished(destination: string, readThrough: { branch: string; sequence: number } | null): void {
|
|
1159
|
+
if (!readThrough) return;
|
|
1160
|
+
const noted = this.publishedThrough.get(destination);
|
|
1161
|
+
if (noted && noted.branch === readThrough.branch && noted.sequence >= readThrough.sequence) return;
|
|
1162
|
+
this.publishedThrough.set(destination, readThrough);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** Whether the agent published into `destination` (its key) having read
|
|
1166
|
+
* the row stored at `sequence` on `branch`. */
|
|
1167
|
+
publishedAfterReading(destination: string, branch: string, sequence: number): boolean {
|
|
1168
|
+
const noted = this.publishedThrough.get(destination);
|
|
1169
|
+
return !!noted && noted.branch === branch && noted.sequence >= sequence;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
981
1172
|
/** Mark everything currently stored as consumed (the agent's creation, at
|
|
982
1173
|
* boot or later, and a tool boundary that injected messages into a live
|
|
983
1174
|
* stream declaring it carries them; see consumedWatermark). */
|
|
@@ -985,7 +1176,8 @@ export class Agent {
|
|
|
985
1176
|
// Tolerates partial context-manager doubles (tests build Agents over
|
|
986
1177
|
// stubs). Without a watermark, a reader that looks for one takes its own
|
|
987
1178
|
// side: the coalescer counts the message as read, and the framework's
|
|
988
|
-
//
|
|
1179
|
+
// unreadNoticesForPrimary counts it as unread. A deferred write's wake
|
|
1180
|
+
// reads the answered watermark instead (deferredWriteDone).
|
|
989
1181
|
this.consumedWatermark = this.pendingWatermark();
|
|
990
1182
|
this.watermarkMarks++;
|
|
991
1183
|
this.shownWatermark = this.consumedWatermark;
|
|
@@ -1057,7 +1249,10 @@ export class Agent {
|
|
|
1057
1249
|
* manager commits what it commits for one, the consumed watermark moves,
|
|
1058
1250
|
* and a prepared budget change settles. To inspect what the agent would
|
|
1059
1251
|
* see, use the framework's `previewActivation`, which compiles as a dry
|
|
1060
|
-
* run.
|
|
1252
|
+
* run. It isn't told the agent's thinking binding (`thinkingBinding.binds`),
|
|
1253
|
+
* so a context manager that keeps a thinking-binding record neither reads
|
|
1254
|
+
* nor writes the record here, and what it returns keeps thinking whose
|
|
1255
|
+
* earlier conversation changed. */
|
|
1061
1256
|
async compileContext(budget?: TokenBudget): Promise<CompileResult> {
|
|
1062
1257
|
// RFC-006 §3.3: consumption is determined at assembly — a compile that
|
|
1063
1258
|
// fails assembled nothing, so the watermark moves only on success.
|
|
@@ -1077,6 +1272,202 @@ export class Agent {
|
|
|
1077
1272
|
return result;
|
|
1078
1273
|
}
|
|
1079
1274
|
|
|
1275
|
+
/**
|
|
1276
|
+
* The stored context injections of this agent's that the last compile it
|
|
1277
|
+
* built to send rendered (stored-injections.ts), the framework's activation
|
|
1278
|
+
* request's or the direct API's: for each namespace and content, the parts
|
|
1279
|
+
* the compile held, and the branch it read. A compile built to send whose
|
|
1280
|
+
* request then isn't sent (a stop, a failure) still records: its rows stay
|
|
1281
|
+
* in the window all the same. Undefined before the first, as after a
|
|
1282
|
+
* restart. The framework's same-content skip reads it (renderedInjection).
|
|
1283
|
+
*/
|
|
1284
|
+
private renderedInjections?: {
|
|
1285
|
+
branch: { readonly id: string; readonly created: number };
|
|
1286
|
+
/** Per namespace, per content hash: the parts any copy of that content
|
|
1287
|
+
* holds, how many it has, and its newest copy's sequence. */
|
|
1288
|
+
byNamespace: Map<string, Map<string, { parts: number; held: Set<number>; sequence: number }>>;
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Per branch (by id and created), the point a stored injection on it has
|
|
1293
|
+
* to come after to have been in no request yet (unsentInjection): the
|
|
1294
|
+
* newest row of this agent's window on that branch when the first plan
|
|
1295
|
+
* there since its last compile built to send (a start's, or a preview's,
|
|
1296
|
+
* which stores nothing) asked about a unit, before that plan's start
|
|
1297
|
+
* stored anything. Each compile built to send that records what it
|
|
1298
|
+
* held unsets its branch's, a restart unsets them all, and a plan asks
|
|
1299
|
+
* here before it stores a unit, so every one of this agent's stored
|
|
1300
|
+
* injections on a branch after its point was stored since that compile.
|
|
1301
|
+
* Kept per branch, so a start or preview on another branch doesn't move
|
|
1302
|
+
* this one's past a copy no request has had.
|
|
1303
|
+
*/
|
|
1304
|
+
private readonly unsentSince = new Map<string, number>();
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* A host's hook, called with the messages of each compile built to send
|
|
1308
|
+
* (never a dry run), as the compile is recorded: what the request carries,
|
|
1309
|
+
* after compression and the window have done their work. The framework
|
|
1310
|
+
* uses it to tell when an MCPL toolset's announcement has left the context
|
|
1311
|
+
* (RFC-014 §8.3 reminders). It must not change the messages; one that
|
|
1312
|
+
* throws is logged, and the start goes on.
|
|
1313
|
+
*/
|
|
1314
|
+
onCompiledToSend?: (messages: readonly unknown[]) => void;
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* The newest content of `namespace` among this agent's rows stored since
|
|
1318
|
+
* its last compile built to send (unsentSince), and whether every part of
|
|
1319
|
+
* it is among them: rows a start stored whose compile then didn't record
|
|
1320
|
+
* (it threw, or the start failed before it), which no request has had
|
|
1321
|
+
* yet, and which are newer than anything renderedInjection knows. Undefined when there are none, as at every
|
|
1322
|
+
* start whose last compile was built and recorded. A fold between that
|
|
1323
|
+
* start and the next (a host's own committing compile, or the compression
|
|
1324
|
+
* an over-budget retry waits for) can summarize them all the same: the
|
|
1325
|
+
* next request then has the summary's word for them, and the start after
|
|
1326
|
+
* it stores them again, as after any fold. The window is read back 64
|
|
1327
|
+
* rows at a time from the newest, stopping at that point.
|
|
1328
|
+
*/
|
|
1329
|
+
unsentInjection(namespace: string): { contentHash: string; whole: boolean } | undefined {
|
|
1330
|
+
let newest: { contentHash: string; parts: number; held: Set<number> } | undefined;
|
|
1331
|
+
try {
|
|
1332
|
+
const branch = branchKey(this.contextManager.currentBranchRef());
|
|
1333
|
+
const since = this.unsentSince.get(branch);
|
|
1334
|
+
if (since === undefined) {
|
|
1335
|
+
const count = this.contextManager.getMessageCount();
|
|
1336
|
+
const top = count > 0 ? this.contextManager.getMessageWindow(count - 1, 1, { resolveBlobs: false }).messages[0]?.sequence ?? 0 : 0;
|
|
1337
|
+
this.unsentSince.set(branch, top);
|
|
1338
|
+
return undefined;
|
|
1339
|
+
}
|
|
1340
|
+
scan: for (let end = this.contextManager.getMessageCount(); end > 0;) {
|
|
1341
|
+
const span = Math.min(end, 64);
|
|
1342
|
+
const { messages } = this.contextManager.getMessageWindow(end - span, span, { resolveBlobs: false });
|
|
1343
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1344
|
+
const row = messages[i]!;
|
|
1345
|
+
if (row.sequence <= since) break scan;
|
|
1346
|
+
const mark = storedInjectionMarkOf(row.metadata);
|
|
1347
|
+
if (mark?.agent !== this.name || mark.namespace !== namespace) continue;
|
|
1348
|
+
// Read from the newest back, the first copy met is the newest
|
|
1349
|
+
// content; its other parts pool with it.
|
|
1350
|
+
newest ??= { contentHash: mark.contentHash, parts: mark.parts, held: new Set() };
|
|
1351
|
+
if (mark.contentHash === newest.contentHash) newest.held.add(mark.part);
|
|
1352
|
+
}
|
|
1353
|
+
end -= span;
|
|
1354
|
+
}
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
// Where the rows read before the failure held a copy, the newest of
|
|
1357
|
+
// them is still the newest content (any unread row is older), and a
|
|
1358
|
+
// part it lacks may be unread, so it's stored again at worst. Where
|
|
1359
|
+
// they held none, the last request's record decides, as it did before
|
|
1360
|
+
// this check: a copy a failed start stored is then stored again, or,
|
|
1361
|
+
// where the source has changed back to what that request held, left
|
|
1362
|
+
// newest for one more request. Either way the start goes on, where
|
|
1363
|
+
// failing here would lose its turn.
|
|
1364
|
+
console.error(`[injection] ${this.name}: could not read its window for copies stored since its last request, so ${newest ? 'the rows it read decide' : "that request's record decides"}:`, err);
|
|
1365
|
+
if (!newest) return undefined;
|
|
1366
|
+
}
|
|
1367
|
+
return newest && { contentHash: newest.contentHash, whole: newest.held.size === newest.parts };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/** Record which of this agent's stored injections a compile built to send
|
|
1371
|
+
* rendered. Another agent's stored rows never reach this agent's compile,
|
|
1372
|
+
* since every compile of a shared store leaves them out by its view
|
|
1373
|
+
* filter (isAnotherAgentsInjection): a compile of a shared store without
|
|
1374
|
+
* that filter would mix them in here. */
|
|
1375
|
+
private noteRenderedInjections(result: CompileResult): void {
|
|
1376
|
+
// The host's look at what this request carries (onCompiledToSend).
|
|
1377
|
+
if (this.onCompiledToSend) {
|
|
1378
|
+
try {
|
|
1379
|
+
this.onCompiledToSend(result.messages);
|
|
1380
|
+
} catch (err) {
|
|
1381
|
+
console.error(`[agent] ${this.name}: the host's compile observer failed:`, err);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const provenance = result.provenance;
|
|
1385
|
+
// The rows stored before this compile on its branch have been compiled
|
|
1386
|
+
// to send, whether the request held them or a fold or the strategy's
|
|
1387
|
+
// window took them (the record below says which): the first plan there
|
|
1388
|
+
// after it, a start's or a preview's, sets the point again
|
|
1389
|
+
// (unsentInjection). Without provenance, which branch isn't known, so
|
|
1390
|
+
// every branch's point goes, rather than one surviving a compile that
|
|
1391
|
+
// held its rows.
|
|
1392
|
+
if (provenance) this.unsentSince.delete(branchKey(provenance.branch));
|
|
1393
|
+
else this.unsentSince.clear();
|
|
1394
|
+
if (!provenance) {
|
|
1395
|
+
this.renderedInjections = undefined;
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const byNamespace = new Map<string, Map<string, { parts: number; held: Set<number>; sequence: number }>>();
|
|
1399
|
+
for (const [i, source] of provenance.messages.entries()) {
|
|
1400
|
+
// Only a message rendered as an injection's participant can be one, so
|
|
1401
|
+
// the store is read for those alone.
|
|
1402
|
+
if (source.kind !== 'raw' || !result.messages[i]?.participant.startsWith('system_context:')) continue;
|
|
1403
|
+
for (const body of source.bodies) {
|
|
1404
|
+
const mark = this.storedMarkOf(body.messageId);
|
|
1405
|
+
if (!mark) continue;
|
|
1406
|
+
// A part counts as held when all its shards are there. One the
|
|
1407
|
+
// strategy altered (cut at its per-message cap, an image stripped)
|
|
1408
|
+
// counts too: a new copy would meet the same, so storing again
|
|
1409
|
+
// would only add copies, every start. A part missing shards still
|
|
1410
|
+
// says which content is newest.
|
|
1411
|
+
const held = body.complete || (body.missing !== undefined && !body.missing.includes('shards'));
|
|
1412
|
+
const contents = byNamespace.get(mark.namespace) ?? new Map();
|
|
1413
|
+
byNamespace.set(mark.namespace, contents);
|
|
1414
|
+
// Copies of the same content in view pool what they hold: the agent
|
|
1415
|
+
// has each part that one of them holds.
|
|
1416
|
+
const known = contents.get(mark.contentHash) ?? { parts: mark.parts, held: new Set<number>(), sequence: body.sequence };
|
|
1417
|
+
if (held) known.held.add(mark.part);
|
|
1418
|
+
known.sequence = Math.max(known.sequence, body.sequence);
|
|
1419
|
+
contents.set(mark.contentHash, known);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
this.renderedInjections = { branch: provenance.branch, byNamespace };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* The newest content of `namespace` among this agent's stored injections
|
|
1427
|
+
* that the last compile built to send rendered, by its newest copy: its
|
|
1428
|
+
* content hash, and whether every part of that content was there in some
|
|
1429
|
+
* copy, every shard, however the strategy shaped it. Undefined when none
|
|
1430
|
+
* was, when this agent hasn't built a request since it started, or when
|
|
1431
|
+
* the window's branch has changed since.
|
|
1432
|
+
*/
|
|
1433
|
+
renderedInjection(namespace: string): { contentHash: string; whole: boolean } | undefined {
|
|
1434
|
+
const rendered = this.renderedInjections;
|
|
1435
|
+
const branch = this.contextManager.currentBranchRef();
|
|
1436
|
+
if (!rendered || rendered.branch.id !== branch.id || rendered.branch.created !== branch.created) return undefined;
|
|
1437
|
+
let newest: [string, { parts: number; held: Set<number>; sequence: number }] | undefined;
|
|
1438
|
+
for (const entry of rendered.byNamespace.get(namespace) ?? []) {
|
|
1439
|
+
if (!newest || entry[1].sequence > newest[1].sequence) newest = entry;
|
|
1440
|
+
}
|
|
1441
|
+
return newest ? { contentHash: newest[0], whole: newest[1].held.size === newest[1].parts } : undefined;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Whether compiled message `i` of `result` is a row of this agent's stored
|
|
1446
|
+
* context injections (stored-injections.ts): rendered as an injection's
|
|
1447
|
+
* participant, and each of its stored rows, by the compile's provenance,
|
|
1448
|
+
* carrying this agent's mark. A speaker merely named like an injection's
|
|
1449
|
+
* participant isn't one. Only such a message's rows are read, so a start
|
|
1450
|
+
* reads the store for those alone.
|
|
1451
|
+
*/
|
|
1452
|
+
private isStoredInjectionRow(result: CompileResult, i: number): boolean {
|
|
1453
|
+
const source = result.provenance?.messages[i];
|
|
1454
|
+
return source?.kind === 'raw' && source.bodies.length > 0
|
|
1455
|
+
&& !!result.messages[i]?.participant.startsWith('system_context:')
|
|
1456
|
+
&& source.bodies.every((b) => this.storedMarkOf(b.messageId)?.agent === this.name);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/** A stored row's injection mark, or undefined for any other row, and for
|
|
1460
|
+
* one the store can't read just now, which is logged: a start doesn't fail
|
|
1461
|
+
* on it (the row then counts as no stored injection). */
|
|
1462
|
+
private storedMarkOf(id: MessageId): ReturnType<typeof storedInjectionMarkOf> {
|
|
1463
|
+
try {
|
|
1464
|
+
return storedInjectionMarkOf(this.contextManager.getMessage(id)?.metadata);
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
console.error(`[injection] ${this.name}: could not read stored row ${id}, so it counts as no stored injection:`, err);
|
|
1467
|
+
return undefined;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1080
1471
|
/**
|
|
1081
1472
|
* Compile the context with injections (e.g. from MCPL servers), which the
|
|
1082
1473
|
* context manager merges into the compiled messages. Without
|
|
@@ -1088,13 +1479,12 @@ export class Agent {
|
|
|
1088
1479
|
async compileWithInjections(
|
|
1089
1480
|
budget?: TokenBudget,
|
|
1090
1481
|
injections?: ContextInjection[],
|
|
1091
|
-
opts?: { kvUnifiedImmutablePrefixHash?: string; dryRun?: boolean },
|
|
1482
|
+
opts?: { kvUnifiedImmutablePrefixHash?: string; dryRun?: boolean } & BindingCompileOptions,
|
|
1092
1483
|
): Promise<CompileResult> {
|
|
1093
1484
|
// A dry run is a compile that will never be sent. The context manager
|
|
1094
1485
|
// persists no fold resolutions for it and queues none of the compression
|
|
1095
|
-
// or merge work its fold plan calls for
|
|
1096
|
-
// (
|
|
1097
|
-
// compile awaiting acceptance for a committing compile only. Here nothing
|
|
1486
|
+
// or merge work its fold plan calls for, and, with the thinking binding
|
|
1487
|
+
// (`thinkingBinding.binds`), writes no binding record. Here nothing
|
|
1098
1488
|
// moves either: the agent never read what a dry run assembled, so the
|
|
1099
1489
|
// consumed watermark stays. A dry run proves nothing about the budget
|
|
1100
1490
|
// either, so it goes straight to the compile, around
|
|
@@ -1135,7 +1525,13 @@ export class Agent {
|
|
|
1135
1525
|
|
|
1136
1526
|
/**
|
|
1137
1527
|
* Run inference and return result with tool calls and speech content.
|
|
1138
|
-
* Updates agent state during execution.
|
|
1528
|
+
* Updates agent state during execution. Its compile commits. With
|
|
1529
|
+
* `thinkingBinding.binds` it also writes the context manager's
|
|
1530
|
+
* thinking-binding record, so then call it only while none of this agent's
|
|
1531
|
+
* requests is streaming: a stream open meanwhile then has its later notes
|
|
1532
|
+
* write nothing, and its replies go without their thinking at the next
|
|
1533
|
+
* compile. Its own reply is never noted, so with `binds` its thinking is
|
|
1534
|
+
* left out at the compile after it's stored.
|
|
1139
1535
|
*/
|
|
1140
1536
|
async runInference(
|
|
1141
1537
|
availableTools: ToolDefinition[],
|
|
@@ -1147,7 +1543,9 @@ export class Agent {
|
|
|
1147
1543
|
|
|
1148
1544
|
/**
|
|
1149
1545
|
* Run inference with context injections.
|
|
1150
|
-
* Same as runInference but passes injections through to compile.
|
|
1546
|
+
* Same as runInference but passes injections through to compile. Its
|
|
1547
|
+
* compile commits, and with `thinkingBinding.binds` writes the
|
|
1548
|
+
* thinking-binding record, as runInference's does (see there).
|
|
1151
1549
|
*/
|
|
1152
1550
|
async runInferenceWithInjections(
|
|
1153
1551
|
availableTools: ToolDefinition[],
|
|
@@ -1191,9 +1589,14 @@ export class Agent {
|
|
|
1191
1589
|
// been retried; one whose store still fails is compiled as the bare
|
|
1192
1590
|
// notice.
|
|
1193
1591
|
let unguardedResults: ReturnType<Agent['buildToolResultMessages']> | undefined;
|
|
1592
|
+
const binding = this.bindingCompileOptions(tools, (injections ?? [])
|
|
1593
|
+
.filter((injection) => injection.position === 'system')
|
|
1594
|
+
.flatMap((injection) => injection.content));
|
|
1194
1595
|
const assemble = async (): Promise<NormalizedRequest> => {
|
|
1195
1596
|
const compiled = await this.toolResultGuard.afterAnnotations(
|
|
1196
|
-
() => this.compileWithInjections(budget, injections));
|
|
1597
|
+
() => this.compileWithInjections(budget, injections, binding.thinkingBinding ? binding : undefined));
|
|
1598
|
+
// What it holds of the stored injections is what the agent last saw.
|
|
1599
|
+
this.noteRenderedInjections(compiled);
|
|
1197
1600
|
const { systemInjections } = compiled;
|
|
1198
1601
|
// The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
|
|
1199
1602
|
const messages = withoutTokenEstimates(compiled.messages);
|
|
@@ -1322,7 +1725,10 @@ export class Agent {
|
|
|
1322
1725
|
* watermark); a preview passes `options.dryRun`, so its compile commits
|
|
1323
1726
|
* none of that (see `compileWithInjections` for what a dry run still
|
|
1324
1727
|
* leaves). Either way the context manager is handed the compression tool
|
|
1325
|
-
* definitions. Safe to call regardless of the agent's current status
|
|
1728
|
+
* definitions. Safe to call regardless of the agent's current status,
|
|
1729
|
+
* except that with `thinkingBinding.binds` a compile that isn't a dry run
|
|
1730
|
+
* writes the context manager's binding record, so while one of the
|
|
1731
|
+
* agent's requests streams, a call to look passes `options.dryRun`.
|
|
1326
1732
|
* It doesn't wait for the tool-result guard's annotations, so a preview
|
|
1327
1733
|
* never waits on a workspace write (rollback, undo and redo report one), and
|
|
1328
1734
|
* may show a withheld stub before its place is added; the activation waits
|
|
@@ -1334,7 +1740,7 @@ export class Agent {
|
|
|
1334
1740
|
budget?: TokenBudget,
|
|
1335
1741
|
compressionTools: ToolDefinition[] = availableTools,
|
|
1336
1742
|
options: ActivationRequestOptions = {},
|
|
1337
|
-
): Promise<{ request: NormalizedRequest; continuePrompt?: RequestOnlyPrompt }> {
|
|
1743
|
+
): Promise<{ request: NormalizedRequest; continuePrompt?: RequestOnlyPrompt; compiled: CompiledForRequest; binding: BindingCompileOptions }> {
|
|
1338
1744
|
// Unstored, so they leave for the request here (text-as-stored.ts), the
|
|
1339
1745
|
// layout hash below included.
|
|
1340
1746
|
injections = injectionsAsStored(injections);
|
|
@@ -1363,16 +1769,63 @@ export class Agent {
|
|
|
1363
1769
|
system: this.buildSystemPrompt(prospectiveSystemInjections),
|
|
1364
1770
|
})
|
|
1365
1771
|
: undefined;
|
|
1366
|
-
|
|
1772
|
+
const binding = this.bindingCompileOptions(availableTools, prospectiveSystemInjections);
|
|
1773
|
+
const compiledResult = await this.compileWithInjections(
|
|
1367
1774
|
budget,
|
|
1368
1775
|
injections,
|
|
1369
|
-
immutablePrefixHash || options.dryRun
|
|
1776
|
+
immutablePrefixHash || options.dryRun || binding.thinkingBinding
|
|
1370
1777
|
? {
|
|
1371
1778
|
...(immutablePrefixHash ? { kvUnifiedImmutablePrefixHash: immutablePrefixHash } : {}),
|
|
1372
1779
|
...(options.dryRun ? { dryRun: true } : {}),
|
|
1780
|
+
...binding,
|
|
1373
1781
|
}
|
|
1374
1782
|
: undefined,
|
|
1375
1783
|
);
|
|
1784
|
+
let { messages } = compiledResult;
|
|
1785
|
+
const { systemInjections } = compiledResult;
|
|
1786
|
+
// It counts what the compile selected, so a preview's stand-ins
|
|
1787
|
+
// (appendedMessages, below) aren't in it.
|
|
1788
|
+
const { renderStats } = compiledResult;
|
|
1789
|
+
// What the request is built from, as the compile returned it: a block the provider drops is found again in
|
|
1790
|
+
// it, through the copies below, by its signature (placeThinkingDrops).
|
|
1791
|
+
const recordedAfter = compiledResult.thinkingRecordedAfter;
|
|
1792
|
+
const compiled: CompiledForRequest = {
|
|
1793
|
+
messages: compiledResult.messages,
|
|
1794
|
+
...(compiledResult.provenance ? { provenance: compiledResult.provenance } : {}),
|
|
1795
|
+
...(recordedAfter !== undefined ? { recordedAfter } : {}),
|
|
1796
|
+
};
|
|
1797
|
+
// Each message's place in the compile, or a preview's stand-in (null),
|
|
1798
|
+
// for the count of stored rows the request ends on, below.
|
|
1799
|
+
let origin: Array<number | null> = messages.map((_, i) => i);
|
|
1800
|
+
// A request that's sent says which stored injections the agent now has in
|
|
1801
|
+
// view, which the next start's same-content skip reads (stored-injections.ts).
|
|
1802
|
+
if (!options.dryRun) this.noteRenderedInjections(compiledResult);
|
|
1803
|
+
const appended = options.appendedMessages ?? [];
|
|
1804
|
+
if (appended.length > 0) {
|
|
1805
|
+
// Where a start's compile would render them, as the window's newest
|
|
1806
|
+
// rows: last, unless the window has no user message, where the compile
|
|
1807
|
+
// places the injections it's given at the end, after the window.
|
|
1808
|
+
const sources = compiledResult.provenance?.messages;
|
|
1809
|
+
let at = messages.length;
|
|
1810
|
+
if (sources && !messages.some((m, i) => sources[i]?.kind !== 'injection' && m.participant.toLowerCase() === 'user')) {
|
|
1811
|
+
while (at > 0 && sources[at - 1]?.kind === 'injection') at--;
|
|
1812
|
+
}
|
|
1813
|
+
origin = [...origin.slice(0, at), ...appended.map(() => null), ...origin.slice(at)];
|
|
1814
|
+
messages = [...messages.slice(0, at), ...appended, ...messages.slice(at)];
|
|
1815
|
+
}
|
|
1816
|
+
// The stored context injections the request ends on, counted over the
|
|
1817
|
+
// messages the sanitizer below keeps (hasRenderedContent), since it drops
|
|
1818
|
+
// a message it leaves empty, a placed blank injection among them.
|
|
1819
|
+
// [Continue] goes after them, and a tool loop's prompt is read from
|
|
1820
|
+
// before them: they're context, not what woke the agent. An injection
|
|
1821
|
+
// compile() places ends the tail, as on main, where every one was placed.
|
|
1822
|
+
let injectionTail = 0;
|
|
1823
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1824
|
+
if (!hasRenderedContent(messages[i]!.content)) continue;
|
|
1825
|
+
const from = origin[i] ?? null;
|
|
1826
|
+
if (from !== null && !this.isStoredInjectionRow(compiledResult, from)) break;
|
|
1827
|
+
injectionTail++;
|
|
1828
|
+
}
|
|
1376
1829
|
|
|
1377
1830
|
// Sanitize: strip empty/whitespace text blocks and drop messages left with
|
|
1378
1831
|
// no content. The Anthropic API rejects empty text blocks with 400
|
|
@@ -1385,15 +1838,10 @@ export class Agent {
|
|
|
1385
1838
|
// 2026-07-10 on a resident agent: one empty block muted the agent's live
|
|
1386
1839
|
// path entirely; twin of context-manager's stripEmptyTextBlocks on the
|
|
1387
1840
|
// compression path.)
|
|
1841
|
+
// The same pass a tool boundary applies to what it notes as sent
|
|
1842
|
+
// (sent-rows.ts), so the two can't drift.
|
|
1388
1843
|
messages = messages
|
|
1389
|
-
.map((m) => ({
|
|
1390
|
-
...m,
|
|
1391
|
-
content: m.content.filter(
|
|
1392
|
-
// typeof guard is deliberate runtime defense: history loaded from
|
|
1393
|
-
// disk can carry a non-string `text` despite what the types claim.
|
|
1394
|
-
(b: ContentBlock) => !(b.type === "text" && (typeof b.text !== "string" || b.text.trim() === "")),
|
|
1395
|
-
),
|
|
1396
|
-
}))
|
|
1844
|
+
.map((m) => ({ ...m, content: m.content.filter(carriedBlock) }))
|
|
1397
1845
|
.filter((m) => m.content.length > 0);
|
|
1398
1846
|
// The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
|
|
1399
1847
|
messages = withoutTokenEstimates(messages);
|
|
@@ -1419,21 +1867,29 @@ export class Agent {
|
|
|
1419
1867
|
|
|
1420
1868
|
// The [Continue] prompt this request's reply follows, read from the
|
|
1421
1869
|
// request itself: a new one when it ends on [Continue]; else, when it
|
|
1422
|
-
// ends on tool results
|
|
1423
|
-
//
|
|
1870
|
+
// ends on tool results (apart from stored context injections after
|
|
1871
|
+
// them), the prompt of the reply that called those tools, whose tool
|
|
1872
|
+
// loop this reply continues; else none. A request that ends
|
|
1424
1873
|
// on anything else (a message that arrived during a cooldown, say) gets
|
|
1425
1874
|
// a reply that follows that message and carries no prompt.
|
|
1426
1875
|
let continuePrompt: RequestOnlyPrompt | undefined;
|
|
1427
1876
|
if (openingTick) {
|
|
1428
1877
|
messages = [...messages, silentHeartbeatSeparatorTurn()];
|
|
1429
|
-
} else if (messages
|
|
1878
|
+
} else if (endsOnOwnMessage(messages, this.name, injectionTail)) {
|
|
1430
1879
|
// Safety: ensure messages don't end with an assistant message.
|
|
1431
1880
|
// Some models reject trailing assistant messages ("prefill not supported"),
|
|
1432
1881
|
// and after context compression a stale assistant turn can end up last.
|
|
1882
|
+
// Where stored context injections follow the agent's message, as a
|
|
1883
|
+
// changed one does on a wake with nothing new, [Continue] goes after
|
|
1884
|
+
// them, so the cue stays last: the block isn't taken for what woke
|
|
1885
|
+
// the agent. Its reply's promptedBy puts it there in later compiles too,
|
|
1886
|
+
// where they can recognise the reply (renderRequestOnlyRows).
|
|
1433
1887
|
messages = [...messages, continueTurn()];
|
|
1434
1888
|
continuePrompt = { turn: randomUUID(), text: CONTINUE_ROW };
|
|
1435
1889
|
} else {
|
|
1436
|
-
|
|
1890
|
+
// What the request ends on apart from the stored injections after it,
|
|
1891
|
+
// which aren't something the reply answers.
|
|
1892
|
+
continuePrompt = toolLoopPrompt(messages.at(-1 - injectionTail), index);
|
|
1437
1893
|
}
|
|
1438
1894
|
|
|
1439
1895
|
const request: NormalizedRequest = {
|
|
@@ -1443,25 +1899,27 @@ export class Agent {
|
|
|
1443
1899
|
...this.requestSettings(),
|
|
1444
1900
|
...(kvUnified ? { cacheMarkers: 'cm-owned' as const } : {}),
|
|
1445
1901
|
};
|
|
1446
|
-
|
|
1902
|
+
if (options.dryRun) dryCompileStats.set(request, renderStats);
|
|
1903
|
+
return { request, ...(continuePrompt ? { continuePrompt } : {}), compiled, binding };
|
|
1447
1904
|
}
|
|
1448
1905
|
|
|
1449
1906
|
/**
|
|
1450
1907
|
* The request fields that come from this agent's own settings: its model
|
|
1451
|
-
* config, prompt caching and cache TTL, prefill message,
|
|
1452
|
-
* parameters and participant name. Every request the agent sends
|
|
1453
|
-
* them from here, a framework turn's (assembleActivationRequest) and
|
|
1454
|
-
* direct API's (runInferenceWithInjections), so a setting reaches both
|
|
1455
|
-
* neither.
|
|
1908
|
+
* config, thinking binding, prompt caching and cache TTL, prefill message,
|
|
1909
|
+
* provider parameters and participant name. Every request the agent sends
|
|
1910
|
+
* takes them from here, a framework turn's (assembleActivationRequest) and
|
|
1911
|
+
* the direct API's (runInferenceWithInjections), so a setting reaches both
|
|
1912
|
+
* or neither.
|
|
1456
1913
|
*/
|
|
1457
1914
|
private requestSettings(): Pick<NormalizedRequest,
|
|
1458
1915
|
'config' | 'promptCaching' | 'cacheTtl' | 'prefillUserMessage' | 'providerParams' | 'assistantParticipant'> {
|
|
1916
|
+
const thinking = this.requestThinking();
|
|
1459
1917
|
return {
|
|
1460
1918
|
config: {
|
|
1461
1919
|
model: this.model,
|
|
1462
1920
|
maxTokens: this.maxTokens,
|
|
1463
1921
|
...(this.temperature !== undefined && { temperature: this.temperature }),
|
|
1464
|
-
...(
|
|
1922
|
+
...(thinking !== undefined && { thinking }),
|
|
1465
1923
|
},
|
|
1466
1924
|
promptCaching: this.promptCaching,
|
|
1467
1925
|
cacheTtl: this.cacheTtl,
|
|
@@ -1471,6 +1929,71 @@ export class Agent {
|
|
|
1471
1929
|
};
|
|
1472
1930
|
}
|
|
1473
1931
|
|
|
1932
|
+
/**
|
|
1933
|
+
* The thinking config a request carries: the agent's own, with the binding
|
|
1934
|
+
* control added when `thinkingBinding.dropBlock` asks for it. A model whose
|
|
1935
|
+
* thinking is always on takes the control whatever `enabled` says, so an
|
|
1936
|
+
* agent with no thinking config of its own still sends one, holding only
|
|
1937
|
+
* the control and `enabled: false`. Membrane sends nothing from it to any
|
|
1938
|
+
* other model, since thinking is off there.
|
|
1939
|
+
*/
|
|
1940
|
+
private requestThinking(): NonNullable<NormalizedRequest['config']['thinking']> | undefined {
|
|
1941
|
+
if (!this.thinkingDropBlock) return this.thinking;
|
|
1942
|
+
const own = this.thinking !== null && typeof this.thinking === 'object' ? this.thinking : { enabled: false };
|
|
1943
|
+
return { ...own, blockBinding: { prefixMismatchBehavior: 'drop_block' } };
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** The stream's way to tell the context manager what its later rounds sent, with the head its compile was given:
|
|
1947
|
+
* where the agent's model binds its thinking (context-manager's `noteRequestSent`). */
|
|
1948
|
+
private requestNotes(binding: BindingCompileOptions, compiled: CompiledForRequest): Pick<StartStreamResult, 'noteRequestSent'> {
|
|
1949
|
+
const identity = binding.identity;
|
|
1950
|
+
// Each note names the compile the stream was built from, so a note can't vouch for this stream under a record a
|
|
1951
|
+
// later committing compile with binds wrote. A compile that reports no provenance gives the stream nothing to name, and the
|
|
1952
|
+
// stream notes nothing: its replies go without their thinking at the next compile, never refused.
|
|
1953
|
+
const compileId = compiled.provenance?.compileId;
|
|
1954
|
+
if (!identity || compileId === undefined) return {};
|
|
1955
|
+
const cm = this.contextManager;
|
|
1956
|
+
return { noteRequestSent: (rows) => cm.noteRequestSent(rows, identity, compileId) };
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* The binding a compile of this agent is told of (bindingCompileOptions), and a preview of its context too: the
|
|
1961
|
+
* participant whose messages are its replies is its own name. Undefined where the model doesn't bind.
|
|
1962
|
+
* @internal For the framework's preview and its budget rule; not a host API.
|
|
1963
|
+
*/
|
|
1964
|
+
thinkingBindingOption(): { binds: true; dropBlock: boolean; participant: string } | undefined {
|
|
1965
|
+
return this.thinkingBinds ? { binds: true, dropBlock: this.thinkingDropBlock, participant: this.name } : undefined;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* What a compile the agent sends tells the context manager when the model binds its thinking
|
|
1970
|
+
* (`thinkingBinding.binds`): the binding as set, with the agent's own name as the participant whose messages
|
|
1971
|
+
* are its replies (an agent stores its replies under its name), and a digest of each part of the request ahead of
|
|
1972
|
+
* its messages, so a changed one can stand for a first difference where the context manager keeps a
|
|
1973
|
+
* thinking-binding record. The system text is the one the request will carry, built from the system-position
|
|
1974
|
+
* injections given to the compile as the kv-unified prefix hash builds it; the tools are the ones it will carry, in
|
|
1975
|
+
* their order; `request` covers the thinking config, `maxTokens` and `providerParams`, where a host puts its own
|
|
1976
|
+
* thinking config and `tool_choice`, and `prefillUserMessage`, which membrane sends as a leading user turn ahead
|
|
1977
|
+
* of a conversation that would begin with the assistant. Nothing without `binds`: the compile is as it was.
|
|
1978
|
+
*/
|
|
1979
|
+
private bindingCompileOptions(tools: readonly ToolDefinition[], systemInjections: ContentBlock[]): BindingCompileOptions {
|
|
1980
|
+
const thinkingBinding = this.thinkingBindingOption();
|
|
1981
|
+
if (!thinkingBinding) return {};
|
|
1982
|
+
const { config, providerParams, prefillUserMessage } = this.requestSettings();
|
|
1983
|
+
return {
|
|
1984
|
+
thinkingBinding,
|
|
1985
|
+
identity: {
|
|
1986
|
+
system: wireDigest(this.buildSystemPrompt(systemInjections)),
|
|
1987
|
+
tools: wireDigest(tools),
|
|
1988
|
+
model: wireDigest(this.model),
|
|
1989
|
+
request: wireDigest({
|
|
1990
|
+
thinking: config.thinking ?? null, maxTokens: config.maxTokens, providerParams: providerParams ?? null,
|
|
1991
|
+
prefillUserMessage: prefillUserMessage ?? null,
|
|
1992
|
+
}),
|
|
1993
|
+
},
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1474
1997
|
/** The request `assembleActivationRequest` builds (see there), for preview
|
|
1475
1998
|
* and debug tooling: what this agent's next activation would send. */
|
|
1476
1999
|
async buildActivationRequest(
|
|
@@ -1563,7 +2086,7 @@ export class Agent {
|
|
|
1563
2086
|
// Compiled only once each withheld stub's annotation has finished,
|
|
1564
2087
|
// including one that began during the compile, and after a stub edit that
|
|
1565
2088
|
// failed has been retried (see afterAnnotations).
|
|
1566
|
-
const { request, continuePrompt } = await this.toolResultGuard.afterAnnotations(
|
|
2089
|
+
const { request, continuePrompt, compiled, binding } = await this.toolResultGuard.afterAnnotations(
|
|
1567
2090
|
() => this.assembleActivationRequest(availableTools, injections, budget, compressionTools, options));
|
|
1568
2091
|
request.messages = this.toolResultGuard.prepareRequest(request.messages, true);
|
|
1569
2092
|
|
|
@@ -1595,6 +2118,12 @@ export class Agent {
|
|
|
1595
2118
|
emitTokens: true,
|
|
1596
2119
|
emitBlocks: false,
|
|
1597
2120
|
emitUsage: true,
|
|
2121
|
+
// A caller that retries itself (the framework's turns: errorPolicy,
|
|
2122
|
+
// provider holds, stated waits, each attempt a fresh compile a stop can
|
|
2123
|
+
// cancel) has membrane send a failed round once and hand the error up:
|
|
2124
|
+
// since membrane 0.7 its own schedules, ten minutes at a 529, would
|
|
2125
|
+
// otherwise run inside each of the caller's attempts, out of its sight.
|
|
2126
|
+
...(options.membraneRetries === false ? { retry: false } : {}),
|
|
1598
2127
|
// Retry a content-policy refusal at the provider seam rather than at
|
|
1599
2128
|
// the framework level: membrane replays the SAME request immediately
|
|
1600
2129
|
// (cache-warm), where a framework-level requeue would recompile and
|
|
@@ -1614,6 +2143,10 @@ export class Agent {
|
|
|
1614
2143
|
stream,
|
|
1615
2144
|
request,
|
|
1616
2145
|
...(continuePrompt ? { continuePrompt } : {}),
|
|
2146
|
+
// Read against the messages the stream sends, which membrane's report indexes (RoundReport's coordinates).
|
|
2147
|
+
placeDrops: (entries) => placeThinkingDrops(entries, request.messages, compiled),
|
|
2148
|
+
...this.requestNotes(binding, compiled),
|
|
2149
|
+
...(compiled.recordedAfter !== undefined ? { thinkingRecordedAfter: compiled.recordedAfter } : {}),
|
|
1617
2150
|
...(kvEnabled
|
|
1618
2151
|
? {
|
|
1619
2152
|
takeKvSubmission: () => kvQueue.shift(),
|
|
@@ -1667,7 +2200,7 @@ export class Agent {
|
|
|
1667
2200
|
* Add an assistant response to context.
|
|
1668
2201
|
* Called by framework when stream completes.
|
|
1669
2202
|
*/
|
|
1670
|
-
addAssistantResponse(content: ContentBlock[], metadata?: MessageMetadata):
|
|
2203
|
+
addAssistantResponse(content: ContentBlock[], metadata?: MessageMetadata): MessageId | undefined {
|
|
1671
2204
|
// A turn whose entire output is thinking blocks produced NOTHING: no
|
|
1672
2205
|
// speech, no tool call. That is what a refusal looks like on the wire —
|
|
1673
2206
|
// the provider returns signed thinking (often with empty text, the
|
|
@@ -1691,9 +2224,9 @@ export class Agent {
|
|
|
1691
2224
|
`message (${content.length} block(s), no text and no tool_use) — the turn produced no ` +
|
|
1692
2225
|
`output. See the refusal/inference-failed record for why.`,
|
|
1693
2226
|
);
|
|
1694
|
-
return;
|
|
2227
|
+
return undefined;
|
|
1695
2228
|
}
|
|
1696
|
-
this.contextManager.addMessage(this.name, content, metadata);
|
|
2229
|
+
return this.contextManager.addMessage(this.name, content, metadata);
|
|
1697
2230
|
}
|
|
1698
2231
|
|
|
1699
2232
|
/**
|
|
@@ -1938,7 +2471,7 @@ export class Agent {
|
|
|
1938
2471
|
toolUseId: r.id,
|
|
1939
2472
|
content: isError
|
|
1940
2473
|
? truncateForHistory(typeof error === 'string' ? error.toWellFormed() : error as string, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS)
|
|
1941
|
-
: nonErrorResultText(r.result, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS),
|
|
2474
|
+
: nonErrorResultText(r.result, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, { toolName: r.name }),
|
|
1942
2475
|
isError,
|
|
1943
2476
|
};
|
|
1944
2477
|
});
|