@animalabs/agent-framework 0.27.0 → 0.28.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 (234) hide show
  1. package/README.md +56 -15
  2. package/dist/src/agent.d.ts +29 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +69 -11
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +4 -2
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +3 -2
  10. package/dist/src/auto-continue.d.ts.map +1 -1
  11. package/dist/src/auto-continue.js +4 -3
  12. package/dist/src/auto-continue.js.map +1 -1
  13. package/dist/src/cut-off-round.d.ts +24 -0
  14. package/dist/src/cut-off-round.d.ts.map +1 -0
  15. package/dist/src/cut-off-round.js +87 -0
  16. package/dist/src/cut-off-round.js.map +1 -0
  17. package/dist/src/framework.d.ts +304 -89
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +2577 -678
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +99 -3
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +538 -13
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/gate/index.d.ts +2 -1
  26. package/dist/src/gate/index.d.ts.map +1 -1
  27. package/dist/src/gate/index.js +1 -0
  28. package/dist/src/gate/index.js.map +1 -1
  29. package/dist/src/gate/reply-judge.d.ts +116 -0
  30. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  31. package/dist/src/gate/reply-judge.js +467 -0
  32. package/dist/src/gate/reply-judge.js.map +1 -0
  33. package/dist/src/gate/types.d.ts +161 -0
  34. package/dist/src/gate/types.d.ts.map +1 -1
  35. package/dist/src/heartbeat/heartbeat.d.ts +184 -0
  36. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  37. package/dist/src/heartbeat/heartbeat.js +752 -0
  38. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  39. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  40. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  41. package/dist/src/heartbeat/reminder-time.js +107 -0
  42. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  43. package/dist/src/index.d.ts +4 -0
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +2 -0
  46. package/dist/src/index.js.map +1 -1
  47. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  48. package/dist/src/mcpl/capability-mask.js +3 -2
  49. package/dist/src/mcpl/capability-mask.js.map +1 -1
  50. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  51. package/dist/src/mcpl/channel-registry.js +2 -1
  52. package/dist/src/mcpl/channel-registry.js.map +1 -1
  53. package/dist/src/mcpl/inbound-source.d.ts +26 -10
  54. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  55. package/dist/src/mcpl/inbound-source.js +40 -69
  56. package/dist/src/mcpl/inbound-source.js.map +1 -1
  57. package/dist/src/mcpl/server-connection.d.ts +18 -8
  58. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  59. package/dist/src/mcpl/server-connection.js +50 -10
  60. package/dist/src/mcpl/server-connection.js.map +1 -1
  61. package/dist/src/mcpl/tags.d.ts +9 -2
  62. package/dist/src/mcpl/tags.d.ts.map +1 -1
  63. package/dist/src/mcpl/tags.js +12 -2
  64. package/dist/src/mcpl/tags.js.map +1 -1
  65. package/dist/src/mcpl/tool-presence.d.ts +66 -14
  66. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  67. package/dist/src/mcpl/tool-presence.js +134 -65
  68. package/dist/src/mcpl/tool-presence.js.map +1 -1
  69. package/dist/src/mcpl/tool-signature.d.ts +10 -3
  70. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  71. package/dist/src/mcpl/tool-signature.js +47 -18
  72. package/dist/src/mcpl/tool-signature.js.map +1 -1
  73. package/dist/src/mcpl/toolset-host.d.ts +88 -37
  74. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  75. package/dist/src/mcpl/toolset-host.js +207 -108
  76. package/dist/src/mcpl/toolset-host.js.map +1 -1
  77. package/dist/src/mcpl/toolsets-module.d.ts +14 -11
  78. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  79. package/dist/src/mcpl/toolsets-module.js +141 -49
  80. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  81. package/dist/src/mcpl/types.d.ts +3 -1
  82. package/dist/src/mcpl/types.d.ts.map +1 -1
  83. package/dist/src/mcpl/types.js.map +1 -1
  84. package/dist/src/module-registry.d.ts +4 -1
  85. package/dist/src/module-registry.d.ts.map +1 -1
  86. package/dist/src/module-registry.js +9 -2
  87. package/dist/src/module-registry.js.map +1 -1
  88. package/dist/src/modules/history/index.d.ts.map +1 -1
  89. package/dist/src/modules/history/index.js +22 -11
  90. package/dist/src/modules/history/index.js.map +1 -1
  91. package/dist/src/modules/workspace/answers.d.ts +22 -0
  92. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  93. package/dist/src/modules/workspace/answers.js +123 -0
  94. package/dist/src/modules/workspace/answers.js.map +1 -0
  95. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  96. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  97. package/dist/src/modules/workspace/branch-intent.js +6 -2
  98. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  99. package/dist/src/modules/workspace/direct-write.d.ts +3 -0
  100. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  101. package/dist/src/modules/workspace/direct-write.js +57 -24
  102. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  103. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  104. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  105. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  106. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  107. package/dist/src/modules/workspace/edit-text.d.ts +2 -1
  108. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  109. package/dist/src/modules/workspace/edit-text.js +4 -2
  110. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  111. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  112. package/dist/src/modules/workspace/effects.js +20 -13
  113. package/dist/src/modules/workspace/effects.js.map +1 -1
  114. package/dist/src/modules/workspace/index.d.ts +62 -51
  115. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  116. package/dist/src/modules/workspace/index.js +934 -531
  117. package/dist/src/modules/workspace/index.js.map +1 -1
  118. package/dist/src/modules/workspace/observe.d.ts +75 -63
  119. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  120. package/dist/src/modules/workspace/observe.js +366 -180
  121. package/dist/src/modules/workspace/observe.js.map +1 -1
  122. package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
  123. package/dist/src/modules/workspace/read-page.js +7 -1
  124. package/dist/src/modules/workspace/read-page.js.map +1 -1
  125. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  126. package/dist/src/modules/workspace/reconcile.js +26 -16
  127. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  128. package/dist/src/modules/workspace/result-text.d.ts +1 -0
  129. package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
  130. package/dist/src/modules/workspace/result-text.js.map +1 -1
  131. package/dist/src/modules/workspace/sync.d.ts +9 -0
  132. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  133. package/dist/src/modules/workspace/sync.js +14 -0
  134. package/dist/src/modules/workspace/sync.js.map +1 -1
  135. package/dist/src/modules/workspace/types.d.ts +96 -43
  136. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  137. package/dist/src/modules/workspace/types.js.map +1 -1
  138. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  139. package/dist/src/modules/workspace/watcher.js +8 -3
  140. package/dist/src/modules/workspace/watcher.js.map +1 -1
  141. package/dist/src/prose-drafts.d.ts +4 -3
  142. package/dist/src/prose-drafts.d.ts.map +1 -1
  143. package/dist/src/prose-drafts.js +3 -2
  144. package/dist/src/prose-drafts.js.map +1 -1
  145. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  146. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  147. package/dist/src/reasoning-loss-notice.js +50 -0
  148. package/dist/src/reasoning-loss-notice.js.map +1 -0
  149. package/dist/src/reasoning-loss.d.ts +24 -0
  150. package/dist/src/reasoning-loss.d.ts.map +1 -0
  151. package/dist/src/reasoning-loss.js +102 -0
  152. package/dist/src/reasoning-loss.js.map +1 -0
  153. package/dist/src/send-tools.d.ts +3 -2
  154. package/dist/src/send-tools.d.ts.map +1 -1
  155. package/dist/src/send-tools.js +3 -2
  156. package/dist/src/send-tools.js.map +1 -1
  157. package/dist/src/sent-rows.js +1 -1
  158. package/dist/src/thinking-drops.d.ts +4 -11
  159. package/dist/src/thinking-drops.d.ts.map +1 -1
  160. package/dist/src/thinking-drops.js +7 -14
  161. package/dist/src/thinking-drops.js.map +1 -1
  162. package/dist/src/tool-args.d.ts +12 -0
  163. package/dist/src/tool-args.d.ts.map +1 -1
  164. package/dist/src/tool-args.js +35 -1
  165. package/dist/src/tool-args.js.map +1 -1
  166. package/dist/src/tool-result-guard.d.ts +8 -4
  167. package/dist/src/tool-result-guard.d.ts.map +1 -1
  168. package/dist/src/tool-result-guard.js +23 -11
  169. package/dist/src/tool-result-guard.js.map +1 -1
  170. package/dist/src/tune-out/tools.d.ts +4 -1
  171. package/dist/src/tune-out/tools.d.ts.map +1 -1
  172. package/dist/src/types/agent.d.ts +40 -9
  173. package/dist/src/types/agent.d.ts.map +1 -1
  174. package/dist/src/types/events.d.ts +18 -8
  175. package/dist/src/types/events.d.ts.map +1 -1
  176. package/dist/src/types/framework.d.ts +15 -0
  177. package/dist/src/types/framework.d.ts.map +1 -1
  178. package/dist/src/types/module.d.ts +5 -2
  179. package/dist/src/types/module.d.ts.map +1 -1
  180. package/dist/src/types/trace.d.ts +34 -1
  181. package/dist/src/types/trace.d.ts.map +1 -1
  182. package/package.json +3 -3
  183. package/src/agent.ts +72 -11
  184. package/src/api/server.ts +4 -2
  185. package/src/auto-continue.ts +4 -3
  186. package/src/cut-off-round.ts +98 -0
  187. package/src/framework.ts +2694 -700
  188. package/src/gate/event-gate.ts +579 -15
  189. package/src/gate/index.ts +8 -0
  190. package/src/gate/reply-judge.ts +512 -0
  191. package/src/gate/types.ts +164 -1
  192. package/src/heartbeat/heartbeat.ts +804 -0
  193. package/src/heartbeat/reminder-time.ts +107 -0
  194. package/src/index.ts +4 -0
  195. package/src/mcpl/capability-mask.ts +3 -2
  196. package/src/mcpl/channel-registry.ts +2 -1
  197. package/src/mcpl/inbound-source.ts +42 -69
  198. package/src/mcpl/server-connection.ts +56 -16
  199. package/src/mcpl/tags.ts +13 -2
  200. package/src/mcpl/tool-presence.ts +133 -67
  201. package/src/mcpl/tool-signature.ts +51 -20
  202. package/src/mcpl/toolset-host.ts +197 -107
  203. package/src/mcpl/toolsets-module.ts +143 -49
  204. package/src/mcpl/types.ts +3 -1
  205. package/src/module-registry.ts +8 -2
  206. package/src/modules/history/index.ts +22 -12
  207. package/src/modules/workspace/answers.ts +133 -0
  208. package/src/modules/workspace/branch-intent.ts +10 -5
  209. package/src/modules/workspace/direct-write.ts +58 -23
  210. package/src/modules/workspace/disk-agreement.ts +52 -21
  211. package/src/modules/workspace/edit-text.ts +7 -5
  212. package/src/modules/workspace/effects.ts +21 -12
  213. package/src/modules/workspace/index.ts +927 -541
  214. package/src/modules/workspace/observe.ts +381 -177
  215. package/src/modules/workspace/read-page.ts +7 -1
  216. package/src/modules/workspace/reconcile.ts +27 -15
  217. package/src/modules/workspace/result-text.ts +2 -0
  218. package/src/modules/workspace/sync.ts +16 -0
  219. package/src/modules/workspace/types.ts +96 -43
  220. package/src/modules/workspace/watcher.ts +8 -3
  221. package/src/prose-drafts.ts +4 -3
  222. package/src/reasoning-loss-notice.ts +62 -0
  223. package/src/reasoning-loss.ts +120 -0
  224. package/src/send-tools.ts +3 -2
  225. package/src/sent-rows.ts +1 -1
  226. package/src/thinking-drops.ts +10 -16
  227. package/src/tool-args.ts +34 -1
  228. package/src/tool-result-guard.ts +26 -11
  229. package/src/tune-out/tools.ts +4 -1
  230. package/src/types/agent.ts +40 -9
  231. package/src/types/events.ts +18 -8
  232. package/src/types/framework.ts +15 -0
  233. package/src/types/module.ts +5 -2
  234. package/src/types/trace.ts +36 -1
