@animalabs/agent-framework 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent.d.ts +5 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +27 -8
- package/dist/src/agent.js.map +1 -1
- package/dist/src/clock-rows.d.ts +115 -0
- package/dist/src/clock-rows.d.ts.map +1 -0
- package/dist/src/clock-rows.js +222 -0
- package/dist/src/clock-rows.js.map +1 -0
- package/dist/src/framework.d.ts +98 -6
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +514 -80
- package/dist/src/framework.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +9 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +49 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/protocol-family.js +1 -1
- package/dist/src/mcpl/references.d.ts +4 -1
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +5 -2
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +9 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +118 -28
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +6 -2
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/text-as-stored.d.ts +31 -0
- package/dist/src/text-as-stored.d.ts.map +1 -0
- package/dist/src/text-as-stored.js +70 -0
- package/dist/src/text-as-stored.js.map +1 -0
- package/dist/src/timezone.d.ts +4 -2
- package/dist/src/timezone.d.ts.map +1 -1
- package/dist/src/timezone.js +4 -2
- package/dist/src/timezone.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts +46 -7
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +60 -7
- 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 +9 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +10 -8
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +12 -10
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +17 -6
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +35 -10
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.js +3 -3
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +13 -3
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +7 -4
- package/dist/src/types/framework.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +27 -8
- package/src/clock-rows.ts +302 -0
- package/src/framework.ts +538 -85
- package/src/index.ts +2 -0
- package/src/mcpl/channel-registry.ts +54 -8
- package/src/mcpl/protocol-family.ts +1 -1
- package/src/mcpl/references.ts +5 -2
- package/src/modules/workspace/index.ts +121 -29
- package/src/modules/workspace/types.ts +6 -2
- package/src/text-as-stored.ts +73 -0
- package/src/timezone.ts +4 -2
- package/src/tool-image-ledger.ts +62 -8
- package/src/tool-presentation.ts +8 -1
- package/src/tool-result-guard.ts +12 -10
- package/src/tool-result-history.ts +35 -9
- package/src/tune-out/coordinator.ts +3 -3
- package/src/types/agent.ts +28 -0
- package/src/types/events.ts +13 -3
- package/src/types/framework.ts +7 -4
package/src/framework.ts
CHANGED
|
@@ -124,12 +124,13 @@ import {
|
|
|
124
124
|
import {
|
|
125
125
|
TOOL_IMAGE_REF_RE,
|
|
126
126
|
ToolImageLedger,
|
|
127
|
-
|
|
127
|
+
cutStoredToolResultToFit,
|
|
128
128
|
formatKeptImageSlots,
|
|
129
129
|
parseImagePlaceholders,
|
|
130
130
|
sha256Hex,
|
|
131
131
|
storedImageSlots,
|
|
132
132
|
toolResultCutAt,
|
|
133
|
+
toolResultShare,
|
|
133
134
|
type ParsedImagePlaceholder,
|
|
134
135
|
type ShownRanges,
|
|
135
136
|
} from './tool-image-ledger.js';
|
|
@@ -156,6 +157,7 @@ import { bounded, renderList, renderReturn, VALUE_CHARS } from './code-execution
|
|
|
156
157
|
import { splitProseRuns, withoutRoundsProse } from './prose-segments.js';
|
|
157
158
|
import { cumulativeDelta } from './usage-accounting.js';
|
|
158
159
|
import { stampThinkingTokenEstimates } from './thinking-token-stamp.js';
|
|
160
|
+
import { contentAsStored, nameAsStored } from './text-as-stored.js';
|
|
159
161
|
|
|
160
162
|
/** An MCPL tool call refused because its server isn't in the registry: one
|
|
161
163
|
* closed for good, one removed, or one whose connect failed without
|
|
@@ -413,9 +415,12 @@ function malformedResponseContent(response: EventResponse): { where: string; pla
|
|
|
413
415
|
* mid-turn message's text at chars/4 and its other blocks about as
|
|
414
416
|
* context-manager's store prices them (injectedBlockTokens), and a small
|
|
415
417
|
* per-block envelope allowance. On membrane's native tool path a mid-turn
|
|
416
|
-
* message rides into the next round
|
|
417
|
-
*
|
|
418
|
-
*
|
|
418
|
+
* message rides into the next round as the boundary injects it, which is as
|
|
419
|
+
* the strategy renders it for the next compile, cut and with its images
|
|
420
|
+
* limited (renderForInjection), or its stored content where the strategy
|
|
421
|
+
* can't say (provideToolResults' injectedMessages). Its XML tool path declares that it
|
|
422
|
+
* can't carry injected messages, so the tool boundary injects none there and
|
|
423
|
+
* that term is zero. Deliberately a
|
|
419
424
|
* ceiling-ish heuristic, but for a mid-turn message's media at the store's
|
|
420
425
|
* flat prices — the cost of overestimating is one early recompile;
|
|
421
426
|
* the cost of underestimating is the 400 this projection exists to prevent.
|
|
@@ -693,6 +698,7 @@ import type {
|
|
|
693
698
|
} from './mcpl/types.js';
|
|
694
699
|
import type { ContextInjection } from '@animalabs/context-manager';
|
|
695
700
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
701
|
+
import { CLOCK_ROW_KIND, clockStampOf, planClock, takesClockBefore } from './clock-rows.js';
|
|
696
702
|
import { firstFreeBranchName } from './branch-names.js';
|
|
697
703
|
import {
|
|
698
704
|
DEFAULT_DISCORD_AWARENESS_EMOJI,
|
|
@@ -841,10 +847,13 @@ type InboundResponder = {
|
|
|
841
847
|
* For an entry whose write threw after storing it, found in the store and
|
|
842
848
|
* counted as written, it is what landed, joined the same way, which for a
|
|
843
849
|
* sharded body can be its first shards alone. A store that can't be read
|
|
844
|
-
* back (storedCopiesOf logs it) leaves a sharded body as given.
|
|
850
|
+
* back (storedCopiesOf logs it) leaves a sharded body as given. `parts` is
|
|
851
|
+
* how many store messages hold it, as read back: a sharded body's shards,
|
|
852
|
+
* consecutive from `id`, and 1 where it couldn't be read back. */
|
|
845
853
|
interface DrainedWrite {
|
|
846
854
|
id: MessageId;
|
|
847
855
|
content: ContentBlock[];
|
|
856
|
+
parts: number;
|
|
848
857
|
}
|
|
849
858
|
|
|
850
859
|
/** What later compiles render from the store for the copies of one write
|
|
@@ -860,12 +869,33 @@ function asCompiled(landed: ReadonlyArray<{ content: ContentBlock[] }>): Content
|
|
|
860
869
|
const text = blocks.filter((b) => b.type === 'text').map((b) => (b as { text: string }).text).join('');
|
|
861
870
|
return [...blocks.filter((b) => b.type !== 'text'), { type: 'text', text } as ContentBlock];
|
|
862
871
|
}
|
|
872
|
+
|
|
873
|
+
/** A message's size as a notice gives it: its text's characters and its
|
|
874
|
+
* images, each left out when there are none, so empty when it has
|
|
875
|
+
* neither. */
|
|
876
|
+
function contentSize(content: readonly ContentBlock[]): string {
|
|
877
|
+
let chars = 0;
|
|
878
|
+
let images = 0;
|
|
879
|
+
for (const b of content) {
|
|
880
|
+
if (b.type === 'text') chars += b.text.length;
|
|
881
|
+
else if (b.type === 'image' || b.type === 'generated_image') images++;
|
|
882
|
+
}
|
|
883
|
+
const parts: string[] = [];
|
|
884
|
+
if (chars > 0) parts.push(`${chars.toLocaleString('en-US')} character${chars === 1 ? '' : 's'}`);
|
|
885
|
+
if (images > 0) parts.push(`${images} image${images === 1 ? '' : 's'}`);
|
|
886
|
+
return parts.join(' and ');
|
|
887
|
+
}
|
|
888
|
+
|
|
863
889
|
/** One stored copy of a deferred write, as the drain reads it back: a
|
|
864
890
|
* sharded body's copies carry its `bodyGroupId`. */
|
|
865
891
|
type StoredCopy = { id: MessageId; content: ContentBlock[]; bodyGroupId?: string };
|
|
866
892
|
const storedCopy = (m: { id: MessageId; content: ContentBlock[]; bodyGroupId?: string }): StoredCopy =>
|
|
867
893
|
({ id: m.id, content: m.content, ...(m.bodyGroupId !== undefined ? { bodyGroupId: m.bodyGroupId } : {}) });
|
|
868
894
|
|
|
895
|
+
/** A tool result's spill file as writeToolResultFile left it: its path, with
|
|
896
|
+
* an error if the write failed, or null with no writable workspace. */
|
|
897
|
+
type SpillWrite = { path: string; error?: string; branchLeft?: true } | null;
|
|
898
|
+
|
|
869
899
|
/** A released agent's window as its pass to the primary reads it
|
|
870
900
|
* (AgentFramework.releasedWindow). */
|
|
871
901
|
type ReleasedWindow = {
|
|
@@ -900,7 +930,8 @@ function describeDeferredWrite(msg: { id: string; participant: string; content:
|
|
|
900
930
|
function isInjectableMidTurn(msg: { participant: string; content: ContentBlock[] }, agentName: string): boolean {
|
|
901
931
|
// Exact, as membrane's role test is: only a participant equal to the
|
|
902
932
|
// request's assistantParticipant (Agent's name) renders as the assistant.
|
|
903
|
-
|
|
933
|
+
// It's the name as sent, which the boundary takes as the store keeps it.
|
|
934
|
+
return nameAsStored(msg.participant) !== agentName
|
|
904
935
|
&& !msg.content.some((b) => b.type === 'tool_use' || b.type === 'tool_result');
|
|
905
936
|
}
|
|
906
937
|
|
|
@@ -1112,9 +1143,10 @@ type SpeechPublished = (to: PublishDestination, text: string, messageId: string
|
|
|
1112
1143
|
|
|
1113
1144
|
/**
|
|
1114
1145
|
* How a round's prose is delivered: under which turn's routing, and how what
|
|
1115
|
-
* it doesn't send is held. `notice` says
|
|
1116
|
-
* bounces or is held back as a draft is
|
|
1117
|
-
*
|
|
1146
|
+
* it doesn't send is held. `notice` says whether the notice for an envelope
|
|
1147
|
+
* that bounces or is held back as a draft is queued now, for this round's tool
|
|
1148
|
+
* boundary on a stream that presents injected messages ('now'), or not at all
|
|
1149
|
+
* ('later'), which leaves the drafts to be named as holdProseDrafts says.
|
|
1118
1150
|
* `keepPrivate` is a skip_reply round: its envelopes all stay in the context,
|
|
1119
1151
|
* and none is sent, bounced or held.
|
|
1120
1152
|
*/
|
|
@@ -1222,6 +1254,12 @@ function isTurnContinuation(reason: string): boolean {
|
|
|
1222
1254
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
1223
1255
|
|| reason === 'tool_result_guard_retry';
|
|
1224
1256
|
}
|
|
1257
|
+
|
|
1258
|
+
/** Wake sources the framework generates for itself (bounce wakes,
|
|
1259
|
+
* continuations, retries, unsticks, admission requeues). A wake from any
|
|
1260
|
+
* other source — a channel message, a heartbeat or other server event, a
|
|
1261
|
+
* gate flush, a timer, an operator nudge — is a cause of its own. */
|
|
1262
|
+
const FRAMEWORK_WAKE_SOURCES = new Set(['framework', 'scheduler']);
|
|
1225
1263
|
const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
|
|
1226
1264
|
const INFERENCE_LOG_ID = 'framework/inference-log';
|
|
1227
1265
|
const PROCESS_LOG_ID = 'framework/process-log';
|
|
@@ -2875,6 +2913,10 @@ export class AgentFramework {
|
|
|
2875
2913
|
* ephemeral agents have isolated slots and are their own only readers.
|
|
2876
2914
|
*/
|
|
2877
2915
|
private readonly sharedSlotAgents = new Set<Agent>();
|
|
2916
|
+
/** Rounds of tool calls, by their results array, that a save_recent_image
|
|
2917
|
+
* in the round counted by their share of the bound: such a round is
|
|
2918
|
+
* stored by its share whether or not it fits (buildStoredToolResultContent). */
|
|
2919
|
+
private readonly toolRoundsByShare = new WeakSet<CompletedToolCall[]>();
|
|
2878
2920
|
/** The windows of forks and runs deregistered while the host was quiesced,
|
|
2879
2921
|
* by agent name (releaseWindowNowOrAfterResume); resume() releases them
|
|
2880
2922
|
* (runReleasesAfterResume), or stop() passes on what they left, unread or
|
|
@@ -3612,6 +3654,8 @@ export class AgentFramework {
|
|
|
3612
3654
|
},
|
|
3613
3655
|
);
|
|
3614
3656
|
framework.tuneOutCoordinator.resumeActiveEpochs();
|
|
3657
|
+
} else {
|
|
3658
|
+
framework.endTuneOutsWithoutCoordinator();
|
|
3615
3659
|
}
|
|
3616
3660
|
|
|
3617
3661
|
// Modules, agents, MCPL servers and the tune-out tool are all in place:
|
|
@@ -11529,6 +11573,10 @@ export class AgentFramework {
|
|
|
11529
11573
|
// Speech happens only via speak_in_channel (its own marked voice).
|
|
11530
11574
|
proseRouting: 'explicit',
|
|
11531
11575
|
allowedTools: [...SUBCONSCIOUS_TOOL_NAMES, 'think', 'skip_reply', 'end_turn'],
|
|
11576
|
+
// No clock rows of its own: its merged view carries the residents'
|
|
11577
|
+
// clocks, and clocks it planned from its own rows alone would read the
|
|
11578
|
+
// time a second time beside them, or disagree with them.
|
|
11579
|
+
clock: undefined,
|
|
11532
11580
|
};
|
|
11533
11581
|
const agent = new Agent(agentConfig, contextManager, this.membrane);
|
|
11534
11582
|
this.adoptToolResultGuard(agent);
|
|
@@ -11694,7 +11742,7 @@ export class AgentFramework {
|
|
|
11694
11742
|
if (currentState.status === 'ready') {
|
|
11695
11743
|
// Effective inline cap: agent_settings override → durable config →
|
|
11696
11744
|
// house default, clamped to the strategy bound (see resolver).
|
|
11697
|
-
const maxChars = this.resolveToolResultInlineCap(agent)
|
|
11745
|
+
const { cap: maxChars, bound } = this.resolveToolResultInlineCap(agent);
|
|
11698
11746
|
|
|
11699
11747
|
// Oversized results — successes AND errors — spill to a workspace
|
|
11700
11748
|
// file (truncated preview + file reference) instead of being
|
|
@@ -11702,8 +11750,8 @@ export class AgentFramework {
|
|
|
11702
11750
|
// history copy and the live wire copy below — the two must stay
|
|
11703
11751
|
// byte-matched (the window stores what the membrane sends;
|
|
11704
11752
|
// divergence breaks the compile prefix).
|
|
11705
|
-
const { blocks: toolResultContent, spilled } =
|
|
11706
|
-
await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars);
|
|
11753
|
+
const { blocks: toolResultContent, spilled, keptCap } =
|
|
11754
|
+
await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars, bound);
|
|
11707
11755
|
|
|
11708
11756
|
// A spill writes to the workspace, and the turn can be stopped
|
|
11709
11757
|
// meanwhile. A host's cancelStream, stop() and abortInference cancel
|
|
@@ -11759,7 +11807,7 @@ export class AgentFramework {
|
|
|
11759
11807
|
const membraneResults = currentState.toolResults.map(tc =>
|
|
11760
11808
|
this.toMembraneToolResult(tc.id, tc.result, maxChars, spilled.get(tc.id))
|
|
11761
11809
|
);
|
|
11762
|
-
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata,
|
|
11810
|
+
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata, keptCap);
|
|
11763
11811
|
|
|
11764
11812
|
// Flush any messages that were deferred while this turn was in
|
|
11765
11813
|
// flight: this agent's own entries, those addressed to it and, for
|
|
@@ -11772,14 +11820,21 @@ export class AgentFramework {
|
|
|
11772
11820
|
// live turn sees them instead of the agent staying deaf until the
|
|
11773
11821
|
// turn ends. The window stores them here, right after the
|
|
11774
11822
|
// tool_result message, which is exactly where the membrane appends
|
|
11775
|
-
// them in the live conversation — same participant,
|
|
11776
|
-
//
|
|
11777
|
-
//
|
|
11778
|
-
//
|
|
11779
|
-
//
|
|
11780
|
-
//
|
|
11781
|
-
//
|
|
11782
|
-
//
|
|
11823
|
+
// them in the live conversation — same participant, and neither
|
|
11824
|
+
// carries the row's metadata — and each goes in as the next compile
|
|
11825
|
+
// will send it, which the strategy renders (renderForInjection): cut
|
|
11826
|
+
// by the per-message cap, a sharded body joined, and its images as
|
|
11827
|
+
// the image limits leave them at the newest position. So the next
|
|
11828
|
+
// turn's compiled prefix matches the live request (prompt-cache
|
|
11829
|
+
// safe), up to what came after them: a later message can still
|
|
11830
|
+
// take an earlier one past an image limit, as for any message, and
|
|
11831
|
+
// the next compile then strips that image where the live rounds
|
|
11832
|
+
// carried it. One the next compile won't send among its newest
|
|
11833
|
+
// isn't injected; a notice after the batch tells the agent of it
|
|
11834
|
+
// (recordLeftOutNotice). A write that threw after it was stored, whose landing
|
|
11835
|
+
// couldn't be read, is the exception: it's carried at the flush
|
|
11836
|
+
// that finds it, after what was stored since, so there the store and
|
|
11837
|
+
// the live request differ (writeDrainedInOrder).
|
|
11783
11838
|
//
|
|
11784
11839
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
11785
11840
|
// draining at another agent's boundary would store the messages
|
|
@@ -11814,22 +11869,30 @@ export class AgentFramework {
|
|
|
11814
11869
|
// a held message does, rather than sitting in the window between
|
|
11815
11870
|
// rounds the stream never showed it.
|
|
11816
11871
|
const midTurnInjections: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata }> = [];
|
|
11817
|
-
//
|
|
11818
|
-
//
|
|
11819
|
-
|
|
11872
|
+
// What this boundary stored for the agent to hear, in store order,
|
|
11873
|
+
// injected or left out, for a boundary that leaves it to the turn's
|
|
11874
|
+
// next start as arrivals: that start's compile sends it.
|
|
11875
|
+
const heard: Array<{ msg: DeferredWrite; id: MessageId; content: ContentBlock[]; parts: number }> = [];
|
|
11820
11876
|
if (currentState.supportsInjectedMessages !== false) {
|
|
11877
|
+
// A clock row due (storeDueClock) goes right after the round's
|
|
11878
|
+
// results, ahead of what is drained below, and joins the stream
|
|
11879
|
+
// the same way. A boundary that passed during the round can't
|
|
11880
|
+
// stand between a tool's call and its results, so this clock
|
|
11881
|
+
// reads the time it's written (clock-rows.ts).
|
|
11882
|
+
const clock = this.storeDueClock(agent, 'tool-boundary');
|
|
11821
11883
|
const deferred = this.drainDeferredFor(agent.name, (msg) => !isInjectableMidTurn(msg, agent.name));
|
|
11822
11884
|
// Stored in queue order, cut at the first that can't be stored: it
|
|
11823
11885
|
// and what follows wait for the turn's end, and only what was
|
|
11824
11886
|
// stored is injected (the round's results go on either way). One
|
|
11825
11887
|
// the store refuses as malformed is dropped instead, and what
|
|
11826
|
-
// follows goes on.
|
|
11827
|
-
//
|
|
11828
|
-
//
|
|
11829
|
-
//
|
|
11888
|
+
// follows goes on. What was stored is injected as the next compile
|
|
11889
|
+
// will send it (renderForInjection). Where the strategy can't say,
|
|
11890
|
+
// a sharded body read back from its store is injected as later
|
|
11891
|
+
// compiles join it, and a write that threw after storing, which
|
|
11892
|
+
// counts as stored, as what landed of it (writeDrainedInOrder).
|
|
11830
11893
|
const stored = this.writeDrainedInOrder(deferred, `tool boundary for ${agent.name}`, (msg) =>
|
|
11831
11894
|
agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
11832
|
-
for (const [msg, { id, content }] of stored) {
|
|
11895
|
+
for (const [msg, { id, content, parts }] of stored) {
|
|
11833
11896
|
// Every other resident reads what this boundary stores; for its
|
|
11834
11897
|
// own agent, the boundary weighs what it injects below.
|
|
11835
11898
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
@@ -11838,13 +11901,44 @@ export class AgentFramework {
|
|
|
11838
11901
|
// compiled view; injecting it here would put in the live turn
|
|
11839
11902
|
// what the next compile hides (#232 gap 5).
|
|
11840
11903
|
if ((msg.metadata as { tuneOut?: unknown } | undefined)?.tuneOut) continue;
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11904
|
+
heard.push({ msg, id, content, parts });
|
|
11905
|
+
}
|
|
11906
|
+
// Rendered in one call: a render prepares as a compile does, so
|
|
11907
|
+
// the batch shares one preparation. The image limits count from
|
|
11908
|
+
// the batch's oldest message to the newest, as a compile does.
|
|
11909
|
+
// The clock, stored ahead of them, renders first in the same batch
|
|
11910
|
+
// and is injected first, as the next compile sends it.
|
|
11911
|
+
const rendered = this.renderForInjection(agent, [...(clock ? [clock.id] : []), ...heard.map((h) => h.id)]);
|
|
11912
|
+
const first = clock ? 1 : 0;
|
|
11913
|
+
if (clock) {
|
|
11914
|
+
const sent = rendered ? rendered[0]! : clock.content;
|
|
11915
|
+
if (sent !== null) {
|
|
11916
|
+
midTurnInjections.push({ participant: 'user', content: sent, metadata: clock.metadata });
|
|
11917
|
+
} else {
|
|
11918
|
+
console.error(`[clock] ${agent.name}: clock ${clock.id} left out of the live round, since the next compile won't send it among its newest`);
|
|
11919
|
+
}
|
|
11920
|
+
}
|
|
11921
|
+
const leftOut: typeof heard = [];
|
|
11922
|
+
heard.forEach((h, i) => {
|
|
11923
|
+
const sent = rendered ? rendered[i + first]! : h.content;
|
|
11924
|
+
// The next compile won't send it among its newest: injected, it
|
|
11925
|
+
// would put in the live turn what that compile sends elsewhere,
|
|
11926
|
+
// or not at all. It's under any consumed mark the boundary sets
|
|
11927
|
+
// (below), as anything stored here is.
|
|
11928
|
+
if (sent === null) { leftOut.push(h); return; }
|
|
11929
|
+
midTurnInjections.push({
|
|
11930
|
+
participant: h.msg.participant,
|
|
11931
|
+
content: sent,
|
|
11932
|
+
...(h.msg.metadata ? { metadata: h.msg.metadata } : {}),
|
|
11933
|
+
});
|
|
11934
|
+
});
|
|
11935
|
+
if (leftOut.length > 0) {
|
|
11936
|
+
console.error(`[injection] ${agent.name}: left out of the live round, since the next compile won't send them among its newest: ${leftOut.map((h) => h.id).join(', ')}`);
|
|
11937
|
+
// The agent is told what it didn't hear, after the batch, as
|
|
11938
|
+
// the routing notice is: stored, so the next compile sends it
|
|
11939
|
+
// where the live round heard it.
|
|
11940
|
+
const notice = this.recordLeftOutNotice(agent, leftOut);
|
|
11941
|
+
if (notice) midTurnInjections.push(notice);
|
|
11848
11942
|
}
|
|
11849
11943
|
}
|
|
11850
11944
|
|
|
@@ -11982,10 +12076,11 @@ export class AgentFramework {
|
|
|
11982
12076
|
}
|
|
11983
12077
|
} else if (routesLocus && !shouldEndTurn) {
|
|
11984
12078
|
// A boundary that can't re-pin now (over budget, or no live
|
|
11985
|
-
// stream to tell) leaves
|
|
11986
|
-
// as arrivals: a budget restart, or a
|
|
11987
|
-
// results.
|
|
11988
|
-
|
|
12079
|
+
// stream to tell) leaves what it stored for the agent to the
|
|
12080
|
+
// turn's next start, as arrivals: a budget restart, or a
|
|
12081
|
+
// re-inference on these tool results. That includes a message
|
|
12082
|
+
// left out of the injection, since that start compiles anew.
|
|
12083
|
+
for (const { msg, id } of heard) this.noteArrival(agent.name, msg.metadata, id);
|
|
11989
12084
|
}
|
|
11990
12085
|
|
|
11991
12086
|
if (shouldEndTurn) {
|
|
@@ -12058,11 +12153,14 @@ export class AgentFramework {
|
|
|
12058
12153
|
// 0.5.x, and from 0.6.0 declares it can't carry them, so none are
|
|
12059
12154
|
// collected for it. Each goes as its participant and content
|
|
12060
12155
|
// alone: a stored row's metadata is the host's record, not input
|
|
12061
|
-
// for membrane.
|
|
12156
|
+
// for membrane. Each goes with its name and text as the store
|
|
12157
|
+
// keeps them, whether it's a heard message, rendered or not, or a
|
|
12158
|
+
// notice the boundary made: this is where they leave for the
|
|
12159
|
+
// request.
|
|
12062
12160
|
currentState.stream.provideToolResults(
|
|
12063
12161
|
agent.toolResultGuard.submissionResults(membraneResults),
|
|
12064
12162
|
midTurnInjections.length > 0
|
|
12065
|
-
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
12163
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant: nameAsStored(participant), content: contentAsStored(content) })) }
|
|
12066
12164
|
: undefined,
|
|
12067
12165
|
);
|
|
12068
12166
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
@@ -12088,6 +12186,18 @@ export class AgentFramework {
|
|
|
12088
12186
|
// Under context-manager#160 such a write already gives the
|
|
12089
12187
|
// replies stored after it stamps their requests didn't carry,
|
|
12090
12188
|
// whatever the mark says; deferring those writers is the fix.
|
|
12189
|
+
// The mark also covers a message the boundary stored and left out
|
|
12190
|
+
// of the injection, since the next compile won't send it among its
|
|
12191
|
+
// newest: the notice telling the agent of it is injected, so a
|
|
12192
|
+
// boundary that leaves one out injects something, unless that
|
|
12193
|
+
// notice couldn't be stored. The message then counts as read
|
|
12194
|
+
// though no request carried it: a coalesced render's wake to read
|
|
12195
|
+
// it drops, and a release doesn't pass it to the primary. The
|
|
12196
|
+
// notice has told the agent it came, from whom, and how to read
|
|
12197
|
+
// it. Where the view leaves it out, no request will carry it.
|
|
12198
|
+
// Where the memory strategy places it before its recent window, a
|
|
12199
|
+
// batch bigger than that window, the next compile carries it, in
|
|
12200
|
+
// whatever turn something else starts.
|
|
12091
12201
|
if (midTurnInjections.length > 0) {
|
|
12092
12202
|
// Possibly shown, whatever the stream declares: the first-view
|
|
12093
12203
|
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
@@ -12416,6 +12526,11 @@ export class AgentFramework {
|
|
|
12416
12526
|
if (!target) return undefined;
|
|
12417
12527
|
if (this.conversationRouter && this.conversationAgentHomes.has(target.name)) {
|
|
12418
12528
|
metadata.triggered = event.triggerInference ?? false;
|
|
12529
|
+
// A fork's channel messages are stored past its deferral; a clock goes
|
|
12530
|
+
// ahead of one only while the fork has no turn alive (storeDueClock).
|
|
12531
|
+
if (!this.turnAlive(target) && takesClockBefore('user', event.content, metadata, target.name)) {
|
|
12532
|
+
this.storeDueClock(target, 'event');
|
|
12533
|
+
}
|
|
12419
12534
|
const id = target.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(metadata, event.content));
|
|
12420
12535
|
// A correction may target an older engagement. Refresh only the
|
|
12421
12536
|
// binding whose agent actually received the message.
|
|
@@ -12599,6 +12714,10 @@ export class AgentFramework {
|
|
|
12599
12714
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
12600
12715
|
messageMetadata.triggered = trigger;
|
|
12601
12716
|
|
|
12717
|
+
// As for a correction above: a clock ahead only while no turn is alive.
|
|
12718
|
+
if (!this.turnAlive(agent) && takesClockBefore('user', event.content, messageMetadata, agent.name)) {
|
|
12719
|
+
this.storeDueClock(agent, 'event');
|
|
12720
|
+
}
|
|
12602
12721
|
const id = agent.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(messageMetadata, event.content));
|
|
12603
12722
|
router.touch(event.channelId);
|
|
12604
12723
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
@@ -14350,6 +14469,16 @@ export class AgentFramework {
|
|
|
14350
14469
|
// requests[0] in that mixed batch bypassed the turn lock because a
|
|
14351
14470
|
// restart existed, then treated the continuation as a fresh turn.
|
|
14352
14471
|
const trigger = budgetRestart ?? requests[0];
|
|
14472
|
+
// The bounce-wake cap (bounceProse) stops a bounce → wake → bounce loop
|
|
14473
|
+
// that nothing else feeds. A turn with a cause of its own is not that
|
|
14474
|
+
// loop, so the count starts again. Otherwise only a delivered prose
|
|
14475
|
+
// envelope reset it, and a resident that recovers from bounces with a
|
|
14476
|
+
// send tool kept the count for good: every later bounce, a reply to a
|
|
14477
|
+
// fresh message included, appended its notice without a wake.
|
|
14478
|
+
// (Optional chaining: prototype-built harnesses leave the map unset.)
|
|
14479
|
+
if (requests.some((r) => !FRAMEWORK_WAKE_SOURCES.has(r.source))) {
|
|
14480
|
+
this.proseBounceStreaks?.delete(agentName);
|
|
14481
|
+
}
|
|
14353
14482
|
// Route this turn's auto-published speech to the channel that triggered
|
|
14354
14483
|
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
14355
14484
|
// (messages arrived in >1 channel while the agent was busy/idle):
|
|
@@ -15423,6 +15552,102 @@ export class AgentFramework {
|
|
|
15423
15552
|
return routing.engaged.has(m.channelId) ? m.channelId : undefined;
|
|
15424
15553
|
}
|
|
15425
15554
|
|
|
15555
|
+
/** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
|
|
15556
|
+
private turnAlive(agent: Agent): boolean {
|
|
15557
|
+
return this.activeTurnTokens.has(agent.name)
|
|
15558
|
+
|| this.pendingAssistantBlocks.has(agent.name)
|
|
15559
|
+
|| this.activeStreams.has(agent.name);
|
|
15560
|
+
}
|
|
15561
|
+
|
|
15562
|
+
/**
|
|
15563
|
+
* Store the clock row due in `agent`'s window (clock-rows.ts), if the agent
|
|
15564
|
+
* has clock rows and a boundary has passed since the window's last clock,
|
|
15565
|
+
* or the window holds no clock row yet (planClock), and return it. The
|
|
15566
|
+
* caller places it, by calling this right before what the clock precedes
|
|
15567
|
+
* is written: an incoming event while no turn of the agent's is alive (or
|
|
15568
|
+
* one is assembling before its compile), a fresh turn's inputs before its
|
|
15569
|
+
* compile, or, mid-turn, the messages a tool boundary injects, where the
|
|
15570
|
+
* caller injects the clock first. Each is a place where a row is either in
|
|
15571
|
+
* the request that follows or before every request still to come, so the
|
|
15572
|
+
* window and the live stream keep matching.
|
|
15573
|
+
*
|
|
15574
|
+
* None while the host is quiesced (the window is frozen for surgery), and
|
|
15575
|
+
* none in the residents' shared slot while another of its readers (the
|
|
15576
|
+
* subconscious among them, through its merged view) has a turn alive: the
|
|
15577
|
+
* row would sit inside that turn without joining its stream. A clock that
|
|
15578
|
+
* can't be planned or stored is logged and skipped: it never holds up what
|
|
15579
|
+
* it would have preceded. One whose write threw after it landed is the
|
|
15580
|
+
* window's all the same, so it's returned, and a tool boundary injects it.
|
|
15581
|
+
*/
|
|
15582
|
+
private storeDueClock(
|
|
15583
|
+
agent: Agent,
|
|
15584
|
+
occasion: 'event' | 'turn-start' | 'tool-boundary',
|
|
15585
|
+
): { id: MessageId; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
15586
|
+
const config = agent.clock;
|
|
15587
|
+
if (!config || this.quiesced) return undefined;
|
|
15588
|
+
if (this.sharedSlotAgents.has(agent)) {
|
|
15589
|
+
for (const reader of this.sharedSlotAgents) {
|
|
15590
|
+
if (reader !== agent && this.turnAlive(reader)) return undefined;
|
|
15591
|
+
}
|
|
15592
|
+
}
|
|
15593
|
+
let planned: ReturnType<typeof planClock>;
|
|
15594
|
+
try {
|
|
15595
|
+
planned = planClock({ now: Date.now(), ...this.lastClockOf(agent.getContextManager()), config, zone: this.timeZone });
|
|
15596
|
+
} catch (err) {
|
|
15597
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}:`, err);
|
|
15598
|
+
return undefined;
|
|
15599
|
+
}
|
|
15600
|
+
if (!planned) return undefined;
|
|
15601
|
+
const metadata = { system: true, kind: CLOCK_ROW_KIND, clock: planned.stamp } as MessageMetadata;
|
|
15602
|
+
const before = this.storedCountOf(agent);
|
|
15603
|
+
let id: MessageId;
|
|
15604
|
+
try {
|
|
15605
|
+
id = agent.getContextManager().addMessage('user', planned.content, metadata);
|
|
15606
|
+
} catch (err) {
|
|
15607
|
+
// A write can throw after its row landed, as when context-manager's own
|
|
15608
|
+
// propagation of it fails: what landed is read back from where the
|
|
15609
|
+
// store stood before, as writeDrainedInOrder reads a drained write's.
|
|
15610
|
+
const at = planned.stamp.at;
|
|
15611
|
+
const landed = before === undefined
|
|
15612
|
+
? undefined
|
|
15613
|
+
: this.storedSince(agent, before, (m) => clockStampOf(m.metadata)?.at === at, 'clock', 'the clock write');
|
|
15614
|
+
if (!landed || landed.length === 0) {
|
|
15615
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}${landed ? '' : ' (whether it landed is unknown)'}:`, err);
|
|
15616
|
+
return undefined;
|
|
15617
|
+
}
|
|
15618
|
+
console.error(`[clock] ${agent.name}: the clock row at ${occasion} landed though its write threw:`, err);
|
|
15619
|
+
id = landed[0]!.id;
|
|
15620
|
+
}
|
|
15621
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: `clock:${occasion}` });
|
|
15622
|
+
return { id, content: planned.content, metadata };
|
|
15623
|
+
}
|
|
15624
|
+
|
|
15625
|
+
/** Rows a window's last-clock scan reads at a time. */
|
|
15626
|
+
private static readonly CLOCK_SCAN_ROWS = 256;
|
|
15627
|
+
|
|
15628
|
+
/** What the next clock is planned from: the window's newest row's time and
|
|
15629
|
+
* the time its last clock row reads, found by reading back from the
|
|
15630
|
+
* newest row, CLOCK_SCAN_ROWS at a time, until a clock row comes, however
|
|
15631
|
+
* far back it is. In the steady state the newest chunk holds it, unless
|
|
15632
|
+
* more rows than that have come since the last clock; a window with no
|
|
15633
|
+
* clock row is read whole once, and then takes one (planClock). */
|
|
15634
|
+
private lastClockOf(cm: ContextManager): { newestRowAt: number | undefined; lastClockAt: number | undefined } {
|
|
15635
|
+
let newestRowAt: number | undefined;
|
|
15636
|
+
for (let end = cm.getMessageCount(); end > 0;) {
|
|
15637
|
+
const span = Math.min(end, AgentFramework.CLOCK_SCAN_ROWS);
|
|
15638
|
+
const { messages } = cm.getMessageWindow(end - span, span, { resolveBlobs: false });
|
|
15639
|
+
if (newestRowAt === undefined && messages.length > 0) {
|
|
15640
|
+
newestRowAt = new Date(messages[messages.length - 1]!.timestamp).getTime();
|
|
15641
|
+
}
|
|
15642
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15643
|
+
const stamp = clockStampOf(messages[i]!.metadata);
|
|
15644
|
+
if (stamp) return { newestRowAt, lastClockAt: stamp.at };
|
|
15645
|
+
}
|
|
15646
|
+
end -= span;
|
|
15647
|
+
}
|
|
15648
|
+
return { newestRowAt, lastClockAt: undefined };
|
|
15649
|
+
}
|
|
15650
|
+
|
|
15426
15651
|
/**
|
|
15427
15652
|
* Weigh a message that no tool boundary of the agent's open logical turn
|
|
15428
15653
|
* will weigh: one that would re-pin the locus becomes one of the turn's
|
|
@@ -16430,7 +16655,9 @@ export class AgentFramework {
|
|
|
16430
16655
|
}
|
|
16431
16656
|
}
|
|
16432
16657
|
|
|
16433
|
-
/** Cap on consecutive bounce-triggered wakes (notices still append after).
|
|
16658
|
+
/** Cap on consecutive bounce-triggered wakes (notices still append after).
|
|
16659
|
+
* The count resets on a delivered prose envelope, and when a turn starts
|
|
16660
|
+
* with a cause of its own (processInferenceRequests). */
|
|
16434
16661
|
private static readonly PROSE_BOUNCE_WAKE_CAP = 2;
|
|
16435
16662
|
|
|
16436
16663
|
/**
|
|
@@ -16971,6 +17198,16 @@ export class AgentFramework {
|
|
|
16971
17198
|
// turn, and flushing there would insert messages between its rounds.
|
|
16972
17199
|
// Before recordTurnCheckpoint: these are the turn's inputs, not its
|
|
16973
17200
|
// products — undoing the turn must not destroy them.
|
|
17201
|
+
// A clock row due (storeDueClock) goes ahead of the turn's inputs, so a
|
|
17202
|
+
// turn starts with a current clock, a silent heartbeat's tick (which
|
|
17203
|
+
// stores no row of its own) included, unless storeDueClock writes none:
|
|
17204
|
+
// while the host is quiesced, while another reader of the residents'
|
|
17205
|
+
// shared slot has a turn alive, or when its write fails without landing.
|
|
17206
|
+
// Then a later event, turn start or tool boundary takes it. Fresh turns
|
|
17207
|
+
// only, as the flush below: a turn that resumes continues its logical
|
|
17208
|
+
// turn, which took its clock at its start or at a tool boundary that
|
|
17209
|
+
// could carry one.
|
|
17210
|
+
if (attempt === 0 && !resumesTurn) this.storeDueClock(agent, 'turn-start');
|
|
16974
17211
|
if (
|
|
16975
17212
|
attempt === 0 &&
|
|
16976
17213
|
!resumesTurn &&
|
|
@@ -18508,15 +18745,16 @@ export class AgentFramework {
|
|
|
18508
18745
|
// one door a giant blob still walks through).
|
|
18509
18746
|
const readyState = agent.state as AgentState;
|
|
18510
18747
|
if (readyState.status === 'ready') {
|
|
18511
|
-
const cap = this.resolveToolResultInlineCap(agent)
|
|
18512
|
-
const { blocks: toolResultContent, spilled } = await this.buildStoredToolResultContent(
|
|
18748
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
18749
|
+
const { blocks: toolResultContent, spilled, keptCap } = await this.buildStoredToolResultContent(
|
|
18513
18750
|
agent.name,
|
|
18514
18751
|
readyState.toolResults,
|
|
18515
18752
|
cap,
|
|
18753
|
+
bound,
|
|
18516
18754
|
);
|
|
18517
18755
|
agent.toolResultGuard.storeResults(toolResultContent, readyState.toolResults.map((tc) =>
|
|
18518
18756
|
this.toMembraneToolResult(tc.id, tc.result, cap, spilled.get(tc.id))), readyState.toolResults,
|
|
18519
|
-
this.silentTurnRowMetadata(agent.name),
|
|
18757
|
+
this.silentTurnRowMetadata(agent.name), keptCap);
|
|
18520
18758
|
// The response is already complete: this batch is never
|
|
18521
18759
|
// submitted in this turn, so it cannot be refused. Admit it.
|
|
18522
18760
|
agent.toolResultGuard.settleTurnEnded();
|
|
@@ -19826,10 +20064,12 @@ export class AgentFramework {
|
|
|
19826
20064
|
// token (as during a real tool call) and lands AFTER the pair, never
|
|
19827
20065
|
// between tool_use and its tool_result. The two addMessage calls
|
|
19828
20066
|
// below are synchronous and adjacent — nothing can interleave.
|
|
20067
|
+
const { bound } = this.resolveToolResultInlineCap(agent);
|
|
19829
20068
|
const { blocks } = await this.buildStoredToolResultContent(
|
|
19830
20069
|
agentName,
|
|
19831
20070
|
[{ id: toolUseId, name: toolName, input, result, durationMs }],
|
|
19832
20071
|
call.resultMaxChars,
|
|
20072
|
+
bound,
|
|
19833
20073
|
);
|
|
19834
20074
|
const toolUse: ContentBlock[] = [
|
|
19835
20075
|
{ type: 'tool_use', id: toolUseId, name: toolName, input } as ContentBlock,
|
|
@@ -20291,10 +20531,12 @@ export class AgentFramework {
|
|
|
20291
20531
|
// An agent that has gone gets nothing, its drop logged once (noticeTargetFor).
|
|
20292
20532
|
if (!this.noticeTargetFor(agentName, `script ${scriptId} wake`)) throw new Error(`agent ${agentName} has no context to wake`);
|
|
20293
20533
|
const agent = this.agents.get(agentName);
|
|
20294
|
-
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20534
|
+
// A reader's strategy cuts the wake's whole message, so its spill fits the
|
|
20535
|
+
// readers' bound with its notice.
|
|
20536
|
+
const { cap: maxChars, bound } = agent
|
|
20537
|
+
? this.resolveToolResultInlineCap(agent)
|
|
20538
|
+
: { cap: DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, bound: undefined };
|
|
20539
|
+
const { text: shown } = await this.spillOrTruncate(text, maxChars, `script-${label}`, null, bound);
|
|
20298
20540
|
// The spill can take a while: the agent may have gone since, or the host begun to stop.
|
|
20299
20541
|
if (this.stopHasBegun()) throw new Error('this host is stopping');
|
|
20300
20542
|
const target = this.noticeTargetFor(agentName, `script ${scriptId} wake`);
|
|
@@ -20344,15 +20586,38 @@ export class AgentFramework {
|
|
|
20344
20586
|
agentName: string,
|
|
20345
20587
|
toolResults: CompletedToolCall[],
|
|
20346
20588
|
maxChars: number | undefined,
|
|
20589
|
+
/** The readers' smallest per-message bound (resolveToolResultInlineCap). */
|
|
20590
|
+
bound: number | undefined = undefined,
|
|
20347
20591
|
): Promise<{
|
|
20348
20592
|
blocks: ContentBlock[];
|
|
20349
20593
|
spilled: Map<string, { text: string; filePath: string | null }>;
|
|
20594
|
+
/** Where a withheld stub's kept fields are cut: the share's keptCap
|
|
20595
|
+
* (toolResultShare), which is the cap where there's no bound. */
|
|
20596
|
+
keptCap: number | undefined;
|
|
20350
20597
|
}> {
|
|
20351
20598
|
const spilled = new Map<string, { text: string; filePath: string | null }>();
|
|
20352
20599
|
const dateLabel = new Date().toISOString().slice(0, 10);
|
|
20600
|
+
// The results are stored as one message, whose bound a reader's
|
|
20601
|
+
// strategy spends across them in order. Each is stored as it would be
|
|
20602
|
+
// alone, unless together they pass the bound, or a save in the round
|
|
20603
|
+
// counted the round by its share (toolRoundsByShare): then each is
|
|
20604
|
+
// stored under its share (toolResultShare). A result carrying an image
|
|
20605
|
+
// keeps its live copy cut at the cap; any other's live copy is its stored
|
|
20606
|
+
// text.
|
|
20607
|
+
const alone = new Map<string, { text: string; shown: ShownRanges | null; stored: { text: string; filePath: string | null; written?: SpillWrite } }>();
|
|
20353
20608
|
for (const tc of toolResults) {
|
|
20354
20609
|
const { text, shown } = this.storedToolResultText(agentName, tc, maxChars);
|
|
20355
|
-
|
|
20610
|
+
alone.set(tc.id, { text, shown, stored: await this.spillOrTruncate(text, maxChars, `${dateLabel}-${tc.id}`, shown, bound) });
|
|
20611
|
+
}
|
|
20612
|
+
const share = maxChars === undefined ? undefined : toolResultShare(maxChars, bound, toolResults.length);
|
|
20613
|
+
const total = [...alone.values()].reduce((sum, { stored }) => sum + stored.text.length, 0);
|
|
20614
|
+
const byShare = share?.bound !== undefined && toolResults.length > 1
|
|
20615
|
+
&& (this.toolRoundsByShare.has(toolResults) || total > bound!);
|
|
20616
|
+
for (const tc of toolResults) {
|
|
20617
|
+
const { text, shown, stored } = alone.get(tc.id)!;
|
|
20618
|
+
spilled.set(tc.id, byShare
|
|
20619
|
+
? await this.spillOrTruncate(text, share!.cap, `${dateLabel}-${tc.id}`, shown, share!.bound, stored.written)
|
|
20620
|
+
: stored);
|
|
20356
20621
|
}
|
|
20357
20622
|
const blocks: ContentBlock[] = toolResults.map(tc => ({
|
|
20358
20623
|
type: 'tool_result' as const,
|
|
@@ -20363,7 +20628,10 @@ export class AgentFramework {
|
|
|
20363
20628
|
content: spilled.get(tc.id)!.text,
|
|
20364
20629
|
isError: tc.result.isError,
|
|
20365
20630
|
}));
|
|
20366
|
-
|
|
20631
|
+
// A withheld round's stubs carry the guard's notice and place beside the
|
|
20632
|
+
// fields they keep, so under a bound their fields are cut within the
|
|
20633
|
+
// share, whether or not the round's results were stored by it.
|
|
20634
|
+
return { blocks, spilled, keptCap: share?.keptCap ?? maxChars };
|
|
20367
20635
|
}
|
|
20368
20636
|
|
|
20369
20637
|
/**
|
|
@@ -20386,7 +20654,7 @@ export class AgentFramework {
|
|
|
20386
20654
|
tc: CompletedToolCall,
|
|
20387
20655
|
maxChars: number | undefined,
|
|
20388
20656
|
): { text: string; shown: ShownRanges | null } {
|
|
20389
|
-
if (tc.result.isError) return { text: tc.result.error ?? 'Unknown error', shown: null };
|
|
20657
|
+
if (tc.result.isError) return { text: (tc.result.error ?? 'Unknown error').toWellFormed(), shown: null };
|
|
20390
20658
|
const ledger = this.toolImageLedgerFor(agentName);
|
|
20391
20659
|
let blocks = null as HistoryBlockSpan[] | null;
|
|
20392
20660
|
const text = nonErrorResultText(tc.result, undefined, {
|
|
@@ -20492,7 +20760,7 @@ export class AgentFramework {
|
|
|
20492
20760
|
label: string,
|
|
20493
20761
|
content: string,
|
|
20494
20762
|
branch?: string,
|
|
20495
|
-
): Promise<
|
|
20763
|
+
): Promise<SpillWrite> {
|
|
20496
20764
|
const place = this.toolResultFilePlace(label);
|
|
20497
20765
|
if (!place) return null;
|
|
20498
20766
|
const { workspace, path, safeLabel } = place;
|
|
@@ -20536,16 +20804,18 @@ export class AgentFramework {
|
|
|
20536
20804
|
}
|
|
20537
20805
|
|
|
20538
20806
|
/** How to read a tool result's file back a page at a time, with each page
|
|
20539
|
-
* sized to come back within `cap
|
|
20540
|
-
|
|
20807
|
+
* sized to come back within `cap`, and, where `canRaise`, that raising
|
|
20808
|
+
* the inline cap would show more: where a cap at the readers' bound would
|
|
20809
|
+
* keep a longer head, or the whole result. */
|
|
20810
|
+
private spillPageAdvice(path: string, cap: number, canRaise: boolean): string {
|
|
20541
20811
|
// JSON serialization can expand each read-back code unit to six
|
|
20542
20812
|
// chars (\\u0000). Leave room for the page's metadata and a possible
|
|
20543
20813
|
// extra code unit at a surrogate-pair boundary.
|
|
20544
20814
|
const pageOverhead = JSON.stringify(path).length + 256;
|
|
20545
20815
|
const readLimit = Math.max(1, Math.min(2000, Math.floor((cap - pageOverhead) / 6) - 1));
|
|
20546
20816
|
return `Page with workspace--read ${JSON.stringify({ path, offsetChars: 0, limitChars: readLimit })}; `
|
|
20547
|
-
+ 'use nextOffsetChars as offsetChars until null, keeping limitChars.
|
|
20548
|
-
+ 'agent_settings update tool_result_inline_max_chars.';
|
|
20817
|
+
+ 'use nextOffsetChars as offsetChars until null, keeping limitChars.'
|
|
20818
|
+
+ (canRaise ? ' Or raise the inline cap via agent_settings update tool_result_inline_max_chars.' : '');
|
|
20549
20819
|
}
|
|
20550
20820
|
|
|
20551
20821
|
private async spillOrTruncate(
|
|
@@ -20555,7 +20825,14 @@ export class AgentFramework {
|
|
|
20555
20825
|
/** What the live copy showed beside this text (storedToolResultText);
|
|
20556
20826
|
* null when the live copy was this text. */
|
|
20557
20827
|
shown: ShownRanges | null = null,
|
|
20558
|
-
|
|
20828
|
+
/** The most a reader's strategy keeps of this copy: the readers'
|
|
20829
|
+
* smallest per-message bound, or the result's share of it
|
|
20830
|
+
* (buildStoredToolResultContent). */
|
|
20831
|
+
bound: number | undefined = undefined,
|
|
20832
|
+
/** The spill file an earlier cut of this text wrote: reused, not written
|
|
20833
|
+
* again. Undefined when none was cut. */
|
|
20834
|
+
written?: SpillWrite,
|
|
20835
|
+
): Promise<{ text: string; filePath: string | null; written?: SpillWrite }> {
|
|
20559
20836
|
if (!cap || content.length <= cap) return { text: content, filePath: null };
|
|
20560
20837
|
// The stored text is the only place save_recent_image discovers tool
|
|
20561
20838
|
// images. When the live wire delivered them beside the text, whatever
|
|
@@ -20563,20 +20840,24 @@ export class AgentFramework {
|
|
|
20563
20840
|
// saw that the index would silently slide past (issue #104), so the
|
|
20564
20841
|
// newest slots past the cut that the live copy showed are re-appended
|
|
20565
20842
|
// after the notice. Otherwise the live copy is this text, and nothing
|
|
20566
|
-
// past the cut is kept (#403).
|
|
20567
|
-
|
|
20843
|
+
// past the cut is kept (#403). The stored copy, notice and kept slots
|
|
20844
|
+
// included, fits `bound`, the most a reader's strategy keeps of it, so
|
|
20845
|
+
// no reader's cut takes the notice (cutStoredToolResultToFit), and the
|
|
20846
|
+
// spill notices name the head they kept.
|
|
20847
|
+
const cut = cutStoredToolResultToFit(content, cap, shown, bound);
|
|
20568
20848
|
const { head } = cut;
|
|
20569
20849
|
const kept = formatKeptImageSlots(cut);
|
|
20570
20850
|
|
|
20571
|
-
|
|
20851
|
+
if (written === undefined) written = await this.writeToolResultFile(label, content);
|
|
20572
20852
|
if (written && written.error === undefined) {
|
|
20573
20853
|
const path = written.path;
|
|
20574
20854
|
return {
|
|
20575
20855
|
text: head
|
|
20576
20856
|
+ `\n\n[truncated — showing ${head.length} of ${content.length} chars; full content: workspace file ${path}`
|
|
20577
|
-
+ `. ${this.spillPageAdvice(path, cap)}]`
|
|
20857
|
+
+ `. ${this.spillPageAdvice(path, cap, bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > head.length)}]`
|
|
20578
20858
|
+ kept,
|
|
20579
20859
|
filePath: path,
|
|
20860
|
+
written,
|
|
20580
20861
|
};
|
|
20581
20862
|
}
|
|
20582
20863
|
if (written) {
|
|
@@ -20589,6 +20870,7 @@ export class AgentFramework {
|
|
|
20589
20870
|
+ ` (${written.error}); content over the cap was not retained]`
|
|
20590
20871
|
+ kept,
|
|
20591
20872
|
filePath: null,
|
|
20873
|
+
written,
|
|
20592
20874
|
};
|
|
20593
20875
|
}
|
|
20594
20876
|
return {
|
|
@@ -20596,6 +20878,7 @@ export class AgentFramework {
|
|
|
20596
20878
|
+ '\n\n[truncated — original was ' + content.length + ' chars; no writable workspace mount without an include, full content not retained]'
|
|
20597
20879
|
+ kept,
|
|
20598
20880
|
filePath: null,
|
|
20881
|
+
written,
|
|
20599
20882
|
};
|
|
20600
20883
|
}
|
|
20601
20884
|
|
|
@@ -20899,8 +21182,10 @@ export class AgentFramework {
|
|
|
20899
21182
|
* the array is text-only (let JSON path handle it; saves a code path).
|
|
20900
21183
|
* `maxChars`, when provided, caps each accompanying text block so an image
|
|
20901
21184
|
* inlined alongside an enormous text payload can't blow the context. A
|
|
20902
|
-
* block is cut as the stored copy
|
|
20903
|
-
* slot, so the stored copy can keep
|
|
21185
|
+
* block is cut at the cap as the stored copy's cut at that cap is
|
|
21186
|
+
* (toolResultCutAt), never inside a slot, so the stored copy can keep
|
|
21187
|
+
* exactly the slots this showed (#403); under a readers' bound the stored
|
|
21188
|
+
* copy's cut can come earlier (cutStoredToolResultToFit).
|
|
20904
21189
|
*/
|
|
20905
21190
|
private tryNativeToolResultContent(
|
|
20906
21191
|
data: unknown,
|
|
@@ -20923,7 +21208,8 @@ export class AgentFramework {
|
|
|
20923
21208
|
if (b.type === 'text') {
|
|
20924
21209
|
// A text that isn't a string shows as the converters' placeholder,
|
|
20925
21210
|
// rather than sending the whole array to the JSON fallback.
|
|
20926
|
-
|
|
21211
|
+
// Well formed, as the stored copy's text is (nonErrorResultText).
|
|
21212
|
+
let text = typeof b.text === 'string' ? b.text.toWellFormed() : NON_STRING_TEXT_PLACEHOLDER;
|
|
20927
21213
|
if (maxChars && text.length > maxChars) {
|
|
20928
21214
|
text = text.slice(0, toolResultCutAt(text, maxChars))
|
|
20929
21215
|
+ '\n\n[truncated — original was ' + text.length + ' chars'
|
|
@@ -20977,12 +21263,21 @@ export class AgentFramework {
|
|
|
20977
21263
|
* (Sol's #94 ruling: a durable preference must not be a durable path for
|
|
20978
21264
|
* one tool result to exceed maxMessageTokens; the way to see the whole
|
|
20979
21265
|
* result is the spill file, not an over-bound blob in live context).
|
|
21266
|
+
* An ordinary resident's results land in the shared slot, which each of
|
|
21267
|
+
* its readers compiles under its own strategy, so the bound is the
|
|
21268
|
+
* smallest of theirs, as the tune-out dump's is (residentMessageChars); a
|
|
21269
|
+
* fork's or an ephemeral agent's store has itself as its only reader.
|
|
20980
21270
|
*/
|
|
20981
21271
|
private resolveToolResultInlineCap(agent: Agent): {
|
|
20982
21272
|
cap: number;
|
|
20983
21273
|
source: 'agent-settings-override' | 'framework-config' | 'default';
|
|
20984
|
-
/** Set when the desired value was reduced to the
|
|
20985
|
-
|
|
21274
|
+
/** Set when the desired value was reduced to a bound: the agent's own
|
|
21275
|
+
* strategy's, or a smaller one of another reader of its slot. */
|
|
21276
|
+
clampedBy: 'strategy-bound' | 'reader-bound' | null;
|
|
21277
|
+
/** The smallest per-message bound among the readers of the agent's
|
|
21278
|
+
* store, which a stored copy, notice included, must fit
|
|
21279
|
+
* (cutStoredToolResultToFit); undefined when no reader has one. */
|
|
21280
|
+
bound: number | undefined;
|
|
20986
21281
|
} {
|
|
20987
21282
|
const override = this.toolResultInlineMaxCharsOverride.get(agent.name);
|
|
20988
21283
|
const configured = this.toolResultInlineMaxCharsConfig;
|
|
@@ -20990,12 +21285,24 @@ export class AgentFramework {
|
|
|
20990
21285
|
const source = override !== undefined
|
|
20991
21286
|
? 'agent-settings-override' as const
|
|
20992
21287
|
: configured !== null ? 'framework-config' as const : 'default' as const;
|
|
20993
|
-
|
|
20994
|
-
|
|
21288
|
+
let bound = this.strategyDerivedToolResultChars(agent);
|
|
21289
|
+
let byReader = false;
|
|
21290
|
+
if (this.isOrdinaryResident(agent)) {
|
|
21291
|
+
for (const reader of this.sharedSlotAgents) {
|
|
21292
|
+
if (reader === agent) continue;
|
|
21293
|
+
const readerBound = this.strategyDerivedToolResultChars(reader);
|
|
21294
|
+
if (readerBound !== undefined && (bound === undefined || readerBound < bound)) {
|
|
21295
|
+
bound = readerBound;
|
|
21296
|
+
byReader = true;
|
|
21297
|
+
}
|
|
21298
|
+
}
|
|
21299
|
+
}
|
|
21300
|
+
const clamped = bound !== undefined && bound < desired;
|
|
20995
21301
|
return {
|
|
20996
|
-
cap: clamped ?
|
|
21302
|
+
cap: clamped ? bound! : desired,
|
|
20997
21303
|
source,
|
|
20998
|
-
clampedBy: clamped ? 'strategy-bound' : null,
|
|
21304
|
+
clampedBy: clamped ? (byReader ? 'reader-bound' : 'strategy-bound') : null,
|
|
21305
|
+
bound,
|
|
20999
21306
|
};
|
|
21000
21307
|
}
|
|
21001
21308
|
|
|
@@ -21004,8 +21311,9 @@ export class AgentFramework {
|
|
|
21004
21311
|
* its caller whole. A call from a running script resolves to the script,
|
|
21005
21312
|
* which takes any result that fits its memory (scriptResultMaxChars). Any
|
|
21006
21313
|
* other call's result is stored in its agent's context under the agent's
|
|
21007
|
-
* inline cap,
|
|
21008
|
-
*
|
|
21314
|
+
* inline cap, unless its round's results together pass the readers' bound,
|
|
21315
|
+
* when each takes its share (buildStoredToolResultContent), and a call for
|
|
21316
|
+
* an agent the framework doesn't hold gets the house default.
|
|
21009
21317
|
*/
|
|
21010
21318
|
private resultMaxCharsFor(agentName: string, callId: string): number {
|
|
21011
21319
|
const ce = this.codeExecution;
|
|
@@ -21653,8 +21961,9 @@ export class AgentFramework {
|
|
|
21653
21961
|
// messages flush at the next turn's START (before its compile — riding
|
|
21654
21962
|
// their own wake), at the agent's next tool boundary (where they are
|
|
21655
21963
|
// ALSO injected into the live stream — hear-while-acting — all but a
|
|
21656
|
-
// message a tune-out holds from the residents
|
|
21657
|
-
// finally when the
|
|
21964
|
+
// message a tune-out holds from the residents, and one the next compile
|
|
21965
|
+
// won't send among its newest), or in driveStream's finally when the
|
|
21966
|
+
// turn ends.
|
|
21658
21967
|
// Quiesce (issue #122): the window exists to run compression/refold
|
|
21659
21968
|
// against a FROZEN context — module events and api message.send must not
|
|
21660
21969
|
// append mid-surgery. Deferred here, flushed by resume(). tool_result
|
|
@@ -21722,6 +22031,12 @@ export class AgentFramework {
|
|
|
21722
22031
|
return '' as MessageId; // Deferred — flushed at the target's next boundary
|
|
21723
22032
|
}
|
|
21724
22033
|
|
|
22034
|
+
// A clock row due goes first (storeDueClock). A write that gets here is a
|
|
22035
|
+
// tool result (which takes none), or no turn of the agent's is alive, or
|
|
22036
|
+
// its turn is assembling before its compile, or it comes from stop()'s
|
|
22037
|
+
// drain, once every stream it could join has settled: the clock is in
|
|
22038
|
+
// whatever request comes next.
|
|
22039
|
+
if (takesClockBefore(participant, content, metadata, agent.name)) this.storeDueClock(agent, 'event');
|
|
21725
22040
|
const stored = agent.getContextManager().addMessage(
|
|
21726
22041
|
participant,
|
|
21727
22042
|
content,
|
|
@@ -21805,6 +22120,74 @@ export class AgentFramework {
|
|
|
21805
22120
|
if (this.quiesced || this.deferredWritesPersisted) this.persistDeferredWrites();
|
|
21806
22121
|
}
|
|
21807
22122
|
|
|
22123
|
+
/**
|
|
22124
|
+
* What the next compile sends for each of `ids`, messages a tool boundary
|
|
22125
|
+
* has just stored, as the agent's strategy renders them as the newest of
|
|
22126
|
+
* its window (ContextManager.renderAsNewest): cut by the per-message cap,
|
|
22127
|
+
* a sharded body joined, and the image limits applied over the batch, so
|
|
22128
|
+
* the live stream hears what the next compile sends. An id comes back as
|
|
22129
|
+
* null where that compile won't send the message among its newest.
|
|
22130
|
+
* Undefined where the strategy has no such hook, or the render threw,
|
|
22131
|
+
* which is logged: the boundary then injects what it stored, as it did
|
|
22132
|
+
* before strategies could say.
|
|
22133
|
+
*/
|
|
22134
|
+
private renderForInjection(agent: Agent, ids: MessageId[]): Array<ContentBlock[] | null> | undefined {
|
|
22135
|
+
if (ids.length === 0) return undefined;
|
|
22136
|
+
try {
|
|
22137
|
+
return agent.getContextManager().renderAsNewest(ids);
|
|
22138
|
+
} catch (error) {
|
|
22139
|
+
console.error(`[injection] ${agent.name}: couldn't render the messages a tool boundary stored as the next compile sends them, so they're injected as stored:`, error);
|
|
22140
|
+
return undefined;
|
|
22141
|
+
}
|
|
22142
|
+
}
|
|
22143
|
+
|
|
22144
|
+
/**
|
|
22145
|
+
* Tell the agent of the messages a tool boundary stored and left out of
|
|
22146
|
+
* its injection, since the next compile won't send them among its newest:
|
|
22147
|
+
* who sent each and its size, and how to read it, `history--extract`
|
|
22148
|
+
* around it where the agent is shown that tool, else its id. None of a
|
|
22149
|
+
* message's words: the line stands in for a message the live round
|
|
22150
|
+
* doesn't carry. A system line, so it's no conversation: it neither
|
|
22151
|
+
* re-pins the turn nor clears its send suppression. It names the agent
|
|
22152
|
+
* it's for, since every resident sharing the slot reads it too. Returns
|
|
22153
|
+
* the notice as stored, for the boundary to inject after the batch, or
|
|
22154
|
+
* undefined when it couldn't be stored, which is logged.
|
|
22155
|
+
*/
|
|
22156
|
+
private recordLeftOutNotice(
|
|
22157
|
+
agent: Agent,
|
|
22158
|
+
leftOut: ReadonlyArray<{ msg: { participant: string }; id: MessageId; content: ContentBlock[]; parts: number }>,
|
|
22159
|
+
): { participant: string; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
22160
|
+
const canExtract = this.advertisedToolsForAgent(agent.name).some((t) => t.name === 'history--extract');
|
|
22161
|
+
const one = leftOut.length === 1;
|
|
22162
|
+
const lines = leftOut.map(({ msg, id, content, parts }) => {
|
|
22163
|
+
const size = contentSize(content);
|
|
22164
|
+
// A sharded body is that many store messages, consecutive from its
|
|
22165
|
+
// first, which extract reads around it one by one. (Its "after" stops
|
|
22166
|
+
// at 100, so a body of more parts, over 300,000 tokens at the memory
|
|
22167
|
+
// strategy's default shard size, is read only that far.)
|
|
22168
|
+
const read = canExtract
|
|
22169
|
+
? `history--extract with aroundId "${id}", before 0 and after ${parts - 1} reads it` +
|
|
22170
|
+
`${parts > 1 ? `, stored as ${parts} parts` : ''}.`
|
|
22171
|
+
: `Its id is ${id}${parts > 1 ? `, the first of the ${parts} parts it's stored as` : ''}.`;
|
|
22172
|
+
return `- From ${msg.participant}${size ? `, ${size}` : ''}. ${read}`;
|
|
22173
|
+
});
|
|
22174
|
+
const content: ContentBlock[] = [{
|
|
22175
|
+
type: 'text',
|
|
22176
|
+
text: `[not shown] For ${agent.name}: ${one ? 'A message' : `${leftOut.length} messages`} arrived during your turn that ` +
|
|
22177
|
+
`${one ? "isn't" : "aren't"} shown here, since your context doesn't keep ${one ? 'it' : 'them'} among ` +
|
|
22178
|
+
`your newest messages:\n${lines.join('\n')}`,
|
|
22179
|
+
}];
|
|
22180
|
+
const metadata = { system: true, kind: 'left-out-notice' } as MessageMetadata;
|
|
22181
|
+
try {
|
|
22182
|
+
const id = agent.getContextManager().addMessage('user', content, metadata);
|
|
22183
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'left-out-notice' });
|
|
22184
|
+
return { participant: 'user', content, metadata };
|
|
22185
|
+
} catch (err) {
|
|
22186
|
+
console.error(`[injection] ${agent.name}: couldn't store the notice of messages left out of the live round:`, err);
|
|
22187
|
+
return undefined;
|
|
22188
|
+
}
|
|
22189
|
+
}
|
|
22190
|
+
|
|
21808
22191
|
/**
|
|
21809
22192
|
* Write drained entries in queue (seq) order, whatever their targets:
|
|
21810
22193
|
* residents share one message slot, so the order entries arrived in is the
|
|
@@ -21927,7 +22310,7 @@ export class AgentFramework {
|
|
|
21927
22310
|
continue;
|
|
21928
22311
|
}
|
|
21929
22312
|
if (found.length > 0) {
|
|
21930
|
-
written.set(msg, { id: found[0]!.id, content: asCompiled(found) });
|
|
22313
|
+
written.set(msg, { id: found[0]!.id, content: asCompiled(found), parts: found.length });
|
|
21931
22314
|
this.logDeferredWriteError(msg, where, undefined, { count: found.length, sharded: found[0]!.bodyGroupId !== undefined, foundLater: true });
|
|
21932
22315
|
onLanded?.(msg, found[0]!.id);
|
|
21933
22316
|
continue;
|
|
@@ -21944,14 +22327,14 @@ export class AgentFramework {
|
|
|
21944
22327
|
const copies = agent && before !== undefined && (this.storedCountOf(agent) ?? before) - before > 1
|
|
21945
22328
|
? this.storedCopiesOf(agent, before, msg.id) ?? []
|
|
21946
22329
|
: [];
|
|
21947
|
-
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content });
|
|
22330
|
+
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content, parts: Math.max(1, copies.length) });
|
|
21948
22331
|
} catch (error) {
|
|
21949
22332
|
// What landed, read back from where the store stood before the
|
|
21950
22333
|
// write; undefined when the store couldn't say, before the write or
|
|
21951
22334
|
// after it. With no agent there's no store it could have landed in.
|
|
21952
22335
|
const landed = !agent ? [] : before === undefined ? undefined : this.storedCopiesOf(agent, before, msg.id);
|
|
21953
22336
|
if (landed && landed.length > 0) {
|
|
21954
|
-
written.set(msg, { id: landed[0]!.id, content: asCompiled(landed) });
|
|
22337
|
+
written.set(msg, { id: landed[0]!.id, content: asCompiled(landed), parts: landed.length });
|
|
21955
22338
|
this.logDeferredWriteError(msg, where, error, { count: landed.length, sharded: landed[0]!.bodyGroupId !== undefined });
|
|
21956
22339
|
onLanded?.(msg, landed[0]!.id);
|
|
21957
22340
|
continue;
|
|
@@ -21992,13 +22375,26 @@ export class AgentFramework {
|
|
|
21992
22375
|
* `bodyGroupId`. Undefined when the store can't be read back, which is
|
|
21993
22376
|
* logged: not the same as nothing found. */
|
|
21994
22377
|
private storedCopiesOf(agent: Agent, from: number, deferredWriteId: string): StoredCopy[] | undefined {
|
|
22378
|
+
return this.storedSince(
|
|
22379
|
+
agent, from, (m) => m.metadata?.deferredWriteId === deferredWriteId, 'deferral', `deferred write ${deferredWriteId}`);
|
|
22380
|
+
}
|
|
22381
|
+
|
|
22382
|
+
/** The messages `match` picks among those the agent's store gained from
|
|
22383
|
+
* position `from` on, in store order, with their blobs resolved. Undefined
|
|
22384
|
+
* when the store can't be read back, which is logged under `tag`, naming
|
|
22385
|
+
* `what` was written: not the same as nothing found. */
|
|
22386
|
+
private storedSince(
|
|
22387
|
+
agent: Agent,
|
|
22388
|
+
from: number,
|
|
22389
|
+
match: (m: StoredMessage) => boolean,
|
|
22390
|
+
tag: string,
|
|
22391
|
+
what: string,
|
|
22392
|
+
): StoredCopy[] | undefined {
|
|
21995
22393
|
try {
|
|
21996
22394
|
const cm = agent.getContextManager();
|
|
21997
|
-
return cm.getMessageWindow(from, Math.max(0, cm.getMessageCount() - from)).messages
|
|
21998
|
-
.filter((m) => m.metadata?.deferredWriteId === deferredWriteId)
|
|
21999
|
-
.map(storedCopy);
|
|
22395
|
+
return cm.getMessageWindow(from, Math.max(0, cm.getMessageCount() - from)).messages.filter(match).map(storedCopy);
|
|
22000
22396
|
} catch (error) {
|
|
22001
|
-
console.error(`[
|
|
22397
|
+
console.error(`[${tag}] could not read back what ${what} stored in ${agent.name}'s store:`, error);
|
|
22002
22398
|
return undefined;
|
|
22003
22399
|
}
|
|
22004
22400
|
}
|
|
@@ -25185,6 +25581,9 @@ export class AgentFramework {
|
|
|
25185
25581
|
// configured servers can still deploy its first one at runtime.
|
|
25186
25582
|
if (!this.mcplServerRegistry || !this.mcplHostCapabilities) {
|
|
25187
25583
|
await this.initializeMcpl([], this.mcplInferenceRoutingConfig ?? undefined);
|
|
25584
|
+
// The lifecycle log's replay restored any tune-out on record, and no
|
|
25585
|
+
// coordinator is made here to carry one.
|
|
25586
|
+
this.endTuneOutsWithoutCoordinator();
|
|
25188
25587
|
}
|
|
25189
25588
|
|
|
25190
25589
|
const collision = this.mcplPrefixCollision(config, undefined, claim);
|
|
@@ -27050,6 +27449,47 @@ export class AgentFramework {
|
|
|
27050
27449
|
};
|
|
27051
27450
|
}
|
|
27052
27451
|
|
|
27452
|
+
/**
|
|
27453
|
+
* A tune-out on record where this host has no tune-out coordinator: a
|
|
27454
|
+
* start with no subconscious makes none, and neither does a channel
|
|
27455
|
+
* subsystem brought up after start by connectMcplServer. Nothing then
|
|
27456
|
+
* diverts the channel's traffic or ever cancels its tune-out, tune_out
|
|
27457
|
+
* isn't offered, and channel_open, channel_close and channel_decline
|
|
27458
|
+
* refuse a tuned-out channel, so the channel would stay held by a decision
|
|
27459
|
+
* nothing here can change. Each one ends here instead, its channel decided
|
|
27460
|
+
* open again as a cancel leaves it, under the tune-out's own source, so an
|
|
27461
|
+
* agent's tune-out keeps the agent's provenance (the housekeeping guard
|
|
27462
|
+
* reads it). A log line names the channel and says how many of the
|
|
27463
|
+
* messages it diverted are held here, stored or still queued to be
|
|
27464
|
+
* written, which nothing will now deliver as its backlog.
|
|
27465
|
+
*/
|
|
27466
|
+
private endTuneOutsWithoutCoordinator(): void {
|
|
27467
|
+
const registry = this.channelRegistry;
|
|
27468
|
+
if (!registry || this.tuneOutCoordinator) return;
|
|
27469
|
+
const primary = this.primaryAgentName ? this.agents.get(this.primaryAgentName) : undefined;
|
|
27470
|
+
const ofEpoch = (epochId: string) => (m: { metadata?: unknown }): boolean =>
|
|
27471
|
+
(m.metadata as { tuneOut?: { epochId?: string } } | undefined)?.tuneOut?.epochId === epochId;
|
|
27472
|
+
for (const { serverId, channelId, params, source } of registry.tuneOutsOnRecord()) {
|
|
27473
|
+
const diverted = typeof params.epochId !== 'string' ? 0
|
|
27474
|
+
: (primary?.getContextManager().getAllMessages() ?? []).filter(ofEpoch(params.epochId)).length +
|
|
27475
|
+
this.deferredMessages.filter((m) => (m.forAgent ?? this.primaryAgentName) === this.primaryAgentName)
|
|
27476
|
+
.filter(ofEpoch(params.epochId)).length;
|
|
27477
|
+
const held = diverted === 0 ? ''
|
|
27478
|
+
: diverted === 1 ? ' The 1 message it diverted stays here, and nothing will deliver it as its backlog.'
|
|
27479
|
+
: ` The ${diverted} messages it diverted stay here, and nothing will deliver them as its backlog.`;
|
|
27480
|
+
try {
|
|
27481
|
+
registry.cancelTuneOut(serverId, channelId, 'open', source);
|
|
27482
|
+
} catch (err) {
|
|
27483
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server is tuned out, and nothing on this host watches ` +
|
|
27484
|
+
`or cancels tune-outs, but ending this one failed, so it stays recorded until a start that can end it: ` +
|
|
27485
|
+
`${err instanceof Error ? err.message : String(err)}.${held}`);
|
|
27486
|
+
continue;
|
|
27487
|
+
}
|
|
27488
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server was tuned out, and nothing on this host watches ` +
|
|
27489
|
+
`or cancels tune-outs, so the tune-out ends here and the channel is decided open again.${held}`);
|
|
27490
|
+
}
|
|
27491
|
+
}
|
|
27492
|
+
|
|
27053
27493
|
/**
|
|
27054
27494
|
* Build BeforeInferenceParams from agent state and trigger context.
|
|
27055
27495
|
*/
|
|
@@ -27924,11 +28364,24 @@ export class AgentFramework {
|
|
|
27924
28364
|
// carries the same ref.
|
|
27925
28365
|
const state = agent.state;
|
|
27926
28366
|
if (state.status === 'waiting_for_tools') {
|
|
27927
|
-
|
|
28367
|
+
// The commit stores a round as each result would be alone, or by
|
|
28368
|
+
// its share where they pass the bound together, which a sibling
|
|
28369
|
+
// still running can decide. Where the two ways differ for a result
|
|
28370
|
+
// counted here, the round is marked to be stored by its share
|
|
28371
|
+
// (toolRoundsByShare), so the count holds either way: a result
|
|
28372
|
+
// counted before the mark is the same both ways. The share is of
|
|
28373
|
+
// the round's calls, finished or not.
|
|
28374
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
28375
|
+
const share = toolResultShare(cap, bound, state.pending.size + state.completed.length);
|
|
27928
28376
|
for (let i = state.completed.length - 1; i >= 0 && collecting(); i--) {
|
|
27929
28377
|
const tc = state.completed[i]!;
|
|
27930
28378
|
const { text, shown } = this.storedToolResultText(agentName, tc, cap);
|
|
27931
|
-
|
|
28379
|
+
const alone = storedImageSlots(text, cap, shown, bound);
|
|
28380
|
+
const byShare = storedImageSlots(text, share.cap, shown, share.bound);
|
|
28381
|
+
if (alone.length !== byShare.length || alone.some((slot, j) => slot.text !== byShare[j]!.text)) {
|
|
28382
|
+
this.toolRoundsByShare.add(state.completed);
|
|
28383
|
+
}
|
|
28384
|
+
considerToolResultImages(tc.id, tc.name, this.toolRoundsByShare.has(state.completed) ? byShare : alone, 0);
|
|
27932
28385
|
}
|
|
27933
28386
|
}
|
|
27934
28387
|
|