@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/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';
|
|
@@ -294,9 +294,12 @@ function malformedResponseContent(response) {
|
|
|
294
294
|
* mid-turn message's text at chars/4 and its other blocks about as
|
|
295
295
|
* context-manager's store prices them (injectedBlockTokens), and a small
|
|
296
296
|
* per-block envelope allowance. On membrane's native tool path a mid-turn
|
|
297
|
-
* message rides into the next round
|
|
298
|
-
*
|
|
299
|
-
*
|
|
297
|
+
* message rides into the next round as the boundary injects it, which is as
|
|
298
|
+
* the strategy renders it for the next compile, cut and with its images
|
|
299
|
+
* limited (renderForInjection), or its stored content where the strategy
|
|
300
|
+
* can't say (provideToolResults' injectedMessages). Its XML tool path declares that it
|
|
301
|
+
* can't carry injected messages, so the tool boundary injects none there and
|
|
302
|
+
* that term is zero. Deliberately a
|
|
300
303
|
* ceiling-ish heuristic, but for a mid-turn message's media at the store's
|
|
301
304
|
* flat prices — the cost of overestimating is one early recompile;
|
|
302
305
|
* the cost of underestimating is the 400 this projection exists to prevent.
|
|
@@ -533,6 +536,7 @@ import { MALFORMED_CONTENT_NOTICE, normalizeStandardToolResult } from './mcpl/to
|
|
|
533
536
|
import { boundedForLog, describeCause } from './mcpl/errors.js';
|
|
534
537
|
import { assertServerConfigBooleans, buildWebSocketUrl, isWebSocketTransport } from './mcpl/transport.js';
|
|
535
538
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
539
|
+
import { CLOCK_ROW_KIND, clockStampOf, planClock, takesClockBefore } from './clock-rows.js';
|
|
536
540
|
import { firstFreeBranchName } from './branch-names.js';
|
|
537
541
|
import { DEFAULT_DISCORD_AWARENESS_EMOJI, DiscordAwarenessOutbox, DiscordAwarenessRefusal, defaultDiscordAwarenessOutboxPath, extractDiscordAwarenessRefs, selectDiscordAwarenessRefs, checkedDiscordAwarenessMarks, boundDiscordAwarenessText, discordChannelOf, isPermanentDiscordReactionFailure, unsyncedWriteText, } from './recovery/discord-awareness-outbox.js';
|
|
538
542
|
import { OperatorActionError, OperatorLog, capIds, defaultOperatorLogPath, } from './operator-log.js';
|
|
@@ -572,6 +576,25 @@ function asCompiled(landed) {
|
|
|
572
576
|
const text = blocks.filter((b) => b.type === 'text').map((b) => b.text).join('');
|
|
573
577
|
return [...blocks.filter((b) => b.type !== 'text'), { type: 'text', text }];
|
|
574
578
|
}
|
|
579
|
+
/** A message's size as a notice gives it: its text's characters and its
|
|
580
|
+
* images, each left out when there are none, so empty when it has
|
|
581
|
+
* neither. */
|
|
582
|
+
function contentSize(content) {
|
|
583
|
+
let chars = 0;
|
|
584
|
+
let images = 0;
|
|
585
|
+
for (const b of content) {
|
|
586
|
+
if (b.type === 'text')
|
|
587
|
+
chars += b.text.length;
|
|
588
|
+
else if (b.type === 'image' || b.type === 'generated_image')
|
|
589
|
+
images++;
|
|
590
|
+
}
|
|
591
|
+
const parts = [];
|
|
592
|
+
if (chars > 0)
|
|
593
|
+
parts.push(`${chars.toLocaleString('en-US')} character${chars === 1 ? '' : 's'}`);
|
|
594
|
+
if (images > 0)
|
|
595
|
+
parts.push(`${images} image${images === 1 ? '' : 's'}`);
|
|
596
|
+
return parts.join(' and ');
|
|
597
|
+
}
|
|
575
598
|
const storedCopy = (m) => ({ id: m.id, content: m.content, ...(m.bodyGroupId !== undefined ? { bodyGroupId: m.bodyGroupId } : {}) });
|
|
576
599
|
/** Stamp a deferred write's durable id into the metadata it is stored with
|
|
577
600
|
* (boot recovery dedups replays by it). Idempotent for an already-stamped
|
|
@@ -1983,6 +2006,10 @@ export class AgentFramework {
|
|
|
1983
2006
|
* ephemeral agents have isolated slots and are their own only readers.
|
|
1984
2007
|
*/
|
|
1985
2008
|
sharedSlotAgents = new Set();
|
|
2009
|
+
/** Rounds of tool calls, by their results array, that a save_recent_image
|
|
2010
|
+
* in the round counted by their share of the bound: such a round is
|
|
2011
|
+
* stored by its share whether or not it fits (buildStoredToolResultContent). */
|
|
2012
|
+
toolRoundsByShare = new WeakSet();
|
|
1986
2013
|
/** The windows of forks and runs deregistered while the host was quiesced,
|
|
1987
2014
|
* by agent name (releaseWindowNowOrAfterResume); resume() releases them
|
|
1988
2015
|
* (runReleasesAfterResume), or stop() passes on what they left, unread or
|
|
@@ -2630,6 +2657,9 @@ export class AgentFramework {
|
|
|
2630
2657
|
});
|
|
2631
2658
|
framework.tuneOutCoordinator.resumeActiveEpochs();
|
|
2632
2659
|
}
|
|
2660
|
+
else {
|
|
2661
|
+
framework.endTuneOutsWithoutCoordinator();
|
|
2662
|
+
}
|
|
2633
2663
|
// Modules, agents, MCPL servers and the tune-out tool are all in place:
|
|
2634
2664
|
// the first point at which "matches no tool" can be judged. Later tool
|
|
2635
2665
|
// refreshes re-check patterns still undecided.
|
|
@@ -9969,6 +9999,10 @@ export class AgentFramework {
|
|
|
9969
9999
|
// Speech happens only via speak_in_channel (its own marked voice).
|
|
9970
10000
|
proseRouting: 'explicit',
|
|
9971
10001
|
allowedTools: [...SUBCONSCIOUS_TOOL_NAMES, 'think', 'skip_reply', 'end_turn'],
|
|
10002
|
+
// No clock rows of its own: its merged view carries the residents'
|
|
10003
|
+
// clocks, and clocks it planned from its own rows alone would read the
|
|
10004
|
+
// time a second time beside them, or disagree with them.
|
|
10005
|
+
clock: undefined,
|
|
9972
10006
|
};
|
|
9973
10007
|
const agent = new Agent(agentConfig, contextManager, this.membrane);
|
|
9974
10008
|
this.adoptToolResultGuard(agent);
|
|
@@ -10115,14 +10149,14 @@ export class AgentFramework {
|
|
|
10115
10149
|
if (currentState.status === 'ready') {
|
|
10116
10150
|
// Effective inline cap: agent_settings override → durable config →
|
|
10117
10151
|
// house default, clamped to the strategy bound (see resolver).
|
|
10118
|
-
const maxChars = this.resolveToolResultInlineCap(agent)
|
|
10152
|
+
const { cap: maxChars, bound } = this.resolveToolResultInlineCap(agent);
|
|
10119
10153
|
// Oversized results — successes AND errors — spill to a workspace
|
|
10120
10154
|
// file (truncated preview + file reference) instead of being
|
|
10121
10155
|
// blind-truncated. Computed ONCE per call and reused for both the
|
|
10122
10156
|
// history copy and the live wire copy below — the two must stay
|
|
10123
10157
|
// byte-matched (the window stores what the membrane sends;
|
|
10124
10158
|
// divergence breaks the compile prefix).
|
|
10125
|
-
const { blocks: toolResultContent, spilled } = await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars);
|
|
10159
|
+
const { blocks: toolResultContent, spilled, keptCap } = await this.buildStoredToolResultContent(agent.name, currentState.toolResults, maxChars, bound);
|
|
10126
10160
|
// A spill writes to the workspace, and the turn can be stopped
|
|
10127
10161
|
// meanwhile. A host's cancelStream, stop() and abortInference cancel
|
|
10128
10162
|
// the round's stream at `ready` and leave the agent idle, and an
|
|
@@ -10172,7 +10206,7 @@ export class AgentFramework {
|
|
|
10172
10206
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
10173
10207
|
}
|
|
10174
10208
|
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,
|
|
10209
|
+
agent.toolResultGuard.storeResults(toolResultContent, membraneResults, currentState.toolResults, turnRowMetadata, keptCap);
|
|
10176
10210
|
// Flush any messages that were deferred while this turn was in
|
|
10177
10211
|
// flight: this agent's own entries, those addressed to it and, for
|
|
10178
10212
|
// the primary, those addressed to no one (module messages, push
|
|
@@ -10184,14 +10218,21 @@ export class AgentFramework {
|
|
|
10184
10218
|
// live turn sees them instead of the agent staying deaf until the
|
|
10185
10219
|
// turn ends. The window stores them here, right after the
|
|
10186
10220
|
// 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
|
-
//
|
|
10221
|
+
// them in the live conversation — same participant, and neither
|
|
10222
|
+
// carries the row's metadata — and each goes in as the next compile
|
|
10223
|
+
// will send it, which the strategy renders (renderForInjection): cut
|
|
10224
|
+
// by the per-message cap, a sharded body joined, and its images as
|
|
10225
|
+
// the image limits leave them at the newest position. So the next
|
|
10226
|
+
// turn's compiled prefix matches the live request (prompt-cache
|
|
10227
|
+
// safe), up to what came after them: a later message can still
|
|
10228
|
+
// take an earlier one past an image limit, as for any message, and
|
|
10229
|
+
// the next compile then strips that image where the live rounds
|
|
10230
|
+
// carried it. One the next compile won't send among its newest
|
|
10231
|
+
// isn't injected; a notice after the batch tells the agent of it
|
|
10232
|
+
// (recordLeftOutNotice). A write that threw after it was stored, whose landing
|
|
10233
|
+
// couldn't be read, is the exception: it's carried at the flush
|
|
10234
|
+
// that finds it, after what was stored since, so there the store and
|
|
10235
|
+
// the live request differ (writeDrainedInOrder).
|
|
10195
10236
|
//
|
|
10196
10237
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
10197
10238
|
// draining at another agent's boundary would store the messages
|
|
@@ -10226,21 +10267,29 @@ export class AgentFramework {
|
|
|
10226
10267
|
// a held message does, rather than sitting in the window between
|
|
10227
10268
|
// rounds the stream never showed it.
|
|
10228
10269
|
const midTurnInjections = [];
|
|
10229
|
-
//
|
|
10230
|
-
//
|
|
10231
|
-
|
|
10270
|
+
// What this boundary stored for the agent to hear, in store order,
|
|
10271
|
+
// injected or left out, for a boundary that leaves it to the turn's
|
|
10272
|
+
// next start as arrivals: that start's compile sends it.
|
|
10273
|
+
const heard = [];
|
|
10232
10274
|
if (currentState.supportsInjectedMessages !== false) {
|
|
10275
|
+
// A clock row due (storeDueClock) goes right after the round's
|
|
10276
|
+
// results, ahead of what is drained below, and joins the stream
|
|
10277
|
+
// the same way. A boundary that passed during the round can't
|
|
10278
|
+
// stand between a tool's call and its results, so this clock
|
|
10279
|
+
// reads the time it's written (clock-rows.ts).
|
|
10280
|
+
const clock = this.storeDueClock(agent, 'tool-boundary');
|
|
10233
10281
|
const deferred = this.drainDeferredFor(agent.name, (msg) => !isInjectableMidTurn(msg, agent.name));
|
|
10234
10282
|
// Stored in queue order, cut at the first that can't be stored: it
|
|
10235
10283
|
// and what follows wait for the turn's end, and only what was
|
|
10236
10284
|
// stored is injected (the round's results go on either way). One
|
|
10237
10285
|
// the store refuses as malformed is dropped instead, and what
|
|
10238
|
-
// follows goes on.
|
|
10239
|
-
//
|
|
10240
|
-
//
|
|
10241
|
-
//
|
|
10286
|
+
// follows goes on. What was stored is injected as the next compile
|
|
10287
|
+
// will send it (renderForInjection). Where the strategy can't say,
|
|
10288
|
+
// a sharded body read back from its store is injected as later
|
|
10289
|
+
// compiles join it, and a write that threw after storing, which
|
|
10290
|
+
// counts as stored, as what landed of it (writeDrainedInOrder).
|
|
10242
10291
|
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) {
|
|
10292
|
+
for (const [msg, { id, content, parts }] of stored) {
|
|
10244
10293
|
// Every other resident reads what this boundary stores; for its
|
|
10245
10294
|
// own agent, the boundary weighs what it injects below.
|
|
10246
10295
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
@@ -10250,13 +10299,49 @@ export class AgentFramework {
|
|
|
10250
10299
|
// what the next compile hides (#232 gap 5).
|
|
10251
10300
|
if (msg.metadata?.tuneOut)
|
|
10252
10301
|
continue;
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10302
|
+
heard.push({ msg, id, content, parts });
|
|
10303
|
+
}
|
|
10304
|
+
// Rendered in one call: a render prepares as a compile does, so
|
|
10305
|
+
// the batch shares one preparation. The image limits count from
|
|
10306
|
+
// the batch's oldest message to the newest, as a compile does.
|
|
10307
|
+
// The clock, stored ahead of them, renders first in the same batch
|
|
10308
|
+
// and is injected first, as the next compile sends it.
|
|
10309
|
+
const rendered = this.renderForInjection(agent, [...(clock ? [clock.id] : []), ...heard.map((h) => h.id)]);
|
|
10310
|
+
const first = clock ? 1 : 0;
|
|
10311
|
+
if (clock) {
|
|
10312
|
+
const sent = rendered ? rendered[0] : clock.content;
|
|
10313
|
+
if (sent !== null) {
|
|
10314
|
+
midTurnInjections.push({ participant: 'user', content: sent, metadata: clock.metadata });
|
|
10315
|
+
}
|
|
10316
|
+
else {
|
|
10317
|
+
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`);
|
|
10318
|
+
}
|
|
10319
|
+
}
|
|
10320
|
+
const leftOut = [];
|
|
10321
|
+
heard.forEach((h, i) => {
|
|
10322
|
+
const sent = rendered ? rendered[i + first] : h.content;
|
|
10323
|
+
// The next compile won't send it among its newest: injected, it
|
|
10324
|
+
// would put in the live turn what that compile sends elsewhere,
|
|
10325
|
+
// or not at all. It's under any consumed mark the boundary sets
|
|
10326
|
+
// (below), as anything stored here is.
|
|
10327
|
+
if (sent === null) {
|
|
10328
|
+
leftOut.push(h);
|
|
10329
|
+
return;
|
|
10330
|
+
}
|
|
10331
|
+
midTurnInjections.push({
|
|
10332
|
+
participant: h.msg.participant,
|
|
10333
|
+
content: sent,
|
|
10334
|
+
...(h.msg.metadata ? { metadata: h.msg.metadata } : {}),
|
|
10335
|
+
});
|
|
10336
|
+
});
|
|
10337
|
+
if (leftOut.length > 0) {
|
|
10338
|
+
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(', ')}`);
|
|
10339
|
+
// The agent is told what it didn't hear, after the batch, as
|
|
10340
|
+
// the routing notice is: stored, so the next compile sends it
|
|
10341
|
+
// where the live round heard it.
|
|
10342
|
+
const notice = this.recordLeftOutNotice(agent, leftOut);
|
|
10343
|
+
if (notice)
|
|
10344
|
+
midTurnInjections.push(notice);
|
|
10260
10345
|
}
|
|
10261
10346
|
}
|
|
10262
10347
|
// Deferred writes handed to the store above: sync, then drop them
|
|
@@ -10391,11 +10476,12 @@ export class AgentFramework {
|
|
|
10391
10476
|
}
|
|
10392
10477
|
else if (routesLocus && !shouldEndTurn) {
|
|
10393
10478
|
// 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
|
-
|
|
10479
|
+
// stream to tell) leaves what it stored for the agent to the
|
|
10480
|
+
// turn's next start, as arrivals: a budget restart, or a
|
|
10481
|
+
// re-inference on these tool results. That includes a message
|
|
10482
|
+
// left out of the injection, since that start compiles anew.
|
|
10483
|
+
for (const { msg, id } of heard)
|
|
10484
|
+
this.noteArrival(agent.name, msg.metadata, id);
|
|
10399
10485
|
}
|
|
10400
10486
|
if (shouldEndTurn) {
|
|
10401
10487
|
// endTurn: what this boundary stored stays; cancel stream, reset to idle.
|
|
@@ -10496,6 +10582,18 @@ export class AgentFramework {
|
|
|
10496
10582
|
// Under context-manager#160 such a write already gives the
|
|
10497
10583
|
// replies stored after it stamps their requests didn't carry,
|
|
10498
10584
|
// whatever the mark says; deferring those writers is the fix.
|
|
10585
|
+
// The mark also covers a message the boundary stored and left out
|
|
10586
|
+
// of the injection, since the next compile won't send it among its
|
|
10587
|
+
// newest: the notice telling the agent of it is injected, so a
|
|
10588
|
+
// boundary that leaves one out injects something, unless that
|
|
10589
|
+
// notice couldn't be stored. The message then counts as read
|
|
10590
|
+
// though no request carried it: a coalesced render's wake to read
|
|
10591
|
+
// it drops, and a release doesn't pass it to the primary. The
|
|
10592
|
+
// notice has told the agent it came, from whom, and how to read
|
|
10593
|
+
// it. Where the view leaves it out, no request will carry it.
|
|
10594
|
+
// Where the memory strategy places it before its recent window, a
|
|
10595
|
+
// batch bigger than that window, the next compile carries it, in
|
|
10596
|
+
// whatever turn something else starts.
|
|
10499
10597
|
if (midTurnInjections.length > 0) {
|
|
10500
10598
|
// Possibly shown, whatever the stream declares: the first-view
|
|
10501
10599
|
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
@@ -10774,6 +10872,11 @@ export class AgentFramework {
|
|
|
10774
10872
|
return undefined;
|
|
10775
10873
|
if (this.conversationRouter && this.conversationAgentHomes.has(target.name)) {
|
|
10776
10874
|
metadata.triggered = event.triggerInference ?? false;
|
|
10875
|
+
// A fork's channel messages are stored past its deferral; a clock goes
|
|
10876
|
+
// ahead of one only while the fork has no turn alive (storeDueClock).
|
|
10877
|
+
if (!this.turnAlive(target) && takesClockBefore('user', event.content, metadata, target.name)) {
|
|
10878
|
+
this.storeDueClock(target, 'event');
|
|
10879
|
+
}
|
|
10777
10880
|
const id = target.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(metadata, event.content));
|
|
10778
10881
|
// A correction may target an older engagement. Refresh only the
|
|
10779
10882
|
// binding whose agent actually received the message.
|
|
@@ -10933,6 +11036,10 @@ export class AgentFramework {
|
|
|
10933
11036
|
// on top of the router's own trigger policy.
|
|
10934
11037
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
10935
11038
|
messageMetadata.triggered = trigger;
|
|
11039
|
+
// As for a correction above: a clock ahead only while no turn is alive.
|
|
11040
|
+
if (!this.turnAlive(agent) && takesClockBefore('user', event.content, messageMetadata, agent.name)) {
|
|
11041
|
+
this.storeDueClock(agent, 'event');
|
|
11042
|
+
}
|
|
10936
11043
|
const id = agent.getContextManager().addMessage('user', event.content, AgentFramework.withStoredDigest(messageMetadata, event.content));
|
|
10937
11044
|
router.touch(event.channelId);
|
|
10938
11045
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
@@ -13671,6 +13778,101 @@ export class AgentFramework {
|
|
|
13671
13778
|
return m.channelId;
|
|
13672
13779
|
return routing.engaged.has(m.channelId) ? m.channelId : undefined;
|
|
13673
13780
|
}
|
|
13781
|
+
/** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
|
|
13782
|
+
turnAlive(agent) {
|
|
13783
|
+
return this.activeTurnTokens.has(agent.name)
|
|
13784
|
+
|| this.pendingAssistantBlocks.has(agent.name)
|
|
13785
|
+
|| this.activeStreams.has(agent.name);
|
|
13786
|
+
}
|
|
13787
|
+
/**
|
|
13788
|
+
* Store the clock row due in `agent`'s window (clock-rows.ts), if the agent
|
|
13789
|
+
* has clock rows and a boundary has passed since the window's last clock,
|
|
13790
|
+
* or the window holds no clock row yet (planClock), and return it. The
|
|
13791
|
+
* caller places it, by calling this right before what the clock precedes
|
|
13792
|
+
* is written: an incoming event while no turn of the agent's is alive (or
|
|
13793
|
+
* one is assembling before its compile), a fresh turn's inputs before its
|
|
13794
|
+
* compile, or, mid-turn, the messages a tool boundary injects, where the
|
|
13795
|
+
* caller injects the clock first. Each is a place where a row is either in
|
|
13796
|
+
* the request that follows or before every request still to come, so the
|
|
13797
|
+
* window and the live stream keep matching.
|
|
13798
|
+
*
|
|
13799
|
+
* None while the host is quiesced (the window is frozen for surgery), and
|
|
13800
|
+
* none in the residents' shared slot while another of its readers (the
|
|
13801
|
+
* subconscious among them, through its merged view) has a turn alive: the
|
|
13802
|
+
* row would sit inside that turn without joining its stream. A clock that
|
|
13803
|
+
* can't be planned or stored is logged and skipped: it never holds up what
|
|
13804
|
+
* it would have preceded. One whose write threw after it landed is the
|
|
13805
|
+
* window's all the same, so it's returned, and a tool boundary injects it.
|
|
13806
|
+
*/
|
|
13807
|
+
storeDueClock(agent, occasion) {
|
|
13808
|
+
const config = agent.clock;
|
|
13809
|
+
if (!config || this.quiesced)
|
|
13810
|
+
return undefined;
|
|
13811
|
+
if (this.sharedSlotAgents.has(agent)) {
|
|
13812
|
+
for (const reader of this.sharedSlotAgents) {
|
|
13813
|
+
if (reader !== agent && this.turnAlive(reader))
|
|
13814
|
+
return undefined;
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
let planned;
|
|
13818
|
+
try {
|
|
13819
|
+
planned = planClock({ now: Date.now(), ...this.lastClockOf(agent.getContextManager()), config, zone: this.timeZone });
|
|
13820
|
+
}
|
|
13821
|
+
catch (err) {
|
|
13822
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}:`, err);
|
|
13823
|
+
return undefined;
|
|
13824
|
+
}
|
|
13825
|
+
if (!planned)
|
|
13826
|
+
return undefined;
|
|
13827
|
+
const metadata = { system: true, kind: CLOCK_ROW_KIND, clock: planned.stamp };
|
|
13828
|
+
const before = this.storedCountOf(agent);
|
|
13829
|
+
let id;
|
|
13830
|
+
try {
|
|
13831
|
+
id = agent.getContextManager().addMessage('user', planned.content, metadata);
|
|
13832
|
+
}
|
|
13833
|
+
catch (err) {
|
|
13834
|
+
// A write can throw after its row landed, as when context-manager's own
|
|
13835
|
+
// propagation of it fails: what landed is read back from where the
|
|
13836
|
+
// store stood before, as writeDrainedInOrder reads a drained write's.
|
|
13837
|
+
const at = planned.stamp.at;
|
|
13838
|
+
const landed = before === undefined
|
|
13839
|
+
? undefined
|
|
13840
|
+
: this.storedSince(agent, before, (m) => clockStampOf(m.metadata)?.at === at, 'clock', 'the clock write');
|
|
13841
|
+
if (!landed || landed.length === 0) {
|
|
13842
|
+
console.error(`[clock] ${agent.name}: no clock row at ${occasion}${landed ? '' : ' (whether it landed is unknown)'}:`, err);
|
|
13843
|
+
return undefined;
|
|
13844
|
+
}
|
|
13845
|
+
console.error(`[clock] ${agent.name}: the clock row at ${occasion} landed though its write threw:`, err);
|
|
13846
|
+
id = landed[0].id;
|
|
13847
|
+
}
|
|
13848
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: `clock:${occasion}` });
|
|
13849
|
+
return { id, content: planned.content, metadata };
|
|
13850
|
+
}
|
|
13851
|
+
/** Rows a window's last-clock scan reads at a time. */
|
|
13852
|
+
static CLOCK_SCAN_ROWS = 256;
|
|
13853
|
+
/** What the next clock is planned from: the window's newest row's time and
|
|
13854
|
+
* the time its last clock row reads, found by reading back from the
|
|
13855
|
+
* newest row, CLOCK_SCAN_ROWS at a time, until a clock row comes, however
|
|
13856
|
+
* far back it is. In the steady state the newest chunk holds it, unless
|
|
13857
|
+
* more rows than that have come since the last clock; a window with no
|
|
13858
|
+
* clock row is read whole once, and then takes one (planClock). */
|
|
13859
|
+
lastClockOf(cm) {
|
|
13860
|
+
let newestRowAt;
|
|
13861
|
+
for (let end = cm.getMessageCount(); end > 0;) {
|
|
13862
|
+
const span = Math.min(end, AgentFramework.CLOCK_SCAN_ROWS);
|
|
13863
|
+
const { messages } = cm.getMessageWindow(end - span, span, { resolveBlobs: false });
|
|
13864
|
+
if (newestRowAt === undefined && messages.length > 0) {
|
|
13865
|
+
newestRowAt = new Date(messages[messages.length - 1].timestamp).getTime();
|
|
13866
|
+
}
|
|
13867
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
13868
|
+
const stamp = clockStampOf(messages[i].metadata);
|
|
13869
|
+
if (stamp)
|
|
13870
|
+
return { newestRowAt, lastClockAt: stamp.at };
|
|
13871
|
+
}
|
|
13872
|
+
end -= span;
|
|
13873
|
+
}
|
|
13874
|
+
return { newestRowAt, lastClockAt: undefined };
|
|
13875
|
+
}
|
|
13674
13876
|
/**
|
|
13675
13877
|
* Weigh a message that no tool boundary of the agent's open logical turn
|
|
13676
13878
|
* will weigh: one that would re-pin the locus becomes one of the turn's
|
|
@@ -15133,6 +15335,17 @@ export class AgentFramework {
|
|
|
15133
15335
|
// turn, and flushing there would insert messages between its rounds.
|
|
15134
15336
|
// Before recordTurnCheckpoint: these are the turn's inputs, not its
|
|
15135
15337
|
// products — undoing the turn must not destroy them.
|
|
15338
|
+
// A clock row due (storeDueClock) goes ahead of the turn's inputs, so a
|
|
15339
|
+
// turn starts with a current clock, a silent heartbeat's tick (which
|
|
15340
|
+
// stores no row of its own) included, unless storeDueClock writes none:
|
|
15341
|
+
// while the host is quiesced, while another reader of the residents'
|
|
15342
|
+
// shared slot has a turn alive, or when its write fails without landing.
|
|
15343
|
+
// Then a later event, turn start or tool boundary takes it. Fresh turns
|
|
15344
|
+
// only, as the flush below: a turn that resumes continues its logical
|
|
15345
|
+
// turn, which took its clock at its start or at a tool boundary that
|
|
15346
|
+
// could carry one.
|
|
15347
|
+
if (attempt === 0 && !resumesTurn)
|
|
15348
|
+
this.storeDueClock(agent, 'turn-start');
|
|
15136
15349
|
if (attempt === 0 &&
|
|
15137
15350
|
!resumesTurn &&
|
|
15138
15351
|
this.deferredMessages.length > 0) {
|
|
@@ -16614,9 +16827,9 @@ export class AgentFramework {
|
|
|
16614
16827
|
// one door a giant blob still walks through).
|
|
16615
16828
|
const readyState = agent.state;
|
|
16616
16829
|
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),
|
|
16830
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
16831
|
+
const { blocks: toolResultContent, spilled, keptCap } = await this.buildStoredToolResultContent(agent.name, readyState.toolResults, cap, bound);
|
|
16832
|
+
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
16833
|
// The response is already complete: this batch is never
|
|
16621
16834
|
// submitted in this turn, so it cannot be refused. Admit it.
|
|
16622
16835
|
agent.toolResultGuard.settleTurnEnded();
|
|
@@ -17852,7 +18065,8 @@ export class AgentFramework {
|
|
|
17852
18065
|
// token (as during a real tool call) and lands AFTER the pair, never
|
|
17853
18066
|
// between tool_use and its tool_result. The two addMessage calls
|
|
17854
18067
|
// below are synchronous and adjacent — nothing can interleave.
|
|
17855
|
-
const {
|
|
18068
|
+
const { bound } = this.resolveToolResultInlineCap(agent);
|
|
18069
|
+
const { blocks } = await this.buildStoredToolResultContent(agentName, [{ id: toolUseId, name: toolName, input, result, durationMs }], call.resultMaxChars, bound);
|
|
17856
18070
|
const toolUse = [
|
|
17857
18071
|
{ type: 'tool_use', id: toolUseId, name: toolName, input },
|
|
17858
18072
|
];
|
|
@@ -18294,10 +18508,12 @@ export class AgentFramework {
|
|
|
18294
18508
|
if (!this.noticeTargetFor(agentName, `script ${scriptId} wake`))
|
|
18295
18509
|
throw new Error(`agent ${agentName} has no context to wake`);
|
|
18296
18510
|
const agent = this.agents.get(agentName);
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18511
|
+
// A reader's strategy cuts the wake's whole message, so its spill fits the
|
|
18512
|
+
// readers' bound with its notice.
|
|
18513
|
+
const { cap: maxChars, bound } = agent
|
|
18514
|
+
? this.resolveToolResultInlineCap(agent)
|
|
18515
|
+
: { cap: DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, bound: undefined };
|
|
18516
|
+
const { text: shown } = await this.spillOrTruncate(text, maxChars, `script-${label}`, null, bound);
|
|
18301
18517
|
// The spill can take a while: the agent may have gone since, or the host begun to stop.
|
|
18302
18518
|
if (this.stopHasBegun())
|
|
18303
18519
|
throw new Error('this host is stopping');
|
|
@@ -18345,12 +18561,32 @@ export class AgentFramework {
|
|
|
18345
18561
|
* #89). Returns the stored blocks plus the per-call spill outcomes so the
|
|
18346
18562
|
* live wire copy can reuse the identical strings.
|
|
18347
18563
|
*/
|
|
18348
|
-
async buildStoredToolResultContent(agentName, toolResults, maxChars
|
|
18564
|
+
async buildStoredToolResultContent(agentName, toolResults, maxChars,
|
|
18565
|
+
/** The readers' smallest per-message bound (resolveToolResultInlineCap). */
|
|
18566
|
+
bound = undefined) {
|
|
18349
18567
|
const spilled = new Map();
|
|
18350
18568
|
const dateLabel = new Date().toISOString().slice(0, 10);
|
|
18569
|
+
// The results are stored as one message, whose bound a reader's
|
|
18570
|
+
// strategy spends across them in order. Each is stored as it would be
|
|
18571
|
+
// alone, unless together they pass the bound, or a save in the round
|
|
18572
|
+
// counted the round by its share (toolRoundsByShare): then each is
|
|
18573
|
+
// stored under its share (toolResultShare). A result carrying an image
|
|
18574
|
+
// keeps its live copy cut at the cap; any other's live copy is its stored
|
|
18575
|
+
// text.
|
|
18576
|
+
const alone = new Map();
|
|
18351
18577
|
for (const tc of toolResults) {
|
|
18352
18578
|
const { text, shown } = this.storedToolResultText(agentName, tc, maxChars);
|
|
18353
|
-
|
|
18579
|
+
alone.set(tc.id, { text, shown, stored: await this.spillOrTruncate(text, maxChars, `${dateLabel}-${tc.id}`, shown, bound) });
|
|
18580
|
+
}
|
|
18581
|
+
const share = maxChars === undefined ? undefined : toolResultShare(maxChars, bound, toolResults.length);
|
|
18582
|
+
const total = [...alone.values()].reduce((sum, { stored }) => sum + stored.text.length, 0);
|
|
18583
|
+
const byShare = share?.bound !== undefined && toolResults.length > 1
|
|
18584
|
+
&& (this.toolRoundsByShare.has(toolResults) || total > bound);
|
|
18585
|
+
for (const tc of toolResults) {
|
|
18586
|
+
const { text, shown, stored } = alone.get(tc.id);
|
|
18587
|
+
spilled.set(tc.id, byShare
|
|
18588
|
+
? await this.spillOrTruncate(text, share.cap, `${dateLabel}-${tc.id}`, shown, share.bound, stored.written)
|
|
18589
|
+
: stored);
|
|
18354
18590
|
}
|
|
18355
18591
|
const blocks = toolResults.map(tc => ({
|
|
18356
18592
|
type: 'tool_result',
|
|
@@ -18361,7 +18597,10 @@ export class AgentFramework {
|
|
|
18361
18597
|
content: spilled.get(tc.id).text,
|
|
18362
18598
|
isError: tc.result.isError,
|
|
18363
18599
|
}));
|
|
18364
|
-
|
|
18600
|
+
// A withheld round's stubs carry the guard's notice and place beside the
|
|
18601
|
+
// fields they keep, so under a bound their fields are cut within the
|
|
18602
|
+
// share, whether or not the round's results were stored by it.
|
|
18603
|
+
return { blocks, spilled, keptCap: share?.keptCap ?? maxChars };
|
|
18365
18604
|
}
|
|
18366
18605
|
/**
|
|
18367
18606
|
* A tool result's text as its stored copy is written before any cut, and
|
|
@@ -18530,21 +18769,30 @@ export class AgentFramework {
|
|
|
18530
18769
|
return { path, error: failure };
|
|
18531
18770
|
}
|
|
18532
18771
|
/** 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
|
-
|
|
18772
|
+
* sized to come back within `cap`, and, where `canRaise`, that raising
|
|
18773
|
+
* the inline cap would show more: where a cap at the readers' bound would
|
|
18774
|
+
* keep a longer head, or the whole result. */
|
|
18775
|
+
spillPageAdvice(path, cap, canRaise) {
|
|
18535
18776
|
// JSON serialization can expand each read-back code unit to six
|
|
18536
18777
|
// chars (\\u0000). Leave room for the page's metadata and a possible
|
|
18537
18778
|
// extra code unit at a surrogate-pair boundary.
|
|
18538
18779
|
const pageOverhead = JSON.stringify(path).length + 256;
|
|
18539
18780
|
const readLimit = Math.max(1, Math.min(2000, Math.floor((cap - pageOverhead) / 6) - 1));
|
|
18540
18781
|
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.';
|
|
18782
|
+
+ 'use nextOffsetChars as offsetChars until null, keeping limitChars.'
|
|
18783
|
+
+ (canRaise ? ' Or raise the inline cap via agent_settings update tool_result_inline_max_chars.' : '');
|
|
18543
18784
|
}
|
|
18544
18785
|
async spillOrTruncate(content, cap, label,
|
|
18545
18786
|
/** What the live copy showed beside this text (storedToolResultText);
|
|
18546
18787
|
* null when the live copy was this text. */
|
|
18547
|
-
shown = null
|
|
18788
|
+
shown = null,
|
|
18789
|
+
/** The most a reader's strategy keeps of this copy: the readers'
|
|
18790
|
+
* smallest per-message bound, or the result's share of it
|
|
18791
|
+
* (buildStoredToolResultContent). */
|
|
18792
|
+
bound = undefined,
|
|
18793
|
+
/** The spill file an earlier cut of this text wrote: reused, not written
|
|
18794
|
+
* again. Undefined when none was cut. */
|
|
18795
|
+
written) {
|
|
18548
18796
|
if (!cap || content.length <= cap)
|
|
18549
18797
|
return { text: content, filePath: null };
|
|
18550
18798
|
// The stored text is the only place save_recent_image discovers tool
|
|
@@ -18553,19 +18801,24 @@ export class AgentFramework {
|
|
|
18553
18801
|
// saw that the index would silently slide past (issue #104), so the
|
|
18554
18802
|
// newest slots past the cut that the live copy showed are re-appended
|
|
18555
18803
|
// after the notice. Otherwise the live copy is this text, and nothing
|
|
18556
|
-
// past the cut is kept (#403).
|
|
18557
|
-
|
|
18804
|
+
// past the cut is kept (#403). The stored copy, notice and kept slots
|
|
18805
|
+
// included, fits `bound`, the most a reader's strategy keeps of it, so
|
|
18806
|
+
// no reader's cut takes the notice (cutStoredToolResultToFit), and the
|
|
18807
|
+
// spill notices name the head they kept.
|
|
18808
|
+
const cut = cutStoredToolResultToFit(content, cap, shown, bound);
|
|
18558
18809
|
const { head } = cut;
|
|
18559
18810
|
const kept = formatKeptImageSlots(cut);
|
|
18560
|
-
|
|
18811
|
+
if (written === undefined)
|
|
18812
|
+
written = await this.writeToolResultFile(label, content);
|
|
18561
18813
|
if (written && written.error === undefined) {
|
|
18562
18814
|
const path = written.path;
|
|
18563
18815
|
return {
|
|
18564
18816
|
text: head
|
|
18565
18817
|
+ `\n\n[truncated — showing ${head.length} of ${content.length} chars; full content: workspace file ${path}`
|
|
18566
|
-
+ `. ${this.spillPageAdvice(path, cap)}]`
|
|
18818
|
+
+ `. ${this.spillPageAdvice(path, cap, bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > head.length)}]`
|
|
18567
18819
|
+ kept,
|
|
18568
18820
|
filePath: path,
|
|
18821
|
+
written,
|
|
18569
18822
|
};
|
|
18570
18823
|
}
|
|
18571
18824
|
if (written) {
|
|
@@ -18578,6 +18831,7 @@ export class AgentFramework {
|
|
|
18578
18831
|
+ ` (${written.error}); content over the cap was not retained]`
|
|
18579
18832
|
+ kept,
|
|
18580
18833
|
filePath: null,
|
|
18834
|
+
written,
|
|
18581
18835
|
};
|
|
18582
18836
|
}
|
|
18583
18837
|
return {
|
|
@@ -18585,6 +18839,7 @@ export class AgentFramework {
|
|
|
18585
18839
|
+ '\n\n[truncated — original was ' + content.length + ' chars; no writable workspace mount without an include, full content not retained]'
|
|
18586
18840
|
+ kept,
|
|
18587
18841
|
filePath: null,
|
|
18842
|
+
written,
|
|
18588
18843
|
};
|
|
18589
18844
|
}
|
|
18590
18845
|
/**
|
|
@@ -18870,8 +19125,10 @@ export class AgentFramework {
|
|
|
18870
19125
|
* the array is text-only (let JSON path handle it; saves a code path).
|
|
18871
19126
|
* `maxChars`, when provided, caps each accompanying text block so an image
|
|
18872
19127
|
* 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
|
|
19128
|
+
* block is cut at the cap as the stored copy's cut at that cap is
|
|
19129
|
+
* (toolResultCutAt), never inside a slot, so the stored copy can keep
|
|
19130
|
+
* exactly the slots this showed (#403); under a readers' bound the stored
|
|
19131
|
+
* copy's cut can come earlier (cutStoredToolResultToFit).
|
|
18875
19132
|
*/
|
|
18876
19133
|
tryNativeToolResultContent(data, maxChars,
|
|
18877
19134
|
/** Spill file written by the history path — referenced from truncation
|
|
@@ -18949,6 +19206,10 @@ export class AgentFramework {
|
|
|
18949
19206
|
* (Sol's #94 ruling: a durable preference must not be a durable path for
|
|
18950
19207
|
* one tool result to exceed maxMessageTokens; the way to see the whole
|
|
18951
19208
|
* result is the spill file, not an over-bound blob in live context).
|
|
19209
|
+
* An ordinary resident's results land in the shared slot, which each of
|
|
19210
|
+
* its readers compiles under its own strategy, so the bound is the
|
|
19211
|
+
* smallest of theirs, as the tune-out dump's is (residentMessageChars); a
|
|
19212
|
+
* fork's or an ephemeral agent's store has itself as its only reader.
|
|
18952
19213
|
*/
|
|
18953
19214
|
resolveToolResultInlineCap(agent) {
|
|
18954
19215
|
const override = this.toolResultInlineMaxCharsOverride.get(agent.name);
|
|
@@ -18957,12 +19218,25 @@ export class AgentFramework {
|
|
|
18957
19218
|
const source = override !== undefined
|
|
18958
19219
|
? 'agent-settings-override'
|
|
18959
19220
|
: configured !== null ? 'framework-config' : 'default';
|
|
18960
|
-
|
|
18961
|
-
|
|
19221
|
+
let bound = this.strategyDerivedToolResultChars(agent);
|
|
19222
|
+
let byReader = false;
|
|
19223
|
+
if (this.isOrdinaryResident(agent)) {
|
|
19224
|
+
for (const reader of this.sharedSlotAgents) {
|
|
19225
|
+
if (reader === agent)
|
|
19226
|
+
continue;
|
|
19227
|
+
const readerBound = this.strategyDerivedToolResultChars(reader);
|
|
19228
|
+
if (readerBound !== undefined && (bound === undefined || readerBound < bound)) {
|
|
19229
|
+
bound = readerBound;
|
|
19230
|
+
byReader = true;
|
|
19231
|
+
}
|
|
19232
|
+
}
|
|
19233
|
+
}
|
|
19234
|
+
const clamped = bound !== undefined && bound < desired;
|
|
18962
19235
|
return {
|
|
18963
|
-
cap: clamped ?
|
|
19236
|
+
cap: clamped ? bound : desired,
|
|
18964
19237
|
source,
|
|
18965
|
-
clampedBy: clamped ? 'strategy-bound' : null,
|
|
19238
|
+
clampedBy: clamped ? (byReader ? 'reader-bound' : 'strategy-bound') : null,
|
|
19239
|
+
bound,
|
|
18966
19240
|
};
|
|
18967
19241
|
}
|
|
18968
19242
|
/**
|
|
@@ -18970,8 +19244,9 @@ export class AgentFramework {
|
|
|
18970
19244
|
* its caller whole. A call from a running script resolves to the script,
|
|
18971
19245
|
* which takes any result that fits its memory (scriptResultMaxChars). Any
|
|
18972
19246
|
* other call's result is stored in its agent's context under the agent's
|
|
18973
|
-
* inline cap,
|
|
18974
|
-
*
|
|
19247
|
+
* inline cap, unless its round's results together pass the readers' bound,
|
|
19248
|
+
* when each takes its share (buildStoredToolResultContent), and a call for
|
|
19249
|
+
* an agent the framework doesn't hold gets the house default.
|
|
18975
19250
|
*/
|
|
18976
19251
|
resultMaxCharsFor(agentName, callId) {
|
|
18977
19252
|
const ce = this.codeExecution;
|
|
@@ -19544,8 +19819,9 @@ export class AgentFramework {
|
|
|
19544
19819
|
// messages flush at the next turn's START (before its compile — riding
|
|
19545
19820
|
// their own wake), at the agent's next tool boundary (where they are
|
|
19546
19821
|
// 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
|
|
19822
|
+
// message a tune-out holds from the residents, and one the next compile
|
|
19823
|
+
// won't send among its newest), or in driveStream's finally when the
|
|
19824
|
+
// turn ends.
|
|
19549
19825
|
// Quiesce (issue #122): the window exists to run compression/refold
|
|
19550
19826
|
// against a FROZEN context — module events and api message.send must not
|
|
19551
19827
|
// append mid-surgery. Deferred here, flushed by resume(). tool_result
|
|
@@ -19614,6 +19890,13 @@ export class AgentFramework {
|
|
|
19614
19890
|
}
|
|
19615
19891
|
return ''; // Deferred — flushed at the target's next boundary
|
|
19616
19892
|
}
|
|
19893
|
+
// A clock row due goes first (storeDueClock). A write that gets here is a
|
|
19894
|
+
// tool result (which takes none), or no turn of the agent's is alive, or
|
|
19895
|
+
// its turn is assembling before its compile, or it comes from stop()'s
|
|
19896
|
+
// drain, once every stream it could join has settled: the clock is in
|
|
19897
|
+
// whatever request comes next.
|
|
19898
|
+
if (takesClockBefore(participant, content, metadata, agent.name))
|
|
19899
|
+
this.storeDueClock(agent, 'event');
|
|
19617
19900
|
const stored = agent.getContextManager().addMessage(participant, content, opts?.fromQueue ? withDeferredWriteId(metadata, opts.fromQueue.id) : metadata);
|
|
19618
19901
|
if (opts?.placement) {
|
|
19619
19902
|
opts.placement.agent = agent.name;
|
|
@@ -19696,6 +19979,72 @@ export class AgentFramework {
|
|
|
19696
19979
|
if (this.quiesced || this.deferredWritesPersisted)
|
|
19697
19980
|
this.persistDeferredWrites();
|
|
19698
19981
|
}
|
|
19982
|
+
/**
|
|
19983
|
+
* What the next compile sends for each of `ids`, messages a tool boundary
|
|
19984
|
+
* has just stored, as the agent's strategy renders them as the newest of
|
|
19985
|
+
* its window (ContextManager.renderAsNewest): cut by the per-message cap,
|
|
19986
|
+
* a sharded body joined, and the image limits applied over the batch, so
|
|
19987
|
+
* the live stream hears what the next compile sends. An id comes back as
|
|
19988
|
+
* null where that compile won't send the message among its newest.
|
|
19989
|
+
* Undefined where the strategy has no such hook, or the render threw,
|
|
19990
|
+
* which is logged: the boundary then injects what it stored, as it did
|
|
19991
|
+
* before strategies could say.
|
|
19992
|
+
*/
|
|
19993
|
+
renderForInjection(agent, ids) {
|
|
19994
|
+
if (ids.length === 0)
|
|
19995
|
+
return undefined;
|
|
19996
|
+
try {
|
|
19997
|
+
return agent.getContextManager().renderAsNewest(ids);
|
|
19998
|
+
}
|
|
19999
|
+
catch (error) {
|
|
20000
|
+
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);
|
|
20001
|
+
return undefined;
|
|
20002
|
+
}
|
|
20003
|
+
}
|
|
20004
|
+
/**
|
|
20005
|
+
* Tell the agent of the messages a tool boundary stored and left out of
|
|
20006
|
+
* its injection, since the next compile won't send them among its newest:
|
|
20007
|
+
* who sent each and its size, and how to read it, `history--extract`
|
|
20008
|
+
* around it where the agent is shown that tool, else its id. None of a
|
|
20009
|
+
* message's words: the line stands in for a message the live round
|
|
20010
|
+
* doesn't carry. A system line, so it's no conversation: it neither
|
|
20011
|
+
* re-pins the turn nor clears its send suppression. It names the agent
|
|
20012
|
+
* it's for, since every resident sharing the slot reads it too. Returns
|
|
20013
|
+
* the notice as stored, for the boundary to inject after the batch, or
|
|
20014
|
+
* undefined when it couldn't be stored, which is logged.
|
|
20015
|
+
*/
|
|
20016
|
+
recordLeftOutNotice(agent, leftOut) {
|
|
20017
|
+
const canExtract = this.advertisedToolsForAgent(agent.name).some((t) => t.name === 'history--extract');
|
|
20018
|
+
const one = leftOut.length === 1;
|
|
20019
|
+
const lines = leftOut.map(({ msg, id, content, parts }) => {
|
|
20020
|
+
const size = contentSize(content);
|
|
20021
|
+
// A sharded body is that many store messages, consecutive from its
|
|
20022
|
+
// first, which extract reads around it one by one. (Its "after" stops
|
|
20023
|
+
// at 100, so a body of more parts, over 300,000 tokens at the memory
|
|
20024
|
+
// strategy's default shard size, is read only that far.)
|
|
20025
|
+
const read = canExtract
|
|
20026
|
+
? `history--extract with aroundId "${id}", before 0 and after ${parts - 1} reads it` +
|
|
20027
|
+
`${parts > 1 ? `, stored as ${parts} parts` : ''}.`
|
|
20028
|
+
: `Its id is ${id}${parts > 1 ? `, the first of the ${parts} parts it's stored as` : ''}.`;
|
|
20029
|
+
return `- From ${msg.participant}${size ? `, ${size}` : ''}. ${read}`;
|
|
20030
|
+
});
|
|
20031
|
+
const content = [{
|
|
20032
|
+
type: 'text',
|
|
20033
|
+
text: `[not shown] For ${agent.name}: ${one ? 'A message' : `${leftOut.length} messages`} arrived during your turn that ` +
|
|
20034
|
+
`${one ? "isn't" : "aren't"} shown here, since your context doesn't keep ${one ? 'it' : 'them'} among ` +
|
|
20035
|
+
`your newest messages:\n${lines.join('\n')}`,
|
|
20036
|
+
}];
|
|
20037
|
+
const metadata = { system: true, kind: 'left-out-notice' };
|
|
20038
|
+
try {
|
|
20039
|
+
const id = agent.getContextManager().addMessage('user', content, metadata);
|
|
20040
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'left-out-notice' });
|
|
20041
|
+
return { participant: 'user', content, metadata };
|
|
20042
|
+
}
|
|
20043
|
+
catch (err) {
|
|
20044
|
+
console.error(`[injection] ${agent.name}: couldn't store the notice of messages left out of the live round:`, err);
|
|
20045
|
+
return undefined;
|
|
20046
|
+
}
|
|
20047
|
+
}
|
|
19699
20048
|
/**
|
|
19700
20049
|
* Write drained entries in queue (seq) order, whatever their targets:
|
|
19701
20050
|
* residents share one message slot, so the order entries arrived in is the
|
|
@@ -19814,7 +20163,7 @@ export class AgentFramework {
|
|
|
19814
20163
|
continue;
|
|
19815
20164
|
}
|
|
19816
20165
|
if (found.length > 0) {
|
|
19817
|
-
written.set(msg, { id: found[0].id, content: asCompiled(found) });
|
|
20166
|
+
written.set(msg, { id: found[0].id, content: asCompiled(found), parts: found.length });
|
|
19818
20167
|
this.logDeferredWriteError(msg, where, undefined, { count: found.length, sharded: found[0].bodyGroupId !== undefined, foundLater: true });
|
|
19819
20168
|
onLanded?.(msg, found[0].id);
|
|
19820
20169
|
continue;
|
|
@@ -19831,7 +20180,7 @@ export class AgentFramework {
|
|
|
19831
20180
|
const copies = agent && before !== undefined && (this.storedCountOf(agent) ?? before) - before > 1
|
|
19832
20181
|
? this.storedCopiesOf(agent, before, msg.id) ?? []
|
|
19833
20182
|
: [];
|
|
19834
|
-
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content });
|
|
20183
|
+
written.set(msg, { id, content: copies.length > 1 ? asCompiled(copies) : msg.content, parts: Math.max(1, copies.length) });
|
|
19835
20184
|
}
|
|
19836
20185
|
catch (error) {
|
|
19837
20186
|
// What landed, read back from where the store stood before the
|
|
@@ -19839,7 +20188,7 @@ export class AgentFramework {
|
|
|
19839
20188
|
// after it. With no agent there's no store it could have landed in.
|
|
19840
20189
|
const landed = !agent ? [] : before === undefined ? undefined : this.storedCopiesOf(agent, before, msg.id);
|
|
19841
20190
|
if (landed && landed.length > 0) {
|
|
19842
|
-
written.set(msg, { id: landed[0].id, content: asCompiled(landed) });
|
|
20191
|
+
written.set(msg, { id: landed[0].id, content: asCompiled(landed), parts: landed.length });
|
|
19843
20192
|
this.logDeferredWriteError(msg, where, error, { count: landed.length, sharded: landed[0].bodyGroupId !== undefined });
|
|
19844
20193
|
onLanded?.(msg, landed[0].id);
|
|
19845
20194
|
continue;
|
|
@@ -19880,14 +20229,19 @@ export class AgentFramework {
|
|
|
19880
20229
|
* `bodyGroupId`. Undefined when the store can't be read back, which is
|
|
19881
20230
|
* logged: not the same as nothing found. */
|
|
19882
20231
|
storedCopiesOf(agent, from, deferredWriteId) {
|
|
20232
|
+
return this.storedSince(agent, from, (m) => m.metadata?.deferredWriteId === deferredWriteId, 'deferral', `deferred write ${deferredWriteId}`);
|
|
20233
|
+
}
|
|
20234
|
+
/** The messages `match` picks among those the agent's store gained from
|
|
20235
|
+
* position `from` on, in store order, with their blobs resolved. Undefined
|
|
20236
|
+
* when the store can't be read back, which is logged under `tag`, naming
|
|
20237
|
+
* `what` was written: not the same as nothing found. */
|
|
20238
|
+
storedSince(agent, from, match, tag, what) {
|
|
19883
20239
|
try {
|
|
19884
20240
|
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);
|
|
20241
|
+
return cm.getMessageWindow(from, Math.max(0, cm.getMessageCount() - from)).messages.filter(match).map(storedCopy);
|
|
19888
20242
|
}
|
|
19889
20243
|
catch (error) {
|
|
19890
|
-
console.error(`[
|
|
20244
|
+
console.error(`[${tag}] could not read back what ${what} stored in ${agent.name}'s store:`, error);
|
|
19891
20245
|
return undefined;
|
|
19892
20246
|
}
|
|
19893
20247
|
}
|
|
@@ -22799,6 +23153,9 @@ export class AgentFramework {
|
|
|
22799
23153
|
// configured servers can still deploy its first one at runtime.
|
|
22800
23154
|
if (!this.mcplServerRegistry || !this.mcplHostCapabilities) {
|
|
22801
23155
|
await this.initializeMcpl([], this.mcplInferenceRoutingConfig ?? undefined);
|
|
23156
|
+
// The lifecycle log's replay restored any tune-out on record, and no
|
|
23157
|
+
// coordinator is made here to carry one.
|
|
23158
|
+
this.endTuneOutsWithoutCoordinator();
|
|
22802
23159
|
}
|
|
22803
23160
|
const collision = this.mcplPrefixCollision(config, undefined, claim);
|
|
22804
23161
|
if (collision)
|
|
@@ -24468,6 +24825,47 @@ export class AgentFramework {
|
|
|
24468
24825
|
...structured,
|
|
24469
24826
|
};
|
|
24470
24827
|
}
|
|
24828
|
+
/**
|
|
24829
|
+
* A tune-out on record where this host has no tune-out coordinator: a
|
|
24830
|
+
* start with no subconscious makes none, and neither does a channel
|
|
24831
|
+
* subsystem brought up after start by connectMcplServer. Nothing then
|
|
24832
|
+
* diverts the channel's traffic or ever cancels its tune-out, tune_out
|
|
24833
|
+
* isn't offered, and channel_open, channel_close and channel_decline
|
|
24834
|
+
* refuse a tuned-out channel, so the channel would stay held by a decision
|
|
24835
|
+
* nothing here can change. Each one ends here instead, its channel decided
|
|
24836
|
+
* open again as a cancel leaves it, under the tune-out's own source, so an
|
|
24837
|
+
* agent's tune-out keeps the agent's provenance (the housekeeping guard
|
|
24838
|
+
* reads it). A log line names the channel and says how many of the
|
|
24839
|
+
* messages it diverted are held here, stored or still queued to be
|
|
24840
|
+
* written, which nothing will now deliver as its backlog.
|
|
24841
|
+
*/
|
|
24842
|
+
endTuneOutsWithoutCoordinator() {
|
|
24843
|
+
const registry = this.channelRegistry;
|
|
24844
|
+
if (!registry || this.tuneOutCoordinator)
|
|
24845
|
+
return;
|
|
24846
|
+
const primary = this.primaryAgentName ? this.agents.get(this.primaryAgentName) : undefined;
|
|
24847
|
+
const ofEpoch = (epochId) => (m) => m.metadata?.tuneOut?.epochId === epochId;
|
|
24848
|
+
for (const { serverId, channelId, params, source } of registry.tuneOutsOnRecord()) {
|
|
24849
|
+
const diverted = typeof params.epochId !== 'string' ? 0
|
|
24850
|
+
: (primary?.getContextManager().getAllMessages() ?? []).filter(ofEpoch(params.epochId)).length +
|
|
24851
|
+
this.deferredMessages.filter((m) => (m.forAgent ?? this.primaryAgentName) === this.primaryAgentName)
|
|
24852
|
+
.filter(ofEpoch(params.epochId)).length;
|
|
24853
|
+
const held = diverted === 0 ? ''
|
|
24854
|
+
: diverted === 1 ? ' The 1 message it diverted stays here, and nothing will deliver it as its backlog.'
|
|
24855
|
+
: ` The ${diverted} messages it diverted stay here, and nothing will deliver them as its backlog.`;
|
|
24856
|
+
try {
|
|
24857
|
+
registry.cancelTuneOut(serverId, channelId, 'open', source);
|
|
24858
|
+
}
|
|
24859
|
+
catch (err) {
|
|
24860
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server is tuned out, and nothing on this host watches ` +
|
|
24861
|
+
`or cancels tune-outs, but ending this one failed, so it stays recorded until a start that can end it: ` +
|
|
24862
|
+
`${err instanceof Error ? err.message : String(err)}.${held}`);
|
|
24863
|
+
continue;
|
|
24864
|
+
}
|
|
24865
|
+
console.error(`[tune-out] "${channelId}" on the "${serverId}" server was tuned out, and nothing on this host watches ` +
|
|
24866
|
+
`or cancels tune-outs, so the tune-out ends here and the channel is decided open again.${held}`);
|
|
24867
|
+
}
|
|
24868
|
+
}
|
|
24471
24869
|
/**
|
|
24472
24870
|
* Build BeforeInferenceParams from agent state and trigger context.
|
|
24473
24871
|
*/
|
|
@@ -25262,11 +25660,24 @@ export class AgentFramework {
|
|
|
25262
25660
|
// carries the same ref.
|
|
25263
25661
|
const state = agent.state;
|
|
25264
25662
|
if (state.status === 'waiting_for_tools') {
|
|
25265
|
-
|
|
25663
|
+
// The commit stores a round as each result would be alone, or by
|
|
25664
|
+
// its share where they pass the bound together, which a sibling
|
|
25665
|
+
// still running can decide. Where the two ways differ for a result
|
|
25666
|
+
// counted here, the round is marked to be stored by its share
|
|
25667
|
+
// (toolRoundsByShare), so the count holds either way: a result
|
|
25668
|
+
// counted before the mark is the same both ways. The share is of
|
|
25669
|
+
// the round's calls, finished or not.
|
|
25670
|
+
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
25671
|
+
const share = toolResultShare(cap, bound, state.pending.size + state.completed.length);
|
|
25266
25672
|
for (let i = state.completed.length - 1; i >= 0 && collecting(); i--) {
|
|
25267
25673
|
const tc = state.completed[i];
|
|
25268
25674
|
const { text, shown } = this.storedToolResultText(agentName, tc, cap);
|
|
25269
|
-
|
|
25675
|
+
const alone = storedImageSlots(text, cap, shown, bound);
|
|
25676
|
+
const byShare = storedImageSlots(text, share.cap, shown, share.bound);
|
|
25677
|
+
if (alone.length !== byShare.length || alone.some((slot, j) => slot.text !== byShare[j].text)) {
|
|
25678
|
+
this.toolRoundsByShare.add(state.completed);
|
|
25679
|
+
}
|
|
25680
|
+
considerToolResultImages(tc.id, tc.name, this.toolRoundsByShare.has(state.completed) ? byShare : alone, 0);
|
|
25270
25681
|
}
|
|
25271
25682
|
}
|
|
25272
25683
|
// Walk the message store tail-first in bounded windows, re-inlining
|