@@ -29,10 +29,11 @@ export interface CompiledForRequest {
29
29
  recordedAfter?: number;
30
30
  }
31
31
 
32
- /** What a stream's driver reads a call's dropped blocks against (`StartStreamResult`'s two fields, together). */
32
+ /** What a stream's driver reads a call's dropped blocks against (`StartStreamResult`'s fields, together). */
33
33
  export interface StreamThinkingDrops {
34
34
  place: (entries: readonly ThinkingBindingEntry[]) => PlacedDrop[];
35
35
  recordedAfter?: number;
36
+ branch?: string;
36
37
  }
37
38
 
38
39
  /** A signed block's identity on the wire: a thinking block's signature, a redacted block's data. */
@@ -112,26 +113,19 @@ export function splitThinkingDrops(
112
113
  /** Binding failures the context manager's record doesn't track: the account's, not the conversation's. */
113
114
  const UNTRACKED_REASONS = new Set(['organization_binding_mismatch', 'end_user_binding_mismatch']);
114
115
 
115
- /**
116
- * When an agent's unexpected drops call for an operator alert: at the first call that shows any, and again only
117
- * when a call shows more than the alert last counted. Each call of a stream, and each stream until a strip takes
118
- * them out, drops the same blocks again, so an unchanged count isn't news. A call that shows no unexpected drop, its
119
- * report empty, absent or naming only expected ones, ends the incident: the blocks it counted no longer drop. The next
120
- * one alerts afresh. It's held in memory, so a restart mid-incident alerts once more, and an agent's removal ends its
121
- * incident (`forget`).
122
- */
123
116
  export class ThinkingLossIncidents {
124
- private readonly alerted = new Map<string, number>();
117
+ private readonly alerted = new Map<string, { count: number; scope: string | undefined }>();
125
118
 
126
- /** A call's report of `unexpected` drops for `agent`: the count to alert with, or undefined for no alert. */
127
- observe(agent: string, unexpected: number): number | undefined {
128
- if (unexpected === 0) {
119
+ /** A call's report of `count` drops for `agent` in `scope`: the count to alert with, or undefined for no alert. */
120
+ observe(agent: string, count: number, scope?: string): number | undefined {
121
+ const open = this.alerted.get(agent);
122
+ if (count === 0) {
129
123
  this.alerted.delete(agent);
130
124
  return undefined;
131
125
  }
132
- if (unexpected <= (this.alerted.get(agent) ?? 0)) return undefined;
133
- this.alerted.set(agent, unexpected);
134
- return unexpected;
126
+ if (open && open.scope === scope && count <= open.count) return undefined;
127
+ this.alerted.set(agent, { count, scope });
128
+ return count;
135
129
  }
136
130
 
137
131
  /** The agent is gone: its incident, if one is open, ends with it. */
package/src/tool-args.ts CHANGED
@@ -67,6 +67,39 @@ function shown(value: unknown): string {
67
67
  return head;
68
68
  }
69
69
 
70
+ function notAnObject(input: unknown, field = 'input'): string {
71
+ return `its ${field} must be an object, and this call gave ${withArticle(kindOf(input))}`;
72
+ }
73
+
74
+ export function markedInputRefusal(call: { name: string; unparseableInput?: string }): string | undefined {
75
+ const text = call.unparseableInput;
76
+ if (text === undefined) return undefined;
77
+ // When a stream ends before a call's block stops, membrane marks the call
78
+ // with what had arrived of its arguments: nothing, a part, or a whole
79
+ // object. A block still open when a stream breaks the API's block order
80
+ // is marked the same way, and one that had stopped with its object's JSON,
81
+ // so the arguments may be another call's (ToolUseContent.unparseableInput).
82
+ if (/^[ \t\n\r]*$/.test(text)) return `Nothing was done: ${call.name} its arguments never arrived; call it again with them whole.`;
83
+ let parsed: unknown;
84
+ try {
85
+ parsed = JSON.parse(text);
86
+ } catch {
87
+ return `Nothing was done: ${call.name} its arguments must be one JSON object, and this call gave text that isn't JSON, which may have been cut off: ${shown(text)}; call it again with one JSON object.`;
88
+ }
89
+ if (parsed === null) return undefined;
90
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) return `Nothing was done: ${call.name} ${notAnObject(parsed)}.`;
91
+ return `Nothing was done: ${call.name} its arguments came in a response that didn't arrive whole and in order, so they can't be taken as this call's; call it again.`;
92
+ }
93
+
94
+ export function cutOffRefusal(call: { name: string }): string {
95
+ return `Nothing was done: ${call.name} didn't run, because the response it was in was cut off at the output limit. ` +
96
+ 'To make the call, send less in one response: shorter text before it, shorter arguments, or fewer calls at once.';
97
+ }
98
+
99
+ export function asCalled<T extends { name: string; input?: unknown; unparseableInput?: string }>(call: T): T {
100
+ return call.unparseableInput !== undefined && markedInputRefusal(call) === undefined ? { ...call, input: {} } : call;
101
+ }
102
+
70
103
  /** A list in prose: `a`, `a or b`, `a, b or c`. */
71
104
  function oneOf(items: string[]): string {
72
105
  return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} or ${items[items.length - 1]}`;
@@ -169,7 +202,7 @@ export function toolArgsProblems(
169
202
  if (!schema) return [];
170
203
  const absent = input === undefined || input === null;
171
204
  if (!absent && (typeof input !== 'object' || Array.isArray(input))) {
172
- return [`its ${options.field ?? 'input'} must be an object, and this call gave ${withArticle(kindOf(input))}`];
205
+ return [notAnObject(input, options.field)];
173
206
  }
174
207
  const obj = (absent ? {} : input) as Record<string, unknown>;
175
208
  const properties = (isMap(schema.properties) ? schema.properties : {}) as Record<string, { type?: unknown; nullable?: unknown; enum?: unknown } | undefined>;
@@ -9,6 +9,14 @@ import { resultClaimsOf, rfc005TextOf, toolResultDataToHistoryString } from './t
9
9
  import { safeSlice } from './safe-slice.js';
10
10
 
11
11
  export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
12
+ export type WithheldNoticeKind = 'ran' | 'refused' | 'error';
13
+ const GUARD_NOTICES: Record<WithheldNoticeKind, string> = {
14
+ ran: TOOL_RESULT_GUARD_NOTICE,
15
+ refused: "Tool result withheld by the guard. The tool didn't run.",
16
+ error: "Tool result withheld by the guard. The call returned an error, and this notice can't say whether the tool ran.",
17
+ };
18
+ const noticeKind = (result: CompletedToolCall['result'] | undefined): WithheldNoticeKind =>
19
+ result?.refused === true ? 'refused' : result?.isError === true ? 'error' : 'ran';
12
20
  export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
13
21
 
14
22
  /** Where a withheld result's original went: a workspace path; the path a
@@ -54,9 +62,12 @@ export interface WithheldHost {
54
62
  }
55
63
 
56
64
  /**
57
- * The stub a withheld result settles to: #159's neutral notice, then where
58
- * the original is, then, when its tool marked any, the fields kept from its
59
- * result (`kept`, from keptWhenWithheld). It names no refusal and no
65
+ * The stub a withheld result settles to: #159's neutral notice for its
66
+ * `kind` (a result that isn't an error, a refused call's, or another error's),
67
+ * then where the original is, then, for an error whose original is in a file
68
+ * known written, a line telling the agent to read it, then, when its tool
69
+ * marked any, the fields kept from its result (`kept`, from
70
+ * keptWhenWithheld). It names no refusal and no
60
71
  * category (#159 keeps those in operational logs), so reading the rest back
61
72
  * is the agent's deliberate choice (agent-framework #277). The place holds
62
73
  * the result as stored, which for one over the inline cap is its capped
@@ -67,7 +78,7 @@ export interface WithheldHost {
67
78
  * save failed is still queued in memory, and a restart before its retry
68
79
  * loses it.
69
80
  */
70
- export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string): string {
81
+ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string, kind: WithheldNoticeKind = 'ran'): string {
71
82
  const audit = `the guard's audit record (${TOOL_RESULT_GUARD_AUDIT_STATE}), for an operator`;
72
83
  const keptIn = (only: string) => auditDurable
73
84
  ? `is kept ${only}in ${audit}`
@@ -80,8 +91,10 @@ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean
80
91
  : spill.error === undefined
81
92
  ? `Its result is in workspace file ${spill.path}.`
82
93
  : `Writing its result to workspace file ${spill.path} failed, so it ${keptIn('only ')}.`;
94
+ const readable = kind === 'error' && !!spill && 'path' in spill && spill.error === undefined && !spill.unfinished;
95
+ const readFirst = readable ? ' Read it before calling again.' : '';
83
96
  const fields = kept === undefined ? '' : ` Kept from its result: ${kept}`;
84
- return `${TOOL_RESULT_GUARD_NOTICE} ${place}${fields}`;
97
+ return `${GUARD_NOTICES[kind]} ${place}${readFirst}${fields}`;
85
98
  }
86
99
 
87
100
  /**
@@ -140,6 +153,7 @@ interface PendingBatch {
140
153
  /** What each result's stub keeps if the batch is withheld, by its index in
141
154
  * content: the fields its tool marked (keptWhenWithheld). */
142
155
  kept: Map<number, string>;
156
+ notices: Map<string, WithheldNoticeKind>;
143
157
  }
144
158
 
145
159
  interface AuditOperation {
@@ -248,7 +262,7 @@ export class ToolResultGuard {
248
262
  if (block.type === 'image') images += 1;
249
263
  else chars += JSON.stringify(block).length;
250
264
  }
251
- chars -= TOOL_RESULT_GUARD_NOTICE.length;
265
+ chars -= GUARD_NOTICES[pending.notices.get(result.toolUseId) ?? 'ran'].length;
252
266
  }
253
267
  return Math.max(0, Math.ceil(chars / 4) + images * 1600);
254
268
  }
