@animalabs/agent-framework 0.22.0 → 0.22.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.
Files changed (147) hide show
  1. package/README.md +11 -4
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +3 -1
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/api/server.d.ts.map +1 -1
  6. package/dist/src/api/server.js +41 -4
  7. package/dist/src/api/server.js.map +1 -1
  8. package/dist/src/framework.d.ts +319 -20
  9. package/dist/src/framework.d.ts.map +1 -1
  10. package/dist/src/framework.js +1500 -182
  11. package/dist/src/framework.js.map +1 -1
  12. package/dist/src/index.d.ts +2 -2
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/mcpl/index.d.ts +2 -0
  16. package/dist/src/mcpl/index.d.ts.map +1 -1
  17. package/dist/src/mcpl/index.js.map +1 -1
  18. package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
  19. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  20. package/dist/src/mcpl/reference-fetcher.js +20 -9
  21. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  22. package/dist/src/mcpl/references.d.ts +29 -6
  23. package/dist/src/mcpl/references.d.ts.map +1 -1
  24. package/dist/src/mcpl/references.js +57 -16
  25. package/dist/src/mcpl/references.js.map +1 -1
  26. package/dist/src/mcpl/server-connection.d.ts +28 -1
  27. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  28. package/dist/src/mcpl/server-connection.js +32 -4
  29. package/dist/src/mcpl/server-connection.js.map +1 -1
  30. package/dist/src/module-registry.d.ts +9 -3
  31. package/dist/src/module-registry.d.ts.map +1 -1
  32. package/dist/src/module-registry.js +3 -2
  33. package/dist/src/module-registry.js.map +1 -1
  34. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  35. package/dist/src/modules/workspace/index.js +46 -4
  36. package/dist/src/modules/workspace/index.js.map +1 -1
  37. package/dist/src/modules/workspace/observe.d.ts +13 -3
  38. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  39. package/dist/src/modules/workspace/observe.js +24 -4
  40. package/dist/src/modules/workspace/observe.js.map +1 -1
  41. package/dist/src/modules/workspace/reconcile.d.ts +14 -0
  42. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  43. package/dist/src/modules/workspace/reconcile.js +10 -2
  44. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  45. package/dist/src/operator-log.d.ts +41 -0
  46. package/dist/src/operator-log.d.ts.map +1 -1
  47. package/dist/src/operator-log.js.map +1 -1
  48. package/dist/src/provider-waits.d.ts +216 -0
  49. package/dist/src/provider-waits.d.ts.map +1 -0
  50. package/dist/src/provider-waits.js +482 -0
  51. package/dist/src/provider-waits.js.map +1 -0
  52. package/dist/src/refusal-reactions.d.ts +14 -12
  53. package/dist/src/refusal-reactions.d.ts.map +1 -1
  54. package/dist/src/refusal-reactions.js +14 -12
  55. package/dist/src/refusal-reactions.js.map +1 -1
  56. package/dist/src/tool-result-guard.d.ts +30 -13
  57. package/dist/src/tool-result-guard.d.ts.map +1 -1
  58. package/dist/src/tool-result-guard.js +71 -25
  59. package/dist/src/tool-result-guard.js.map +1 -1
  60. package/dist/src/types/events.d.ts +12 -0
  61. package/dist/src/types/events.d.ts.map +1 -1
  62. package/dist/src/types/module.d.ts +12 -4
  63. package/dist/src/types/module.d.ts.map +1 -1
  64. package/dist/src/types/trace.d.ts +14 -3
  65. package/dist/src/types/trace.d.ts.map +1 -1
  66. package/dist/test/discord-awareness-outbox.test.js +3 -0
  67. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  68. package/dist/test/explicit-prose-routing.test.js +0 -2
  69. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  70. package/dist/test/framework.test.js +160 -1
  71. package/dist/test/framework.test.js.map +1 -1
  72. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  73. package/dist/test/helpers/coalescing-fixture.js +3 -2
  74. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  75. package/dist/test/inference-exhaustion-rewind.test.js +142 -14
  76. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  77. package/dist/test/inference-failure-bounds.test.d.ts +2 -0
  78. package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
  79. package/dist/test/inference-failure-bounds.test.js +1201 -0
  80. package/dist/test/inference-failure-bounds.test.js.map +1 -0
  81. package/dist/test/late-events-after-stop.test.d.ts +2 -0
  82. package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
  83. package/dist/test/late-events-after-stop.test.js +122 -0
  84. package/dist/test/late-events-after-stop.test.js.map +1 -0
  85. package/dist/test/mcpl-awareness-barrier.test.js +141 -0
  86. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  87. package/dist/test/mcpl-coalescing.test.js +78 -16
  88. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  89. package/dist/test/mcpl-config-diagnostics.test.js +6 -3
  90. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
  91. package/dist/test/mcpl-manifest-changed.test.js +2 -1
  92. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  93. package/dist/test/mcpl-reference-fetcher.test.js +232 -4
  94. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  95. package/dist/test/mcpl-reference-fields.test.js +69 -2
  96. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  97. package/dist/test/mcpl-ws-transport.test.js +103 -0
  98. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  99. package/dist/test/ops-alert.test.js +2 -1
  100. package/dist/test/ops-alert.test.js.map +1 -1
  101. package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
  102. package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
  103. package/dist/test/placed-reaction-baseline.test.js +312 -0
  104. package/dist/test/placed-reaction-baseline.test.js.map +1 -0
  105. package/dist/test/provider-acceleration-cooldown.test.js +5 -1
  106. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  107. package/dist/test/provider-waits.test.d.ts +2 -0
  108. package/dist/test/provider-waits.test.d.ts.map +1 -0
  109. package/dist/test/provider-waits.test.js +2196 -0
  110. package/dist/test/provider-waits.test.js.map +1 -0
  111. package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
  112. package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
  113. package/dist/test/reaction-baseline-platform.test.js +124 -0
  114. package/dist/test/reaction-baseline-platform.test.js.map +1 -0
  115. package/dist/test/refusal-reaction-baseline.test.js +10 -3
  116. package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
  117. package/dist/test/save-recent-image.test.js +138 -2
  118. package/dist/test/save-recent-image.test.js.map +1 -1
  119. package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
  120. package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
  121. package/dist/test/surgery-marks-receipt.test.js +540 -0
  122. package/dist/test/surgery-marks-receipt.test.js.map +1 -0
  123. package/dist/test/tool-result-guard.test.js +221 -7
  124. package/dist/test/tool-result-guard.test.js.map +1 -1
  125. package/dist/test/workspace-three-way.test.js +115 -0
  126. package/dist/test/workspace-three-way.test.js.map +1 -1
  127. package/dist/tsconfig.tsbuildinfo +1 -1
  128. package/package.json +1 -1
  129. package/src/agent.ts +4 -1
  130. package/src/api/server.ts +43 -4
  131. package/src/framework.ts +1538 -189
  132. package/src/index.ts +7 -2
  133. package/src/mcpl/index.ts +2 -0
  134. package/src/mcpl/reference-fetcher.ts +18 -8
  135. package/src/mcpl/references.ts +57 -13
  136. package/src/mcpl/server-connection.ts +55 -4
  137. package/src/module-registry.ts +17 -9
  138. package/src/modules/workspace/index.ts +44 -4
  139. package/src/modules/workspace/observe.ts +31 -5
  140. package/src/modules/workspace/reconcile.ts +23 -2
  141. package/src/operator-log.ts +30 -0
  142. package/src/provider-waits.ts +523 -0
  143. package/src/refusal-reactions.ts +14 -12
  144. package/src/tool-result-guard.ts +72 -24
  145. package/src/types/events.ts +12 -0
  146. package/src/types/module.ts +12 -4
  147. package/src/types/trace.ts +14 -3
