@animalabs/agent-framework 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/src/agent.d.ts +5 -0
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +27 -8
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/clock-rows.d.ts +115 -0
  6. package/dist/src/clock-rows.d.ts.map +1 -0
  7. package/dist/src/clock-rows.js +222 -0
  8. package/dist/src/clock-rows.js.map +1 -0
  9. package/dist/src/framework.d.ts +98 -6
  10. package/dist/src/framework.d.ts.map +1 -1
  11. package/dist/src/framework.js +514 -80
  12. package/dist/src/framework.js.map +1 -1
  13. package/dist/src/index.d.ts +2 -0
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +1 -0
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/mcpl/channel-registry.d.ts +9 -0
  18. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  19. package/dist/src/mcpl/channel-registry.js +49 -6
  20. package/dist/src/mcpl/channel-registry.js.map +1 -1
  21. package/dist/src/mcpl/protocol-family.js +1 -1
  22. package/dist/src/mcpl/references.d.ts +4 -1
  23. package/dist/src/mcpl/references.d.ts.map +1 -1
  24. package/dist/src/mcpl/references.js +5 -2
  25. package/dist/src/mcpl/references.js.map +1 -1
  26. package/dist/src/modules/workspace/index.d.ts +9 -1
  27. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  28. package/dist/src/modules/workspace/index.js +118 -28
  29. package/dist/src/modules/workspace/index.js.map +1 -1
  30. package/dist/src/modules/workspace/types.d.ts +6 -2
  31. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  32. package/dist/src/modules/workspace/types.js.map +1 -1
  33. package/dist/src/text-as-stored.d.ts +31 -0
  34. package/dist/src/text-as-stored.d.ts.map +1 -0
  35. package/dist/src/text-as-stored.js +70 -0
  36. package/dist/src/text-as-stored.js.map +1 -0
  37. package/dist/src/timezone.d.ts +4 -2
  38. package/dist/src/timezone.d.ts.map +1 -1
  39. package/dist/src/timezone.js +4 -2
  40. package/dist/src/timezone.js.map +1 -1
  41. package/dist/src/tool-image-ledger.d.ts +46 -7
  42. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  43. package/dist/src/tool-image-ledger.js +60 -7
  44. package/dist/src/tool-image-ledger.js.map +1 -1
  45. package/dist/src/tool-presentation.d.ts.map +1 -1
  46. package/dist/src/tool-presentation.js +9 -1
  47. package/dist/src/tool-presentation.js.map +1 -1
  48. package/dist/src/tool-result-guard.d.ts +10 -8
  49. package/dist/src/tool-result-guard.d.ts.map +1 -1
  50. package/dist/src/tool-result-guard.js +12 -10
  51. package/dist/src/tool-result-guard.js.map +1 -1
  52. package/dist/src/tool-result-history.d.ts +17 -6
  53. package/dist/src/tool-result-history.d.ts.map +1 -1
  54. package/dist/src/tool-result-history.js +35 -10
  55. package/dist/src/tool-result-history.js.map +1 -1
  56. package/dist/src/tune-out/coordinator.js +3 -3
  57. package/dist/src/tune-out/coordinator.js.map +1 -1
  58. package/dist/src/types/agent.d.ts +27 -0
  59. package/dist/src/types/agent.d.ts.map +1 -1
  60. package/dist/src/types/events.d.ts +13 -3
  61. package/dist/src/types/events.d.ts.map +1 -1
  62. package/dist/src/types/framework.d.ts +7 -4
  63. package/dist/src/types/framework.d.ts.map +1 -1
  64. package/package.json +3 -3
  65. package/src/agent.ts +27 -8
  66. package/src/clock-rows.ts +302 -0
  67. package/src/framework.ts +538 -85
  68. package/src/index.ts +2 -0
  69. package/src/mcpl/channel-registry.ts +54 -8
  70. package/src/mcpl/protocol-family.ts +1 -1
  71. package/src/mcpl/references.ts +5 -2
  72. package/src/modules/workspace/index.ts +121 -29
  73. package/src/modules/workspace/types.ts +6 -2
  74. package/src/text-as-stored.ts +73 -0
  75. package/src/timezone.ts +4 -2
  76. package/src/tool-image-ledger.ts +62 -8
  77. package/src/tool-presentation.ts +8 -1
  78. package/src/tool-result-guard.ts +12 -10
  79. package/src/tool-result-history.ts +35 -9
  80. package/src/tune-out/coordinator.ts +3 -3
  81. package/src/types/agent.ts +28 -0
  82. package/src/types/events.ts +13 -3
  83. package/src/types/framework.ts +7 -4