@@ -361,12 +375,13 @@ export class ToolResultGuard {
361
375
  // with a placeholder if the audit cannot become durable below.
362
376
  console.error(`[tool-result-guard] agent=${this.agentName} staged audit write queued:`, error);
363
377
  }
378
+ const results = new Map(originals.map((call) => [call.id, call.result]));
379
+ const notices = new Map([...results].map(([id, result]) => [id, noticeKind(result)]));
364
380
  const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
365
- ? { type: 'tool_result', toolUseId: block.toolUseId, content: TOOL_RESULT_GUARD_NOTICE, isError: block.isError }
381
+ ? { type: 'tool_result', toolUseId: block.toolUseId, content: GUARD_NOTICES[notices.get(block.toolUseId) ?? 'ran'], isError: block.isError }
366
382
  : block);
367
383
  // What each stub keeps if the batch is withheld, matched to its tool's
368
384
  // result by tool use id, keyed by index as the stubs and files are.
369
- const results = new Map(originals.map((call) => [call.id, call.result]));
370
385
  const kept = new Map<number, string>();
371
386
  for (const [index, block] of content.entries()) {
372
387
  const result = block.type === 'tool_result' ? results.get(block.toolUseId) : undefined;
@@ -379,7 +394,7 @@ export class ToolResultGuard {
379
394
  // which is correct since a reopened guard has no pending batch.
380
395
  const messageId = this.cm.addMessage('user', withheld, metadata, undefined, { holdCompression: true });
381
396
  this.pending = { id, messageId, content, withheld, wireResults,
382
- branch: this.cm.currentBranch().name, submitted: false, durable: false, kept };
397
+ branch: this.cm.currentBranch().name, submitted: false, durable: false, kept, notices };
383
398
  // Durability barrier: the audit must reach Chronicle's chain heads before
384
399
  // the originals can go to a provider. On a failed sync the batch fails
385
400
  // CLOSED: the placeholders go on the wire instead (the turn continues,
@@ -406,7 +421,7 @@ export class ToolResultGuard {
406
421
  if (pending.durable) { pending.submitted = true; return results; }
407
422
  const ids = new Set(pending.wireResults.map((result) => result.toolUseId));
408
423
  return results.map((result) => ids.has(result.toolUseId)
409
- ? { ...result, content: TOOL_RESULT_GUARD_NOTICE } : result);
424
+ ? { ...result, content: GUARD_NOTICES[pending.notices.get(result.toolUseId) ?? 'ran'] } : result);
410
425
  }