@@ -4,6 +4,8 @@ import type { ContextManager, MessageId, MessageMetadata } from '@animalabs/cont
4
4
  import type { ContentBlock, NormalizedMessage, ToolResult } from '@animalabs/membrane';
5
5
  import type { CompletedToolCall } from './types/index.js';
6
6
  import { isStateExistsError } from './module-registry.js';
7
+ import { toolResultDataToHistoryString } from './tool-result-history.js';
8
+ import { safeSlice } from './safe-slice.js';
7
9
 
8
10
  export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
9
11
  export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
@@ -52,31 +54,60 @@ export interface WithheldHost {
52
54
 
53
55
  /**
54
56
  * The stub a withheld result settles to: #159's neutral notice, then where
55
- * the original is. It names no refusal and no category (#159 keeps those in
56
- * operational logs), only the place, so reading it back is the agent's
57
- * deliberate choice (agent-framework #277). The place holds the result as
58
- * stored, which for one over the inline cap is its capped text, whose own
59
- * note names the file with the whole output; so the stub says "its result",
60
- * not its full result. A failed write's own error text
61
- * stays in the audit record and the logs, not here. The audit
62
- * record is said to keep the original only when it was saved: a staged
63
- * record whose save failed is still queued in memory, and a restart before
64
- * its retry loses it.
57
+ * the original is, then, when its tool marked any, the fields kept from its
58
+ * result (`kept`, from keptWhenWithheld). It names no refusal and no
59
+ * category (#159 keeps those in operational logs), so reading the rest back
60
+ * is the agent's deliberate choice (agent-framework #277). The place holds
61
+ * the result as stored, which for one over the inline cap is its capped
62
+ * text, whose own note names the file with the whole output; so the stub
63
+ * says "its result", not its full result. A failed write's own error text
64
+ * stays in the audit record and the logs, not here. The audit record is
65
+ * said to keep the original only when it was saved: a staged record whose
66
+ * save failed is still queued in memory, and a restart before its retry
67
+ * loses it.
65
68
  */
66
- export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean): string {
69
+ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string): string {
67
70
  const audit = `the guard's audit record (${TOOL_RESULT_GUARD_AUDIT_STATE}), for an operator`;
68
- const kept = (only: string) => auditDurable
71
+ const keptIn = (only: string) => auditDurable
69
72
  ? `is kept ${only}in ${audit}`
70
73
  : `went ${only}to ${audit}, though saving that record had failed when the result arrived`;
71
- if (!spill || 'skipped' in spill) return `${TOOL_RESULT_GUARD_NOTICE} Its result ${kept('')}.`;
72
- if (spill.unfinished) {
73
- return `${TOOL_RESULT_GUARD_NOTICE} Its result was still being written to workspace file ${spill.path} ` +
74
- `when this note was made; if that file is missing, it ${kept('')}.`;
75
- }
76
- if (spill.error === undefined) {
77
- return `${TOOL_RESULT_GUARD_NOTICE} Its result is in workspace file ${spill.path}.`;
74
+ const place = !spill || 'skipped' in spill
75
+ ? `Its result ${keptIn('')}.`
76
+ : spill.unfinished
77
+ ? `Its result was still being written to workspace file ${spill.path} when this note was made; ` +
78
+ `if that file is missing, it ${keptIn('')}.`
79
+ : spill.error === undefined
80
+ ? `Its result is in workspace file ${spill.path}.`
81
+ : `Writing its result to workspace file ${spill.path} failed, so it ${keptIn('only ')}.`;
82
+ const fields = kept === undefined ? '' : ` Kept from its result: ${kept}`;
83
+ return `${TOOL_RESULT_GUARD_NOTICE} ${place}${fields}`;
84
+ }
85
+
86
+ /**
87
+ * What a withheld result's stub keeps of it: the fields of its data that its
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
93
+ * list is evidence of what it did, rarely what drew a refusal, and keeping it
94
+ * is what shows a clobbered file when the receipt is withheld.
95
+ */
96
+ export function keptWhenWithheld(result: CompletedToolCall['result'], maxChars: number | undefined): string | undefined {
97
+ const { data, keepWhenWithheld: keys } = result;
98
+ if (!result.success || result.isError || !keys?.length || !data || typeof data !== 'object' || Array.isArray(data)) {
99
+ return undefined;
78
100
  }
79
- return `${TOOL_RESULT_GUARD_NOTICE} Writing its result to workspace file ${spill.path} failed, so it ${kept('only ')}.`;
101
+ const fields = data as Record<string, unknown>;
102
+ const kept = Object.fromEntries(keys.filter((key) => Object.hasOwn(fields, key)).map((key) => [key, fields[key]]));
103
+ if (Object.keys(kept).length === 0) return undefined;
104
+ const whole = toolResultDataToHistoryString(kept);
105
+ if (!maxChars || whole.length <= maxChars) return whole;
106
+ // Never half a surrogate pair: the stub goes to the provider as text, so
107
+ // the cut can be one character short of the cap.
108
+ const head = safeSlice(whole, 0, maxChars);
109
+ return `${head} [cut at the inline cap: ${head.length} of these fields' ${whole.length} ` +
110
+ 'characters; the whole result is where this note says]';
80
111
  }
81
112
 
82
113
  interface PendingBatch {
@@ -89,6 +120,9 @@ interface PendingBatch {
89
120
  submitted: boolean;
90
121
  /** False when the audit sync failed: originals then never go on the wire. */
91
122
  durable: boolean;
123
+ /** What each result's stub keeps if the batch is withheld, by its index in
124
+ * content: the fields its tool marked (keptWhenWithheld). */
125
+ kept: Map<number, string>;
92
126
  }
93
127
 
94
128
  interface AuditOperation {
@@ -280,11 +314,15 @@ export class ToolResultGuard {
280
314
  : JSON.parse(json);
281
315
  }
282
316
 
317
+ /** `inlineCap` is the cap the caller stored `content` under; what a stub
318
+ * keeps of a withheld result is cut at it too. */
283
319
  storeResults(
284
320
  content: ContentBlock[],
285
321
  wireResults: ToolResult[],
286
322
  originals: CompletedToolCall[],
287
- metadata?: MessageMetadata,
323
+ metadata: MessageMetadata | undefined,
324
+ /** The cap the results were stored under: what a stub keeps is cut there too. */
325
+ inlineCap: number | undefined,
288
326
  ): MessageId {
289
327
  if (!this.enabled) return this.cm.addMessage('user', content, metadata);
290
328
  if (this.pending) {
@@ -312,13 +350,22 @@ export class ToolResultGuard {
312
350
  const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
313
351
  ? { type: 'tool_result', toolUseId: block.toolUseId, content: TOOL_RESULT_GUARD_NOTICE, isError: block.isError }
314
352
  : block);
353
+ // What each stub keeps if the batch is withheld, matched to its tool's
354
+ // result by tool use id, keyed by index as the stubs and files are.
355
+ const results = new Map(originals.map((call) => [call.id, call.result]));
356
+ const kept = new Map<number, string>();
357
+ for (const [index, block] of content.entries()) {
358
+ const result = block.type === 'tool_result' ? results.get(block.toolUseId) : undefined;
359
+ const text = result ? keptWhenWithheld(result, inlineCap) : undefined;
360
+ if (text !== undefined) kept.set(index, text);
361
+ }
315
362
  // Compression hold (CM >= 0.12): placed BEFORE onNewMessage fires, so no
316
363
  // chunk is ever summarized from the placeholder; released after the
317
364
  // settlement edit. Holds are in-memory only — a reopened manager has none,
318
365
  // which is correct since a reopened guard has no pending batch.
319
366
  const messageId = this.cm.addMessage('user', withheld, metadata, undefined, { holdCompression: true });
320
367
  this.pending = { id, messageId, content, withheld, wireResults,
321
- branch: this.cm.currentBranch().name, submitted: false, durable: false };
368
+ branch: this.cm.currentBranch().name, submitted: false, durable: false, kept };
322
369
  // Durability barrier: the audit must reach Chronicle's chain heads before
323
370
  // the originals can go to a provider. On a failed sync the batch fails
324
371
  // CLOSED: the placeholders go on the wire instead (the turn continues,
@@ -452,7 +499,8 @@ export class ToolResultGuard {
452
499
  /**
453
500
  * Turn a withheld batch's notices into stubs that say where each original
454
501
  * is (agent-framework #277): written to the workspace when a writable mount
455
- * takes it, else kept in the audit record. Its own unedited placeholder is
502
+ * takes it, else kept in the audit record. A stub also keeps the file
503
+ * lists its tool marked. Its own unedited placeholder is
456
504
  * the only thing it edits: an operator's later edit, removal or rollback
457
505
  * stands.
458
506
  *
@@ -540,7 +588,7 @@ export class ToolResultGuard {
540
588
  spills.set(index, spill);
541
589
  }
542
590
  const annotated: ContentBlock[] = pending.withheld.map((block, index) => block.type === 'tool_result'
543
- ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable) }
591
+ ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index)) }
544
592
  : block);
545
593
  try {
546
594
  this.append({
@@ -199,6 +199,18 @@ export interface ToolResult {
199
199
  * Adapted from Anarchid/agent-framework@mcpl-module-proto.
200
200
  */
201
201
  endTurn?: boolean;
202
+ /**
203
+ * Fields of `data` that say what the tool acted on, such as the paths a
204
+ * write wrote, removed or took from disk, the path it deleted, or a mount
205
+ * whose root it accepted, in the order they should be kept. When the
206
+ * tool-result guard withholds this result, these fields stay in its stub,
207
+ * so the agent still sees what the tool did; the result as stored goes
208
+ * where every withheld original goes (agent-framework #277). Past the
209
+ * inline cap they're cut, in this order, so a later field can be cut away
210
+ * entirely; put the short, telling ones first. Only data's own fields can
211
+ * be kept, and a failed result (success false) keeps nothing.
212
+ */
213
+ keepWhenWithheld?: string[];
202
214
  }
203
215
 
204
216
  /**
@@ -217,14 +217,19 @@ export interface ModuleContext {
217
217
  getLogLength(name: string): number;
218
218
 
219
219
  /**
220
- * Process queue for pushing events from external listeners.
220
+ * Process queue for pushing events from external listeners. Once stop()
221
+ * has begun, a push is dropped with a log line naming this module, as
222
+ * pushEvent()'s is.
221
223
  * @deprecated Use pushEvent() instead — it also emits process:received traces.
222
224
  */
223
225
  readonly queue: ProcessQueue;
224
226
 
225
227
  /**
226
228
  * Push a process event to the queue and emit a process:received trace.
227
- * Preferred over direct queue.push() for proper observability.
229
+ * Preferred over direct queue.push() for proper observability. Once stop()
230
+ * has begun, nothing will process the event: it's dropped with a log line
231
+ * naming this module, untraced, rather than thrown into the listener or
232
+ * stop() that pushed it.
228
233
  */
229
234
  pushEvent(event: ProcessEvent): void;
230
235
 
@@ -370,14 +375,17 @@ export interface ProcessState {
370
375
  getActiveTools(): ToolDefinition[];
371
376
 
372
377
  /**
373
- * Queue for emitting follow-up events.
378
+ * Queue for emitting follow-up events. Once stop() has begun, a push is
379
+ * dropped with a log line naming the module, as pushEvent()'s is.
374
380
  * @deprecated Use pushEvent() instead — it also emits process:received traces.
375
381
  */
376
382
  readonly queue: ProcessQueue;
377
383
 
378
384
  /**
379
385
  * Push a process event to the queue and emit a process:received trace.
380
- * Preferred over direct queue.push() for proper observability.
386
+ * Preferred over direct queue.push() for proper observability. Once stop()
387
+ * has begun, the event is dropped with a log line naming the module,
388
+ * untraced.
381
389
  */
382
390
  pushEvent(event: ProcessEvent): void;
383
391
  }
@@ -66,20 +66,29 @@ export type TraceEvent =
66
66
  | (TraceEventBase & {
67
67
  type: 'inference:failed';
68
68
  agentName: string;
69
+ /** The failure's message, bounded as every trace event's `error` is (2,000 characters). */
69
70
  error: string;
71
+ /** Bounded like `error`. */
70
72
  stack?: string;
71
73
  })
72
74
  | (TraceEventBase & {
73
75
  type: 'inference:exhausted';
74
76
  agentName: string;
77
+ /** The failure's message, bounded as every trace event's `error` is (2,000 characters). */
75
78
  error: string;
76
79
  /** Whether membrane classified the failure as retryable (observability). */
77
80
  retryable?: boolean;
78
81
  /**
79
- * The membrane error type (e.g. 'invalid_request', 'auth', 'context_length').
80
- * The poison-history breaker fires only on 'invalid_request'.
82
+ * The membrane error type (e.g. 'invalid_request', 'auth', 'context_length'),
83
+ * at most 128 characters. The poison-history breaker fires only on
84
+ * 'invalid_request' at HTTP 400, 413 or 422 or with no status, and on
85
+ * 'context_length' at 413 (shedMayHelp).
81
86
  */
82
87
  errorType?: string;
88
+ /** The provider's HTTP status, when the failure carried one. */
89
+ httpStatus?: number;
90
+ /** The provider's own error code, when it sent one (at most 128 characters). */
91
+ providerErrorCode?: string;
83
92
  })
84
93
 
85
94
  // Streaming inference lifecycle
@@ -534,8 +543,10 @@ export type TraceEvent =
534
543
  kind: string;
535
544
  /** Agent name, or a server/component id for non-agent alerts (e.g. mcpl-down). */
536
545
  agentName: string;
546
+ /** At most 2,000 characters. */
537
547
  message: string;
538
- /** Kind-specific structured payload (mirrors the failures.log record). */
548
+ /** Kind-specific structured payload (mirrors the failures.log record),
549
+ * bounded field by field as that record is. */
539
550
  data?: Record<string, unknown>;
540
551
  })
541
552