@animalabs/agent-framework 0.19.0 → 0.21.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 (214) hide show
  1. package/README.md +29 -0
  2. package/dist/src/agent.d.ts +22 -0
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +128 -10
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/types.d.ts +1 -1
  7. package/dist/src/api/types.d.ts.map +1 -1
  8. package/dist/src/code-execution/py-runner.d.ts +10 -1
  9. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  10. package/dist/src/code-execution/py-runner.js +27 -2
  11. package/dist/src/code-execution/py-runner.js.map +1 -1
  12. package/dist/src/code-execution/tool-definition.d.ts +21 -0
  13. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  14. package/dist/src/code-execution/tool-definition.js +34 -0
  15. package/dist/src/code-execution/tool-definition.js.map +1 -1
  16. package/dist/src/framework.d.ts +153 -2
  17. package/dist/src/framework.d.ts.map +1 -1
  18. package/dist/src/framework.js +1175 -76
  19. package/dist/src/framework.js.map +1 -1
  20. package/dist/src/index.d.ts +2 -0
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +4 -0
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/mcpl/capability-grant.d.ts +1 -1
  25. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  26. package/dist/src/mcpl/capability-grant.js +49 -5
  27. package/dist/src/mcpl/capability-grant.js.map +1 -1
  28. package/dist/src/mcpl/channel-registry.d.ts +59 -2
  29. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  30. package/dist/src/mcpl/channel-registry.js +106 -6
  31. package/dist/src/mcpl/channel-registry.js.map +1 -1
  32. package/dist/src/mcpl/conversation-router.d.ts +8 -6
  33. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  34. package/dist/src/mcpl/conversation-router.js +12 -6
  35. package/dist/src/mcpl/conversation-router.js.map +1 -1
  36. package/dist/src/mcpl/index.d.ts +3 -0
  37. package/dist/src/mcpl/index.d.ts.map +1 -1
  38. package/dist/src/mcpl/index.js +3 -0
  39. package/dist/src/mcpl/index.js.map +1 -1
  40. package/dist/src/mcpl/push-coalescer.d.ts +318 -0
  41. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -0
  42. package/dist/src/mcpl/push-coalescer.js +709 -0
  43. package/dist/src/mcpl/push-coalescer.js.map +1 -0
  44. package/dist/src/mcpl/push-handler.d.ts +7 -2
  45. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  46. package/dist/src/mcpl/push-handler.js +43 -4
  47. package/dist/src/mcpl/push-handler.js.map +1 -1
  48. package/dist/src/mcpl/references.d.ts.map +1 -1
  49. package/dist/src/mcpl/references.js +9 -4
  50. package/dist/src/mcpl/references.js.map +1 -1
  51. package/dist/src/mcpl/server-connection.d.ts +18 -0
  52. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  53. package/dist/src/mcpl/server-connection.js +44 -1
  54. package/dist/src/mcpl/server-connection.js.map +1 -1
  55. package/dist/src/mcpl/tool-classes.d.ts +71 -0
  56. package/dist/src/mcpl/tool-classes.d.ts.map +1 -0
  57. package/dist/src/mcpl/tool-classes.js +170 -0
  58. package/dist/src/mcpl/tool-classes.js.map +1 -0
  59. package/dist/src/mcpl/tool-glob.d.ts +16 -0
  60. package/dist/src/mcpl/tool-glob.d.ts.map +1 -0
  61. package/dist/src/mcpl/tool-glob.js +41 -0
  62. package/dist/src/mcpl/tool-glob.js.map +1 -0
  63. package/dist/src/mcpl/tool-lifecycle.d.ts +253 -0
  64. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -0
  65. package/dist/src/mcpl/tool-lifecycle.js +607 -0
  66. package/dist/src/mcpl/tool-lifecycle.js.map +1 -0
  67. package/dist/src/mcpl/transport.d.ts +15 -0
  68. package/dist/src/mcpl/transport.d.ts.map +1 -1
  69. package/dist/src/mcpl/transport.js +65 -1
  70. package/dist/src/mcpl/transport.js.map +1 -1
  71. package/dist/src/mcpl/types.d.ts +68 -1
  72. package/dist/src/mcpl/types.d.ts.map +1 -1
  73. package/dist/src/mcpl/types.js +6 -0
  74. package/dist/src/mcpl/types.js.map +1 -1
  75. package/dist/src/modules/discord/index.js +1 -1
  76. package/dist/src/modules/discord/index.js.map +1 -1
  77. package/dist/src/modules/history/index.d.ts +85 -0
  78. package/dist/src/modules/history/index.d.ts.map +1 -1
  79. package/dist/src/modules/history/index.js +1019 -24
  80. package/dist/src/modules/history/index.js.map +1 -1
  81. package/dist/src/modules/history/semantic.d.ts +202 -0
  82. package/dist/src/modules/history/semantic.d.ts.map +1 -0
  83. package/dist/src/modules/history/semantic.js +418 -0
  84. package/dist/src/modules/history/semantic.js.map +1 -0
  85. package/dist/src/modules/index.d.ts +2 -1
  86. package/dist/src/modules/index.d.ts.map +1 -1
  87. package/dist/src/modules/index.js.map +1 -1
  88. package/dist/src/tool-result-guard.d.ts +82 -0
  89. package/dist/src/tool-result-guard.d.ts.map +1 -0
  90. package/dist/src/tool-result-guard.js +306 -0
  91. package/dist/src/tool-result-guard.js.map +1 -0
  92. package/dist/src/types/agent.d.ts +14 -0
  93. package/dist/src/types/agent.d.ts.map +1 -1
  94. package/dist/src/types/framework.d.ts +21 -0
  95. package/dist/src/types/framework.d.ts.map +1 -1
  96. package/dist/src/types/trace.d.ts +17 -0
  97. package/dist/src/types/trace.d.ts.map +1 -1
  98. package/dist/test/channel-registry-routing.test.js +59 -0
  99. package/dist/test/channel-registry-routing.test.js.map +1 -1
  100. package/dist/test/code-execution.test.js +139 -0
  101. package/dist/test/code-execution.test.js.map +1 -1
  102. package/dist/test/conversation-router.test.js +33 -19
  103. package/dist/test/conversation-router.test.js.map +1 -1
  104. package/dist/test/conversation-routing.test.js +89 -0
  105. package/dist/test/conversation-routing.test.js.map +1 -1
  106. package/dist/test/helpers/coalescing-fixture.d.ts +72 -0
  107. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -0
  108. package/dist/test/helpers/coalescing-fixture.js +145 -0
  109. package/dist/test/helpers/coalescing-fixture.js.map +1 -0
  110. package/dist/test/helpers/coalescing-kill-child.d.ts +2 -0
  111. package/dist/test/helpers/coalescing-kill-child.d.ts.map +1 -0
  112. package/dist/test/helpers/coalescing-kill-child.js +24 -0
  113. package/dist/test/helpers/coalescing-kill-child.js.map +1 -0
  114. package/dist/test/history-module-realstore.test.d.ts +2 -0
  115. package/dist/test/history-module-realstore.test.d.ts.map +1 -0
  116. package/dist/test/history-module-realstore.test.js +363 -0
  117. package/dist/test/history-module-realstore.test.js.map +1 -0
  118. package/dist/test/history-module-semantic.test.d.ts +2 -0
  119. package/dist/test/history-module-semantic.test.d.ts.map +1 -0
  120. package/dist/test/history-module-semantic.test.js +213 -0
  121. package/dist/test/history-module-semantic.test.js.map +1 -0
  122. package/dist/test/history-module-ux.test.d.ts +2 -0
  123. package/dist/test/history-module-ux.test.d.ts.map +1 -0
  124. package/dist/test/history-module-ux.test.js +304 -0
  125. package/dist/test/history-module-ux.test.js.map +1 -0
  126. package/dist/test/history-module.test.js +25 -1
  127. package/dist/test/history-module.test.js.map +1 -1
  128. package/dist/test/history-semantic-review-findings.test.d.ts +2 -0
  129. package/dist/test/history-semantic-review-findings.test.d.ts.map +1 -0
  130. package/dist/test/history-semantic-review-findings.test.js +338 -0
  131. package/dist/test/history-semantic-review-findings.test.js.map +1 -0
  132. package/dist/test/history-semantic-sync-converges.test.d.ts +2 -0
  133. package/dist/test/history-semantic-sync-converges.test.d.ts.map +1 -0
  134. package/dist/test/history-semantic-sync-converges.test.js +109 -0
  135. package/dist/test/history-semantic-sync-converges.test.js.map +1 -0
  136. package/dist/test/mcpl-capability-grant.test.js +3 -1
  137. package/dist/test/mcpl-capability-grant.test.js.map +1 -1
  138. package/dist/test/mcpl-child-env.test.d.ts +2 -0
  139. package/dist/test/mcpl-child-env.test.d.ts.map +1 -0
  140. package/dist/test/mcpl-child-env.test.js +158 -0
  141. package/dist/test/mcpl-child-env.test.js.map +1 -0
  142. package/dist/test/mcpl-coalescing.test.d.ts +2 -0
  143. package/dist/test/mcpl-coalescing.test.d.ts.map +1 -0
  144. package/dist/test/mcpl-coalescing.test.js +1050 -0
  145. package/dist/test/mcpl-coalescing.test.js.map +1 -0
  146. package/dist/test/mcpl-list-status.test.js +4 -0
  147. package/dist/test/mcpl-list-status.test.js.map +1 -1
  148. package/dist/test/mcpl-reference-fields.test.d.ts +2 -0
  149. package/dist/test/mcpl-reference-fields.test.d.ts.map +1 -0
  150. package/dist/test/mcpl-reference-fields.test.js +82 -0
  151. package/dist/test/mcpl-reference-fields.test.js.map +1 -0
  152. package/dist/test/mcpl-tool-class-listing.test.d.ts +2 -0
  153. package/dist/test/mcpl-tool-class-listing.test.d.ts.map +1 -0
  154. package/dist/test/mcpl-tool-class-listing.test.js +147 -0
  155. package/dist/test/mcpl-tool-class-listing.test.js.map +1 -0
  156. package/dist/test/mcpl-tool-classes.test.d.ts +7 -0
  157. package/dist/test/mcpl-tool-classes.test.d.ts.map +1 -0
  158. package/dist/test/mcpl-tool-classes.test.js +80 -0
  159. package/dist/test/mcpl-tool-classes.test.js.map +1 -0
  160. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +2 -0
  161. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +1 -0
  162. package/dist/test/mcpl-tool-lifecycle-e2e.test.js +237 -0
  163. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -0
  164. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +11 -0
  165. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +1 -0
  166. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +307 -0
  167. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -0
  168. package/dist/test/mcpl-tool-lifecycle.test.d.ts +11 -0
  169. package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +1 -0
  170. package/dist/test/mcpl-tool-lifecycle.test.js +270 -0
  171. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -0
  172. package/dist/test/silent-heartbeat.test.d.ts +2 -0
  173. package/dist/test/silent-heartbeat.test.d.ts.map +1 -0
  174. package/dist/test/silent-heartbeat.test.js +154 -0
  175. package/dist/test/silent-heartbeat.test.js.map +1 -0
  176. package/dist/test/tool-result-guard.test.d.ts +2 -0
  177. package/dist/test/tool-result-guard.test.d.ts.map +1 -0
  178. package/dist/test/tool-result-guard.test.js +1081 -0
  179. package/dist/test/tool-result-guard.test.js.map +1 -0
  180. package/dist/test/tools-observe-parser-conformance.test.d.ts +2 -0
  181. package/dist/test/tools-observe-parser-conformance.test.d.ts.map +1 -0
  182. package/dist/test/tools-observe-parser-conformance.test.js +129 -0
  183. package/dist/test/tools-observe-parser-conformance.test.js.map +1 -0
  184. package/dist/test/trunk-channel-routing.test.js +67 -0
  185. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  186. package/dist/tsconfig.tsbuildinfo +1 -1
  187. package/package.json +2 -2
  188. package/src/agent.ts +128 -10
  189. package/src/api/types.ts +2 -0
  190. package/src/code-execution/py-runner.ts +34 -2
  191. package/src/code-execution/tool-definition.ts +46 -0
  192. package/src/framework.ts +1207 -80
  193. package/src/index.ts +13 -0
  194. package/src/mcpl/capability-grant.ts +54 -6
  195. package/src/mcpl/channel-registry.ts +115 -7
  196. package/src/mcpl/conversation-router.ts +12 -8
  197. package/src/mcpl/index.ts +29 -0
  198. package/src/mcpl/push-coalescer.ts +821 -0
  199. package/src/mcpl/push-handler.ts +40 -4
  200. package/src/mcpl/references.ts +11 -3
  201. package/src/mcpl/server-connection.ts +48 -2
  202. package/src/mcpl/tool-classes.ts +188 -0
  203. package/src/mcpl/tool-glob.ts +36 -0
  204. package/src/mcpl/tool-lifecycle.ts +745 -0
  205. package/src/mcpl/transport.ts +67 -1
  206. package/src/mcpl/types.ts +71 -1
  207. package/src/modules/discord/index.ts +1 -1
  208. package/src/modules/history/index.ts +1089 -24
  209. package/src/modules/history/semantic.ts +473 -0
  210. package/src/modules/index.ts +2 -1
  211. package/src/tool-result-guard.ts +318 -0
  212. package/src/types/agent.ts +15 -0
  213. package/src/types/framework.ts +23 -0
  214. package/src/types/trace.ts +17 -0