411
426
 
412
427
  /** The stream carrying this batch ended without a clean round (abort or
@@ -603,7 +618,7 @@ export class ToolResultGuard {
603
618
  spills.set(index, spill);
604
619
  }
605
620
  const annotated: ContentBlock[] = pending.withheld.map((block, index) => block.type === 'tool_result'
606
- ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index)) }
621
+ ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index), pending.notices.get(block.toolUseId)) }
607
622
  : block);
608
623
  try {
609
624
  this.append({
@@ -113,7 +113,10 @@ export interface SubconsciousConfig {
113
113
  * agent-prefixed identifier.
114
114
  */
115
115
  name?: string;
116
- /** Model id; defaults to the primary agent's (same-model side-process). */
116
+ /** Model id; defaults to the primary agent's (same-model side-process).
117
+ * Another id, compared exactly, makes the subconscious leave out the
118
+ * fields that describe the primary's model, its window and its thinking
119
+ * binding (SUBCONSCIOUS_MODEL_FIELDS); nothing here sets its own. */
117
120
  model?: string;
118
121
  /**
119
122
  * The voice/criteria mode block — recipe-side and co-authored with the
@@ -14,8 +14,9 @@ export type SameRoundThinkTextPolicySource =
14
14
  export interface AutoContinueRun {
15
15
  /** How the agent stops, as the row puts it ("call skip_reply"). */
