@animalabs/agent-framework 0.24.1 → 0.26.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.map +1 -1
- package/dist/src/agent.js +21 -8
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +18 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +87 -37
- package/dist/src/framework.js.map +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.map +1 -1
- package/dist/src/modules/workspace/index.js +74 -20
- 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/tool-image-ledger.d.ts +26 -3
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +31 -4
- 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-history.d.ts +15 -5
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +33 -9
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/events.d.ts +8 -2
- package/dist/src/types/events.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +21 -8
- package/src/framework.ts +92 -36
- package/src/mcpl/references.ts +5 -2
- package/src/modules/workspace/index.ts +74 -20
- package/src/modules/workspace/types.ts +6 -2
- package/src/text-as-stored.ts +73 -0
- package/src/tool-image-ledger.ts +31 -4
- package/src/tool-presentation.ts +8 -1
- package/src/tool-result-history.ts +33 -8
- package/src/types/events.ts +8 -2
package/dist/src/framework.js
CHANGED
|
@@ -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
|
|
@@ -623,7 +624,8 @@ function describeDeferredWrite(msg) {
|
|
|
623
624
|
function isInjectableMidTurn(msg, agentName) {
|
|
624
625
|
// Exact, as membrane's role test is: only a participant equal to the
|
|
625
626
|
// request's assistantParticipant (Agent's name) renders as the assistant.
|
|
626
|
-
|
|
627
|
+
// It's the name as sent, which the boundary takes as the store keeps it.
|
|
628
|
+
return nameAsStored(msg.participant) !== agentName
|
|
627
629
|
&& !msg.content.some((b) => b.type === 'tool_use' || b.type === 'tool_result');
|
|
628
630
|
}
|
|
629
631
|
/**
|
|
@@ -714,6 +716,11 @@ function isTurnContinuation(reason) {
|
|
|
714
716
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
715
717
|
|| reason === 'tool_result_guard_retry';
|
|
716
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']);
|
|
717
724
|
const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
|
|
718
725
|
const INFERENCE_LOG_ID = 'framework/inference-log';
|
|
719
726
|
const PROCESS_LOG_ID = 'framework/process-log';
|
|
@@ -10555,9 +10562,12 @@ export class AgentFramework {
|
|
|
10555
10562
|
// 0.5.x, and from 0.6.0 declares it can't carry them, so none are
|
|
10556
10563
|
// collected for it. Each goes as its participant and content
|
|
10557
10564
|
// alone: a stored row's metadata is the host's record, not input
|
|
10558
|
-
// 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.
|
|
10559
10569
|
currentState.stream.provideToolResults(agent.toolResultGuard.submissionResults(membraneResults), midTurnInjections.length > 0
|
|
10560
|
-
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
10570
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant: nameAsStored(participant), content: contentAsStored(content) })) }
|
|
10561
10571
|
: undefined);
|
|
10562
10572
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
10563
10573
|
// is assembly too when the stream carries the injected messages
|
|
@@ -12726,6 +12736,16 @@ export class AgentFramework {
|
|
|
12726
12736
|
// requests[0] in that mixed batch bypassed the turn lock because a
|
|
12727
12737
|
// restart existed, then treated the continuation as a fresh turn.
|
|
12728
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
|
+
}
|
|
12729
12749
|
// Route this turn's auto-published speech to the channel that triggered
|
|
12730
12750
|
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
12731
12751
|
// (messages arrived in >1 channel while the agent was busy/idle):
|
|
@@ -14824,7 +14844,9 @@ export class AgentFramework {
|
|
|
14824
14844
|
delete routing.pin;
|
|
14825
14845
|
}
|
|
14826
14846
|
}
|
|
14827
|
-
/** 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). */
|
|
14828
14850
|
static PROSE_BOUNCE_WAKE_CAP = 2;
|
|
14829
14851
|
/**
|
|
14830
14852
|
* Words about to be held that may use `{{unsent}}` (a bounce, or an
|
|
@@ -18585,7 +18607,7 @@ export class AgentFramework {
|
|
|
18585
18607
|
for (const tc of toolResults) {
|
|
18586
18608
|
const { text, shown, stored } = alone.get(tc.id);
|
|
18587
18609
|
spilled.set(tc.id, byShare
|
|
18588
|
-
? await this.spillOrTruncate(text, share.cap, `${dateLabel}-${tc.id}`, shown, share.bound, stored.written)
|
|
18610
|
+
? await this.spillOrTruncate(text, share.cap, `${dateLabel}-${tc.id}`, shown, share.bound, stored.written, { readCap: maxChars })
|
|
18589
18611
|
: stored);
|
|
18590
18612
|
}
|
|
18591
18613
|
const blocks = toolResults.map(tc => ({
|
|
@@ -18619,7 +18641,7 @@ export class AgentFramework {
|
|
|
18619
18641
|
*/
|
|
18620
18642
|
storedToolResultText(agentName, tc, maxChars) {
|
|
18621
18643
|
if (tc.result.isError)
|
|
18622
|
-
return { text: tc.result.error ?? 'Unknown error', shown: null };
|
|
18644
|
+
return { text: (tc.result.error ?? 'Unknown error').toWellFormed(), shown: null };
|
|
18623
18645
|
const ledger = this.toolImageLedgerFor(agentName);
|
|
18624
18646
|
let blocks = null;
|
|
18625
18647
|
const text = nonErrorResultText(tc.result, undefined, {
|
|
@@ -18792,7 +18814,12 @@ export class AgentFramework {
|
|
|
18792
18814
|
bound = undefined,
|
|
18793
18815
|
/** The spill file an earlier cut of this text wrote: reused, not written
|
|
18794
18816
|
* again. Undefined when none was cut. */
|
|
18795
|
-
written
|
|
18817
|
+
written,
|
|
18818
|
+
/** Set where `cap` and `bound` are this text's share of one turn's
|
|
18819
|
+
* bound (buildStoredToolResultContent): `readCap` is the inline cap,
|
|
18820
|
+
* which a later read of the file comes back under alone in its own turn
|
|
18821
|
+
* and its paging advice is sized to. */
|
|
18822
|
+
share) {
|
|
18796
18823
|
if (!cap || content.length <= cap)
|
|
18797
18824
|
return { text: content, filePath: null };
|
|
18798
18825
|
// The stored text is the only place save_recent_image discovers tool
|
|
@@ -18802,45 +18829,63 @@ export class AgentFramework {
|
|
|
18802
18829
|
// newest slots past the cut that the live copy showed are re-appended
|
|
18803
18830
|
// after the notice. Otherwise the live copy is this text, and nothing
|
|
18804
18831
|
// 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,
|
|
18806
|
-
// no reader's cut takes the
|
|
18807
|
-
// spill notices name the head
|
|
18808
|
-
|
|
18832
|
+
// included, fits `bound`, the most a reader's strategy keeps of it,
|
|
18833
|
+
// where `bound` leaves the notice its room, so no reader's cut takes the
|
|
18834
|
+
// notice (cutStoredToolResultToFit), and the spill notices name the head
|
|
18835
|
+
// they kept. Under a share too small for that, a cut keeps no head and
|
|
18836
|
+
// stores its notice alone, and a text no longer than that notice is
|
|
18837
|
+
// stored whole, with no file written: each result at its least.
|
|
18838
|
+
const readCap = share?.readCap ?? cap;
|
|
18839
|
+
const cut = cutStoredToolResultToFit(content, cap, shown, bound, share ? this.spillNoticeLength(label, content, shown, bound, readCap) : undefined);
|
|
18840
|
+
if (cut.head === content)
|
|
18841
|
+
return { text: content, filePath: null };
|
|
18809
18842
|
const { head } = cut;
|
|
18810
18843
|
const kept = formatKeptImageSlots(cut);
|
|
18811
18844
|
if (written === undefined)
|
|
18812
18845
|
written = await this.writeToolResultFile(label, content);
|
|
18846
|
+
const notice = this.spillNotice(head.length, content, shown, bound, readCap, written);
|
|
18847
|
+
// The notice was measured as a written file's reads. A write that failed
|
|
18848
|
+
// keeps nothing, and its notice, naming the failure, can be longer: under
|
|
18849
|
+
// a share, a cut that keeps no head and would store no less than the
|
|
18850
|
+
// text is stored as the text itself, where the text holds no image slot.
|
|
18851
|
+
// One that does is stored as it was measured, cut, since save_recent_image
|
|
18852
|
+
// counted it so before the write.
|
|
18853
|
+
if (share && head.length === 0 && notice.length >= content.length && parseImagePlaceholders(content).length === 0) {
|
|
18854
|
+
return { text: content, filePath: null, written };
|
|
18855
|
+
}
|
|
18856
|
+
return { text: head + notice + kept, filePath: written && written.error === undefined ? written.path : null, written };
|
|
18857
|
+
}
|
|
18858
|
+
/**
|
|
18859
|
+
* The notice a cut copy carries after its head (spillOrTruncate): the file
|
|
18860
|
+
* holding the whole result and how to read it back, or why there is none.
|
|
18861
|
+
*/
|
|
18862
|
+
spillNotice(headLength, content, shown, bound, readCap, written) {
|
|
18813
18863
|
if (written && written.error === undefined) {
|
|
18814
|
-
const
|
|
18815
|
-
return {
|
|
18816
|
-
|
|
18817
|
-
+ `\n\n[truncated — showing ${head.length} of ${content.length} chars; full content: workspace file ${path}`
|
|
18818
|
-
+ `. ${this.spillPageAdvice(path, cap, bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > head.length)}]`
|
|
18819
|
-
+ kept,
|
|
18820
|
-
filePath: path,
|
|
18821
|
-
written,
|
|
18822
|
-
};
|
|
18864
|
+
const canRaise = bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > headLength;
|
|
18865
|
+
return `\n\n[truncated — showing ${headLength} of ${content.length} chars; full content: workspace file ${written.path}`
|
|
18866
|
+
+ `. ${this.spillPageAdvice(written.path, readCap, canRaise)}]`;
|
|
18823
18867
|
}
|
|
18824
18868
|
if (written) {
|
|
18825
18869
|
// A workspace was there and the write FAILED — say that, loudly and
|
|
18826
18870
|
// distinctly. Telling the agent "no writable workspace" would teach
|
|
18827
18871
|
// them their residence lacks a capability it actually has.
|
|
18828
|
-
return {
|
|
18829
|
-
|
|
18830
|
-
+ `\n\n[truncated — showing ${head.length} of ${content.length} chars; spill to workspace file ${written.path} FAILED`
|
|
18831
|
-
+ ` (${written.error}); content over the cap was not retained]`
|
|
18832
|
-
+ kept,
|
|
18833
|
-
filePath: null,
|
|
18834
|
-
written,
|
|
18835
|
-
};
|
|
18872
|
+
return `\n\n[truncated — showing ${headLength} of ${content.length} chars; spill to workspace file ${written.path} FAILED`
|
|
18873
|
+
+ ` (${written.error}); content over the cap was not retained]`;
|
|
18836
18874
|
}
|
|
18837
|
-
return
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18875
|
+
return '\n\n[truncated — original was ' + content.length + ' chars; no writable workspace mount without an include, full content not retained]';
|
|
18876
|
+
}
|
|
18877
|
+
/**
|
|
18878
|
+
* The length of the notice a cut of `content` that keeps no head would
|
|
18879
|
+
* carry, before any file is written, as it reads once its file is
|
|
18880
|
+
* (toolResultFilePlace, where any earlier cut of the text wrote it too), so
|
|
18881
|
+
* the cut and save_recent_image's count of the result before it measure
|
|
18882
|
+
* alike. For keeping whole, under a share, a text no longer than its notice
|
|
18883
|
+
* (cutStoredToolResultToFit). A write that fails carries a notice naming
|
|
18884
|
+
* the failure instead (spillOrTruncate).
|
|
18885
|
+
*/
|
|
18886
|
+
spillNoticeLength(label, content, shown, bound, readCap) {
|
|
18887
|
+
const place = this.toolResultFilePlace(label);
|
|
18888
|
+
return this.spillNotice(0, content, shown, bound, readCap, place ? { path: place.path } : null).length;
|
|
18844
18889
|
}
|
|
18845
18890
|
/**
|
|
18846
18891
|
* The bridge: one of a script's calls, as its agent's, through the ordinary
|
|
@@ -19150,7 +19195,8 @@ export class AgentFramework {
|
|
|
19150
19195
|
if (b.type === 'text') {
|
|
19151
19196
|
// A text that isn't a string shows as the converters' placeholder,
|
|
19152
19197
|
// rather than sending the whole array to the JSON fallback.
|
|
19153
|
-
|
|
19198
|
+
// Well formed, as the stored copy's text is (nonErrorResultText).
|
|
19199
|
+
let text = typeof b.text === 'string' ? b.text.toWellFormed() : NON_STRING_TEXT_PLACEHOLDER;
|
|
19154
19200
|
if (maxChars && text.length > maxChars) {
|
|
19155
19201
|
text = text.slice(0, toolResultCutAt(text, maxChars))
|
|
19156
19202
|
+ '\n\n[truncated — original was ' + text.length + ' chars'
|
|
@@ -25669,11 +25715,15 @@ export class AgentFramework {
|
|
|
25669
25715
|
// the round's calls, finished or not.
|
|
25670
25716
|
const { cap, bound } = this.resolveToolResultInlineCap(agent);
|
|
25671
25717
|
const share = toolResultShare(cap, bound, state.pending.size + state.completed.length);
|
|
25718
|
+
const dateLabel = new Date().toISOString().slice(0, 10);
|
|
25672
25719
|
for (let i = state.completed.length - 1; i >= 0 && collecting(); i--) {
|
|
25673
25720
|
const tc = state.completed[i];
|
|
25674
25721
|
const { text, shown } = this.storedToolResultText(agentName, tc, cap);
|
|
25675
25722
|
const alone = storedImageSlots(text, cap, shown, bound);
|
|
25676
|
-
|
|
25723
|
+
// By its share, with the notice the commit's cut would carry, so a
|
|
25724
|
+
// result no longer than its notice counts as kept whole, as the
|
|
25725
|
+
// commit stores it.
|
|
25726
|
+
const byShare = storedImageSlots(text, share.cap, shown, share.bound, this.spillNoticeLength(`${dateLabel}-${tc.id}`, text, shown, share.bound, cap));
|
|
25677
25727
|
if (alone.length !== byShare.length || alone.some((slot, j) => slot.text !== byShare[j].text)) {
|
|
25678
25728
|
this.toolRoundsByShare.add(state.completed);
|
|
25679
25729
|
}
|