@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.
Files changed (45) hide show
  1. package/dist/src/agent.d.ts.map +1 -1
  2. package/dist/src/agent.js +21 -8
  3. package/dist/src/agent.js.map +1 -1
  4. package/dist/src/framework.d.ts +18 -1
  5. package/dist/src/framework.d.ts.map +1 -1
  6. package/dist/src/framework.js +87 -37
  7. package/dist/src/framework.js.map +1 -1
  8. package/dist/src/mcpl/references.d.ts +4 -1
  9. package/dist/src/mcpl/references.d.ts.map +1 -1
  10. package/dist/src/mcpl/references.js +5 -2
  11. package/dist/src/mcpl/references.js.map +1 -1
  12. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  13. package/dist/src/modules/workspace/index.js +74 -20
  14. package/dist/src/modules/workspace/index.js.map +1 -1
  15. package/dist/src/modules/workspace/types.d.ts +6 -2
  16. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  17. package/dist/src/modules/workspace/types.js.map +1 -1
  18. package/dist/src/text-as-stored.d.ts +31 -0
  19. package/dist/src/text-as-stored.d.ts.map +1 -0
  20. package/dist/src/text-as-stored.js +70 -0
  21. package/dist/src/text-as-stored.js.map +1 -0
  22. package/dist/src/tool-image-ledger.d.ts +26 -3
  23. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  24. package/dist/src/tool-image-ledger.js +31 -4
  25. package/dist/src/tool-image-ledger.js.map +1 -1
  26. package/dist/src/tool-presentation.d.ts.map +1 -1
  27. package/dist/src/tool-presentation.js +9 -1
  28. package/dist/src/tool-presentation.js.map +1 -1
  29. package/dist/src/tool-result-history.d.ts +15 -5
  30. package/dist/src/tool-result-history.d.ts.map +1 -1
  31. package/dist/src/tool-result-history.js +33 -9
  32. package/dist/src/tool-result-history.js.map +1 -1
  33. package/dist/src/types/events.d.ts +8 -2
  34. package/dist/src/types/events.d.ts.map +1 -1
  35. package/package.json +3 -3
  36. package/src/agent.ts +21 -8
  37. package/src/framework.ts +92 -36
  38. package/src/mcpl/references.ts +5 -2
  39. package/src/modules/workspace/index.ts +74 -20
  40. package/src/modules/workspace/types.ts +6 -2
  41. package/src/text-as-stored.ts +73 -0
  42. package/src/tool-image-ledger.ts +31 -4
  43. package/src/tool-presentation.ts +8 -1
  44. package/src/tool-result-history.ts +33 -8
  45. package/src/types/events.ts +8 -2
@@ -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
- return msg.participant !== agentName
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, 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);
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 path = written.path;
18815
- return {
18816
- text: head
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
- text: head
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
- text: head
18839
- + '\n\n[truncated — original was ' + content.length + ' chars; no writable workspace mount without an include, full content not retained]'
18840
- + kept,
18841
- filePath: null,
18842
- written,
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
- let text = typeof b.text === 'string' ? b.text : NON_STRING_TEXT_PLACEHOLDER;
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
- const byShare = storedImageSlots(text, share.cap, shown, share.bound);
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
  }