16
16
  stop: string;
17
- /** The turn before ended in words that were held, not sent, because it
18
- * had already sent with a send tool. */
17
+ /** The turn before ended in words some or all of which were held, not
18
+ * sent, because a send in it may already have posted where they were
19
+ * going. */
19
20
  heldAfterSend?: boolean;
20
21
  /** Set by the scheduler: the continuation is the turn's only cause. */
21
22
  sole?: boolean;
@@ -254,7 +255,18 @@ export interface AgentConfig {
254
255
  * that the record doesn't account for raises the
255
256
  * `thinking-binding-loss` ops alert, once per incident and again if a
256
257
  * call drops more. The provider reports drops only on a request that
257
- * carries the drop control; without it, such a block is refused.
258
+ * carries the drop control; without it, such a block is refused;
259
+ * - the agent is told, in a `[reasoning] <agent>:` system notice of kind
260
+ * `reasoning-loss` stored for it as the framework's other notices are,
261
+ * when a committing compile leaves out a stored reply's reasoning at a
262
+ * change of the request's head, at a branch's first compile, or in a
263
+ * fold or other change to an earlier message that takes a reply of the
264
+ * current turn, and, with `dropBlock` set too, when the provider drops
265
+ * blocks it can place as the agent's stored replies or the stream's
266
+ * rounds, once per incident and again when a call drops more. The
267
+ * framework hears compiles through `Agent.onThinkingStrip` from the
268
+ * agent's first stream on, unless a host has set its own, so an agent
269
+ * driven only through the direct API is never told.
258
270
  * The record and the notes
259
271
  * vouch for requests as agent-framework hands them to membrane, so a
260
272
  * membrane `beforeRequest` hook that changes a request must change every
@@ -546,10 +558,18 @@ export interface InferenceRequest {
546
558
  * agent while a turn was alive: a coalesced render placed in its queue
547
559
  * (wakeToRead), or a message, notice or event whose wake was queued with
548
560
  * it. Once a request that carried the write was answered and, for a wake
549
- * that carries a channel, the agent has since published into that
550
- * channel having read it, or once the write is in neither the agent's
551
- * queue nor its store's current branch, the wake starts no turn. */
561
+ * that carries a channel and isn't marked `ambient`, the agent has since
562
+ * published into that channel having read it, or once the write is in
563
+ * neither the agent's queue nor its store's current branch, the wake
564
+ * starts no turn. */
552
565
  readsDeferredWrite?: string;
566
+ /** Host-owned: this wake's sole cause is this message, stored in its
567
+ * agent's store as it arrived, not deferred (its row's id). Once a
568
+ * request that carried the message was answered and, for a wake that
569
+ * carries a channel and isn't marked `ambient`, the agent has since
570
+ * published into that channel having read it, or once the message isn't
571
+ * on its store's current branch, the wake starts no turn. */
572
+ readsStoredMessage?: string;
553
573
  agentName: string;
554
574
  reason: string;
555
575
  source: string;
@@ -597,11 +617,22 @@ export interface InferenceRequest {
597
617
  * (`chat:ambient`). A coalesced push's wake carries it only to read a
598
618
  * render, and only when every notice the render stands for was. Such a
599
619
  * message asked nothing of the agent, so a wake for it that carries
600
- * `readsDeferredWrite` is dropped once a request that carried the message
601
- * was answered, with no publish asked of the agent. A message with neither
602
- * tag is in doubt, and is judged as an addressed one.
620
+ * `readsDeferredWrite` or `readsStoredMessage` is dropped once a request
621
+ * that carried the message was answered, with no publish asked of the
622
+ * agent. A message with neither tag is in doubt, and is judged as an
623
+ * addressed one.
603
624
  */
604
625
  ambient?: true;
626
+ /**
627
+ * Host-owned: true when the triggering message's server marked it a
628
+ * `chat:system` notice (RFC-001 rev 3): an event about the place, such as
629
+ * a join or the weather changing, not something anyone said. Such a wake
630
+ * names no channel for its turn's plain speech, though it carries
631
+ * `channelId`: a turn it starts alone resolves its locus as a heartbeat's
632
+ * does. Like an ambient one, a wake to read it is done once read. Never
633
+ * set on a coalesced render's wake, which may fold messages too.
634
+ */
635
+ systemNotice?: true;
605
636
  /**
606
637
  * Who spoke the message that triggered this inference, as an adapter-
607
638
  * namespaced opaque id (`<serverId>:user:<authorId>`) — never a display
@@ -211,14 +211,17 @@ export interface ToolResult {
211
211
  /** Whether this was an error (for LLM) */
212
212
  isError?: boolean;
213
213
  /**
214
- * The dispatcher refused the call before it opened, RFC-007's refusal, so
215
- * nothing ran and the call certainly had no effect. Only the dispatcher's
216
- * path, the model's calls and a script's, sets it: the caller may not use
217
- * the tool (allowedTools), a `skip_reply` wake was refused, the call's MCPL
218
- * server is absent or still connecting, the host's policy for that server
219
- * denies the tool, or a fork reached outside its channel. RFC-007's
220
- * lifecycle refuses the call from the same place, so observers see no
221
- * events for it. A check made after the call opened, such as a module's
214
+ * The call was refused before it opened, RFC-007's refusal, so nothing ran
215
+ * and the call certainly had no effect. The dispatcher's path, the model's
216
+ * calls and a script's, sets it: the caller may not use the tool
217
+ * (allowedTools), a `skip_reply` wake was refused, the call's MCPL server is
218
+ * absent or still connecting, the host's policy for that server denies the
219
+ * tool, or a fork reached outside its channel. RFC-007's lifecycle refuses
220
+ * the call from the same place, so observers see no events for it. The
221
+ * framework also sets it on the result it stores for each call in the last
222
+ * round of a response the output limit cut off: such a call was never
223
+ * dispatched or opened, and its result goes into the context with the reply,
224
+ * not through a tool-result event. A check made after the call opened, such as a module's
222
225
  * answer for a name it has no tool for or arguments that don't fit a
223
226
  * schema, is a failure whose text says nothing was done, as is an MCPL
224
227
  * request that couldn't be written. A programmatic call (`executeToolCall`,
@@ -270,6 +273,13 @@ export interface ToolCall {
270
273
  name: string;
271
274
  /** Tool input */
272
275
  input: unknown;
276
+ /**
277
+ * membrane's marker (ToolUseContent.unparseableInput), present when `input`
278
+ * isn't the model's arguments. The framework refuses such a call without
279
+ * running it, except one whose text is JSON `null`, which runs as a call
280
+ * with no arguments.
281
+ */
282
+ unparseableInput?: string;
273
283
  /** The agent that made this tool call. Set by the framework dispatch layer. */
274
284
  callerAgentName?: string;
275
285
  /**
@@ -299,6 +299,21 @@ export interface FrameworkConfig {
299
299
 
300
300
  /** EventGate config. If omitted, all events trigger inference (unchanged default). */
301
301
  gate?: GateOptions;
302
+ /**
303
+ * The heartbeat: the agent's own periodic check-in and reminders, kept by
304
+ * the framework (src/heartbeat/heartbeat.ts), with the tools
305
+ * `heartbeat--heartbeat_status`, `heartbeat--heartbeat_configure`,
306
+ * `heartbeat--heartbeat_trigger`, `heartbeat--reminder_add`,
307
+ * `heartbeat--reminder_list` and `heartbeat--reminder_cancel`. `true` turns it
308
+ * on with its defaults; `false`, null or leaving it out leaves it off. Its
309
+ * wakes go to the gate as heartbeat-mcpl's pushes did, from the server id
310
+ * `heartbeat`, so the agent's rules decide them and a sleep lets them
311
+ * through. It replaces the heartbeat-mcpl server, which is refused beside it
312
+ * (by its id, a heartbeat tool prefix, or its command), at create and at a
313
+ * runtime connect: both would wake the agent. `importFrom` brings that
314
+ * server's schedule and reminders along.
315
+ */
316
+ heartbeat?: boolean | null | import('../heartbeat/heartbeat.js').HeartbeatOptions;
302
317
 
303
318
  /**
304
319
  * Liveness watchdog: fail hard (and let the supervisor restart) if the main
@@ -232,13 +232,16 @@ export interface AgentSettingsExtension {
232
232
  properties: Record<string, unknown>;
233
233
  /** The setting keys this extension owns (must match `properties` keys). */
234
234
  keys: string[];
235
- /** Current values for the extension's keys. */
235
+ /** Current values for the extension's keys. A refused agent_settings call
236
+ * names what changed by comparing every field this returns, before the
237
+ * call and at its refusal. */
236
238
  get(agentName: string): Record<string, unknown>;
237
239
  /** Apply a partial update (only this extension's keys present) and return
238
240
  * the new values. Throw to surface a validation error to the agent. */
239
241
  update(agentName: string, patch: Record<string, unknown>): Record<string, unknown>;
240
242
  /** Restore the listed keys (or all when omitted) to defaults and return the
241
- * new values. Optional — extensions without reset semantics are skipped. */
243
+ * new values. Optional: without it, a reset of every setting skips the
244
+ * extension, and a reset that names one of its keys is refused. */
242
245
  reset?(agentName: string, keys?: string[]): Record<string, unknown>;
243
246
  }
244
247
 
@@ -114,6 +114,23 @@ export type TraceEvent =
114
114
  /** Bounded like `error`. */
115
115
  stack?: string;
116
116
  })