@@ -0,0 +1,318 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import type { ContextManager, MessageId } from '@animalabs/context-manager';
4
+ import type { ContentBlock, NormalizedMessage, ToolResult } from '@animalabs/membrane';
5
+ import type { CompletedToolCall } from './types/index.js';
6
+ import { isStateExistsError } from './module-registry.js';
7
+
8
+ export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
9
+ export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
10
+
11
+ interface PendingBatch {
12
+ id: string;
13
+ messageId: MessageId;
14
+ content: ContentBlock[];
15
+ withheld: ContentBlock[];
16
+ branch: string;
17
+ wireResults: ToolResult[];
18
+ submitted: boolean;
19
+ /** False when the audit sync failed: originals then never go on the wire. */
20
+ durable: boolean;
21
+ }
22
+
23
+ interface AuditOperation {
24
+ record: Record<string, unknown>;
25
+ /** Acceptance must finish this edit before its outcome can be recorded. */
26
+ admission?: { messageId: MessageId; content: ContentBlock[]; expected: ContentBlock[]; branch: string };
27
+ /** Retained while an acceptance edit needs retrying, not for audit-only retries. */
28
+ heldMessageId?: MessageId;
29
+ }
30
+
31
+ /**
32
+ * Admission of newly returned tool output to durable model-facing memory.
33
+ *
34
+ * Raw output is appended to a separate Chronicle audit slot BEFORE the
35
+ * placeholder enters the context manager. In particular, onNewMessage and
36
+ * speculative compression never see unaccepted output. Acceptance edits the
37
+ * placeholder through CM's versioned edit API; withholding only appends an
38
+ * audit event. Neither operation erases the original output or its blobs.
39
+ */
40
+ export class ToolResultGuard {
41
+ private pending: PendingBatch | undefined;
42
+ private registered = false;
43
+ private override: boolean | undefined;
44
+ /** True until a recovery produces a clean response/new tool round. */
45
+ recovering = false;
46
+
47
+ constructor(
48
+ private readonly agentName: string,
49
+ private readonly cm: ContextManager,
50
+ private readonly configured = false,
51
+ ) {}
52
+
53
+ get enabled(): boolean { return this.override ?? this.configured; }
54
+ setOverride(value: boolean | undefined): void { this.override = value; }
55
+ get settingOverride(): boolean | undefined { return this.override; }
56
+ get hasPending(): boolean { return this.pending !== undefined; }
57
+ /** True only once the pending batch's originals were actually put on the
58
+ * wire. Guard effects (retry suppression, refusal claim, prose buffering)
59
+ * are scoped to this state; a merely staged batch never claims a refusal. */
60
+ get hasSubmittedPending(): boolean { return this.pending?.submitted === true; }
61
+
62
+ /** Extra input tokens the pending batch costs on the wire beyond the
63
+ * placeholder the strategy selected against (chars/4 + flat per image,
64
+ * the same heuristic as the physical-window projection). Compilation
65
+ * reserves this so substituting originals cannot exceed the budget. */
66
+ get pendingWireReserveTokens(): number {
67
+ const pending = this.pending;
68
+ if (!pending) return 0;
69
+ let chars = 0;
70
+ let images = 0;
71
+ for (const result of pending.wireResults) {
72
+ if (typeof result.content === 'string') chars += result.content.length;
73
+ else for (const block of result.content as ContentBlock[]) {
74
+ if (block.type === 'image') images += 1;
75
+ else chars += JSON.stringify(block).length;
76
+ }
77
+ chars -= TOOL_RESULT_GUARD_NOTICE.length;
78
+ }
79
+ return Math.max(0, Math.ceil(chars / 4) + images * 1600);
80
+ }
81
+
82
+ /** Ordered storage work, separate from batches awaiting a provider verdict.
83
+ * A failed acceptance edit retains both the payload and its compression
84
+ * hold here; it can never re-arm refusal handling or tool submission. */
85
+ private unrecorded: AuditOperation[] = [];
86
+
87
+ /** Retry edits and audit writes before a new activation, audit write,
88
+ * maintenance pass, or shutdown. A failed operation stays at the head. */
89
+ flushUnrecorded(): void {
90
+ while (this.unrecorded.length > 0) {
91
+ const operation = this.unrecorded[0];
92
+ try {
93
+ if (operation.admission) {
94
+ const { messageId, content, expected, branch } = operation.admission;
95
+ const sameBranch = this.cm.currentBranch().name === branch;
96
+ const current = sameBranch ? this.cm.getMessage(messageId) : null;
97
+ // A retry must not undo an operator's later edit/removal/rollback.
98
+ // Reapplying our own payload is safe if an earlier edit wrote the
99
+ // message and then failed while propagating to context entries.
100
+ // Chronicle normalizes object-key order and drops undefined fields.
101
+ if (!sameBranch || !current ||
102
+ (!isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(expected)))
103
+ && !isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(content))))) {
104
+ operation.record.historyUpdate = 'superseded';
105
+ } else {
106
+ this.cm.editMessage(messageId, content);
107
+ }
108
+ // An audit-only retry must not repeat an edit that already succeeded.
109
+ operation.admission = undefined;
110
+ }
111
+ this.write(operation.record);
112
+ this.unrecorded.shift();
113
+ } finally {
114
+ if (!operation.admission) this.releaseOperationHold(operation);
115
+ }
116
+ }
117
+ }
118
+
119
+ private append(record: Record<string, unknown>): void {
120
+ const stamped = { agentName: this.agentName, timestamp: Date.now(), ...record };
121
+ this.unrecorded.push({ record: stamped });
122
+ this.flushUnrecorded();
123
+ }
124
+
125
+ private write(record: Record<string, unknown>): void {
126
+ const store = this.cm.getStore();
127
+ if (!this.registered) {
128
+ try {
129
+ store.registerState({ id: TOOL_RESULT_GUARD_AUDIT_STATE, strategy: 'append_log' });
130
+ } catch (error) {
131
+ if (!isStateExistsError(error)) throw error;
132
+ }
133
+ this.registered = true;
134
+ }
135
+ store.appendToStateJson(TOOL_RESULT_GUARD_AUDIT_STATE, record);
136
+ }
137
+
138
+ private archive(value: unknown): unknown {
139
+ const json = JSON.stringify(value);
140
+ // Match inference-log storage: large payloads (especially images and
141
+ // pre-spill output) must not be copied into every append-log snapshot.
142
+ return json.length > 10_000
143
+ ? { blobId: this.cm.getStore().storeBlob(Buffer.from(json), 'application/json') }
144
+ : JSON.parse(json);
145
+ }
146
+
147
+ storeResults(content: ContentBlock[], wireResults: ToolResult[], originals: CompletedToolCall[]): MessageId {
148
+ if (!this.enabled) return this.cm.addMessage('user', content);
149
+ if (this.pending) {
150
+ // Idempotent for the same not-yet-submitted batch: a caller that
151
+ // retries after a failure between staging and submission (e.g. a
152
+ // transient compile error on the direct Agent API) must not wedge.
153
+ const same = !this.pending.submitted
154
+ && this.pending.wireResults.map((r) => r.toolUseId).join('\0')
155
+ === wireResults.map((r) => r.toolUseId).join('\0');
156
+ if (same) return this.pending.messageId;
157
+ throw new Error('Tool result guard already has a pending batch');
158
+ }
159
+ const id = randomUUID();
160
+ // Includes full pre-truncation/error/image payloads, not just the wire
161
+ // preview. This slot is audit data, never a context/compression source.
162
+ const staged = { type: 'staged', batchId: id,
163
+ originals: this.archive(originals), content: this.archive(content), wireResults: this.archive(wireResults) };
164
+ try {
165
+ this.append(staged);
166
+ } catch (error) {
167
+ // Keep the original in the retry queue, but let the tool loop continue
168
+ // with a placeholder if the audit cannot become durable below.
169
+ console.error(`[tool-result-guard] agent=${this.agentName} staged audit write queued:`, error);
170
+ }
171
+ const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
172
+ ? { type: 'tool_result', toolUseId: block.toolUseId, content: TOOL_RESULT_GUARD_NOTICE, isError: block.isError }
173
+ : block);
174
+ // Compression hold (CM >= 0.12): placed BEFORE onNewMessage fires, so no
175
+ // chunk is ever summarized from the placeholder; released after the
176
+ // settlement edit. Holds are in-memory only — a reopened manager has none,
177
+ // which is correct since a reopened guard has no pending batch.
178
+ const messageId = this.cm.addMessage('user', withheld, undefined, undefined, { holdCompression: true });
179
+ this.pending = { id, messageId, content, withheld, wireResults,
180
+ branch: this.cm.currentBranch().name, submitted: false, durable: false };
181
+ // Durability barrier: the audit must reach Chronicle's chain heads before
182
+ // the originals can go to a provider. On a failed sync the batch fails
183
+ // CLOSED: the placeholders go on the wire instead (the turn continues,
184
+ // nothing is stranded), and the batch later settles as unsubmitted.
185
+ try {
186
+ // A linked-append failure follows the same fail-closed path as a sync
187
+ // failure. Throwing here would strand the stream awaiting its results.
188
+ this.append({ type: 'linked', batchId: id, messageId });
189
+ this.cm.getStore().sync();
190
+ this.pending.durable = true;
191
+ } catch (error) {
192
+ console.error(`[tool-result-guard] agent=${this.agentName} audit persistence failed; ` +
193
+ 'submitting placeholders instead of originals:', error);
194
+ }
195
+ return messageId;
196
+ }
197
+
198
+ /** Results for a live continuation (provideToolResults). Marks the batch
199
+ * submitted and returns the originals only when the audit is durable;
200
+ * otherwise returns placeholder results and leaves it unsubmitted. */
201
+ submissionResults(results: ToolResult[]): ToolResult[] {
202
+ const pending = this.pending;
203
+ if (!pending) return results;
204
+ if (pending.durable) { pending.submitted = true; return results; }
205
+ const ids = new Set(pending.wireResults.map((result) => result.toolUseId));
206
+ return results.map((result) => ids.has(result.toolUseId)
207
+ ? { ...result, content: TOOL_RESULT_GUARD_NOTICE } : result);
208
+ }
209
+
210
+ /** The stream carrying this batch ended without a clean round (abort or
211
+ * exhausted errors). Settle conservatively — an interrupted submission
212
+ * does not establish acceptance — so a later turn neither resubmits the
213
+ * originals nor attributes its own refusal to this batch. */
214
+ abandon(reason: string): void {
215
+ const pending = this.pending;
216
+ this.recovering = false;
217
+ if (!pending) return;
218
+ this.settle(pending, { type: 'withheld', reason, submitted: pending.submitted }, false);
219
+ }
220
+
221
+
222
+ /** A budget/error restart compiles placeholders; restore pending output
223
+ * only in this provider request, never in the strategy's view. */
224
+ prepareRequest(messages: NormalizedMessage[], recordSubmission = false): NormalizedMessage[] {
225
+ const pending = this.pending;
226
+ if (!pending || !pending.durable) return messages;
227
+ const byId = new Map(pending.wireResults.map((result) => [result.toolUseId, result]));
228
+ const present = new Set(messages.flatMap((message) => message.content
229
+ .filter((block) => block.type === 'tool_result' && byId.has(block.toolUseId))
230
+ .map((block) => (block as ContentBlock & { toolUseId: string }).toolUseId)));
231
+ // A strategy may have folded the entire exchange away. Do not release
232
+ // content that was never submitted. Its originals remain in the audit.
233
+ const submitted = present.size === byId.size;
234
+ if (recordSubmission) pending.submitted = submitted;
235
+ if (!submitted) return messages;
236
+ return messages.map((message) => ({ ...message, content: message.content.map((block) => {
237
+ const result = block.type === 'tool_result' ? byId.get(block.toolUseId) : undefined;
238
+ return result ? { type: 'tool_result', toolUseId: result.toolUseId, content: result.content, isError: result.isError } : block;
239
+ }) }));
240
+ }
241
+
242
+ /** The turn ended (endTurn/skip_reply) before the batch was submitted:
243
+ * nothing was refused, so admit it exactly as an unguarded agent would.
244
+ * Leaving it pending would turn it into a permanent withheld notice on
245
+ * restart and disable ordinary refusal handling on the next turn. */
246
+ settleTurnEnded(): void {
247
+ const pending = this.pending;
248
+ if (!pending) return;
249
+ this.settle(pending, pending.durable
250
+ ? { type: 'accepted', reason: 'turn_ended' }
251
+ : { type: 'withheld', reason: 'audit_not_durable' }, pending.durable);
252
+ }
253
+
254
+ /** A refusal arrived while the pending batch was never on the wire (the
255
+ * strategy omitted the exchange). The guard cannot have caused it: record
256
+ * the batch as withheld, clear it, and let ordinary refusal handling run.
257
+ * Returns true when it settled such a batch. */
258
+ settleUnsubmitted(category: string): boolean {
259
+ const pending = this.pending;
260
+ if (!pending || pending.submitted) return false;
261
+ this.settle(pending, { type: 'withheld', reason: 'unsubmitted', category }, false);
262
+ return true;
263
+ }
264
+
265
+ /** A clean physical response accepts precisely the last submitted batch. */
266
+ accept(): void {
267
+ const pending = this.pending;
268
+ this.recovering = false;
269
+ if (!pending) return;
270
+ this.settle(pending, pending.submitted
271
+ ? { type: 'accepted' } : { type: 'withheld', reason: 'unsubmitted' }, pending.submitted);
272
+ }
273
+
274
+ /** At most one recovery per batch; no scanning/deleting older history. */
275
+ withhold(category: string): string[] | null {
276
+ const pending = this.pending;
277
+ if (!pending?.submitted) return null;
278
+ const ids = pending.wireResults.map((result) => result.toolUseId);
279
+ // Even a failed outcome-log write must never re-arm rejected output for
280
+ // a later submission. Its originals were archived before admission.
281
+ this.recovering = true;
282
+ this.settle(pending, { type: 'withheld', toolUseIds: ids, category }, false);
283
+ return ids;
284
+ }
285
+
286
+ /** Transfer ownership from provider admission to an ordered storage job.
287
+ * Failure retains the accepted payload and its hold until the edit succeeds.
288
+ * Withheld content is final already, so audit failures cannot keep it held. */
289
+ private settle(pending: PendingBatch, record: Record<string, unknown>, admit: boolean): void {
290
+ this.pending = undefined;
291
+ const operation: AuditOperation = {
292
+ record: { agentName: this.agentName, timestamp: Date.now(), ...record,
293
+ batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch },
294
+ ...(admit ? { admission: { messageId: pending.messageId, content: pending.content,
295
+ expected: pending.withheld, branch: pending.branch } } : {}),
296
+ heldMessageId: pending.messageId,
297
+ };
298
+ this.unrecorded.push(operation);
299
+ try {
300
+ this.flushUnrecorded();
301
+ } catch (error) {
302
+ console.error(`[tool-result-guard] agent=${this.agentName} settlement queued for retry:`, error);
303
+ } finally {
304
+ if (!operation.admission) this.releaseOperationHold(operation);
305
+ }
306
+ }
307
+
308
+ private releaseOperationHold(operation: AuditOperation): void {
309
+ if (operation.heldMessageId !== undefined) {
310
+ this.releaseHold(operation.heldMessageId);
311
+ operation.heldMessageId = undefined;
312
+ }
313
+ }
314
+
315
+ private releaseHold(messageId: MessageId): void {
316
+ this.cm.releaseCompression([messageId]);
317
+ }
318
+ }
@@ -170,6 +170,12 @@ export interface AgentConfig {
170
170
  announceHumanTurns?: boolean;
171
171
  };
