@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/dist/src/framework.js
CHANGED
|
@@ -46,7 +46,7 @@ import { SEND_TOOLS, bareToolName } from './send-tools.js';
|
|
|
46
46
|
import { boundedErrorMessage, errorMessage } from './error-message.js';
|
|
47
47
|
import { ProviderWaits, EVERY_MODEL, isReleaseModel, withoutControls } from './provider-waits.js';
|
|
48
48
|
import { nonErrorResultText, toolResultDataToHistoryString, truncateForHistory, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, } from './tool-result-history.js';
|
|
49
|
-
import { TOOL_IMAGE_REF_RE, ToolImageLedger,
|
|
49
|
+
import { TOOL_IMAGE_REF_RE, ToolImageLedger, cutStoredToolResultToFit, formatKeptImageSlots, parseImagePlaceholders, sha256Hex, storedImageSlots, toolResultCutAt, toolResultShare, } from './tool-image-ledger.js';
|
|
50
50
|
import { randomUUID, createHash } from 'node:crypto';
|
|
51
51
|
import { describeCodeExecution, CODE_EXECUTION_TOOL_NAME, isolateSettings, validateCodeExecutionConfig, } from './code-execution/tool-definition.js';
|
|
52
52
|
import { AgentScripts, endingWakes, ScriptRefusal, } from './code-execution/isolate/scripts.js';
|
|
@@ -56,6 +56,7 @@ import { bounded, renderList, renderReturn, VALUE_CHARS } from './code-execution
|
|
|
56
56
|
import { splitProseRuns, withoutRoundsProse } from './prose-segments.js';
|
|
57
57
|
import { cumulativeDelta } from './usage-accounting.js';
|
|
58
58
|
import { stampThinkingTokenEstimates } from './thinking-token-stamp.js';
|
|
59
|
+
import { contentAsStored, nameAsStored } from './text-as-stored.js';
|
|
59
60
|
/** An MCPL tool call refused because its server isn't in the registry: one
|
|
60
61
|
* closed for good, one removed, or one whose connect failed without
|
|
61
62
|
* reconnect, which this host doesn't bring back on its own. A server that's
|
|
@@ -294,9 +295,12 @@ function malformedResponseContent(response) {
|
|
|
294
295
|
* mid-turn message's text at chars/4 and its other blocks about as
|
|
295
296
|
* context-manager's store prices them (injectedBlockTokens), and a small
|
|
296
297
|
* per-block envelope allowance. On membrane's native tool path a mid-turn
|
|
297
|
-
* message rides into the next round
|
|
298
|
-
*
|
|
299
|
-
*
|
|
298
|
+
* message rides into the next round as the boundary injects it, which is as
|
|
299
|
+
* the strategy renders it for the next compile, cut and with its images
|
|
300
|
+
* limited (renderForInjection), or its stored content where the strategy
|
|
301
|
+
* can't say (provideToolResults' injectedMessages). Its XML tool path declares that it
|
|
302
|
+
* can't carry injected messages, so the tool boundary injects none there and
|
|
303
|
+
* that term is zero. Deliberately a
|
|
300
304
|
* ceiling-ish heuristic, but for a mid-turn message's media at the store's
|
|
301
305
|
* flat prices — the cost of overestimating is one early recompile;
|
|
302
306
|
* the cost of underestimating is the 400 this projection exists to prevent.
|
|
@@ -533,6 +537,7 @@ import { MALFORMED_CONTENT_NOTICE, normalizeStandardToolResult } from './mcpl/to
|
|
|
533
537
|
import { boundedForLog, describeCause } from './mcpl/errors.js';
|
|
534
538
|
import { assertServerConfigBooleans, buildWebSocketUrl, isWebSocketTransport } from './mcpl/transport.js';
|
|
535
539
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
540
|
+
import { CLOCK_ROW_KIND, clockStampOf, planClock, takesClockBefore } from './clock-rows.js';
|
|
536
541
|
import { firstFreeBranchName } from './branch-names.js';
|
|
537
542
|
import { DEFAULT_DISCORD_AWARENESS_EMOJI, DiscordAwarenessOutbox, DiscordAwarenessRefusal, defaultDiscordAwarenessOutboxPath, extractDiscordAwarenessRefs, selectDiscordAwarenessRefs, checkedDiscordAwarenessMarks, boundDiscordAwarenessText, discordChannelOf, isPermanentDiscordReactionFailure, unsyncedWriteText, } from './recovery/discord-awareness-outbox.js';
|
|
538
543
|
import { OperatorActionError, OperatorLog, capIds, defaultOperatorLogPath, } from './operator-log.js';
|
|
@@ -572,6 +577,25 @@ function asCompiled(landed) {
|
|
|
572
577
|
const text = blocks.filter((b) => b.type === 'text').map((b) => b.text).join('');
|
|
573
578
|
return [...blocks.filter((b) => b.type !== 'text'), { type: 'text', text }];
|
|
574
579
|
}
|
|
580
|
+
/** A message's size as a notice gives it: its text's characters and its
|
|
581
|
+
* images, each left out when there are none, so empty when it has
|
|
582
|
+
* neither. */
|
|
583
|
+
function contentSize(content) {
|
|
584
|
+
let chars = 0;
|
|
585
|
+
let images = 0;
|
|
586
|
+
for (const b of content) {
|
|
587
|
+
if (b.type === 'text')
|
|
588
|
+
chars += b.text.length;
|
|
589
|
+
else if (b.type === 'image' || b.type === 'generated_image')
|
|
590
|
+
images++;
|
|
591
|
+
}
|
|
592
|
+
const parts = [];
|
|
593
|
+
if (chars > 0)
|
|
594
|
+
parts.push(`${chars.toLocaleString('en-US')} character${chars === 1 ? '' : 's'}`);
|
|
595
|
+
if (images > 0)
|
|
596
|
+
parts.push(`${images} image${images === 1 ? '' : 's'}`);
|
|
597
|
+
return parts.join(' and ');
|
|
598
|
+
}
|
|
575
599
|
const storedCopy = (m) => ({ id: m.id, content: m.content, ...(m.bodyGroupId !== undefined ? { bodyGroupId: m.bodyGroupId } : {}) });
|
|
576
600
|
/** Stamp a deferred write's durable id into the metadata it is stored with
|
|
577
601
|
* (boot recovery dedups replays by it). Idempotent for an already-stamped
|
|
@@ -600,7 +624,8 @@ function describeDeferredWrite(msg) {
|
|
|
600
624
|
function isInjectableMidTurn(msg, agentName) {
|
|
601
625
|
// Exact, as membrane's role test is: only a participant equal to the
|
|
602
626
|
// request's assistantParticipant (Agent's name) renders as the assistant.
|
|
603
|
-
|
|
627
|
+
// It's the name as sent, which the boundary takes as the store keeps it.
|
|
628
|
+
return nameAsStored(msg.participant) !== agentName
|
|
604
629
|
&& !msg.content.some((b) => b.type === 'tool_use' || b.type === 'tool_result');
|
|
605
630
|
}
|
|
606
631
|
/**
|
|
@@ -691,6 +716,11 @@ function isTurnContinuation(reason) {
|
|
|
691
716
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
692
717
|
|| reason === 'tool_result_guard_retry';
|
|
693
718
|
}
|
|
719
|
+
/** Wake sources the framework generates for itself (bounce wakes,
|
|
720
|
+
* continuations, retries, unsticks, admission requeues). A wake from any
|
|
721
|
+
* other source — a channel message, a heartbeat or other server event, a
|
|
722
|
+
* gate flush, a timer, an operator nudge — is a cause of its own. */
|
|
723
|
+
const FRAMEWORK_WAKE_SOURCES = new Set(['framework', 'scheduler']);
|
|
694
724
|
const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
|
|
695
725
|
const INFERENCE_LOG_ID = 'framework/inference-log';
|
|
696
726
|
const PROCESS_LOG_ID = 'framework/process-log';
|
|
@@ -1983,6 +2013,10 @@ export class AgentFramework {
|
|
|
1983
2013
|
* ephemeral agents have isolated slots and are their own only readers.
|
|
1984
2014
|
*/
|
|
1985
2015
|
sharedSlotAgents = new Set();
|
|
2016
|
+
/** Rounds of tool calls, by their results array, that a save_recent_image
|
|
2017
|
+
* in the round counted by their share of the bound: such a round is
|
|
2018
|
+
* stored by its share whether or not it fits (buildStoredToolResultContent). */
|
|
2019
|
+
toolRoundsByShare = new WeakSet();
|
|
1986
2020
|
/** The windows of forks and runs deregistered while the host was quiesced,
|
|
1987
2021
|
* by agent name (releaseWindowNowOrAfterResume); resume() releases them
|
|
1988
2022
|
* (runReleasesAfterResume), or stop() passes on what they left, unread or
|
|
@@ -2630,6 +2664,9 @@ export class AgentFramework {
|
|
|
2630
2664
|
});
|
|
2631
2665
|
framework.tuneOutCoordinator.resumeActiveEpochs();
|
|
2632
2666
|
}
|
|
2667
|
+
else {
|
|
2668
|
+
framework.endTuneOutsWithoutCoordinator();
|
|
2669
|
+
}
|
|
2633
2670
|
// Modules, agents, MCPL servers and the tune-out tool are all in place:
|
|
2634
2671
|
// the first point at which "matches no tool" can be judged. Later tool
|
|
2635
2672
|
// refreshes re-check patterns still undecided.
|
|
@@ -9969,6 +10006,10 @@ export class AgentFramework {
|
|
|
9969
10006
|
// Speech happens only via speak_in_channel (its own marked voice).
|
|
9970
10007
|
proseRouting: 'explicit',
|
|
9971
10008
|
allowedTools: [...SUBCONSCIOUS_TOOL_NAMES, 'think', 'skip_reply', 'end_turn'],
|
|
10009
|
+
// No clock rows of its own: its merged view carries the residents'
|
|
10010
|
+
// clocks, and clocks it planned from its own rows alone would read the
|
|
10011
|
+
// time a second time beside them, or disagree with them.
|
|
10012
|
+
clock: undefined,
|
|
9972
10013
|
};
|
|
9973
10014
|
const agent = new Agent(agentConfig, contextManager, this.membrane);
|
|
9974
10015
|
this.adoptToolResultGuard(agent);
|
|
@@ -10115,14 +10156,14 @@ export class AgentFramework {
|
|
|
10115
10156
|
if (currentState.status === 'ready') {
|
|
10116
10157
|
// Effective inline cap: agent_settings override → durable config →
|
|
10117
10158
|
// house default, clamped to the strategy bound (see resolver).
|
|
10118
|
-
const maxChars = this.resolveToolResultInlineCap(agent)
|
|
10159
|
+
const { cap: maxChars, bound } = this.resolveToolResultInlineCap(agent);
|
|
10119
10160
|
// Oversized results — successes AND errors — spill to a workspace
|
|
10120
10161
|
// file (truncated preview + file reference) instead of being
|
|
10121
10162
|
// blind-truncated. Computed ONCE per call and reused for both the
|
|
10122
10163
|
// history copy and the live wire copy below — the two must stay
|
|
10123
10164
|
// byte-matched (the window stores what the membrane sends;
|
|
10124
10165
|
// divergence breaks the compile prefix).
|
|
10125
|
-
const { blocks: toolResultContent, spilled } = await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars);
|
|
10166
|
+
const { blocks: toolResultContent, spilled, keptCap } = await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars, bound);
|
|
10126
10167
|
// A spill writes to the workspace, and the turn can be stopped
|
|
10127
10168
|
// meanwhile. A host's cancelStream, stop() and abortInference cancel
|
|
10128
10169
|
// the round's stream at `ready` and leave the agent idle, and an
|
|
@@ -10172,7 +10213,7 @@ export class AgentFramework {
|
|
|
10172
10213
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
10173
10214
|
}
|
|
10174
10215
|
const membraneResults = currentState.toolResults.map(tc => this.toMembraneToolResult(tc.id, tc.result, maxChars, spilled.get(tc.id)));
|
|
10175
|
-
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata,
|
|
10216
|
+
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata, keptCap);
|
|
10176
10217
|
// Flush any messages that were deferred while this turn was in
|
|
10177
10218
|
// flight: this agent's own entries, those addressed to it and, for
|
|
10178
10219
|
// the primary, those addressed to no one (module messages, push
|
|
@@ -10184,14 +10225,21 @@ export class AgentFramework {
|
|
|
10184
10225
|
// live turn sees them instead of the agent staying deaf until the
|
|
10185
10226
|
// turn ends. The window stores them here, right after the
|
|
10186
10227
|
// tool_result message, which is exactly where the membrane appends
|
|
10187
|
-
// them in the live conversation — same participant,
|
|
10188
|
-
//
|
|
10189
|
-
//
|
|
10190
|
-
//
|
|
10191
|
-
//
|
|
10192
|
-
//
|
|
10193
|
-
//
|
|
10194
|
-
//
|
|
10228
|
+
// them in the live conversation — same participant, and neither
|
|
10229
|
+
// carries the row's metadata — and each goes in as the next compile
|
|
10230
|
+
// will send it, which the strategy renders (renderForInjection): cut
|
|
10231
|
+
// by the per-message cap, a sharded body joined, and its images as
|
|
10232
|
+
// the image limits leave them at the newest position. So the next
|
|
10233
|
+
// turn's compiled prefix matches the live request (prompt-cache
|
|
10234
|
+
// safe), up to what came after them: a later message can still
|
|
10235
|
+
// take an earlier one past an image limit, as for any message, and
|
|
10236
|
+
// the next compile then strips that image where the live rounds
|
|
10237
|
+
// carried it. One the next compile won't send among its newest
|
|
10238
|
+
// isn't injected; a notice after the batch tells the agent of it
|
|
10239
|
+
// (recordLeftOutNotice). A write that threw after it was stored, whose landing
|
|
10240
|
+
// couldn't be read, is the exception: it's carried at the flush
|
|
10241
|
+
// that finds it, after what was stored since, so there the store and
|
|
10242
|
+
// the live request differ (writeDrainedInOrder).
|
|
10195
10243
|
//
|
|
10196
10244
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
10197
10245
|
// draining at another agent's boundary would store the messages
|
|
@@ -10226,21 +10274,29 @@ export class AgentFramework {
|
|
|
10226
10274
|
// a held message does, rather than sitting in the window between
|
|
10227
10275
|
// rounds the stream never showed it.
|
|
10228
10276
|
const midTurnInjections = [];
|
|
10229
|
-
//
|
|
10230
|
-
//
|
|
10231
|
-
|
|
10277
|
+
// What this boundary stored for the agent to hear, in store order,
|
|
10278
|
+
// injected or left out, for a boundary that leaves it to the turn's
|
|
10279
|
+
// next start as arrivals: that start's compile sends it.
|
|
10280
|
+
const heard = [];
|
|
10232
10281
|
if (currentState.supportsInjectedMessages !== false) {
|
|
10282
|
+
// A clock row due (storeDueClock) goes right after the round's
|
|
10283
|
+
// results, ahead of what is drained below, and joins the stream
|
|
10284
|
+
// the same way. A boundary that passed during the round can't
|
|
10285
|
+
// stand between a tool's call and its results, so this clock
|
|
10286
|
+
// reads the time it's written (clock-rows.ts).
|
|
10287
|
+
const clock = this.storeDueClock(agent, 'tool-boundary');
|
|
10233
10288
|
const deferred = this.drainDeferredFor(agent.name, (msg) => !isInjectableMidTurn(msg, agent.name));
|
|
10234
10289
|
// Stored in queue order, cut at the first that can't be stored: it
|
|
10235
10290
|
// and what follows wait for the turn's end, and only what was
|
|
10236
10291
|
// stored is injected (the round's results go on either way). One
|
|
10237
10292
|
// the store refuses as malformed is dropped instead, and what
|
|
10238
|
-
// follows goes on.
|
|
10239
|
-
//
|
|
10240
|
-
//
|
|
10241
|
-
//
|
|
10293
|
+
// follows goes on. What was stored is injected as the next compile
|
|
10294
|
+
// will send it (renderForInjection). Where the strategy can't say,
|
|
10295
|
+
// a sharded body read back from its store is injected as later
|
|
10296
|
+
// compiles join it, and a write that threw after storing, which
|
|
10297
|
+
// counts as stored, as what landed of it (writeDrainedInOrder).
|
|
10242
10298
|
const stored = this.writeDrainedInOrder(deferred, `tool boundary for ${agent.name}`, (msg) => agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
10243
|
-
for (const [msg, { id, content }] of stored) {
|
|
10299
|
+
for (const [msg, { id, content, parts }] of stored) {
|
|
10244
10300
|
// Every other resident reads what this boundary stores; for its
|
|
10245
10301
|
// own agent, the boundary weighs what it injects below.
|
|
10246
10302
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
@@ -10250,13 +10306,49 @@ export class AgentFramework {
|
|
|
10250
10306
|
// what the next compile hides (#232 gap 5).
|
|
10251
10307
|
if (msg.metadata?.tuneOut)
|
|
10252
10308
|
continue;
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10309
|
+
heard.push({ msg, id, content, parts });
|
|
10310
|
+
}
|
|
10311
|
+
// Rendered in one call: a render prepares as a compile does, so
|
|
10312
|
+
// the batch shares one preparation. The image limits count from
|
|
10313
|
+
// the batch's oldest message to the newest, as a compile does.
|
|
10314
|
+
// The clock, stored ahead of them, renders first in the same batch
|
|
10315
|
+
// and is injected first, as the next compile sends it.
|
|
10316
|
+
const rendered = this.renderForInjection(agent, [...(clock ? [clock.id] : []), ...heard.map((h) => h.id)]);
|
|
10317
|
+
const first = clock ? 1 : 0;
|
|
10318
|
+
if (clock) {
|
|
10319
|
+
const sent = rendered ? rendered[0] : clock.content;
|
|
10320
|
+
if (sent !== null) {
|
|
10321
|
+
midTurnInjections.push({ participant: 'user', content: sent, metadata: clock.metadata });
|
|
10322
|
+
}
|
|
10323
|
+
else {
|
|
10324
|
+
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`);
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
const leftOut = [];
|
|
10328
|
+
heard.forEach((h, i) => {
|
|
10329
|
+
const sent = rendered ? rendered[i + first] : h.content;
|
|
10330
|
+
// The next compile won't send it among its newest: injected, it
|
|
10331
|
+
// would put in the live turn what that compile sends elsewhere,
|
|
10332
|
+
// or not at all. It's under any consumed mark the boundary sets
|
|
10333
|
+
// (below), as anything stored here is.
|
|
10334
|
+
if (sent === null) {
|
|
10335
|
+
leftOut.push(h);
|
|
10336
|
+
return;
|
|
10337
|
+
}
|
|
10338
|
+
midTurnInjections.push({
|
|
10339
|
+
participant: h.msg.participant,
|
|
10340
|
+
content: sent,
|
|
10341
|
+
...(h.msg.metadata ? { metadata: h.msg.metadata } : {}),
|
|
10342
|
+
});
|
|
10343
|
+
});
|
|
10344
|
+
if (leftOut.length > 0) {
|
|
10345
|
+
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(', ')}`);
|
|
10346
|
+
// The agent is told what it didn't hear, after the batch, as
|
|
10347
|
+
// the routing notice is: stored, so the next compile sends it
|
|
10348
|
+
// where the live round heard it.
|
|
10349
|
+
const notice = this.recordLeftOutNotice(agent, leftOut);
|
|
10350
|
+
if (notice)
|
|
10351
|
+
midTurnInjections.push(notice);
|
|
10260
10352
|
}
|
|
10261
10353
|
}
|
|
10262
10354
|
// Deferred writes handed to the store above: sync, then drop them
|
|
@@ -10391,11 +10483,12 @@ export class AgentFramework {
|
|
|
10391
10483
|
}
|
|
10392
10484
|
else if (routesLocus && !shouldEndTurn) {
|
|
10393
10485
|
// A boundary that can't re-pin now (over budget, or no live
|
|
10394
|
-
// stream to tell) leaves
|
|
10395
|
-
// as arrivals: a budget restart, or a
|
|
10396
|
-
// results.
|
|
10397
|
-
|
|
10398
|
-
|
|
10486
|
+
// stream to tell) leaves what it stored for the agent to the
|
|
10487
|
+
// turn's next start, as arrivals: a budget restart, or a
|
|
10488
|
+
// re-inference on these tool results. That includes a message
|
|
10489
|
+
// left out of the injection, since that start compiles anew.
|
|
10490
|
+
for (const { msg, id } of heard)
|
|
10491
|
+
this.noteArrival(agent.name, msg.metadata, id);
|
|
10399
10492
|
}
|
|
10400
10493
|
if (shouldEndTurn) {
|
|
10401
10494
|
// endTurn: what this boundary stored stays; cancel stream, reset to idle.
|
|
@@ -10469,9 +10562,12 @@ export class AgentFramework {
|
|
|
10469
10562
|
// 0.5.x, and from 0.6.0 declares it can't carry them, so none are
|
|
10470
10563
|
// collected for it. Each goes as its participant and content
|
|
10471
10564
|
// alone: a stored row's metadata is the host's record, not input
|
|
10472
|
-
// for membrane.
|
|
10565
|
+
// for membrane. Each goes with its name and text as the store
|
|
10566
|
+
// keeps them, whether it's a heard message, rendered or not, or a
|
|
10567
|
+
// notice the boundary made: this is where they leave for the
|
|
10568
|
+
// request.
|
|
10473
10569
|
currentState.stream.provideToolResults(agent.toolResultGuard.submissionResults(membraneResults), midTurnInjections.length > 0
|
|
10474
|
-
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
10570
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant: nameAsStored(participant), content: contentAsStored(content) })) }
|
|
10475
10571
|
: undefined);
|
|
10476
10572
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
10477
10573
|
// is assembly too when the stream carries the injected messages
|
|
@@ -10496,6 +10592,18 @@ export class AgentFramework {
|
|
|
10496
10592
|
// Under context-manager#160 such a write already gives the
|
|
10497
10593
|
// replies stored after it stamps their requests didn't carry,
|
|
10498
10594
|
// whatever the mark says; deferring those writers is the fix.
|
|
10595
|
+
// The mark also covers a message the boundary stored and left out
|
|
10596
|
+
// of the injection, since the next compile won't send it among its
|
|
10597
|
+
// newest: the notice telling the agent of it is injected, so a
|
|
10598
|
+
// boundary that leaves one out injects something, unless that
|
|
10599
|
+
// notice couldn't be stored. The message then counts as read
|
|
10600
|
+
// though no request carried it: a coalesced render's wake to read
|
|
10601
|
+
// it drops, and a release doesn't pass it to the primary. The
|
|
10602
|
+
// notice has told the agent it came, from whom, and how to read
|
|
10603
|
+
// it. Where the view leaves it out, no request will carry it.
|
|
10604
|
+
// Where the memory strategy places it before its recent window, a
|
|
10605
|
+
// batch bigger than that window, the next compile carries it, in
|
|
10606
|
+
// whatever turn something else starts.
|
|
10499
10607
|
if (midTurnInjections.length > 0) {
|
|
10500
10608
|
// Possibly shown, whatever the stream declares: the first-view
|
|
10501
10609
|
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
@@ -10774,6 +10882,11 @@ export class AgentFramework {
|
|
|
10774
10882
|
return undefined;
|
|
10775
10883
|
if (this.conversationRouter && this.conversationAgentHomes.has(target.name)) {
|
|
10776
10884
|
metadata.triggered = event.triggerInference ?? false;
|
|
10885
|
+
// A fork's channel messages are stored past its deferral; a clock goes
|
|
10886
|
+
// ahead of one only while the fork has no turn alive (storeDueClock).
|
|
10887
|
+
if (!this.turnAlive(target) && takesClockBefore('user', event.content, metadata, target.name)) {
|
|
10888
|
+
this.storeDueClock(target, 'event');
|
|
10889
|
+
}
|
|
10777
10890
|
const id = target.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(metadata, event.content));
|
|
10778
10891
|
// A correction may target an older engagement. Refresh only the
|
|
10779
10892
|
// binding whose agent actually received the message.
|
|
@@ -10933,6 +11046,10 @@ export class AgentFramework {
|
|
|
10933
11046
|
// on top of the router's own trigger policy.
|
|
10934
11047
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
10935
11048
|
messageMetadata.triggered = trigger;
|
|
11049
|
+
// As for a correction above: a clock ahead only while no turn is alive.
|
|
11050
|
+
if (!this.turnAlive(agent) && takesClockBefore('user', event.content, messageMetadata, agent.name)) {
|
|
11051
|
+
this.storeDueClock(agent, 'event');
|
|
11052
|
+
}
|
|
10936
11053
|
const id = agent.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(messageMetadata, event.content));
|
|
10937
11054
|
router.touch(event.channelId);
|
|
10938
11055
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
@@ -12619,6 +12736,16 @@ export class AgentFramework {
|
|
|
12619
12736
|
// requests[0] in that mixed batch bypassed the turn lock because a
|
|
12620
12737
|
// restart existed, then treated the continuation as a fresh turn.
|
|
12621
12738
|
const trigger = budgetRestart ?? requests[0];
|
|
12739
|
+
// The bounce-wake cap (bounceProse) stops a bounce → wake → bounce loop
|
|
12740
|
+
// that nothing else feeds. A turn with a cause of its own is not that
|
|
12741
|
+
// loop, so the count starts again. Otherwise only a delivered prose
|
|
12742
|
+
// envelope reset it, and a resident that recovers from bounces with a
|
|
12743
|
+
// send tool kept the count for good: every later bounce, a reply to a
|
|
12744
|
+
// fresh message included, appended its notice without a wake.
|
|
12745
|
+
// (Optional chaining: prototype-built harnesses leave the map unset.)
|
|
12746
|
+
if (requests.some((r) => !FRAMEWORK_WAKE_SOURCES.has(r.source))) {
|
|
12747
|
+
this.proseBounceStreaks?.delete(agentName);
|
|
12748
|
+
}
|
|
12622
12749
|
// Route this turn's auto-published speech to the channel that triggered
|
|
12623
12750
|
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
12624
12751
|
// (messages arrived in >1 channel while the agent was busy/idle):
|
|
@@ -13671,6 +13798,101 @@ export class AgentFramework {
|
|
|
13671
13798
|
return m.channelId;
|
|
13672
13799
|
return routing.engaged.has(m.channelId) ? m.channelId : undefined;
|
|
13673
13800
|
}
|
|
13801
|
+
/** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
|
|
13802
|
+
turnAlive(agent) {
|
|
13803
|
+
return this.activeTurnTokens.has(agent.name)
|
|
13804
|
+
|| this.pendingAssistantBlocks.has(agent.name)
|
|
13805
|
+
|| this.activeStreams.has(agent.name);
|
|
13806
|
+
}
|
|
13807
|
+
/**
|
|
13808
|
+
* Store the clock row due in `agent`'s window (clock-rows.ts), if the agent
|
|
13809
|
+
* has clock rows and a boundary has passed since the window's last clock,
|
|
13810
|
+
* or the window holds no clock row yet (planClock), and return it. The
|
|
13811
|
+
* caller places it, by calling this right before what the clock precedes
|
|
13812
|
+
* is written: an incoming event while no turn of the agent's is alive (or
|
|
13813
|
+
* one is assembling before its compile), a fresh turn's inputs before its
|
|
13814
|
+
* compile, or, mid-turn, the messages a tool boundary injects, where the
|
|
13815
|
+
* caller injects the clock first. Each is a place where a row is either in
|
|
13816
|
+
* the request that follows or before every request still to come, so the
|
|
13817
|
+
* window and the live stream keep matching.
|
|
13818
|
+
*
|
|
13819
|
+
* None while the host is quiesced (the window is frozen for surgery), and
|
|
13820
|
+
* none in the residents' shared slot while another of its readers (the
|
|
13821
|
+
* subconscious among them, through its merged view) has a turn alive: the
|
|
13822
|
+
* row would sit inside that turn without joining its stream. A clock that
|
|
13823
|
+
* can't be planned or stored is logged and skipped: it never holds up what
|
|
13824
|
+
* it would have preceded. One whose write threw after it landed is the
|
|
13825
|
+
* window's all the same, so it's returned, and a tool boundary injects it.
|
|
13826
|
+
*/
|
|
13827
|
+
storeDueClock(agent, occasion) {
|
|
13828
|
+
const config = agent.clock;
|
|
13829
|
+
if (!config || this.quiesced)
|
|
13830
|
+
return undefined;
|
|
13831
|
+
if (this.sharedSlotAgents.has(agent)) {
|
|
13832
|
+
for (const reader of this.sharedSlotAgents) {
|
|
13833
|
+
if (reader !== agent && this.turnAlive(reader))
|
|
13834
|
+
return undefined;
|
|
13835
|
+
}
|
|
13836
|
+
}
|
|
13837
|
+
let planned;
|
|
13838
|
+
try {
|
|
13839
|
+
planned = planClock({ now: Date.now(), ...this.lastClockOf(agent.getContextManager()), config, zone: this.timeZone });
|
|
13840
|
+
}
|
|
13841
|
+
catch (err) {
|
|
13842
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}:`, err);
|
|
13843
|
+
return undefined;
|
|
13844
|
+
}
|
|
13845
|
+
if (!planned)
|
|
13846
|
+
return undefined;
|
|
13847
|
+
const metadata = { system: true, kind: CLOCK_ROW_KIND, clock: planned.stamp };
|
|
13848
|
+
const before = this.storedCountOf(agent);
|
|
13849
|
+
let id;
|
|
13850
|
+
try {
|
|
13851
|
+
id = agent.getContextManager().addMessage('user', planned.content, metadata);
|
|
13852
|
+
}
|
|
13853
|
+
catch (err) {
|
|
13854
|
+
// A write can throw after its row landed, as when context-manager's own
|
|
13855
|
+
// propagation of it fails: what landed is read back from where the
|
|
13856
|
+
// store stood before, as writeDrainedInOrder reads a drained write's.
|
|
13857
|
+
const at = planned.stamp.at;
|
|
13858
|
+
const landed = before === undefined
|
|
13859
|
+
? undefined
|
|
13860
|
+
: this.storedSince(agent, before, (m) => clockStampOf(m.metadata)?.at === at, 'clock', 'the clock write');
|
|
13861
|
+
if (!landed || landed.length === 0) {
|
|
13862
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}${landed ? '' : ' (whether it landed is unknown)'}:`, err);
|
|
13863
|
+
return undefined;
|
|
13864
|
+
}
|
|
13865
|
+
console.error(`[clock] ${agent.name}: the clock row at ${occasion} landed though its write threw:`, err);
|
|
13866
|
+
id = landed[0].id;
|
|
13867
|
+
}
|
|
13868
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: `clock:${occasion}` });
|
|
13869
|
+
return { id, content: planned.content, metadata };
|
|
13870
|
+
}
|
|
13871
|
+
/** Rows a window's last-clock scan reads at a time. */
|
|
13872
|
+
static CLOCK_SCAN_ROWS = 256;
|
|
13873
|
+
/** What the next clock is planned from: the window's newest row's time and
|
|
13874
|
+
* the time its last clock row reads, found by reading back from the
|
|
13875
|
+
* newest row, CLOCK_SCAN_ROWS at a time, until a clock row comes, however
|
|
13876
|
+
* far back it is. In the steady state the newest chunk holds it, unless
|
|
13877
|
+
* more rows than that have come since the last clock; a window with no
|
|
13878
|
+
* clock row is read whole once, and then takes one (planClock). */
|
|
13879
|
+
lastClockOf(cm) {
|
|
13880
|
+
let newestRowAt;
|
|
13881
|
+
for (let end = cm.getMessageCount(); end > 0;) {
|
|
13882
|
+
const span = Math.min(end, AgentFramework.CLOCK_SCAN_ROWS);
|
|
13883
|
+
const { messages } = cm.getMessageWindow(end - span, span, { resolveBlobs: false });
|
|
13884
|
+
if (newestRowAt === undefined && messages.length > 0) {
|
|
13885
|
+
newestRowAt = new Date(messages[messages.length - 1].timestamp).getTime();
|
|
13886
|
+
}
|
|
13887
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
13888
|
+
const stamp = clockStampOf(messages[i].metadata);
|
|
13889
|
+
if (stamp)
|
|
13890
|
+
return { newestRowAt, lastClockAt: stamp.at };
|
|
13891
|
+
}
|
|
13892
|
+
end -= span;
|
|
13893
|
+
}
|
|
13894
|
+
return { newestRowAt, lastClockAt: undefined };
|
|
13895
|
+
}
|
|
13674
13896
|
/**
|
|
13675
13897
|
* Weigh a message that no tool boundary of the agent's open logical turn
|
|
13676
13898
|
* will weigh: one that would re-pin the locus becomes one of the turn's
|
|
@@ -14622,7 +14844,9 @@ export class AgentFramework {
|
|
|
14622
14844
|
delete routing.pin;
|
|
14623
14845
|
}
|
|
14624
14846
|
}
|
|
14625
|
-
/** Cap on consecutive bounce-triggered wakes (notices still append after).
|
|
14847
|
+
/** Cap on consecutive bounce-triggered wakes (notices still append after).
|
|
14848
|
+
* The count resets on a delivered prose envelope, and when a turn starts
|
|
14849
|
+
* with a cause of its own (processInferenceRequests). */
|
|
14626
14850
|
static PROSE_BOUNCE_WAKE_CAP = 2;
|
|
14627
14851
|
/**
|
|
14628
14852
|
* Words about to be held that may use `{{unsent}}` (a bounce, or an
|
|
@@ -15133,6 +15357,17 @@ export class AgentFramework {
|
|
|
15133
15357
|
// turn, and flushing there would insert messages between its rounds.
|
|
15134
15358
|
// Before recordTurnCheckpoint: these are the turn's inputs, not its
|
|
15135
15359
|
// products — undoing the turn must not destroy them.
|
|
15360
|
+
// A clock row due (storeDueClock) goes ahead of the turn's inputs, so a
|
|
15361
|
+
// turn starts with a current clock, a silent heartbeat's tick (which
|
|
15362
|
+
// stores no row of its own) included, unless storeDueClock writes none:
|
|
15363
|
+
// while the host is quiesced, while another reader of the residents'
|
|
15364
|
+
// shared slot has a turn alive, or when its write fails without landing.
|
|
15365
|
+
// Then a later event, turn start or tool boundary takes it. Fresh turns
|
|
15366
|
+
// only, as the flush below: a turn that resumes continues its logical
|
|
15367
|
+
// turn, which took its clock at its start or at a tool boundary that
|
|
15368
|
+
// could carry one.
|
|
15369
|
+
if (attempt === 0 && !resumesTurn)
|
|
15370
|
+
this.storeDueClock(agent, 'turn-start');
|
|
15136
15371
|
if (attempt === 0 &&
|
|
15137
15372
|
!resumesTurn &&
|
|
15138
15373
|
this.deferredMessages.length > 0) {
|
|
@@ -16614,9 +16849,9 @@ export class AgentFramework {
|
|
|
16614
16849
|
// one door a giant blob still walks through).
|
|
16615
16850
|
const readyState = agent.state;
|
|
16616
16851
|
if (readyState.status === 'ready') {
|
|
16617
|
-
const cap = this.resolveToolResultInlineCap(agent)
|
|
16618
|
-
const { blocks: toolResultContent, spilled } = await this.buildStoredToolResultContent(agent.name, readyState.toolResults, cap);
|
|
16619
|
-
agent.toolResultGuard.storeResults(toolResultContent, readyState.toolResults.map((tc) => this.toMembraneToolResult(tc.id, tc.result, cap, spilled.get(tc.id))), readyState.toolResults, this.silentTurnRowMetadata(agent.name),
|
|
16852
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
16853
|
+
const { blocks: toolResultContent, spilled, keptCap } = await this.buildStoredToolResultContent(agent.name, readyState.toolResults, cap, bound);
|
|
16854
|
+
agent.toolResultGuard.storeResults(toolResultContent, readyState.toolResults.map((tc) => this.toMembraneToolResult(tc.id, tc.result, cap, spilled.get(tc.id))), readyState.toolResults, this.silentTurnRowMetadata(agent.name), keptCap);
|
|
16620
16855
|
// The response is already complete: this batch is never
|
|
16621
16856
|
// submitted in this turn, so it cannot be refused. Admit it.
|
|
16622
16857
|
agent.toolResultGuard.settleTurnEnded();
|
|
@@ -17852,7 +18087,8 @@ export class AgentFramework {
|
|
|
17852
18087
|
// token (as during a real tool call) and lands AFTER the pair, never
|
|
17853
18088
|
// between tool_use and its tool_result. The two addMessage calls
|
|
17854
18089
|
// below are synchronous and adjacent — nothing can interleave.
|
|
17855
|
-
const {
|
|
18090
|
+
const { bound } = this.resolveToolResultInlineCap(agent);
|
|
18091
|
+
const { blocks } = await this.buildStoredToolResultContent(agentName, [{ id: toolUseId, name: toolName, input, result, durationMs }], call.resultMaxChars, bound);
|
|
17856
18092
|
const toolUse = [
|
|
17857
18093
|
{ type: 'tool_use', id: toolUseId, name: toolName, input },
|
|
17858
18094
|
];
|
|
@@ -18294,10 +18530,12 @@ export class AgentFramework {
|
|
|
18294
18530
|
if (!this.noticeTargetFor(agentName, `script ${scriptId} wake`))
|
|
18295
18531
|
throw new Error(`agent ${agentName} has no context to wake`);
|
|
18296
18532
|
const agent = this.agents.get(agentName);
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18533
|
+
// A reader's strategy cuts the wake's whole message, so its spill fits the
|
|
18534
|
+
// readers' bound with its notice.
|
|
18535
|
+
const { cap: maxChars, bound } = agent
|
|
18536
|
+
? this.resolveToolResultInlineCap(agent)
|
|
18537
|
+
: { cap: DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, bound: undefined };
|
|
18538
|
+
const { text: shown } = await this.spillOrTruncate(text, maxChars, `script-${label}`, null, bound);
|
|
18301
18539
|
// The spill can take a while: the agent may have gone since, or the host begun to stop.
|
|
18302
18540
|
if (this.stopHasBegun())
|
|
18303
18541
|
throw new Error('this host is stopping');
|
|
@@ -18345,12 +18583,32 @@ export class AgentFramework {
|
|
|
18345
18583
|
* #89). Returns the stored blocks plus the per-call spill outcomes so the
|
|
18346
18584
|
* live wire copy can reuse the identical strings.
|
|
18347
18585
|
*/
|
|
18348
|
-
async buildStoredToolResultContent(agentName, toolResults, maxChars
|
|
18586
|
+
async buildStoredToolResultContent(agentName, toolResults, maxChars,
|
|
18587
|
+
/** The readers' smallest per-message bound (resolveToolResultInlineCap). */
|
|
18588
|
+
bound = undefined) {
|
|
18349
18589
|
const spilled = new Map();
|
|
18350
18590
|
const dateLabel = new Date().toISOString().slice(0, 10);
|
|
18591
|
+
// The results are stored as one message, whose bound a reader's
|
|
18592
|
+
// strategy spends across them in order. Each is stored as it would be
|
|
18593
|
+
// alone, unless together they pass the bound, or a save in the round
|
|
18594
|
+
// counted the round by its share (toolRoundsByShare): then each is
|
|
18595
|
+
// stored under its share (toolResultShare). A result carrying an image
|
|
18596
|
+
// keeps its live copy cut at the cap; any other's live copy is its stored
|
|
18597
|
+
// text.
|
|
18598
|
+
const alone = new Map();
|
|
18351
18599
|
for (const tc of toolResults) {
|
|
18352
18600
|
const { text, shown } = this.storedToolResultText(agentName, tc, maxChars);
|
|
18353
|
-
|
|
18601
|
+
alone.set(tc.id, { text, shown, stored: await this.spillOrTruncate(text, maxChars, `${dateLabel}-${tc.id}`, shown, bound) });
|
|
18602
|
+
}
|
|
18603
|
+
const share = maxChars === undefined ? undefined : toolResultShare(maxChars, bound, toolResults.length);
|
|
18604
|
+
const total = [...alone.values()].reduce((sum, { stored }) => sum + stored.text.length, 0);
|
|
18605
|
+
const byShare = share?.bound !== undefined && toolResults.length > 1
|
|
18606
|
+
&& (this.toolRoundsByShare.has(toolResults) || total > bound);
|
|
18607
|
+
for (const tc of toolResults) {
|
|
18608
|
+
const { text, shown, stored } = alone.get(tc.id);
|
|
18609
|
+
spilled.set(tc.id, byShare
|
|
18610
|
+
? await this.spillOrTruncate(text, share.cap, `${dateLabel}-${tc.id}`, shown, share.bound, stored.written)
|
|
18611
|
+
: stored);
|
|
18354
18612
|
}
|
|
18355
18613
|
const blocks = toolResults.map(tc => ({
|
|
18356
18614
|
type: 'tool_result',
|
|
@@ -18361,7 +18619,10 @@ export class AgentFramework {
|
|
|
18361
18619
|
content: spilled.get(tc.id).text,
|
|
18362
18620
|
isError: tc.result.isError,
|
|
18363
18621
|
}));
|
|
18364
|
-
|
|
18622
|
+
// A withheld round's stubs carry the guard's notice and place beside the
|
|
18623
|
+
// fields they keep, so under a bound their fields are cut within the
|
|
18624
|
+
// share, whether or not the round's results were stored by it.
|
|
18625
|
+
return { blocks, spilled, keptCap: share?.keptCap ?? maxChars };
|
|
18365
18626
|
}
|
|
18366
18627
|
/**
|
|
18367
18628
|
* A tool result's text as its stored copy is written before any cut, and
|
|
@@ -18380,7 +18641,7 @@ export class AgentFramework {
|
|
|
18380
18641
|
*/
|
|
18381
18642
|
storedToolResultText(agentName, tc, maxChars) {
|
|
18382
18643
|
if (tc.result.isError)
|
|
18383
|
-
return { text: tc.result.error ?? 'Unknown error', shown: null };
|
|
18644
|
+
return { text: (tc.result.error ?? 'Unknown error').toWellFormed(), shown: null };
|
|
18384
18645
|
const ledger = this.toolImageLedgerFor(agentName);
|
|
18385
18646
|
let blocks = null;
|
|
18386
18647
|
const text = nonErrorResultText(tc.result, undefined, {
|
|
@@ -18530,21 +18791,30 @@ export class AgentFramework {
|
|
|
18530
18791
|
return { path, error: failure };
|
|
18531
18792
|
}
|
|
18532
18793
|
/** How to read a tool result's file back a page at a time, with each page
|
|
18533
|
-
* sized to come back within `cap
|
|
18534
|
-
|
|
18794
|
+
* sized to come back within `cap`, and, where `canRaise`, that raising
|
|
18795
|
+
* the inline cap would show more: where a cap at the readers' bound would
|
|
18796
|
+
* keep a longer head, or the whole result. */
|
|
18797
|
+
spillPageAdvice(path, cap, canRaise) {
|
|
18535
18798
|
// JSON serialization can expand each read-back code unit to six
|
|
18536
18799
|
// chars (\\u0000). Leave room for the page's metadata and a possible
|
|
18537
18800
|
// extra code unit at a surrogate-pair boundary.
|
|
18538
18801
|
const pageOverhead = JSON.stringify(path).length + 256;
|
|
18539
18802
|
const readLimit = Math.max(1, Math.min(2000, Math.floor((cap - pageOverhead) / 6) - 1));
|
|
18540
18803
|
return `Page with workspace--read ${JSON.stringify({ path, offsetChars: 0, limitChars: readLimit })}; `
|
|
18541
|
-
+ 'use nextOffsetChars as offsetChars until null, keeping limitChars.
|
|
18542
|
-
+ 'agent_settings update tool_result_inline_max_chars.';
|
|
18804
|
+
+ 'use nextOffsetChars as offsetChars until null, keeping limitChars.'
|
|
18805
|
+
+ (canRaise ? ' Or raise the inline cap via agent_settings update tool_result_inline_max_chars.' : '');
|
|
18543
18806
|
}
|
|
18544
18807
|
async spillOrTruncate(content, cap, label,
|
|
18545
18808
|
/** What the live copy showed beside this text (storedToolResultText);
|
|
18546
18809
|
* null when the live copy was this text. */
|
|
18547
|
-
shown = null
|
|
18810
|
+
shown = null,
|
|
18811
|
+
/** The most a reader's strategy keeps of this copy: the readers'
|
|
18812
|
+
* smallest per-message bound, or the result's share of it
|
|
18813
|
+
* (buildStoredToolResultContent). */
|
|
18814
|
+
bound = undefined,
|
|
18815
|
+
/** The spill file an earlier cut of this text wrote: reused, not written
|
|
18816
|
+
* again. Undefined when none was cut. */
|
|
18817
|
+
written) {
|
|
18548
18818
|
if (!cap || content.length <= cap)
|
|
18549
18819
|
return { text: content, filePath: null };
|
|
18550
18820
|
// The stored text is the only place save_recent_image discovers tool
|
|
@@ -18553,19 +18823,24 @@ export class AgentFramework {
|
|
|
18553
18823
|
// saw that the index would silently slide past (issue #104), so the
|
|
18554
18824
|
// newest slots past the cut that the live copy showed are re-appended
|
|
18555
18825
|
// after the notice. Otherwise the live copy is this text, and nothing
|
|
18556
|
-
// past the cut is kept (#403).
|
|
18557
|
-
|
|
18826
|
+
// past the cut is kept (#403). The stored copy, notice and kept slots
|
|
18827
|
+
// included, fits `bound`, the most a reader's strategy keeps of it, so
|
|
18828
|
+
// no reader's cut takes the notice (cutStoredToolResultToFit), and the
|
|
18829
|
+
// spill notices name the head they kept.
|
|
18830
|
+
const cut = cutStoredToolResultToFit(content, cap, shown, bound);
|
|
18558
18831
|
const { head } = cut;
|
|
18559
18832
|
const kept = formatKeptImageSlots(cut);
|
|
18560
|
-
|
|
18833
|
+
if (written === undefined)
|
|
18834
|
+
written = await this.writeToolResultFile(label, content);
|
|
18561
18835
|
if (written && written.error === undefined) {
|
|
18562
18836
|
const path = written.path;
|
|
18563
18837
|
return {
|
|
18564
18838
|
text: head
|
|
18565
18839
|
+ `\n\n[truncated — showing ${head.length} of ${content.length} chars; full content: workspace file ${path}`
|
|
18566
|
-
+ `. ${this.spillPageAdvice(path, cap)}]`
|
|
18840
|
+
+ `. ${this.spillPageAdvice(path, cap, bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > head.length)}]`
|
|
18567
18841
|
+ kept,
|
|
18568
18842
|
filePath: path,
|
|
18843
|
+
written,
|
|
18569
18844
|
};
|
|
18570
18845
|
}
|
|
18571
18846
|
if (written) {
|
|
@@ -18578,6 +18853,7 @@ export class AgentFramework {
|
|
|
18578
18853
|
+ ` (${written.error}); content over the cap was not retained]`
|
|
18579
18854
|
+ kept,
|
|
18580
18855
|
filePath: null,
|
|
18856
|
+
written,
|
|
18581
18857
|
};
|
|
18582
18858
|
}
|
|
18583
18859
|
return {
|
|
@@ -18585,6 +18861,7 @@ export class AgentFramework {
|
|
|
18585
18861
|
+ '\n\n[truncated — original was ' + content.length + ' chars; no writable workspace mount without an include, full content not retained]'
|
|
18586
18862
|
+ kept,
|
|
18587
18863
|
filePath: null,
|
|
18864
|
+
written,
|
|
18588
18865
|
};
|
|
18589
18866
|
}
|
|
18590
18867
|
/**
|
|
@@ -18870,8 +19147,10 @@ export class AgentFramework {
|
|
|
18870
19147
|
* the array is text-only (let JSON path handle it; saves a code path).
|
|
18871
19148
|
* `maxChars`, when provided, caps each accompanying text block so an image
|
|
18872
19149
|
* inlined alongside an enormous text payload can't blow the context. A
|
|
18873
|
-
* block is cut as the stored copy
|
|
18874
|
-
* slot, so the stored copy can keep
|
|
19150
|
+
* block is cut at the cap as the stored copy's cut at that cap is
|
|
19151
|
+
* (toolResultCutAt), never inside a slot, so the stored copy can keep
|
|
19152
|
+
* exactly the slots this showed (#403); under a readers' bound the stored
|
|
19153
|
+
* copy's cut can come earlier (cutStoredToolResultToFit).
|
|
18875
19154
|
*/
|
|
18876
19155
|
tryNativeToolResultContent(data, maxChars,
|
|
18877
19156
|
/** Spill file written by the history path — referenced from truncation
|
|
@@ -18893,7 +19172,8 @@ export class AgentFramework {
|
|
|
18893
19172
|
if (b.type === 'text') {
|
|
18894
19173
|
// A text that isn't a string shows as the converters' placeholder,
|
|
18895
19174
|
// rather than sending the whole array to the JSON fallback.
|
|
18896
|
-
|
|
19175
|
+
// Well formed, as the stored copy's text is (nonErrorResultText).
|
|
19176
|
+
let text = typeof b.text === 'string' ? b.text.toWellFormed() : NON_STRING_TEXT_PLACEHOLDER;
|
|
18897
19177
|
if (maxChars && text.length > maxChars) {
|
|
18898
19178
|
text = text.slice(0, toolResultCutAt(text, maxChars))
|
|
18899
19179
|
+ '\n\n[truncated — original was ' + text.length + ' chars'
|
|
@@ -18949,6 +19229,10 @@ export class AgentFramework {
|
|
|
18949
19229
|
* (Sol's #94 ruling: a durable preference must not be a durable path for
|
|
18950
19230
|
* one tool result to exceed maxMessageTokens; the way to see the whole
|
|
18951
19231
|
* result is the spill file, not an over-bound blob in live context).
|
|
19232
|
+
* An ordinary resident's results land in the shared slot, which each of
|
|
19233
|
+
* its readers compiles under its own strategy, so the bound is the
|
|
19234
|
+
* smallest of theirs, as the tune-out dump's is (residentMessageChars); a
|
|
19235
|
+
* fork's or an ephemeral agent's store has itself as its only reader.
|
|
18952
19236
|
*/
|
|
18953
19237
|
resolveToolResultInlineCap(agent) {
|
|
18954
19238
|
const override = this.toolResultInlineMaxCharsOverride.get(agent.name);
|
|
@@ -18957,12 +19241,25 @@ export class AgentFramework {
|
|
|
18957
19241
|
const source = override !== undefined
|
|
18958
19242
|
? 'agent-settings-override'
|
|
18959
19243
|
: configured !== null ? 'framework-config' : 'default';
|
|
18960
|
-
|
|
18961
|
-
|
|
19244
|
+
let bound = this.strategyDerivedToolResultChars(agent);
|
|
19245
|
+
let byReader = false;
|
|
19246
|
+
if (this.isOrdinaryResident(agent)) {
|
|
19247
|
+
for (const reader of this.sharedSlotAgents) {
|
|
19248
|
+
if (reader === agent)
|
|
19249
|
+
continue;
|
|
19250
|
+
const readerBound = this.strategyDerivedToolResultChars(reader);
|
|
19251
|
+
if (readerBound !== undefined && (bound === undefined || readerBound < bound)) {
|
|
19252
|
+
bound = readerBound;
|
|
19253
|
+
byReader = true;
|
|
19254
|
+
}
|
|
19255
|
+
}
|
|
19256
|
+
}
|
|
19257
|
+
const clamped = bound !== undefined && bound < desired;
|
|
18962
19258
|
return {
|
|
18963
|
-
cap: clamped ?
|
|
19259
|
+
cap: clamped ? bound : desired,
|
|
18964
19260
|
source,
|
|
18965
|
-
clampedBy: clamped ? 'strategy-bound' : null,
|
|
19261
|
+
clampedBy: clamped ? (byReader ? 'reader-bound' : 'strategy-bound') : null,
|
|
19262
|
+
bound,
|
|
18966
19263
|
};
|
|
18967
19264
|
}
|
|
18968
19265
|
/**
|
|
@@ -18970,8 +19267,9 @@ export class AgentFramework {
|
|
|
18970
19267
|
* its caller whole. A call from a running script resolves to the script,
|
|
18971
19268
|
* which takes any result that fits its memory (scriptResultMaxChars). Any
|
|
18972
19269
|
* other call's result is stored in its agent's context under the agent's
|
|
18973
|
-
* inline cap,
|
|
18974
|
-
*
|
|
19270
|
+
* inline cap, unless its round's results together pass the readers' bound,
|
|
19271
|
+
* when each takes its share (buildStoredToolResultContent), and a call for
|
|
19272
|
+
* an agent the framework doesn't hold gets the house default.
|
|
18975
19273
|
*/
|
|
18976
19274
|
resultMaxCharsFor(agentName, callId) {
|
|
18977
19275
|
const ce = this.codeExecution;
|
|
@@ -19544,8 +19842,9 @@ export class AgentFramework {
|
|
|
19544
19842
|
// messages flush at the next turn's START (before its compile — riding
|
|
19545
19843
|
// their own wake), at the agent's next tool boundary (where they are
|
|
19546
19844
|
// ALSO injected into the live stream — hear-while-acting — all but a
|
|
19547
|
-
// message a tune-out holds from the residents
|
|
19548
|
-
// finally when the
|
|
19845
|
+
// message a tune-out holds from the residents, and one the next compile
|
|
19846
|
+
// won't send among its newest), or in driveStream's finally when the
|
|
19847
|
+
// turn ends.
|
|
19549
19848
|
// Quiesce (issue #122): the window exists to run compression/refold
|
|
19550
19849
|
// against a FROZEN context — module events and api message.send must not
|
|
19551
19850
|
// append mid-surgery. Deferred here, flushed by resume(). tool_result
|
|
@@ -19614,6 +19913,13 @@ export class AgentFramework {
|
|
|
19614
19913
|
}
|
|
19615
19914
|
return ''; // Deferred — flushed at the target's next boundary
|
|
19616
19915
|
}
|
|
19916
|
+
// A clock row due goes first (storeDueClock). A write that gets here is a
|
|
19917
|
+
// tool result (which takes none), or no turn of the agent's is alive, or
|
|
19918
|
+
// its turn is assembling before its compile, or it comes from stop()'s
|
|
19919
|
+
// drain, once every stream it could join has settled: the clock is in
|
|
19920
|
+
// whatever request comes next.
|
|
19921
|
+
if (takesClockBefore(participant, content, metadata, agent.name))
|
|
19922
|
+
this.storeDueClock(agent, 'event');
|
|
19617
19923
|
const stored = agent.getContextManager().addMessage(participant, content, opts?.fromQueue ? withDeferredWriteId(metadata, opts.fromQueue.id) : metadata);
|
|
19618
19924
|
if (opts?.placement) {
|
|
19619
19925
|
opts.placement.agent = agent.name;
|
|
@@ -19696,6 +20002,72 @@ export class AgentFramework {
|
|
|
19696
20002
|
if (this.quiesced || this.deferredWritesPersisted)
|
|
19697
20003
|
this.persistDeferredWrites();
|
|
19698
20004
|
}
|
|
20005
|
+
/**
|
|
20006
|
+
* What the next compile sends for each of `ids`, messages a tool boundary
|
|
20007
|
+
* has just stored, as the agent's strategy renders them as the newest of
|
|
20008
|
+
* its window (ContextManager.renderAsNewest): cut by the per-message cap,
|
|
20009
|
+
* a sharded body joined, and the image limits applied over the batch, so
|
|
20010
|
+
* the live stream hears what the next compile sends. An id comes back as
|
|
20011
|
+
* null where that compile won't send the message among its newest.
|
|
20012
|
+
* Undefined where the strategy has no such hook, or the render threw,
|
|
20013
|
+
* which is logged: the boundary then injects what it stored, as it did
|
|
20014
|
+
* before strategies could say.
|
|
20015
|
+
*/
|
|
20016
|
+
renderForInjection(agent, ids) {
|
|
20017
|
+
if (ids.length === 0)
|
|
20018
|
+
return undefined;
|
|
20019
|
+
try {
|
|
20020
|
+
return agent.getContextManager().renderAsNewest(ids);
|
|
20021
|
+
}
|
|
20022
|
+
catch (error) {
|
|
20023
|
+
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);
|
|
20024
|
+
return undefined;
|
|
20025
|
+
}
|
|
20026
|
+
}
|
|
20027
|
+
/**
|
|
20028
|
+
* Tell the agent of the messages a tool boundary stored and left out of
|
|
20029
|
+
* its injection, since the next compile won't send them among its newest:
|
|
20030
|
+
* who sent each and its size, and how to read it, `history--extract`
|
|
20031
|
+
* around it where the agent is shown that tool, else its id. None of a
|
|
20032
|
+
* message's words: the line stands in for a message the live round
|
|
20033
|
+
* doesn't carry. A system line, so it's no conversation: it neither
|
|
20034
|
+
* re-pins the turn nor clears its send suppression. It names the agent
|
|
20035
|
+
* it's for, since every resident sharing the slot reads it too. Returns
|
|
20036
|
+
* the notice as stored, for the boundary to inject after the batch, or
|
|
20037
|
+
* undefined when it couldn't be stored, which is logged.
|
|
20038
|
+
*/
|
|
20039
|
+
recordLeftOutNotice(agent, leftOut) {
|
|
20040
|
+
const canExtract = this.advertisedToolsForAgent(agent.name).some((t) => t.name === 'history--extract');
|
|
20041
|
+
const one = leftOut.length === 1;
|
|
20042
|
+
const lines = leftOut.map(({ msg, id, content, parts }) => {
|
|
20043
|
+
const size = contentSize(content);
|
|
20044
|
+
// A sharded body is that many store messages, consecutive from its
|
|
20045
|
+
// first, which extract reads around it one by one. (Its "after" stops
|
|
20046
|
+
// at 100, so a body of more parts, over 300,000 tokens at the memory
|
|
20047
|
+
// strategy's default shard size, is read only that far.)
|
|
20048
|
+
const read = canExtract
|
|
20049
|
+
? `history--extract with aroundId "${id}", before 0 and after ${parts - 1} reads it` +
|
|
20050
|
+
`${parts > 1 ? `, stored as ${parts} parts` : ''}.`
|
|
20051
|
+
: `Its id is ${id}${parts > 1 ? `, the first of the ${parts} parts it's stored as` : ''}.`;
|
|
20052
|
+
return `- From ${msg.participant}${size ? `, ${size}` : ''}. ${read}`;
|
|
20053
|
+
});
|
|
20054
|
+
const content = [{
|
|
20055
|
+
type: 'text',
|
|
20056
|
+
text: `[not shown] For ${agent.name}: ${one ? 'A message' : `${leftOut.length} messages`} arrived during your turn that ` +
|
|
20057
|
+
`${one ? "isn't" : "aren't"} shown here, since your context doesn't keep ${one ? 'it' : 'them'} among ` +
|
|
20058
|
+
`your newest messages:\n${lines.join('\n')}`,
|
|
20059
|
+
}];
|
|
20060
|
+
const metadata = { system: true, kind: 'left-out-notice' };
|
|
20061
|
+
try {
|
|
20062
|
+
const id = agent.getContextManager().addMessage('user', content, metadata);
|
|
20063
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'left-out-notice' });
|
|
20064
|
+
return { participant: 'user', content, metadata };
|
|
20065
|
+
}
|
|
20066
|
+
catch (err) {
|
|
20067
|
+
console.error(`[injection] ${agent.name}: couldn't store the notice of messages left out of the live round:`, err);
|
|
20068
|
+
return undefined;
|
|
20069
|
+
}
|
|
20070
|
+
}
|
|
19699
20071
|
/**
|
|
19700
20072
|
* Write drained entries in queue (seq) order, whatever their targets:
|
|
19701
20073
|
* residents share one message slot, so the order entries arrived in is the
|
|
@@ -19814,7 +20186,7 @@ export class AgentFramework {
|
|
|
19814
20186
|
continue;
|
|
19815
20187
|
}
|
|
19816
20188
|
if (found.length > 0) {
|
|
19817
|
-
written.set(msg, { id: found[0].id, content: asCompiled(found) });
|
|
20189
|
+
written.set(msg, { id: found[0].id, content: asCompiled(found), parts: found.length });
|
|
19818
20190
|
this.logDeferredWriteError(msg, where, undefined, { count: found.length, sharded: found[0].bodyGroupId !== undefined, foundLater: true });
|
|
19819
20191
|
onLanded?.(msg, found[0].id);
|
|
19820
20192
|
continue;
|
|
@@ -19831,7 +20203,7 @@ export class AgentFramework {
|
|
|
19831
20203
|
const copies = agent && before !== undefined && (this.storedCountOf(agent) ?? before) - before > 1
|
|
19832
20204
|
? this.storedCopiesOf(agent, before, msg.id) ?? []
|
|
19833
20205
|
: [];
|
|
19834
|
-
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content });
|
|
20206
|
+
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content, parts: Math.max(1, copies.length) });
|
|
19835
20207
|
}
|
|
19836
20208
|
catch (error) {
|
|
19837
20209
|
// What landed, read back from where the store stood before the
|
|
@@ -19839,7 +20211,7 @@ export class AgentFramework {
|
|
|
19839
20211
|
// after it. With no agent there's no store it could have landed in.
|
|
19840
20212
|
const landed = !agent ? [] : before === undefined ? undefined : this.storedCopiesOf(agent, before, msg.id);
|
|
19841
20213
|
if (landed && landed.length > 0) {
|
|
19842
|
-
written.set(msg, { id: landed[0].id, content: asCompiled(landed) });
|
|
20214
|
+
written.set(msg, { id: landed[0].id, content: asCompiled(landed), parts: landed.length });
|
|
19843
20215
|
this.logDeferredWriteError(msg, where, error, { count: landed.length, sharded: landed[0].bodyGroupId !== undefined });
|
|
19844
20216
|
onLanded?.(msg, landed[0].id);
|
|
19845
20217
|
continue;
|
|
@@ -19880,14 +20252,19 @@ export class AgentFramework {
|
|
|
19880
20252
|
* `bodyGroupId`. Undefined when the store can't be read back, which is
|
|
19881
20253
|
* logged: not the same as nothing found. */
|
|
19882
20254
|
storedCopiesOf(agent, from, deferredWriteId) {
|
|
20255
|
+
return this.storedSince(agent, from, (m) => m.metadata?.deferredWriteId === deferredWriteId, 'deferral', `deferred write ${deferredWriteId}`);
|
|
20256
|
+
}
|
|
20257
|
+
/** The messages `match` picks among those the agent's store gained from
|
|
20258
|
+
* position `from` on, in store order, with their blobs resolved. Undefined
|
|
20259
|
+
* when the store can't be read back, which is logged under `tag`, naming
|
|
20260
|
+
* `what` was written: not the same as nothing found. */
|
|
20261
|
+
storedSince(agent, from, match, tag, what) {
|
|
19883
20262
|
try {
|
|
19884
20263
|
const cm = agent.getContextManager();
|
|
19885
|
-
return cm.getMessageWindow(from, Math.max(0, cm.getMessageCount() - from)).messages
|
|
19886
|
-
.filter((m) => m.metadata?.deferredWriteId === deferredWriteId)
|
|
19887
|
-
.map(storedCopy);
|
|
20264
|
+
return cm.getMessageWindow(from, Math.max(0, cm.getMessageCount() - from)).messages.filter(match).map(storedCopy);
|
|
19888
20265
|
}
|
|
19889
20266
|
catch (error) {
|
|
19890
|
-
console.error(`[
|
|
20267
|
+
console.error(`[${tag}] could not read back what ${what} stored in ${agent.name}'s store:`, error);
|
|
19891
20268
|
return undefined;
|
|
19892
20269
|
}
|
|
19893
20270
|
}
|
|
@@ -22799,6 +23176,9 @@ export class AgentFramework {
|
|
|
22799
23176
|
// configured servers can still deploy its first one at runtime.
|
|
22800
23177
|
if (!this.mcplServerRegistry || !this.mcplHostCapabilities) {
|
|
22801
23178
|
await this.initializeMcpl([], this.mcplInferenceRoutingConfig ?? undefined);
|
|
23179
|
+
// The lifecycle log's replay restored any tune-out on record, and no
|
|
23180
|
+
// coordinator is made here to carry one.
|
|
23181
|
+
this.endTuneOutsWithoutCoordinator();
|
|
22802
23182
|
}
|
|
22803
23183
|
const collision = this.mcplPrefixCollision(config, undefined, claim);
|
|
22804
23184
|
if (collision)
|
|
@@ -24468,6 +24848,47 @@ export class AgentFramework {
|
|
|
24468
24848
|
...structured,
|
|
24469
24849
|
};
|
|
24470
24850
|
}
|
|
24851
|
+
/**
|
|
24852
|
+
* A tune-out on record where this host has no tune-out coordinator: a
|
|
24853
|
+
* start with no subconscious makes none, and neither does a channel
|
|
24854
|
+
* subsystem brought up after start by connectMcplServer. Nothing then
|
|
24855
|
+
* diverts the channel's traffic or ever cancels its tune-out, tune_out
|
|
24856
|
+
* isn't offered, and channel_open, channel_close and channel_decline
|
|
24857
|
+
* refuse a tuned-out channel, so the channel would stay held by a decision
|
|
24858
|
+
* nothing here can change. Each one ends here instead, its channel decided
|
|
24859
|
+
* open again as a cancel leaves it, under the tune-out's own source, so an
|
|
24860
|
+
* agent's tune-out keeps the agent's provenance (the housekeeping guard
|
|
24861
|
+
* reads it). A log line names the channel and says how many of the
|
|
24862
|
+
* messages it diverted are held here, stored or still queued to be
|
|
24863
|
+
* written, which nothing will now deliver as its backlog.
|
|
24864
|
+
*/
|
|
24865
|
+
endTuneOutsWithoutCoordinator() {
|
|
24866
|
+
const registry = this.channelRegistry;
|
|
24867
|
+
if (!registry || this.tuneOutCoordinator)
|
|
24868
|
+
return;
|
|
24869
|
+
const primary = this.primaryAgentName ? this.agents.get(this.primaryAgentName) : undefined;
|
|
24870
|
+
const ofEpoch = (epochId) => (m) => m.metadata?.tuneOut?.epochId === epochId;
|
|
24871
|
+
for (const { serverId, channelId, params, source } of registry.tuneOutsOnRecord()) {
|
|
24872
|
+
const diverted = typeof params.epochId !== 'string' ? 0
|
|
24873
|
+
: (primary?.getContextManager().getAllMessages() ?? []).filter(ofEpoch(params.epochId)).length +
|
|
24874
|
+
this.deferredMessages.filter((m) => (m.forAgent ?? this.primaryAgentName) === this.primaryAgentName)
|
|
24875
|
+
.filter(ofEpoch(params.epochId)).length;
|
|
24876
|
+
const held = diverted === 0 ? ''
|
|
24877
|
+
: diverted === 1 ? ' The 1 message it diverted stays here, and nothing will deliver it as its backlog.'
|
|
24878
|
+
: ` The ${diverted} messages it diverted stay here, and nothing will deliver them as its backlog.`;
|
|
24879
|
+
try {
|
|
24880
|
+
registry.cancelTuneOut(serverId, channelId, 'open', source);
|
|
24881
|
+
}
|
|
24882
|
+
catch (err) {
|
|
24883
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server is tuned out, and nothing on this host watches ` +
|
|
24884
|
+
`or cancels tune-outs, but ending this one failed, so it stays recorded until a start that can end it: ` +
|
|
24885
|
+
`${err instanceof Error ? err.message : String(err)}.${held}`);
|
|
24886
|
+
continue;
|
|
24887
|
+
}
|
|
24888
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server was tuned out, and nothing on this host watches ` +
|
|
24889
|
+
`or cancels tune-outs, so the tune-out ends here and the channel is decided open again.${held}`);
|
|
24890
|
+
}
|
|
24891
|
+
}
|
|
24471
24892
|
/**
|
|
24472
24893
|
* Build BeforeInferenceParams from agent state and trigger context.
|
|
24473
24894
|
*/
|
|
@@ -25262,11 +25683,24 @@ export class AgentFramework {
|
|
|
25262
25683
|
// carries the same ref.
|
|
25263
25684
|
const state = agent.state;
|
|
25264
25685
|
if (state.status === 'waiting_for_tools') {
|
|
25265
|
-
|
|
25686
|
+
// The commit stores a round as each result would be alone, or by
|
|
25687
|
+
// its share where they pass the bound together, which a sibling
|
|
25688
|
+
// still running can decide. Where the two ways differ for a result
|
|
25689
|
+
// counted here, the round is marked to be stored by its share
|
|
25690
|
+
// (toolRoundsByShare), so the count holds either way: a result
|
|
25691
|
+
// counted before the mark is the same both ways. The share is of
|
|
25692
|
+
// the round's calls, finished or not.
|
|
25693
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
25694
|
+
const share = toolResultShare(cap, bound, state.pending.size + state.completed.length);
|
|
25266
25695
|
for (let i = state.completed.length - 1; i >= 0 && collecting(); i--) {
|
|
25267
25696
|
const tc = state.completed[i];
|
|
25268
25697
|
const { text, shown } = this.storedToolResultText(agentName, tc, cap);
|
|
25269
|
-
|
|
25698
|
+
const alone = storedImageSlots(text, cap, shown, bound);
|
|
25699
|
+
const byShare = storedImageSlots(text, share.cap, shown, share.bound);
|
|
25700
|
+
if (alone.length !== byShare.length || alone.some((slot, j) => slot.text !== byShare[j].text)) {
|
|
25701
|
+
this.toolRoundsByShare.add(state.completed);
|
|
25702
|
+
}
|
|
25703
|
+
considerToolResultImages(tc.id, tc.name, this.toolRoundsByShare.has(state.completed) ? byShare : alone, 0);
|
|
25270
25704
|
}
|
|
25271
25705
|
}
|
|
25272
25706
|
// Walk the message store tail-first in bounded windows, re-inlining
|