117
+ | (TraceEventBase & {
118
+ /**
119
+ * A turn waiting out its error policy's delay before a retry says so:
120
+ * as the wait begins, with when it is due to end, and again every 60
121
+ * seconds while it lasts, while its agent is the one registered under
122
+ * its name, and never after the wait ends (its time coming, a stop, or
123
+ * an abort). The failure being retried is the `inference:failed` just
124
+ * before it. A host watching for agents gone quiet can tell a waiting
125
+ * one from a stuck one by it.
126
+ */
127
+ type: 'inference:waiting';
128
+ agentName: string;
129
+ /** When the wait is due to end (epoch ms): it counts down by its
130
+ * timers, so a stalled event loop can end it later. Absent for a wait
131
+ * too long for a date. */
132
+ until?: number;
133
+ })
117
134
  | (TraceEventBase & {
118
135
  type: 'inference:exhausted';
119
136
  agentName: string;
@@ -472,6 +489,24 @@ export type TraceEvent =
472
489
  policyName: string;
473
490
  eventCount: number;
474
491
  })
492
+ | (TraceEventBase & {
493
+ /** The reply judge's verdict on a `judge` rule's batch. `verdict`:
494
+ * `reply` or `wait` as the judge answered, `error` when it failed,
495
+ * timed out or answered nothing usable; `woke` is whether the batch
496
+ * then woke the agent (an error does by the host's onError).
497
+ * The judge's reason is in the agent's gate_status, not here: it's
498
+ * written from the agent's context. */
499
+ type: 'gate:judge-verdict';
500
+ policyName: string;
501
+ verdict: 'reply' | 'wait' | 'error';
502
+ woke: boolean;
503
+ eventCount: number;
504
+ durationMs: number;
505
+ model?: string;
506
+ inputTokens?: number;
507
+ cacheReadTokens?: number;
508
+ outputTokens?: number;
509
+ })
475
510
  | (TraceEventBase & {
476
511
  /** A batched wake's turn started without its `[Gate: …]` line (the
477
512
  * gate hands the line to the wake when its message is the newest at
@@ -836,7 +871,7 @@ export type TraceEvent =
836
871
  parkedAdmissions?: number;
837
872
  /** True when undrained turns were force-cancelled (abandon). */
838
873
  abandoned?: boolean;
839
- /** Agents whose turn the abandon had not ended when its grace ran out: a token held with no stream and no check ahead (a puppet tool turn, a store surgery's reservation, a turn finishing its last delivery), or a turn that hadn't reached its next check, the tool-result guard's wait for its speech included. A turn whose stream the abandon cancelled while it was still streaming isn't listed, even one still running when the grace ran out. */
874
+ /** Agents whose turn the abandon had not ended when its grace ran out: a token held with no stream and no check ahead (a puppet tool turn, a store surgery's reservation, a turn finishing its last delivery), or a turn that hadn't reached its next check, the tool-result guard's wait for its speech included, or a turn whose stream's cancel threw with no check ahead, so that its stream goes on. A turn whose stream the abandon cancelled while it was still streaming isn't listed, even one still running when the grace ran out. */
840
875
  unabandonable?: string[];
841
876
  })
842
877
  | (TraceEventBase & {