172
172
 
173
+ /** Opt-in tool-output admission and one guarded retry after a provider
174
+ * refusal. Agents can persistently override this via agent_settings
175
+ * tool_result_guard. Full originals remain in Chronicle's audit history.
176
+ * Default false. Disabling does not restore previously withheld output. */
177
+ toolResultGuard?: boolean;
178
+
173
179
  /**
174
180
  * How the agent's PLAIN PROSE (non-tool output) reaches channels.
175
181
  * - 'locus' (default): host-inferred — the turn-frozen locus machinery.
@@ -296,6 +302,9 @@ export interface CompletedToolCall {
296
302
  * Inference request for an agent.
297
303
  */
298
304
  export interface InferenceRequest {
305
+ /** Host-owned identity for withdrawal of an unconsumed coalesced wake. */
306
+ coalescingSubject?: string;
307
+ coalescingEventId?: string;
299
308
  agentName: string;
300
309
  reason: string;
301
310
  source: string;
@@ -336,4 +345,10 @@ export interface InferenceRequest {
336
345
  wakeChannelId?: string;
337
346
  /** Timestamp of the event the wake provenance was taken from (ms). */
338
347
  wakeAt?: number;
348
+ /** Suppress every automatic plain-prose delivery for this logical turn.
349
+ * Explicit tool calls remain available. Used by authenticated silent wakes. */
350
+ suppressProse?: boolean;
351
+ /** Ephemeral system-position prompt for this turn only. Never written to
352
+ * Chronicle; callers must supply bounded non-secret control text. */
353
+ ephemeralSystemPrompt?: string;
339
354
  }
@@ -52,6 +52,12 @@ export interface CodeExecutionConfig {
52
52
  toolCallTimeoutMs?: number;
53
53
  /** Whole-script deadline: cancel → grace → SIGKILL (default 600_000 ms). */
54
54
  scriptTimeoutMs?: number;
55
+ /**
56
+ * Longest deadline an agent may ask for on one call with `time_limit_ms`
57
+ * (default: `scriptTimeoutMs`, so agents can only shorten it until this is
58
+ * raised). Longer requests are capped, and the result says so.
59
+ */
60
+ maxScriptTimeoutMs?: number;
55
61
  /**
56
62
  * Idle interpreter reclaim — script globals are lost after this much
57
63
  * inactivity (default 300_000 ms, mirroring ~5-minute container reclaim).
@@ -225,6 +231,23 @@ export interface FrameworkConfig {
225
231
  */
226
232
  toolResultInlineMaxChars?: number;
227
233
 
234
+ /**
235
+ * MCPL RFC-008 operator class overrides: RFC-007 §6.2 patterns over the
236
+ * model-facing tool name → classes. Highest-precedence source of a tool's
237
+ * effective class (first matching pattern wins); replaces, never merges
238
+ * with, what the providing server declared. Use it to correct or tighten a
239
+ * misclassed tool.
240
+ */
241
+ toolClassOverrides?: Record<string, string[]>;
242
+
243
+ /**
244
+ * MCPL RFC-008 host knowledge for tools the EMBEDDING host implements (its
245
+ * own modules), same shape as toolClassOverrides. Consulted before the
246
+ * framework's built-in table and never for MCPL-provided tools, whose
247
+ * class comes from an override or their server's `_meta["mcpl/class"]`.
248
+ */
249
+ hostToolClasses?: Record<string, string[]>;
250
+
228
251
  /** Inference routing policy for server-initiated inference (optional). */
229
252
  inferenceRouting?: InferenceRoutingPolicy;
230
253
 
@@ -48,6 +48,8 @@ export type TraceEvent =
48
48
  * activation with its channel without re-deriving routing.
49
49
  */
50
50
  channelId?: string;
51
+ /** True for a control-plane wake that must not surface typing/prose. */
52
+ silent?: boolean;
51
53
  })
52
54
  | (TraceEventBase & {
53
55
  type: 'inference:completed';
@@ -209,6 +211,21 @@ export type TraceEvent =
209
211
  messageId: string;
210
212
  source: string;
211
213
  })
214
+ /** An unread message withdrawn or replaced by its sender (RFC-006). */
215
+ | (TraceEventBase & {
216
+ type: 'message:removed';
217
+ messageId: string;
218
+ source: string;
219
+ })
220
+ /** RFC-006 coalescing decision record (received / displaced / removed /
221
+ * rendered / revoked / render-cancelled / late-render / render-failed). */
222
+ | (TraceEventBase & {
223
+ type: 'mcpl:coalescing';
224
+ kind: string;
225
+ subject?: string;
226
+ eventId?: string;
227
+ [key: string]: unknown;
228
+ })
212
229
 
213
230
  // EventGate lifecycle
214
231
  | (TraceEventBase & {