@animalabs/agent-framework 0.24.0 → 0.24.1
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 +6 -0
- 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 +95 -5
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +485 -74
- 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/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 +44 -8
- package/dist/src/modules/workspace/index.js.map +1 -1
- 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-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 +2 -1
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +2 -1
- 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 +5 -1
- 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 +1 -1
- package/src/agent.ts +6 -0
- package/src/clock-rows.ts +302 -0
- package/src/framework.ts +508 -79
- package/src/index.ts +2 -0
- package/src/mcpl/channel-registry.ts +54 -8
- package/src/mcpl/protocol-family.ts +1 -1
- package/src/modules/workspace/index.ts +47 -9
- package/src/timezone.ts +4 -2
- package/src/tool-image-ledger.ts +62 -8
- package/src/tool-result-guard.ts +12 -10
- package/src/tool-result-history.ts +2 -1
- package/src/tune-out/coordinator.ts +3 -3
- package/src/types/agent.ts +28 -0
- package/src/types/events.ts +5 -1
- 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';
|
|
@@ -413,9 +414,12 @@ function malformedResponseContent(response: EventResponse): { where: string; pla
|
|
|
413
414
|
* mid-turn message's text at chars/4 and its other blocks about as
|
|
414
415
|
* context-manager's store prices them (injectedBlockTokens), and a small
|
|
415
416
|
* per-block envelope allowance. On membrane's native tool path a mid-turn
|
|
416
|
-
* message rides into the next round
|
|
417
|
-
*
|
|
418
|
-
*
|
|
417
|
+
* message rides into the next round as the boundary injects it, which is as
|
|
418
|
+
* the strategy renders it for the next compile, cut and with its images
|
|
419
|
+
* limited (renderForInjection), or its stored content where the strategy
|
|
420
|
+
* can't say (provideToolResults' injectedMessages). Its XML tool path declares that it
|
|
421
|
+
* can't carry injected messages, so the tool boundary injects none there and
|
|
422
|
+
* that term is zero. Deliberately a
|
|
419
423
|
* ceiling-ish heuristic, but for a mid-turn message's media at the store's
|
|
420
424
|
* flat prices — the cost of overestimating is one early recompile;
|
|
421
425
|
* the cost of underestimating is the 400 this projection exists to prevent.
|
|
@@ -693,6 +697,7 @@ import type {
|
|
|
693
697
|
} from './mcpl/types.js';
|
|
694
698
|
import type { ContextInjection } from '@animalabs/context-manager';
|
|
695
699
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
700
|
+
import { CLOCK_ROW_KIND, clockStampOf, planClock, takesClockBefore } from './clock-rows.js';
|
|
696
701
|
import { firstFreeBranchName } from './branch-names.js';
|
|
697
702
|
import {
|
|
698
703
|
DEFAULT_DISCORD_AWARENESS_EMOJI,
|
|
@@ -841,10 +846,13 @@ type InboundResponder = {
|
|
|
841
846
|
* For an entry whose write threw after storing it, found in the store and
|
|
842
847
|
* counted as written, it is what landed, joined the same way, which for a
|
|
843
848
|
* 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.
|
|
849
|
+
* back (storedCopiesOf logs it) leaves a sharded body as given. `parts` is
|
|
850
|
+
* how many store messages hold it, as read back: a sharded body's shards,
|
|
851
|
+
* consecutive from `id`, and 1 where it couldn't be read back. */
|
|
845
852
|
interface DrainedWrite {
|
|
846
853
|
id: MessageId;
|
|
847
854
|
content: ContentBlock[];
|
|
855
|
+
parts: number;
|
|
848
856
|
}
|
|
849
857
|
|
|
850
858
|
/** What later compiles render from the store for the copies of one write
|
|
@@ -860,12 +868,33 @@ function asCompiled(landed: ReadonlyArray<{ content: ContentBlock[] }>): Content
|
|
|
860
868
|
const text = blocks.filter((b) => b.type === 'text').map((b) => (b as { text: string }).text).join('');
|
|
861
869
|
return [...blocks.filter((b) => b.type !== 'text'), { type: 'text', text } as ContentBlock];
|
|
862
870
|
}
|
|
871
|
+
|
|
872
|
+
/** A message's size as a notice gives it: its text's characters and its
|
|
873
|
+
* images, each left out when there are none, so empty when it has
|
|
874
|
+
* neither. */
|
|
875
|
+
function contentSize(content: readonly ContentBlock[]): string {
|
|
876
|
+
let chars = 0;
|
|
877
|
+
let images = 0;
|
|
878
|
+
for (const b of content) {
|
|
879
|
+
if (b.type === 'text') chars += b.text.length;
|
|
880
|
+
else if (b.type === 'image' || b.type === 'generated_image') images++;
|
|
881
|
+
}
|
|
882
|
+
const parts: string[] = [];
|
|
883
|
+
if (chars > 0) parts.push(`${chars.toLocaleString('en-US')} character${chars === 1 ? '' : 's'}`);
|
|
884
|
+
if (images > 0) parts.push(`${images} image${images === 1 ? '' : 's'}`);
|
|
885
|
+
return parts.join(' and ');
|
|
886
|
+
}
|
|
887
|
+
|
|
863
888
|
/** One stored copy of a deferred write, as the drain reads it back: a
|
|
864
889
|
* sharded body's copies carry its `bodyGroupId`. */
|
|
865
890
|
type StoredCopy = { id: MessageId; content: ContentBlock[]; bodyGroupId?: string };
|
|
866
891
|
const storedCopy = (m: { id: MessageId; content: ContentBlock[]; bodyGroupId?: string }): StoredCopy =>
|
|
867
892
|
({ id: m.id, content: m.content, ...(m.bodyGroupId !== undefined ? { bodyGroupId: m.bodyGroupId } : {}) });
|
|
868
893
|
|
|
894
|
+
/** A tool result's spill file as writeToolResultFile left it: its path, with
|
|
895
|
+
* an error if the write failed, or null with no writable workspace. */
|
|
896
|
+
type SpillWrite = { path: string; error?: string; branchLeft?: true } | null;
|
|
897
|
+
|
|
869
898
|
/** A released agent's window as its pass to the primary reads it
|
|
870
899
|
* (AgentFramework.releasedWindow). */
|
|
871
900
|
type ReleasedWindow = {
|
|
@@ -1112,9 +1141,10 @@ type SpeechPublished = (to: PublishDestination, text: string, messageId: string
|
|
|
1112
1141
|
|
|
1113
1142
|
/**
|
|
1114
1143
|
* 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
|
-
*
|
|
1144
|
+
* it doesn't send is held. `notice` says whether the notice for an envelope
|
|
1145
|
+
* that bounces or is held back as a draft is queued now, for this round's tool
|
|
1146
|
+
* boundary on a stream that presents injected messages ('now'), or not at all
|
|
1147
|
+
* ('later'), which leaves the drafts to be named as holdProseDrafts says.
|
|
1118
1148
|
* `keepPrivate` is a skip_reply round: its envelopes all stay in the context,
|
|
1119
1149
|
* and none is sent, bounced or held.
|
|
1120
1150
|
*/
|
|
@@ -2875,6 +2905,10 @@ export class AgentFramework {
|
|
|
2875
2905
|
* ephemeral agents have isolated slots and are their own only readers.
|
|
2876
2906
|
*/
|
|
2877
2907
|
private readonly sharedSlotAgents = new Set<Agent>();
|
|
2908
|
+
/** Rounds of tool calls, by their results array, that a save_recent_image
|
|
2909
|
+
* in the round counted by their share of the bound: such a round is
|
|
2910
|
+
* stored by its share whether or not it fits (buildStoredToolResultContent). */
|
|
2911
|
+
private readonly toolRoundsByShare = new WeakSet<CompletedToolCall[]>();
|
|
2878
2912
|
/** The windows of forks and runs deregistered while the host was quiesced,
|
|
2879
2913
|
* by agent name (releaseWindowNowOrAfterResume); resume() releases them
|
|
2880
2914
|
* (runReleasesAfterResume), or stop() passes on what they left, unread or
|
|
@@ -3612,6 +3646,8 @@ export class AgentFramework {
|
|
|
3612
3646
|
},
|
|
3613
3647
|
);
|
|
3614
3648
|
framework.tuneOutCoordinator.resumeActiveEpochs();
|
|
3649
|
+
} else {
|
|
3650
|
+
framework.endTuneOutsWithoutCoordinator();
|
|
3615
3651
|
}
|
|
3616
3652
|
|
|
3617
3653
|
// Modules, agents, MCPL servers and the tune-out tool are all in place:
|
|
@@ -11529,6 +11565,10 @@ export class AgentFramework {
|
|
|
11529
11565
|
// Speech happens only via speak_in_channel (its own marked voice).
|
|
11530
11566
|
proseRouting: 'explicit',
|
|
11531
11567
|
allowedTools: [...SUBCONSCIOUS_TOOL_NAMES, 'think', 'skip_reply', 'end_turn'],
|
|
11568
|
+
// No clock rows of its own: its merged view carries the residents'
|
|
11569
|
+
// clocks, and clocks it planned from its own rows alone would read the
|
|
11570
|
+
// time a second time beside them, or disagree with them.
|
|
11571
|
+
clock: undefined,
|
|
11532
11572
|
};
|
|
11533
11573
|
const agent = new Agent(agentConfig, contextManager, this.membrane);
|
|
11534
11574
|
this.adoptToolResultGuard(agent);
|
|
@@ -11694,7 +11734,7 @@ export class AgentFramework {
|
|
|
11694
11734
|
if (currentState.status === 'ready') {
|
|
11695
11735
|
// Effective inline cap: agent_settings override → durable config →
|
|
11696
11736
|
// house default, clamped to the strategy bound (see resolver).
|
|
11697
|
-
const maxChars = this.resolveToolResultInlineCap(agent)
|
|
11737
|
+
const { cap: maxChars, bound } = this.resolveToolResultInlineCap(agent);
|
|
11698
11738
|
|
|
11699
11739
|
// Oversized results — successes AND errors — spill to a workspace
|
|
11700
11740
|
// file (truncated preview + file reference) instead of being
|
|
@@ -11702,8 +11742,8 @@ export class AgentFramework {
|
|
|
11702
11742
|
// history copy and the live wire copy below — the two must stay
|
|
11703
11743
|
// byte-matched (the window stores what the membrane sends;
|
|
11704
11744
|
// divergence breaks the compile prefix).
|
|
11705
|
-
const { blocks: toolResultContent, spilled } =
|
|
11706
|
-
await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars);
|
|
11745
|
+
const { blocks: toolResultContent, spilled, keptCap } =
|
|
11746
|
+
await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars, bound);
|
|
11707
11747
|
|
|
11708
11748
|
// A spill writes to the workspace, and the turn can be stopped
|
|
11709
11749
|
// meanwhile. A host's cancelStream, stop() and abortInference cancel
|
|
@@ -11759,7 +11799,7 @@ export class AgentFramework {
|
|
|
11759
11799
|
const membraneResults = currentState.toolResults.map(tc =>
|
|
11760
11800
|
this.toMembraneToolResult(tc.id, tc.result, maxChars, spilled.get(tc.id))
|
|
11761
11801
|
);
|
|
11762
|
-
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata,
|
|
11802
|
+
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata, keptCap);
|
|
11763
11803
|
|
|
11764
11804
|
// Flush any messages that were deferred while this turn was in
|
|
11765
11805
|
// flight: this agent's own entries, those addressed to it and, for
|
|
@@ -11772,14 +11812,21 @@ export class AgentFramework {
|
|
|
11772
11812
|
// live turn sees them instead of the agent staying deaf until the
|
|
11773
11813
|
// turn ends. The window stores them here, right after the
|
|
11774
11814
|
// 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
|
-
//
|
|
11815
|
+
// them in the live conversation — same participant, and neither
|
|
11816
|
+
// carries the row's metadata — and each goes in as the next compile
|
|
11817
|
+
// will send it, which the strategy renders (renderForInjection): cut
|
|
11818
|
+
// by the per-message cap, a sharded body joined, and its images as
|
|
11819
|
+
// the image limits leave them at the newest position. So the next
|
|
11820
|
+
// turn's compiled prefix matches the live request (prompt-cache
|
|
11821
|
+
// safe), up to what came after them: a later message can still
|
|
11822
|
+
// take an earlier one past an image limit, as for any message, and
|
|
11823
|
+
// the next compile then strips that image where the live rounds
|
|
11824
|
+
// carried it. One the next compile won't send among its newest
|
|
11825
|
+
// isn't injected; a notice after the batch tells the agent of it
|
|
11826
|
+
// (recordLeftOutNotice). A write that threw after it was stored, whose landing
|
|
11827
|
+
// couldn't be read, is the exception: it's carried at the flush
|
|
11828
|
+
// that finds it, after what was stored since, so there the store and
|
|
11829
|
+
// the live request differ (writeDrainedInOrder).
|
|
11783
11830
|
//
|
|
11784
11831
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
11785
11832
|
// draining at another agent's boundary would store the messages
|
|
@@ -11814,22 +11861,30 @@ export class AgentFramework {
|
|
|
11814
11861
|
// a held message does, rather than sitting in the window between
|
|
11815
11862
|
// rounds the stream never showed it.
|
|
11816
11863
|
const midTurnInjections: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata }> = [];
|
|
11817
|
-
//
|
|
11818
|
-
//
|
|
11819
|
-
|
|
11864
|
+
// What this boundary stored for the agent to hear, in store order,
|
|
11865
|
+
// injected or left out, for a boundary that leaves it to the turn's
|
|
11866
|
+
// next start as arrivals: that start's compile sends it.
|
|
11867
|
+
const heard: Array<{ msg: DeferredWrite; id: MessageId; content: ContentBlock[]; parts: number }> = [];
|
|
11820
11868
|
if (currentState.supportsInjectedMessages !== false) {
|
|
11869
|
+
// A clock row due (storeDueClock) goes right after the round's
|
|
11870
|
+
// results, ahead of what is drained below, and joins the stream
|
|
11871
|
+
// the same way. A boundary that passed during the round can't
|
|
11872
|
+
// stand between a tool's call and its results, so this clock
|
|
11873
|
+
// reads the time it's written (clock-rows.ts).
|
|
11874
|
+
const clock = this.storeDueClock(agent, 'tool-boundary');
|
|
11821
11875
|
const deferred = this.drainDeferredFor(agent.name, (msg) => !isInjectableMidTurn(msg, agent.name));
|
|
11822
11876
|
// Stored in queue order, cut at the first that can't be stored: it
|
|
11823
11877
|
// and what follows wait for the turn's end, and only what was
|
|
11824
11878
|
// stored is injected (the round's results go on either way). One
|
|
11825
11879
|
// the store refuses as malformed is dropped instead, and what
|
|
11826
|
-
// follows goes on.
|
|
11827
|
-
//
|
|
11828
|
-
//
|
|
11829
|
-
//
|
|
11880
|
+
// follows goes on. What was stored is injected as the next compile
|
|
11881
|
+
// will send it (renderForInjection). Where the strategy can't say,
|
|
11882
|
+
// a sharded body read back from its store is injected as later
|
|
11883
|
+
// compiles join it, and a write that threw after storing, which
|
|
11884
|
+
// counts as stored, as what landed of it (writeDrainedInOrder).
|
|
11830
11885
|
const stored = this.writeDrainedInOrder(deferred, `tool boundary for ${agent.name}`, (msg) =>
|
|
11831
11886
|
agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
11832
|
-
for (const [msg, { id, content }] of stored) {
|
|
11887
|
+
for (const [msg, { id, content, parts }] of stored) {
|
|
11833
11888
|
// Every other resident reads what this boundary stores; for its
|
|
11834
11889
|
// own agent, the boundary weighs what it injects below.
|
|
11835
11890
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
@@ -11838,13 +11893,44 @@ export class AgentFramework {
|
|
|
11838
11893
|
// compiled view; injecting it here would put in the live turn
|
|
11839
11894
|
// what the next compile hides (#232 gap 5).
|
|
11840
11895
|
if ((msg.metadata as { tuneOut?: unknown } | undefined)?.tuneOut) continue;
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11896
|
+
heard.push({ msg, id, content, parts });
|
|
11897
|
+
}
|
|
11898
|
+
// Rendered in one call: a render prepares as a compile does, so
|
|
11899
|
+
// the batch shares one preparation. The image limits count from
|
|
11900
|
+
// the batch's oldest message to the newest, as a compile does.
|
|
11901
|
+
// The clock, stored ahead of them, renders first in the same batch
|
|
11902
|
+
// and is injected first, as the next compile sends it.
|
|
11903
|
+
const rendered = this.renderForInjection(agent, [...(clock ? [clock.id] : []), ...heard.map((h) => h.id)]);
|
|
11904
|
+
const first = clock ? 1 : 0;
|
|
11905
|
+
if (clock) {
|
|
11906
|
+
const sent = rendered ? rendered[0]! : clock.content;
|
|
11907
|
+
if (sent !== null) {
|
|
11908
|
+
midTurnInjections.push({ participant: 'user', content: sent, metadata: clock.metadata });
|
|
11909
|
+
} else {
|
|
11910
|
+
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`);
|
|
11911
|
+
}
|
|
11912
|
+
}
|
|
11913
|
+
const leftOut: typeof heard = [];
|
|
11914
|
+
heard.forEach((h, i) => {
|
|
11915
|
+
const sent = rendered ? rendered[i + first]! : h.content;
|
|
11916
|
+
// The next compile won't send it among its newest: injected, it
|
|
11917
|
+
// would put in the live turn what that compile sends elsewhere,
|
|
11918
|
+
// or not at all. It's under any consumed mark the boundary sets
|
|
11919
|
+
// (below), as anything stored here is.
|
|
11920
|
+
if (sent === null) { leftOut.push(h); return; }
|
|
11921
|
+
midTurnInjections.push({
|
|
11922
|
+
participant: h.msg.participant,
|
|
11923
|
+
content: sent,
|
|
11924
|
+
...(h.msg.metadata ? { metadata: h.msg.metadata } : {}),
|
|
11925
|
+
});
|
|
11926
|
+
});
|
|
11927
|
+
if (leftOut.length > 0) {
|
|
11928
|
+
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(', ')}`);
|
|
11929
|
+
// The agent is told what it didn't hear, after the batch, as
|
|
11930
|
+
// the routing notice is: stored, so the next compile sends it
|
|
11931
|
+
// where the live round heard it.
|
|
11932
|
+
const notice = this.recordLeftOutNotice(agent, leftOut);
|
|
11933
|
+
if (notice) midTurnInjections.push(notice);
|
|
11848
11934
|
}
|
|
11849
11935
|
}
|
|
11850
11936
|
|
|
@@ -11982,10 +12068,11 @@ export class AgentFramework {
|
|
|
11982
12068
|
}
|
|
11983
12069
|
} else if (routesLocus && !shouldEndTurn) {
|
|
11984
12070
|
// 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
|
-
|
|
12071
|
+
// stream to tell) leaves what it stored for the agent to the
|
|
12072
|
+
// turn's next start, as arrivals: a budget restart, or a
|
|
12073
|
+
// re-inference on these tool results. That includes a message
|
|
12074
|
+
// left out of the injection, since that start compiles anew.
|
|
12075
|
+
for (const { msg, id } of heard) this.noteArrival(agent.name, msg.metadata, id);
|
|
11989
12076
|
}
|
|
11990
12077
|
|
|
11991
12078
|
if (shouldEndTurn) {
|
|
@@ -12088,6 +12175,18 @@ export class AgentFramework {
|
|
|
12088
12175
|
// Under context-manager#160 such a write already gives the
|
|
12089
12176
|
// replies stored after it stamps their requests didn't carry,
|
|
12090
12177
|
// whatever the mark says; deferring those writers is the fix.
|
|
12178
|
+
// The mark also covers a message the boundary stored and left out
|
|
12179
|
+
// of the injection, since the next compile won't send it among its
|
|
12180
|
+
// newest: the notice telling the agent of it is injected, so a
|
|
12181
|
+
// boundary that leaves one out injects something, unless that
|
|
12182
|
+
// notice couldn't be stored. The message then counts as read
|
|
12183
|
+
// though no request carried it: a coalesced render's wake to read
|
|
12184
|
+
// it drops, and a release doesn't pass it to the primary. The
|
|
12185
|
+
// notice has told the agent it came, from whom, and how to read
|
|
12186
|
+
// it. Where the view leaves it out, no request will carry it.
|
|
12187
|
+
// Where the memory strategy places it before its recent window, a
|
|
12188
|
+
// batch bigger than that window, the next compile carries it, in
|
|
12189
|
+
// whatever turn something else starts.
|
|
12091
12190
|
if (midTurnInjections.length > 0) {
|
|
12092
12191
|
// Possibly shown, whatever the stream declares: the first-view
|
|
12093
12192
|
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
@@ -12416,6 +12515,11 @@ export class AgentFramework {
|
|
|
12416
12515
|
if (!target) return undefined;
|
|
12417
12516
|
if (this.conversationRouter && this.conversationAgentHomes.has(target.name)) {
|
|
12418
12517
|
metadata.triggered = event.triggerInference ?? false;
|
|
12518
|
+
// A fork's channel messages are stored past its deferral; a clock goes
|
|
12519
|
+
// ahead of one only while the fork has no turn alive (storeDueClock).
|
|
12520
|
+
if (!this.turnAlive(target) && takesClockBefore('user', event.content, metadata, target.name)) {
|
|
12521
|
+
this.storeDueClock(target, 'event');
|
|
12522
|
+
}
|
|
12419
12523
|
const id = target.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(metadata, event.content));
|
|
12420
12524
|
// A correction may target an older engagement. Refresh only the
|
|
12421
12525
|
// binding whose agent actually received the message.
|
|
@@ -12599,6 +12703,10 @@ export class AgentFramework {
|
|
|
12599
12703
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
12600
12704
|
messageMetadata.triggered = trigger;
|
|
12601
12705
|
|
|
12706
|
+
// As for a correction above: a clock ahead only while no turn is alive.
|
|
12707
|
+
if (!this.turnAlive(agent) && takesClockBefore('user', event.content, messageMetadata, agent.name)) {
|
|
12708
|
+
this.storeDueClock(agent, 'event');
|
|
12709
|
+
}
|
|
12602
12710
|
const id = agent.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(messageMetadata, event.content));
|
|
12603
12711
|
router.touch(event.channelId);
|
|
12604
12712
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
@@ -15423,6 +15531,102 @@ export class AgentFramework {
|
|
|
15423
15531
|
return routing.engaged.has(m.channelId) ? m.channelId : undefined;
|
|
15424
15532
|
}
|
|
15425
15533
|
|
|
15534
|
+
/** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
|
|
15535
|
+
private turnAlive(agent: Agent): boolean {
|
|
15536
|
+
return this.activeTurnTokens.has(agent.name)
|
|
15537
|
+
|| this.pendingAssistantBlocks.has(agent.name)
|
|
15538
|
+
|| this.activeStreams.has(agent.name);
|
|
15539
|
+
}
|
|
15540
|
+
|
|
15541
|
+
/**
|
|
15542
|
+
* Store the clock row due in `agent`'s window (clock-rows.ts), if the agent
|
|
15543
|
+
* has clock rows and a boundary has passed since the window's last clock,
|
|
15544
|
+
* or the window holds no clock row yet (planClock), and return it. The
|
|
15545
|
+
* caller places it, by calling this right before what the clock precedes
|
|
15546
|
+
* is written: an incoming event while no turn of the agent's is alive (or
|
|
15547
|
+
* one is assembling before its compile), a fresh turn's inputs before its
|
|
15548
|
+
* compile, or, mid-turn, the messages a tool boundary injects, where the
|
|
15549
|
+
* caller injects the clock first. Each is a place where a row is either in
|
|
15550
|
+
* the request that follows or before every request still to come, so the
|
|
15551
|
+
* window and the live stream keep matching.
|
|
15552
|
+
*
|
|
15553
|
+
* None while the host is quiesced (the window is frozen for surgery), and
|
|
15554
|
+
* none in the residents' shared slot while another of its readers (the
|
|
15555
|
+
* subconscious among them, through its merged view) has a turn alive: the
|
|
15556
|
+
* row would sit inside that turn without joining its stream. A clock that
|
|
15557
|
+
* can't be planned or stored is logged and skipped: it never holds up what
|
|
15558
|
+
* it would have preceded. One whose write threw after it landed is the
|
|
15559
|
+
* window's all the same, so it's returned, and a tool boundary injects it.
|
|
15560
|
+
*/
|
|
15561
|
+
private storeDueClock(
|
|
15562
|
+
agent: Agent,
|
|
15563
|
+
occasion: 'event' | 'turn-start' | 'tool-boundary',
|
|
15564
|
+
): { id: MessageId; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
15565
|
+
const config = agent.clock;
|
|
15566
|
+
if (!config || this.quiesced) return undefined;
|
|
15567
|
+
if (this.sharedSlotAgents.has(agent)) {
|
|
15568
|
+
for (const reader of this.sharedSlotAgents) {
|
|
15569
|
+
if (reader !== agent && this.turnAlive(reader)) return undefined;
|
|
15570
|
+
}
|
|
15571
|
+
}
|
|
15572
|
+
let planned: ReturnType<typeof planClock>;
|
|
15573
|
+
try {
|
|
15574
|
+
planned = planClock({ now: Date.now(), ...this.lastClockOf(agent.getContextManager()), config, zone: this.timeZone });
|
|
15575
|
+
} catch (err) {
|
|
15576
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}:`, err);
|
|
15577
|
+
return undefined;
|
|
15578
|
+
}
|
|
15579
|
+
if (!planned) return undefined;
|
|
15580
|
+
const metadata = { system: true, kind: CLOCK_ROW_KIND, clock: planned.stamp } as MessageMetadata;
|
|
15581
|
+
const before = this.storedCountOf(agent);
|
|
15582
|
+
let id: MessageId;
|
|
15583
|
+
try {
|
|
15584
|
+
id = agent.getContextManager().addMessage('user', planned.content, metadata);
|
|
15585
|
+
} catch (err) {
|
|
15586
|
+
// A write can throw after its row landed, as when context-manager's own
|
|
15587
|
+
// propagation of it fails: what landed is read back from where the
|
|
15588
|
+
// store stood before, as writeDrainedInOrder reads a drained write's.
|
|
15589
|
+
const at = planned.stamp.at;
|
|
15590
|
+
const landed = before === undefined
|
|
15591
|
+
? undefined
|
|
15592
|
+
: this.storedSince(agent, before, (m) => clockStampOf(m.metadata)?.at === at, 'clock', 'the clock write');
|
|
15593
|
+
if (!landed || landed.length === 0) {
|
|
15594
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}${landed ? '' : ' (whether it landed is unknown)'}:`, err);
|
|
15595
|
+
return undefined;
|
|
15596
|
+
}
|
|
15597
|
+
console.error(`[clock] ${agent.name}: the clock row at ${occasion} landed though its write threw:`, err);
|
|
15598
|
+
id = landed[0]!.id;
|
|
15599
|
+
}
|
|
15600
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: `clock:${occasion}` });
|
|
15601
|
+
return { id, content: planned.content, metadata };
|
|
15602
|
+
}
|
|
15603
|
+
|
|
15604
|
+
/** Rows a window's last-clock scan reads at a time. */
|
|
15605
|
+
private static readonly CLOCK_SCAN_ROWS = 256;
|
|
15606
|
+
|
|
15607
|
+
/** What the next clock is planned from: the window's newest row's time and
|
|
15608
|
+
* the time its last clock row reads, found by reading back from the
|
|
15609
|
+
* newest row, CLOCK_SCAN_ROWS at a time, until a clock row comes, however
|
|
15610
|
+
* far back it is. In the steady state the newest chunk holds it, unless
|
|
15611
|
+
* more rows than that have come since the last clock; a window with no
|
|
15612
|
+
* clock row is read whole once, and then takes one (planClock). */
|
|
15613
|
+
private lastClockOf(cm: ContextManager): { newestRowAt: number | undefined; lastClockAt: number | undefined } {
|
|
15614
|
+
let newestRowAt: number | undefined;
|
|
15615
|
+
for (let end = cm.getMessageCount(); end > 0;) {
|
|
15616
|
+
const span = Math.min(end, AgentFramework.CLOCK_SCAN_ROWS);
|
|
15617
|
+
const { messages } = cm.getMessageWindow(end - span, span, { resolveBlobs: false });
|
|
15618
|
+
if (newestRowAt === undefined && messages.length > 0) {
|
|
15619
|
+
newestRowAt = new Date(messages[messages.length - 1]!.timestamp).getTime();
|
|
15620
|
+
}
|
|
15621
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15622
|
+
const stamp = clockStampOf(messages[i]!.metadata);
|
|
15623
|
+
if (stamp) return { newestRowAt, lastClockAt: stamp.at };
|
|
15624
|
+
}
|
|
15625
|
+
end -= span;
|
|
15626
|
+
}
|
|
15627
|
+
return { newestRowAt, lastClockAt: undefined };
|
|
15628
|
+
}
|
|
15629
|
+
|
|
15426
15630
|
/**
|
|
15427
15631
|
* Weigh a message that no tool boundary of the agent's open logical turn
|
|
15428
15632
|
* will weigh: one that would re-pin the locus becomes one of the turn's
|
|
@@ -16971,6 +17175,16 @@ export class AgentFramework {
|
|
|
16971
17175
|
// turn, and flushing there would insert messages between its rounds.
|
|
16972
17176
|
// Before recordTurnCheckpoint: these are the turn's inputs, not its
|
|
16973
17177
|
// products — undoing the turn must not destroy them.
|
|
17178
|
+
// A clock row due (storeDueClock) goes ahead of the turn's inputs, so a
|
|
17179
|
+
// turn starts with a current clock, a silent heartbeat's tick (which
|
|
17180
|
+
// stores no row of its own) included, unless storeDueClock writes none:
|
|
17181
|
+
// while the host is quiesced, while another reader of the residents'
|
|
17182
|
+
// shared slot has a turn alive, or when its write fails without landing.
|
|
17183
|
+
// Then a later event, turn start or tool boundary takes it. Fresh turns
|
|
17184
|
+
// only, as the flush below: a turn that resumes continues its logical
|
|
17185
|
+
// turn, which took its clock at its start or at a tool boundary that
|
|
17186
|
+
// could carry one.
|
|
17187
|
+
if (attempt === 0 && !resumesTurn) this.storeDueClock(agent, 'turn-start');
|
|
16974
17188
|
if (
|
|
16975
17189
|
attempt === 0 &&
|
|
16976
17190
|
!resumesTurn &&
|
|
@@ -18508,15 +18722,16 @@ export class AgentFramework {
|
|
|
18508
18722
|
// one door a giant blob still walks through).
|
|
18509
18723
|
const readyState = agent.state as AgentState;
|
|
18510
18724
|
if (readyState.status === 'ready') {
|
|
18511
|
-
const cap = this.resolveToolResultInlineCap(agent)
|
|
18512
|
-
const { blocks: toolResultContent, spilled } = await this.buildStoredToolResultContent(
|
|
18725
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
18726
|
+
const { blocks: toolResultContent, spilled, keptCap } = await this.buildStoredToolResultContent(
|
|
18513
18727
|
agent.name,
|
|
18514
18728
|
readyState.toolResults,
|
|
18515
18729
|
cap,
|
|
18730
|
+
bound,
|
|
18516
18731
|
);
|
|
18517
18732
|
agent.toolResultGuard.storeResults(toolResultContent, readyState.toolResults.map((tc) =>
|
|
18518
18733
|
this.toMembraneToolResult(tc.id, tc.result, cap, spilled.get(tc.id))), readyState.toolResults,
|
|
18519
|
-
this.silentTurnRowMetadata(agent.name),
|
|
18734
|
+
this.silentTurnRowMetadata(agent.name), keptCap);
|
|
18520
18735
|
// The response is already complete: this batch is never
|
|
18521
18736
|
// submitted in this turn, so it cannot be refused. Admit it.
|
|
18522
18737
|
agent.toolResultGuard.settleTurnEnded();
|
|
@@ -19826,10 +20041,12 @@ export class AgentFramework {
|
|
|
19826
20041
|
// token (as during a real tool call) and lands AFTER the pair, never
|
|
19827
20042
|
// between tool_use and its tool_result. The two addMessage calls
|
|
19828
20043
|
// below are synchronous and adjacent — nothing can interleave.
|
|
20044
|
+
const { bound } = this.resolveToolResultInlineCap(agent);
|
|
19829
20045
|
const { blocks } = await this.buildStoredToolResultContent(
|
|
19830
20046
|
agentName,
|
|
19831
20047
|
[{ id: toolUseId, name: toolName, input, result, durationMs }],
|
|
19832
20048
|
call.resultMaxChars,
|
|
20049
|
+
bound,
|
|
19833
20050
|
);
|
|
19834
20051
|
const toolUse: ContentBlock[] = [
|
|
19835
20052
|
{ type: 'tool_use', id: toolUseId, name: toolName, input } as ContentBlock,
|
|
@@ -20291,10 +20508,12 @@ export class AgentFramework {
|
|
|
20291
20508
|
// An agent that has gone gets nothing, its drop logged once (noticeTargetFor).
|
|
20292
20509
|
if (!this.noticeTargetFor(agentName, `script ${scriptId} wake`)) throw new Error(`agent ${agentName} has no context to wake`);
|
|
20293
20510
|
const agent = this.agents.get(agentName);
|
|
20294
|
-
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20511
|
+
// A reader's strategy cuts the wake's whole message, so its spill fits the
|
|
20512
|
+
// readers' bound with its notice.
|
|
20513
|
+
const { cap: maxChars, bound } = agent
|
|
20514
|
+
? this.resolveToolResultInlineCap(agent)
|
|
20515
|
+
: { cap: DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, bound: undefined };
|
|
20516
|
+
const { text: shown } = await this.spillOrTruncate(text, maxChars, `script-${label}`, null, bound);
|
|
20298
20517
|
// The spill can take a while: the agent may have gone since, or the host begun to stop.
|
|
20299
20518
|
if (this.stopHasBegun()) throw new Error('this host is stopping');
|
|
20300
20519
|
const target = this.noticeTargetFor(agentName, `script ${scriptId} wake`);
|
|
@@ -20344,15 +20563,38 @@ export class AgentFramework {
|
|
|
20344
20563
|
agentName: string,
|
|
20345
20564
|
toolResults: CompletedToolCall[],
|
|
20346
20565
|
maxChars: number | undefined,
|
|
20566
|
+
/** The readers' smallest per-message bound (resolveToolResultInlineCap). */
|
|
20567
|
+
bound: number | undefined = undefined,
|
|
20347
20568
|
): Promise<{
|
|
20348
20569
|
blocks: ContentBlock[];
|
|
20349
20570
|
spilled: Map<string, { text: string; filePath: string | null }>;
|
|
20571
|
+
/** Where a withheld stub's kept fields are cut: the share's keptCap
|
|
20572
|
+
* (toolResultShare), which is the cap where there's no bound. */
|
|
20573
|
+
keptCap: number | undefined;
|
|
20350
20574
|
}> {
|
|
20351
20575
|
const spilled = new Map<string, { text: string; filePath: string | null }>();
|
|
20352
20576
|
const dateLabel = new Date().toISOString().slice(0, 10);
|
|
20577
|
+
// The results are stored as one message, whose bound a reader's
|
|
20578
|
+
// strategy spends across them in order. Each is stored as it would be
|
|
20579
|
+
// alone, unless together they pass the bound, or a save in the round
|
|
20580
|
+
// counted the round by its share (toolRoundsByShare): then each is
|
|
20581
|
+
// stored under its share (toolResultShare). A result carrying an image
|
|
20582
|
+
// keeps its live copy cut at the cap; any other's live copy is its stored
|
|
20583
|
+
// text.
|
|
20584
|
+
const alone = new Map<string, { text: string; shown: ShownRanges | null; stored: { text: string; filePath: string | null; written?: SpillWrite } }>();
|
|
20353
20585
|
for (const tc of toolResults) {
|
|
20354
20586
|
const { text, shown } = this.storedToolResultText(agentName, tc, maxChars);
|
|
20355
|
-
|
|
20587
|
+
alone.set(tc.id, { text, shown, stored: await this.spillOrTruncate(text, maxChars, `${dateLabel}-${tc.id}`, shown, bound) });
|
|
20588
|
+
}
|
|
20589
|
+
const share = maxChars === undefined ? undefined : toolResultShare(maxChars, bound, toolResults.length);
|
|
20590
|
+
const total = [...alone.values()].reduce((sum, { stored }) => sum + stored.text.length, 0);
|
|
20591
|
+
const byShare = share?.bound !== undefined && toolResults.length > 1
|
|
20592
|
+
&& (this.toolRoundsByShare.has(toolResults) || total > bound!);
|
|
20593
|
+
for (const tc of toolResults) {
|
|
20594
|
+
const { text, shown, stored } = alone.get(tc.id)!;
|
|
20595
|
+
spilled.set(tc.id, byShare
|
|
20596
|
+
? await this.spillOrTruncate(text, share!.cap, `${dateLabel}-${tc.id}`, shown, share!.bound, stored.written)
|
|
20597
|
+
: stored);
|
|
20356
20598
|
}
|
|
20357
20599
|
const blocks: ContentBlock[] = toolResults.map(tc => ({
|
|
20358
20600
|
type: 'tool_result' as const,
|
|
@@ -20363,7 +20605,10 @@ export class AgentFramework {
|
|
|
20363
20605
|
content: spilled.get(tc.id)!.text,
|
|
20364
20606
|
isError: tc.result.isError,
|
|
20365
20607
|
}));
|
|
20366
|
-
|
|
20608
|
+
// A withheld round's stubs carry the guard's notice and place beside the
|
|
20609
|
+
// fields they keep, so under a bound their fields are cut within the
|
|
20610
|
+
// share, whether or not the round's results were stored by it.
|
|
20611
|
+
return { blocks, spilled, keptCap: share?.keptCap ?? maxChars };
|
|
20367
20612
|
}
|
|
20368
20613
|
|
|
20369
20614
|
/**
|
|
@@ -20492,7 +20737,7 @@ export class AgentFramework {
|
|
|
20492
20737
|
label: string,
|
|
20493
20738
|
content: string,
|
|
20494
20739
|
branch?: string,
|
|
20495
|
-
): Promise<
|
|
20740
|
+
): Promise<SpillWrite> {
|
|
20496
20741
|
const place = this.toolResultFilePlace(label);
|
|
20497
20742
|
if (!place) return null;
|
|
20498
20743
|
const { workspace, path, safeLabel } = place;
|
|
@@ -20536,16 +20781,18 @@ export class AgentFramework {
|
|
|
20536
20781
|
}
|
|
20537
20782
|
|
|
20538
20783
|
/** 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
|
-
|
|
20784
|
+
* sized to come back within `cap`, and, where `canRaise`, that raising
|
|
20785
|
+
* the inline cap would show more: where a cap at the readers' bound would
|
|
20786
|
+
* keep a longer head, or the whole result. */
|
|
20787
|
+
private spillPageAdvice(path: string, cap: number, canRaise: boolean): string {
|
|
20541
20788
|
// JSON serialization can expand each read-back code unit to six
|
|
20542
20789
|
// chars (\\u0000). Leave room for the page's metadata and a possible
|
|
20543
20790
|
// extra code unit at a surrogate-pair boundary.
|
|
20544
20791
|
const pageOverhead = JSON.stringify(path).length + 256;
|
|
20545
20792
|
const readLimit = Math.max(1, Math.min(2000, Math.floor((cap - pageOverhead) / 6) - 1));
|
|
20546
20793
|
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.';
|
|
20794
|
+
+ 'use nextOffsetChars as offsetChars until null, keeping limitChars.'
|
|
20795
|
+
+ (canRaise ? ' Or raise the inline cap via agent_settings update tool_result_inline_max_chars.' : '');
|
|
20549
20796
|
}
|
|
20550
20797
|
|
|
20551
20798
|
private async spillOrTruncate(
|
|
@@ -20555,7 +20802,14 @@ export class AgentFramework {
|
|
|
20555
20802
|
/** What the live copy showed beside this text (storedToolResultText);
|
|
20556
20803
|
* null when the live copy was this text. */
|
|
20557
20804
|
shown: ShownRanges | null = null,
|
|
20558
|
-
|
|
20805
|
+
/** The most a reader's strategy keeps of this copy: the readers'
|
|
20806
|
+
* smallest per-message bound, or the result's share of it
|
|
20807
|
+
* (buildStoredToolResultContent). */
|
|
20808
|
+
bound: number | undefined = undefined,
|
|
20809
|
+
/** The spill file an earlier cut of this text wrote: reused, not written
|
|
20810
|
+
* again. Undefined when none was cut. */
|
|
20811
|
+
written?: SpillWrite,
|
|
20812
|
+
): Promise<{ text: string; filePath: string | null; written?: SpillWrite }> {
|
|
20559
20813
|
if (!cap || content.length <= cap) return { text: content, filePath: null };
|
|
20560
20814
|
// The stored text is the only place save_recent_image discovers tool
|
|
20561
20815
|
// images. When the live wire delivered them beside the text, whatever
|
|
@@ -20563,20 +20817,24 @@ export class AgentFramework {
|
|
|
20563
20817
|
// saw that the index would silently slide past (issue #104), so the
|
|
20564
20818
|
// newest slots past the cut that the live copy showed are re-appended
|
|
20565
20819
|
// after the notice. Otherwise the live copy is this text, and nothing
|
|
20566
|
-
// past the cut is kept (#403).
|
|
20567
|
-
|
|
20820
|
+
// past the cut is kept (#403). The stored copy, notice and kept slots
|
|
20821
|
+
// included, fits `bound`, the most a reader's strategy keeps of it, so
|
|
20822
|
+
// no reader's cut takes the notice (cutStoredToolResultToFit), and the
|
|
20823
|
+
// spill notices name the head they kept.
|
|
20824
|
+
const cut = cutStoredToolResultToFit(content, cap, shown, bound);
|
|
20568
20825
|
const { head } = cut;
|
|
20569
20826
|
const kept = formatKeptImageSlots(cut);
|
|
20570
20827
|
|
|
20571
|
-
|
|
20828
|
+
if (written === undefined) written = await this.writeToolResultFile(label, content);
|
|
20572
20829
|
if (written && written.error === undefined) {
|
|
20573
20830
|
const path = written.path;
|
|
20574
20831
|
return {
|
|
20575
20832
|
text: head
|
|
20576
20833
|
+ `\n\n[truncated — showing ${head.length} of ${content.length} chars; full content: workspace file ${path}`
|
|
20577
|
-
+ `. ${this.spillPageAdvice(path, cap)}]`
|
|
20834
|
+
+ `. ${this.spillPageAdvice(path, cap, bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > head.length)}]`
|
|
20578
20835
|
+ kept,
|
|
20579
20836
|
filePath: path,
|
|
20837
|
+
written,
|
|
20580
20838
|
};
|
|
20581
20839
|
}
|
|
20582
20840
|
if (written) {
|
|
@@ -20589,6 +20847,7 @@ export class AgentFramework {
|
|
|
20589
20847
|
+ ` (${written.error}); content over the cap was not retained]`
|
|
20590
20848
|
+ kept,
|
|
20591
20849
|
filePath: null,
|
|
20850
|
+
written,
|
|
20592
20851
|
};
|
|
20593
20852
|
}
|
|
20594
20853
|
return {
|
|
@@ -20596,6 +20855,7 @@ export class AgentFramework {
|
|
|
20596
20855
|
+ '\n\n[truncated — original was ' + content.length + ' chars; no writable workspace mount without an include, full content not retained]'
|
|
20597
20856
|
+ kept,
|
|
20598
20857
|
filePath: null,
|
|
20858
|
+
written,
|
|
20599
20859
|
};
|
|
20600
20860
|
}
|
|
20601
20861
|
|
|
@@ -20899,8 +21159,10 @@ export class AgentFramework {
|
|
|
20899
21159
|
* the array is text-only (let JSON path handle it; saves a code path).
|
|
20900
21160
|
* `maxChars`, when provided, caps each accompanying text block so an image
|
|
20901
21161
|
* 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
|
|
21162
|
+
* block is cut at the cap as the stored copy's cut at that cap is
|
|
21163
|
+
* (toolResultCutAt), never inside a slot, so the stored copy can keep
|
|
21164
|
+
* exactly the slots this showed (#403); under a readers' bound the stored
|
|
21165
|
+
* copy's cut can come earlier (cutStoredToolResultToFit).
|
|
20904
21166
|
*/
|
|
20905
21167
|
private tryNativeToolResultContent(
|
|
20906
21168
|
data: unknown,
|
|
@@ -20977,12 +21239,21 @@ export class AgentFramework {
|
|
|
20977
21239
|
* (Sol's #94 ruling: a durable preference must not be a durable path for
|
|
20978
21240
|
* one tool result to exceed maxMessageTokens; the way to see the whole
|
|
20979
21241
|
* result is the spill file, not an over-bound blob in live context).
|
|
21242
|
+
* An ordinary resident's results land in the shared slot, which each of
|
|
21243
|
+
* its readers compiles under its own strategy, so the bound is the
|
|
21244
|
+
* smallest of theirs, as the tune-out dump's is (residentMessageChars); a
|
|
21245
|
+
* fork's or an ephemeral agent's store has itself as its only reader.
|
|
20980
21246
|
*/
|
|
20981
21247
|
private resolveToolResultInlineCap(agent: Agent): {
|
|
20982
21248
|
cap: number;
|
|
20983
21249
|
source: 'agent-settings-override' | 'framework-config' | 'default';
|
|
20984
|
-
/** Set when the desired value was reduced to the
|
|
20985
|
-
|
|
21250
|
+
/** Set when the desired value was reduced to a bound: the agent's own
|
|
21251
|
+
* strategy's, or a smaller one of another reader of its slot. */
|
|
21252
|
+
clampedBy: 'strategy-bound' | 'reader-bound' | null;
|
|
21253
|
+
/** The smallest per-message bound among the readers of the agent's
|
|
21254
|
+
* store, which a stored copy, notice included, must fit
|
|
21255
|
+
* (cutStoredToolResultToFit); undefined when no reader has one. */
|
|
21256
|
+
bound: number | undefined;
|
|
20986
21257
|
} {
|
|
20987
21258
|
const override = this.toolResultInlineMaxCharsOverride.get(agent.name);
|
|
20988
21259
|
const configured = this.toolResultInlineMaxCharsConfig;
|
|
@@ -20990,12 +21261,24 @@ export class AgentFramework {
|
|
|
20990
21261
|
const source = override !== undefined
|
|
20991
21262
|
? 'agent-settings-override' as const
|
|
20992
21263
|
: configured !== null ? 'framework-config' as const : 'default' as const;
|
|
20993
|
-
|
|
20994
|
-
|
|
21264
|
+
let bound = this.strategyDerivedToolResultChars(agent);
|
|
21265
|
+
let byReader = false;
|
|
21266
|
+
if (this.isOrdinaryResident(agent)) {
|
|
21267
|
+
for (const reader of this.sharedSlotAgents) {
|
|
21268
|
+
if (reader === agent) continue;
|
|
21269
|
+
const readerBound = this.strategyDerivedToolResultChars(reader);
|
|
21270
|
+
if (readerBound !== undefined && (bound === undefined || readerBound < bound)) {
|
|
21271
|
+
bound = readerBound;
|
|
21272
|
+
byReader = true;
|
|
21273
|
+
}
|
|
21274
|
+
}
|
|
21275
|
+
}
|
|
21276
|
+
const clamped = bound !== undefined && bound < desired;
|
|
20995
21277
|
return {
|
|
20996
|
-
cap: clamped ?
|
|
21278
|
+
cap: clamped ? bound! : desired,
|
|
20997
21279
|
source,
|
|
20998
|
-
clampedBy: clamped ? 'strategy-bound' : null,
|
|
21280
|
+
clampedBy: clamped ? (byReader ? 'reader-bound' : 'strategy-bound') : null,
|
|
21281
|
+
bound,
|
|
20999
21282
|
};
|
|
21000
21283
|
}
|
|
21001
21284
|
|
|
@@ -21004,8 +21287,9 @@ export class AgentFramework {
|
|
|
21004
21287
|
* its caller whole. A call from a running script resolves to the script,
|
|
21005
21288
|
* which takes any result that fits its memory (scriptResultMaxChars). Any
|
|
21006
21289
|
* other call's result is stored in its agent's context under the agent's
|
|
21007
|
-
* inline cap,
|
|
21008
|
-
*
|
|
21290
|
+
* inline cap, unless its round's results together pass the readers' bound,
|
|
21291
|
+
* when each takes its share (buildStoredToolResultContent), and a call for
|
|
21292
|
+
* an agent the framework doesn't hold gets the house default.
|
|
21009
21293
|
*/
|
|
21010
21294
|
private resultMaxCharsFor(agentName: string, callId: string): number {
|
|
21011
21295
|
const ce = this.codeExecution;
|
|
@@ -21653,8 +21937,9 @@ export class AgentFramework {
|
|
|
21653
21937
|
// messages flush at the next turn's START (before its compile — riding
|
|
21654
21938
|
// their own wake), at the agent's next tool boundary (where they are
|
|
21655
21939
|
// 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
|
|
21940
|
+
// message a tune-out holds from the residents, and one the next compile
|
|
21941
|
+
// won't send among its newest), or in driveStream's finally when the
|
|
21942
|
+
// turn ends.
|
|
21658
21943
|
// Quiesce (issue #122): the window exists to run compression/refold
|
|
21659
21944
|
// against a FROZEN context — module events and api message.send must not
|
|
21660
21945
|
// append mid-surgery. Deferred here, flushed by resume(). tool_result
|
|
@@ -21722,6 +22007,12 @@ export class AgentFramework {
|
|
|
21722
22007
|
return '' as MessageId; // Deferred — flushed at the target's next boundary
|
|
21723
22008
|
}
|
|
21724
22009
|
|
|
22010
|
+
// A clock row due goes first (storeDueClock). A write that gets here is a
|
|
22011
|
+
// tool result (which takes none), or no turn of the agent's is alive, or
|
|
22012
|
+
// its turn is assembling before its compile, or it comes from stop()'s
|
|
22013
|
+
// drain, once every stream it could join has settled: the clock is in
|
|
22014
|
+
// whatever request comes next.
|
|
22015
|
+
if (takesClockBefore(participant, content, metadata, agent.name)) this.storeDueClock(agent, 'event');
|
|
21725
22016
|
const stored = agent.getContextManager().addMessage(
|
|
21726
22017
|
participant,
|
|
21727
22018
|
content,
|
|
@@ -21805,6 +22096,74 @@ export class AgentFramework {
|
|
|
21805
22096
|
if (this.quiesced || this.deferredWritesPersisted) this.persistDeferredWrites();
|
|
21806
22097
|
}
|
|
21807
22098
|
|
|
22099
|
+
/**
|
|
22100
|
+
* What the next compile sends for each of `ids`, messages a tool boundary
|
|
22101
|
+
* has just stored, as the agent's strategy renders them as the newest of
|
|
22102
|
+
* its window (ContextManager.renderAsNewest): cut by the per-message cap,
|
|
22103
|
+
* a sharded body joined, and the image limits applied over the batch, so
|
|
22104
|
+
* the live stream hears what the next compile sends. An id comes back as
|
|
22105
|
+
* null where that compile won't send the message among its newest.
|
|
22106
|
+
* Undefined where the strategy has no such hook, or the render threw,
|
|
22107
|
+
* which is logged: the boundary then injects what it stored, as it did
|
|
22108
|
+
* before strategies could say.
|
|
22109
|
+
*/
|
|
22110
|
+
private renderForInjection(agent: Agent, ids: MessageId[]): Array<ContentBlock[] | null> | undefined {
|
|
22111
|
+
if (ids.length === 0) return undefined;
|
|
22112
|
+
try {
|
|
22113
|
+
return agent.getContextManager().renderAsNewest(ids);
|
|
22114
|
+
} catch (error) {
|
|
22115
|
+
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);
|
|
22116
|
+
return undefined;
|
|
22117
|
+
}
|
|
22118
|
+
}
|
|
22119
|
+
|
|
22120
|
+
/**
|
|
22121
|
+
* Tell the agent of the messages a tool boundary stored and left out of
|
|
22122
|
+
* its injection, since the next compile won't send them among its newest:
|
|
22123
|
+
* who sent each and its size, and how to read it, `history--extract`
|
|
22124
|
+
* around it where the agent is shown that tool, else its id. None of a
|
|
22125
|
+
* message's words: the line stands in for a message the live round
|
|
22126
|
+
* doesn't carry. A system line, so it's no conversation: it neither
|
|
22127
|
+
* re-pins the turn nor clears its send suppression. It names the agent
|
|
22128
|
+
* it's for, since every resident sharing the slot reads it too. Returns
|
|
22129
|
+
* the notice as stored, for the boundary to inject after the batch, or
|
|
22130
|
+
* undefined when it couldn't be stored, which is logged.
|
|
22131
|
+
*/
|
|
22132
|
+
private recordLeftOutNotice(
|
|
22133
|
+
agent: Agent,
|
|
22134
|
+
leftOut: ReadonlyArray<{ msg: { participant: string }; id: MessageId; content: ContentBlock[]; parts: number }>,
|
|
22135
|
+
): { participant: string; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
22136
|
+
const canExtract = this.advertisedToolsForAgent(agent.name).some((t) => t.name === 'history--extract');
|
|
22137
|
+
const one = leftOut.length === 1;
|
|
22138
|
+
const lines = leftOut.map(({ msg, id, content, parts }) => {
|
|
22139
|
+
const size = contentSize(content);
|
|
22140
|
+
// A sharded body is that many store messages, consecutive from its
|
|
22141
|
+
// first, which extract reads around it one by one. (Its "after" stops
|
|
22142
|
+
// at 100, so a body of more parts, over 300,000 tokens at the memory
|
|
22143
|
+
// strategy's default shard size, is read only that far.)
|
|
22144
|
+
const read = canExtract
|
|
22145
|
+
? `history--extract with aroundId "${id}", before 0 and after ${parts - 1} reads it` +
|
|
22146
|
+
`${parts > 1 ? `, stored as ${parts} parts` : ''}.`
|
|
22147
|
+
: `Its id is ${id}${parts > 1 ? `, the first of the ${parts} parts it's stored as` : ''}.`;
|
|
22148
|
+
return `- From ${msg.participant}${size ? `, ${size}` : ''}. ${read}`;
|
|
22149
|
+
});
|
|
22150
|
+
const content: ContentBlock[] = [{
|
|
22151
|
+
type: 'text',
|
|
22152
|
+
text: `[not shown] For ${agent.name}: ${one ? 'A message' : `${leftOut.length} messages`} arrived during your turn that ` +
|
|
22153
|
+
`${one ? "isn't" : "aren't"} shown here, since your context doesn't keep ${one ? 'it' : 'them'} among ` +
|
|
22154
|
+
`your newest messages:\n${lines.join('\n')}`,
|
|
22155
|
+
}];
|
|
22156
|
+
const metadata = { system: true, kind: 'left-out-notice' } as MessageMetadata;
|
|
22157
|
+
try {
|
|
22158
|
+
const id = agent.getContextManager().addMessage('user', content, metadata);
|
|
22159
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'left-out-notice' });
|
|
22160
|
+
return { participant: 'user', content, metadata };
|
|
22161
|
+
} catch (err) {
|
|
22162
|
+
console.error(`[injection] ${agent.name}: couldn't store the notice of messages left out of the live round:`, err);
|
|
22163
|
+
return undefined;
|
|
22164
|
+
}
|
|
22165
|
+
}
|
|
22166
|
+
|
|
21808
22167
|
/**
|
|
21809
22168
|
* Write drained entries in queue (seq) order, whatever their targets:
|
|
21810
22169
|
* residents share one message slot, so the order entries arrived in is the
|
|
@@ -21927,7 +22286,7 @@ export class AgentFramework {
|
|
|
21927
22286
|
continue;
|
|
21928
22287
|
}
|
|
21929
22288
|
if (found.length > 0) {
|
|
21930
|
-
written.set(msg, { id: found[0]!.id, content: asCompiled(found) });
|
|
22289
|
+
written.set(msg, { id: found[0]!.id, content: asCompiled(found), parts: found.length });
|
|
21931
22290
|
this.logDeferredWriteError(msg, where, undefined, { count: found.length, sharded: found[0]!.bodyGroupId !== undefined, foundLater: true });
|
|
21932
22291
|
onLanded?.(msg, found[0]!.id);
|
|
21933
22292
|
continue;
|
|
@@ -21944,14 +22303,14 @@ export class AgentFramework {
|
|
|
21944
22303
|
const copies = agent && before !== undefined && (this.storedCountOf(agent) ?? before) - before > 1
|
|
21945
22304
|
? this.storedCopiesOf(agent, before, msg.id) ?? []
|
|
21946
22305
|
: [];
|
|
21947
|
-
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content });
|
|
22306
|
+
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content, parts: Math.max(1, copies.length) });
|
|
21948
22307
|
} catch (error) {
|
|
21949
22308
|
// What landed, read back from where the store stood before the
|
|
21950
22309
|
// write; undefined when the store couldn't say, before the write or
|
|
21951
22310
|
// after it. With no agent there's no store it could have landed in.
|
|
21952
22311
|
const landed = !agent ? [] : before === undefined ? undefined : this.storedCopiesOf(agent, before, msg.id);
|
|
21953
22312
|
if (landed && landed.length > 0) {
|
|
21954
|
-
written.set(msg, { id: landed[0]!.id, content: asCompiled(landed) });
|
|
22313
|
+
written.set(msg, { id: landed[0]!.id, content: asCompiled(landed), parts: landed.length });
|
|
21955
22314
|
this.logDeferredWriteError(msg, where, error, { count: landed.length, sharded: landed[0]!.bodyGroupId !== undefined });
|
|
21956
22315
|
onLanded?.(msg, landed[0]!.id);
|
|
21957
22316
|
continue;
|
|
@@ -21992,13 +22351,26 @@ export class AgentFramework {
|
|
|
21992
22351
|
* `bodyGroupId`. Undefined when the store can't be read back, which is
|
|
21993
22352
|
* logged: not the same as nothing found. */
|
|
21994
22353
|
private storedCopiesOf(agent: Agent, from: number, deferredWriteId: string): StoredCopy[] | undefined {
|
|
22354
|
+
return this.storedSince(
|
|
22355
|
+
agent, from, (m) => m.metadata?.deferredWriteId === deferredWriteId, 'deferral', `deferred write ${deferredWriteId}`);
|
|
22356
|
+
}
|
|
22357
|
+
|
|
22358
|
+
/** The messages `match` picks among those the agent's store gained from
|
|
22359
|
+
* position `from` on, in store order, with their blobs resolved. Undefined
|
|
22360
|
+
* when the store can't be read back, which is logged under `tag`, naming
|
|
22361
|
+
* `what` was written: not the same as nothing found. */
|
|
22362
|
+
private storedSince(
|
|
22363
|
+
agent: Agent,
|
|
22364
|
+
from: number,
|
|
22365
|
+
match: (m: StoredMessage) => boolean,
|
|
22366
|
+
tag: string,
|
|
22367
|
+
what: string,
|
|
22368
|
+
): StoredCopy[] | undefined {
|
|
21995
22369
|
try {
|
|
21996
22370
|
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);
|
|
22371
|
+
return cm.getMessageWindow(from, Math.max(0, cm.getMessageCount() - from)).messages.filter(match).map(storedCopy);
|
|
22000
22372
|
} catch (error) {
|
|
22001
|
-
console.error(`[
|
|
22373
|
+
console.error(`[${tag}] could not read back what ${what} stored in ${agent.name}'s store:`, error);
|
|
22002
22374
|
return undefined;
|
|
22003
22375
|
}
|
|
22004
22376
|
}
|
|
@@ -25185,6 +25557,9 @@ export class AgentFramework {
|
|
|
25185
25557
|
// configured servers can still deploy its first one at runtime.
|
|
25186
25558
|
if (!this.mcplServerRegistry || !this.mcplHostCapabilities) {
|
|
25187
25559
|
await this.initializeMcpl([], this.mcplInferenceRoutingConfig ?? undefined);
|
|
25560
|
+
// The lifecycle log's replay restored any tune-out on record, and no
|
|
25561
|
+
// coordinator is made here to carry one.
|
|
25562
|
+
this.endTuneOutsWithoutCoordinator();
|
|
25188
25563
|
}
|
|
25189
25564
|
|
|
25190
25565
|
const collision = this.mcplPrefixCollision(config, undefined, claim);
|
|
@@ -27050,6 +27425,47 @@ export class AgentFramework {
|
|
|
27050
27425
|
};
|
|
27051
27426
|
}
|
|
27052
27427
|
|
|
27428
|
+
/**
|
|
27429
|
+
* A tune-out on record where this host has no tune-out coordinator: a
|
|
27430
|
+
* start with no subconscious makes none, and neither does a channel
|
|
27431
|
+
* subsystem brought up after start by connectMcplServer. Nothing then
|
|
27432
|
+
* diverts the channel's traffic or ever cancels its tune-out, tune_out
|
|
27433
|
+
* isn't offered, and channel_open, channel_close and channel_decline
|
|
27434
|
+
* refuse a tuned-out channel, so the channel would stay held by a decision
|
|
27435
|
+
* nothing here can change. Each one ends here instead, its channel decided
|
|
27436
|
+
* open again as a cancel leaves it, under the tune-out's own source, so an
|
|
27437
|
+
* agent's tune-out keeps the agent's provenance (the housekeeping guard
|
|
27438
|
+
* reads it). A log line names the channel and says how many of the
|
|
27439
|
+
* messages it diverted are held here, stored or still queued to be
|
|
27440
|
+
* written, which nothing will now deliver as its backlog.
|
|
27441
|
+
*/
|
|
27442
|
+
private endTuneOutsWithoutCoordinator(): void {
|
|
27443
|
+
const registry = this.channelRegistry;
|
|
27444
|
+
if (!registry || this.tuneOutCoordinator) return;
|
|
27445
|
+
const primary = this.primaryAgentName ? this.agents.get(this.primaryAgentName) : undefined;
|
|
27446
|
+
const ofEpoch = (epochId: string) => (m: { metadata?: unknown }): boolean =>
|
|
27447
|
+
(m.metadata as { tuneOut?: { epochId?: string } } | undefined)?.tuneOut?.epochId === epochId;
|
|
27448
|
+
for (const { serverId, channelId, params, source } of registry.tuneOutsOnRecord()) {
|
|
27449
|
+
const diverted = typeof params.epochId !== 'string' ? 0
|
|
27450
|
+
: (primary?.getContextManager().getAllMessages() ?? []).filter(ofEpoch(params.epochId)).length +
|
|
27451
|
+
this.deferredMessages.filter((m) => (m.forAgent ?? this.primaryAgentName) === this.primaryAgentName)
|
|
27452
|
+
.filter(ofEpoch(params.epochId)).length;
|
|
27453
|
+
const held = diverted === 0 ? ''
|
|
27454
|
+
: diverted === 1 ? ' The 1 message it diverted stays here, and nothing will deliver it as its backlog.'
|
|
27455
|
+
: ` The ${diverted} messages it diverted stay here, and nothing will deliver them as its backlog.`;
|
|
27456
|
+
try {
|
|
27457
|
+
registry.cancelTuneOut(serverId, channelId, 'open', source);
|
|
27458
|
+
} catch (err) {
|
|
27459
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server is tuned out, and nothing on this host watches ` +
|
|
27460
|
+
`or cancels tune-outs, but ending this one failed, so it stays recorded until a start that can end it: ` +
|
|
27461
|
+
`${err instanceof Error ? err.message : String(err)}.${held}`);
|
|
27462
|
+
continue;
|
|
27463
|
+
}
|
|
27464
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server was tuned out, and nothing on this host watches ` +
|
|
27465
|
+
`or cancels tune-outs, so the tune-out ends here and the channel is decided open again.${held}`);
|
|
27466
|
+
}
|
|
27467
|
+
}
|
|
27468
|
+
|
|
27053
27469
|
/**
|
|
27054
27470
|
* Build BeforeInferenceParams from agent state and trigger context.
|
|
27055
27471
|
*/
|
|
@@ -27924,11 +28340,24 @@ export class AgentFramework {
|
|
|
27924
28340
|
// carries the same ref.
|
|
27925
28341
|
const state = agent.state;
|
|
27926
28342
|
if (state.status === 'waiting_for_tools') {
|
|
27927
|
-
|
|
28343
|
+
// The commit stores a round as each result would be alone, or by
|
|
28344
|
+
// its share where they pass the bound together, which a sibling
|
|
28345
|
+
// still running can decide. Where the two ways differ for a result
|
|
28346
|
+
// counted here, the round is marked to be stored by its share
|
|
28347
|
+
// (toolRoundsByShare), so the count holds either way: a result
|
|
28348
|
+
// counted before the mark is the same both ways. The share is of
|
|
28349
|
+
// the round's calls, finished or not.
|
|
28350
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
28351
|
+
const share = toolResultShare(cap, bound, state.pending.size + state.completed.length);
|
|
27928
28352
|
for (let i = state.completed.length - 1; i >= 0 && collecting(); i--) {
|
|
27929
28353
|
const tc = state.completed[i]!;
|
|
27930
28354
|
const { text, shown } = this.storedToolResultText(agentName, tc, cap);
|
|
27931
|
-
|
|
28355
|
+
const alone = storedImageSlots(text, cap, shown, bound);
|
|
28356
|
+
const byShare = storedImageSlots(text, share.cap, shown, share.bound);
|
|
28357
|
+
if (alone.length !== byShare.length || alone.some((slot, j) => slot.text !== byShare[j]!.text)) {
|
|
28358
|
+
this.toolRoundsByShare.add(state.completed);
|
|
28359
|
+
}
|
|
28360
|
+
considerToolResultImages(tc.id, tc.name, this.toolRoundsByShare.has(state.completed) ? byShare : alone, 0);
|
|
27932
28361
|
}
|
|
27933
28362
|
}
|
|
27934
28363
|
|