@@ -85,7 +85,8 @@ function quotedList(names: string[], conjunction: 'and' | 'or' = 'and', quote: (
85
85
 
86
86
  /**
87
87
  * Every problem that keeps a document from being used, each by where it is;
88
- * none for a usable one. A defaults file (`descriptionsOnly`) may set
88
+ * none for a usable one. An object with no keys never reaches this:
89
+ * parseDocument() reads it as the empty document first. A defaults file (`descriptionsOnly`) may set
89
90
  * descriptions only, since visibility is the resident's to choose.
90
91
  */
91
92
  function documentProblems(doc: unknown, descriptionsOnly: boolean): string[] {
@@ -125,6 +126,12 @@ function parseDocument(raw: string, descriptionsOnly: boolean): PresentationDocu
125
126
  let doc: unknown;
126
127
  try { doc = JSON.parse(raw); }
127
128
  catch (error) { throw new PresentationFileError([`it isn't valid JSON: ${errorMessage(error)}`]); }
129
+ // An object with no keys carries no settings, so it's the empty document,
130
+ // as a missing resident file is: it's what an operator writes by hand to
131
+ // start one. A missing defaults file is still reported, by read().
132
+ // A 0-byte file isn't JSON and still refuses, since the store's
133
+ // temp-and-rename never writes one: that's what a lost file looks like.
134
+ if (doc && typeof doc === 'object' && !Array.isArray(doc) && Object.keys(doc).length === 0) return { version: 1, tools: {} };
128
135
  const problems = documentProblems(doc, descriptionsOnly);
129
136
  if (problems.length) throw new PresentationFileError(problems);
130
137
  return doc as PresentationDocument;
@@ -86,10 +86,11 @@ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean
86
86
  /**
87
87
  * What a withheld result's stub keeps of it: the fields of its data that its
88
88
  * tool marked as saying what it acted on (ToolResult.keepWhenWithheld), in
89
- * the order marked, serialized as a stored result is. Past the inline cap
90
- * they're cut, with a note that says so and where the whole result is, since
91
- * a later field can then be cut away entirely. Undefined when it keeps
92
- * nothing, as for a failed result (agent-framework #277). A write's file
89
+ * the order marked, serialized as a stored result is. Past `maxChars`, the
90
+ * inline cap or less (storeResults' keptCap), they're cut, with a note that
91
+ * says so and where the whole result is, since a later field can then be cut
92
+ * away entirely. Undefined when it keeps nothing, as for a failed result
93
+ * (agent-framework #277). A write's file
93
94
  * list is evidence of what it did, rarely what drew a refusal, and keeping it
94
95
  * is what shows a clobbered file when the receipt is withheld.
95
96
  */
@@ -109,7 +110,7 @@ export function keptWhenWithheld(result: CompletedToolCall['result'], maxChars:
109
110
  // Never half a surrogate pair: the stub goes to the provider as text, so
110
111
  // the cut can be one character short of the cap.
111
112
  const head = safeSlice(whole, 0, maxChars);
112
- return `${head} [cut at the inline cap: ${head.length} of these fields' ${whole.length} ` +
113
+ return `${head} [cut: ${head.length} of these fields' ${whole.length} ` +
113
114
  'characters; the whole result is where this note says]';
114
115
  }
115
116
 
@@ -313,15 +314,16 @@ export class ToolResultGuard {
313
314
  : JSON.parse(json);
314
315
  }
315
316
 
316
- /** `inlineCap` is the cap the caller stored `content` under; what a stub
317
- * keeps of a withheld result is cut at it too. */
317
+ /** `keptCap` is where what a stub keeps of a withheld result is cut: the
318
+ * cap the caller stored `content` under, less the stub's notice room where
319
+ * its message's bound sets it (toolResultShare, tool-image-ledger.ts). */
318
320
  storeResults(
319
321
  content: ContentBlock[],
320
322
  wireResults: ToolResult[],
321
323
  originals: CompletedToolCall[],
322
324
  metadata: MessageMetadata | undefined,
323
- /** The cap the results were stored under: what a stub keeps is cut there too. */
324
- inlineCap: number | undefined,
325
+ /** Where what a stub keeps is cut. */
326
+ keptCap: number | undefined,
325
327
  ): MessageId {
326
328
  if (!this.enabled) return this.cm.addMessage('user', content, metadata);
327
329
  if (this.pending) {
@@ -355,7 +357,7 @@ export class ToolResultGuard {
355
357
  const kept = new Map<number, string>();
356
358
  for (const [index, block] of content.entries()) {
357
359
  const result = block.type === 'tool_result' ? results.get(block.toolUseId) : undefined;
358
- const text = result ? keptWhenWithheld(result, inlineCap) : undefined;
360
+ const text = result ? keptWhenWithheld(result, keptCap) : undefined;
359
361
  if (text !== undefined) kept.set(index, text);
360
362
  }
361
363
  // Compression hold (CM >= 0.12): placed BEFORE onNewMessage fires, so no
@@ -9,12 +9,14 @@
9
9
  * `maxChars` slice and re-introduces, one turn deferred, exactly the silent
10
10
  * hallucination this feature exists to kill.
11
11
  *
12
- * For MCP-shaped content arrays, this helper keeps text blocks verbatim and
12
+ * For MCP-shaped content arrays, this helper keeps text blocks verbatim,
13
+ * apart from a lone UTF-16 surrogate, which becomes U+FFFD (#695), and
13
14
  * replaces image blocks with a short `[image: mimeType, ~NKB]` placeholder.
14
- * For anything else it falls back to JSON, written by RFC-005's rules at
15
- * every depth (historyJson): a reference is shown as its stub wherever it
16
- * sits in the data, so its uri never reaches history, and inline data that
17
- * claims a uri or a disposition is withheld.
15
+ * A string is already text and is kept as it is. For anything else it falls
16
+ * back to JSON, written by RFC-005's rules at every depth (historyJson): a
17
+ * reference is shown as its stub wherever it sits in the data, so its uri
18
+ * never reaches history, and inline data that claims a uri or a disposition
19
+ * is withheld.
18
20
  */
19
21
 
20
22
  import { INLINE_WITHHELD_TEXT, isInlineContradiction, referenceStubOrNull } from './mcpl/references.js';
@@ -72,9 +74,19 @@ export function toolResultDataToHistoryString(
72
74
  options?: HistorySerializeOptions,
73
75
  ): string {
74
76
  const fromArray = tryHistoryStringFromContentArray(data, options);
77
+ // A string is the text the tool means to say, kept as it is: JSON would
78
+ // quote it and escape every newline, so a multi-line result would reach
79
+ // the model, a script and its spill file as one escaped line. Kept well
80
+ // formed: a lone UTF-16 surrogate, which JSON escaped, isn't text, a
81
+ // provider can refuse a request that carries one, and UTF-8 can't store
82
+ // it in a spill file, so it reads as U+FFFD.
75
83
  // JSON leaves out undefined, a function and a symbol: a result with no data
76
84
  // is stored as no text, as an empty content array is.
77
- const str = fromArray ?? historyJson(data) ?? '';
85
+ // RFC-005 shows only objects otherwise (shownInHistory), so a string has
86
+ // nothing for historyJson to stub or withhold.
87
+ const str = fromArray
88
+ ?? (typeof data === 'string' ? data.toWellFormed() : historyJson(data))
89
+ ?? '';
78
90
  return maxChars ? truncateForHistory(str, maxChars) : str;
79
91
  }
80
92
 
@@ -86,6 +98,14 @@ export function toolResultDataToHistoryString(
86
98
  *
87
99
  * Each field is read once: the direct Agent API passes its caller's own
88
100
  * result here, unchecked, and a getter needn't answer the same way twice.
101
+ *
102
+ * Each text a tool result's copies carry is made well formed where it's
103
+ * taken from the result: a lone UTF-16 surrogate becomes U+FFFD, as the
104
+ * context store makes it when it writes, so the copy sent to membrane and the
105
+ * copy stored carry the same text (#695). Other data goes through JSON,
106
+ * which escapes a lone half. toWellFormed() keeps the length,
107
+ * so the cut, the spans and the slots are computed from the well-formed text
108
+ * as before; a cut at a lone high half no longer steps back over it.
89
109
  */
90
110
  export function nonErrorResultText(
91
111
  result: { data?: unknown; error?: string },
@@ -95,13 +115,17 @@ export function nonErrorResultText(
95
115
  const data = result.data;
96
116
  if (data === undefined || data === null) {
97
117
  const error = result.error;
98
- if (typeof error === 'string' && error) return maxChars ? truncateForHistory(error, maxChars) : error;
118
+ if (typeof error === 'string' && error) {
119
+ const text = error.toWellFormed();
120
+ return maxChars ? truncateForHistory(text, maxChars) : text;
121
+ }
99
122
  }
100
123
  return toolResultDataToHistoryString(data, maxChars, options);
101
124
  }
102
125
 
103
126
  /** Bounded copy of an arbitrary string with the standard truncation notice,
104
- * cut at the cap as a tool result's stored copy is (toolResultCutAt), so it
127
+ * cut at the cap where a tool result's stored copy is cut at that cap
128
+ * (toolResultCutAt), so it
105
129
  * never ends in part of a slot. Each of its callers' readers sees this
106
130
  * very text, never the images behind a placeholder, so nothing past the cut
107
131
  * is kept for save_recent_image's index (#403). The framework stores a tool
@@ -127,7 +151,9 @@ function tryHistoryStringFromContentArray(data: unknown, options?: HistorySerial
127
151
  if (b.type === 'text') {
128
152
  // A text that isn't a string shows as the converters' placeholder,
129
153
  // rather than sending the whole array to the JSON fallback.
130
- parts.push({ kind: 'text', text: typeof b.text === 'string' ? b.text : NON_STRING_TEXT_PLACEHOLDER });
154
+ // Read once: a direct Agent API result is unchecked.
155
+ const text = b.text;
156
+ parts.push({ kind: 'text', text: typeof text === 'string' ? text.toWellFormed() : NON_STRING_TEXT_PLACEHOLDER });
131
157
  } else if (isInlineContradiction(b)) {
132
158
  // RFC-005 vector 2: checked before the data branch so inline data
133
159
  // claiming bulk disposition is withheld, not rendered.
@@ -290,9 +290,9 @@ export class TuneOutCoordinator {
290
290
  startedAtSequence: this.hooks.currentSequence(),
291
291
  ...(expiresAtMs !== undefined ? { expiresAtMs } : {}),
292
292
  };
293
- // A tune-out can end in the registry without cancel (a send into the
294
- // channel or a close sets its state there), leaving its timers armed:
295
- // clear them, so none of them acts on this one.
293
+ // A tune-out that ended in the registry without this coordinator's
294
+ // cancel would have left its timers armed: clear them, so none of them
295
+ // acts on this one.
296
296
  this.clearTimers(serverId, channelId);
297
297
  this.channelRegistry.enterTuneOut(serverId, channelId, params, source);
298
298
  this.armExpiry(serverId, channelId, params);
@@ -218,6 +218,34 @@ export interface AgentConfig {
218
218
  * content-free system receipt is stored instead. Default false.
219
219
  */
220
220
  toolWrapperProseGuard?: boolean;
221
+
222
+ /**
223
+ * Clock rows (off when left out): when a boundary of this interval on the
224
+ * agent's local wall clock (counted from local midnight, in the
225
+ * framework's `timeZone`) has passed since the last clock row, the window
226
+ * takes one more — `Clock: 10:50 Europe/Berlin (UTC+02:00)`, with the date
227
+ * when the day has changed — before the next thing it takes: an incoming
228
+ * event, a turn's start, or, mid-turn, after a tool round's results, where
229
+ * it also joins the live stream. Only the latest boundary is written, and
230
+ * a window that holds no clock row yet takes one at the next of these. A
231
+ * clock reads the boundary's time where its tick would have stood, else
232
+ * the time it is written (after a tool's results, say), so it never
233
+ * reads earlier than a row before it. A clock wakes no one. See
234
+ * src/clock-rows.ts.
235
+ *
236
+ * - A stream that can't carry injected messages (membrane's XML prefill
237
+ * path) takes no mid-turn clock: the next event or turn start writes it.
238
+ * - On some models a clock after a tool's results drops the loop's
239
+ * earlier thinking from the request, as any message heard while acting
240
+ * does there: at most once an interval.
241
+ * - Residents share one window, so every resident reads the clock rows any
242
+ * of them writes, a resident with no `clock` of its own included. Each
243
+ * writes one when a boundary of its own interval has passed since the
244
+ * window's last clock, whichever resident wrote that. The subconscious
245
+ * takes none of its own: it reads the residents' through its merged
246
+ * view.
247
+ */
248
+ clock?: import('../clock-rows.js').ClockConfig;
221
249
  }
222
250
 
223
251
  /** The intentionally small set of agent settings that may change in-process. */
@@ -186,7 +186,13 @@ export interface ModuleEvent {
186
186
  export interface ToolResult {
187
187
  /** Whether the tool succeeded */
188
188
  success: boolean;
189
- /** Result data (if success) */
189
+ /**
190
+ * Result data (if success). The model reads a string as that text, an MCP
191
+ * content array whose every block the framework knows as those blocks, and
192
+ * anything else as JSON, where RFC-005 shows a reference as its stub and
193
+ * withholds inline data that claims a uri or a disposition. With no data,
194
+ * it reads the result's error text, if any.
195
+ */
190
196
  data?: unknown;
191
197
  /** Error message (if failure) */
192
198
  error?: string;
@@ -270,9 +276,13 @@ export interface ToolCall {
270
276
  /**
271
277
  * How long this call's result may be and still reach its caller whole,
272
278
  * measured as the framework stores it: an error's text, or a success's data
273
- * as JSON (a content array as its text). For a model's call it is the
279
+ * as JSON (a string or a content array as its text). For a model's call it is the
274
280
  * agent's inline cap, past which the framework spills the result to a
275
- * workspace file and sends a preview. For a call from a running
281
+ * workspace file and sends a preview. A result that long is stored whole
282
+ * unless its round's results together pass the readers' per-message bound,
283
+ * or a save_recent_image in the round counted it by its share: then each
284
+ * result of the round takes an equal share of that bound, and one past its
285
+ * share spills. For a call from a running
276
286
  * code_execution script it is the longest result that reaches the script
277
287
  * whole whatever its characters, from the script's memory: the script
278
288
  * takes an answer of up to a third of its memoryMaxBytes, in which a
@@ -117,9 +117,10 @@ export interface FrameworkConfig {
117
117
  subconscious?: import('../tune-out/tools.js').SubconsciousConfig;
118
118
 
119
119
  /**
120
- * IANA zone used only when rendering wall-clock times for the agent.
121
- * Stored/protocol timestamps remain epoch/UTC. Defaults to AGENT_TIMEZONE,
122
- * then the process zone.
120
+ * IANA zone of the agent's wall clock: it renders wall-clock times for the
121
+ * agent, and it sets where clock rows' boundaries fall (AgentConfig.clock,
122
+ * counted from local midnight here). Stored/protocol timestamps remain
123
+ * epoch/UTC. Defaults to AGENT_TIMEZONE, then the process zone.
123
124
  */
124
125
  timeZone?: string;
125
126
 
@@ -247,7 +248,9 @@ export interface FrameworkConfig {
247
248
  * takes precedence over this for that agent; the EFFECTIVE cap for every
248
249
  * source is min(desired, strategy bound) — desired values above the
249
250
  * strategy-derived bound (maxMessageTokens * 4) are hard-clamped to it,
250
- * with the clamp reported via agent_settings get.
251
+ * with the clamp reported via agent_settings get. For an ordinary resident
252
+ * the bound is the smallest among the agents that read the shared slot,
253
+ * reported as clamped by `reader-bound` when another reader's sets it.
251
254
  */
252
255
  toolResultInlineMaxChars?: number;
253
256