@animalabs/agent-framework 0.28.0 → 0.29.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 (240) hide show
  1. package/README.md +88 -25
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +4 -1
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  6. package/dist/src/api/mcp-server-core.js +3 -12
  7. package/dist/src/api/mcp-server-core.js.map +1 -1
  8. package/dist/src/api/server.d.ts.map +1 -1
  9. package/dist/src/api/server.js +7 -3
  10. package/dist/src/api/server.js.map +1 -1
  11. package/dist/src/auto-continue.d.ts +2 -0
  12. package/dist/src/auto-continue.d.ts.map +1 -1
  13. package/dist/src/auto-continue.js +5 -2
  14. package/dist/src/auto-continue.js.map +1 -1
  15. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  16. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  17. package/dist/src/code-execution/isolate/runner.js +7 -5
  18. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  19. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.js +3 -2
  21. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  22. package/dist/src/framework.d.ts +164 -36
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1120 -479
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +34 -10
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +118 -48
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/index.d.ts +1 -1
  31. package/dist/src/gate/index.d.ts.map +1 -1
  32. package/dist/src/gate/index.js +1 -1
  33. package/dist/src/gate/index.js.map +1 -1
  34. package/dist/src/gate/types.d.ts +12 -2
  35. package/dist/src/gate/types.d.ts.map +1 -1
  36. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  37. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +34 -8
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/held-writes.d.ts +3 -3
  41. package/dist/src/held-writes.js +3 -3
  42. package/dist/src/host-presentation.d.ts +170 -0
  43. package/dist/src/host-presentation.d.ts.map +1 -0
  44. package/dist/src/host-presentation.js +561 -0
  45. package/dist/src/host-presentation.js.map +1 -0
  46. package/dist/src/index.d.ts +2 -2
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/dist/src/index.js +1 -1
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/line-reader.d.ts +4 -0
  51. package/dist/src/line-reader.d.ts.map +1 -0
  52. package/dist/src/line-reader.js +59 -0
  53. package/dist/src/line-reader.js.map +1 -0
  54. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  55. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  56. package/dist/src/mcpl/channel-registry.js +131 -53
  57. package/dist/src/mcpl/channel-registry.js.map +1 -1
  58. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  59. package/dist/src/mcpl/conversation-router.js +2 -1
  60. package/dist/src/mcpl/conversation-router.js.map +1 -1
  61. package/dist/src/mcpl/convert-block.d.ts +1 -0
  62. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  63. package/dist/src/mcpl/convert-block.js +16 -4
  64. package/dist/src/mcpl/convert-block.js.map +1 -1
  65. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  66. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  67. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  68. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  69. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  70. package/dist/src/mcpl/inbound-source.js +3 -1
  71. package/dist/src/mcpl/inbound-source.js.map +1 -1
  72. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  73. package/dist/src/mcpl/modern-connection.js +11 -7
  74. package/dist/src/mcpl/modern-connection.js.map +1 -1
  75. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  76. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  77. package/dist/src/mcpl/protocol-family.js +25 -3
  78. package/dist/src/mcpl/protocol-family.js.map +1 -1
  79. package/dist/src/mcpl/push-handler.js +2 -2
  80. package/dist/src/mcpl/push-handler.js.map +1 -1
  81. package/dist/src/mcpl/references.d.ts +12 -40
  82. package/dist/src/mcpl/references.d.ts.map +1 -1
  83. package/dist/src/mcpl/references.js +29 -23
  84. package/dist/src/mcpl/references.js.map +1 -1
  85. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  86. package/dist/src/mcpl/server-connection.js +20 -20
  87. package/dist/src/mcpl/server-connection.js.map +1 -1
  88. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  89. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  90. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  91. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  92. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  93. package/dist/src/mcpl/tool-presence.js +18 -17
  94. package/dist/src/mcpl/tool-presence.js.map +1 -1
  95. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  96. package/dist/src/mcpl/tool-signature.js +17 -16
  97. package/dist/src/mcpl/tool-signature.js.map +1 -1
  98. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  99. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  100. package/dist/src/mcpl/toolset-host.js +16 -6
  101. package/dist/src/mcpl/toolset-host.js.map +1 -1
  102. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  103. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.js +180 -50
  105. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  106. package/dist/src/mcpl/transport.d.ts +1 -1
  107. package/dist/src/mcpl/transport.d.ts.map +1 -1
  108. package/dist/src/mcpl/transport.js +16 -16
  109. package/dist/src/mcpl/transport.js.map +1 -1
  110. package/dist/src/mcpl/types.d.ts +28 -12
  111. package/dist/src/mcpl/types.d.ts.map +1 -1
  112. package/dist/src/mcpl/types.js.map +1 -1
  113. package/dist/src/module-registry.d.ts +6 -7
  114. package/dist/src/module-registry.d.ts.map +1 -1
  115. package/dist/src/module-registry.js +6 -7
  116. package/dist/src/module-registry.js.map +1 -1
  117. package/dist/src/modules/api/index.d.ts.map +1 -1
  118. package/dist/src/modules/api/index.js +4 -2
  119. package/dist/src/modules/api/index.js.map +1 -1
  120. package/dist/src/modules/discord/index.d.ts.map +1 -1
  121. package/dist/src/modules/discord/index.js +33 -25
  122. package/dist/src/modules/discord/index.js.map +1 -1
  123. package/dist/src/modules/history/index.js +8 -6
  124. package/dist/src/modules/history/index.js.map +1 -1
  125. package/dist/src/modules/sender-name.d.ts +7 -0
  126. package/dist/src/modules/sender-name.d.ts.map +1 -0
  127. package/dist/src/modules/sender-name.js +10 -0
  128. package/dist/src/modules/sender-name.js.map +1 -0
  129. package/dist/src/modules/workspace/answers.d.ts +5 -1
  130. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/answers.js +16 -1
  132. package/dist/src/modules/workspace/answers.js.map +1 -1
  133. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.js +6 -4
  135. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  136. package/dist/src/modules/workspace/effects.d.ts +4 -2
  137. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  138. package/dist/src/modules/workspace/effects.js +50 -40
  139. package/dist/src/modules/workspace/effects.js.map +1 -1
  140. package/dist/src/modules/workspace/index.d.ts +1 -0
  141. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  142. package/dist/src/modules/workspace/index.js +66 -36
  143. package/dist/src/modules/workspace/index.js.map +1 -1
  144. package/dist/src/modules/workspace/observe.d.ts +51 -20
  145. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/observe.js +120 -82
  147. package/dist/src/modules/workspace/observe.js.map +1 -1
  148. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  149. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  150. package/dist/src/modules/workspace/reconcile.js +133 -37
  151. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  152. package/dist/src/notice-tags.d.ts +79 -0
  153. package/dist/src/notice-tags.d.ts.map +1 -0
  154. package/dist/src/notice-tags.js +83 -0
  155. package/dist/src/notice-tags.js.map +1 -0
  156. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  157. package/dist/src/reasoning-loss-notice.js +5 -3
  158. package/dist/src/reasoning-loss-notice.js.map +1 -1
  159. package/dist/src/silent-heartbeat.d.ts +1 -53
  160. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  161. package/dist/src/silent-heartbeat.js +56 -3
  162. package/dist/src/silent-heartbeat.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +35 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -0
  165. package/dist/src/supplied-text.js +141 -0
  166. package/dist/src/supplied-text.js.map +1 -0
  167. package/dist/src/tool-args.d.ts.map +1 -1
  168. package/dist/src/tool-args.js +9 -8
  169. package/dist/src/tool-args.js.map +1 -1
  170. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  171. package/dist/src/tool-image-ledger.js +2 -1
  172. package/dist/src/tool-image-ledger.js.map +1 -1
  173. package/dist/src/tool-presentation.d.ts.map +1 -1
  174. package/dist/src/tool-presentation.js +5 -4
  175. package/dist/src/tool-presentation.js.map +1 -1
  176. package/dist/src/tune-out/coordinator.d.ts +3 -2
  177. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  178. package/dist/src/tune-out/coordinator.js +24 -26
  179. package/dist/src/tune-out/coordinator.js.map +1 -1
  180. package/dist/src/types/agent.d.ts +23 -9
  181. package/dist/src/types/agent.d.ts.map +1 -1
  182. package/dist/src/types/framework.d.ts +43 -0
  183. package/dist/src/types/framework.d.ts.map +1 -1
  184. package/dist/src/types/module.d.ts +22 -11
  185. package/dist/src/types/module.d.ts.map +1 -1
  186. package/package.json +2 -2
  187. package/src/agent.ts +4 -1
  188. package/src/api/mcp-server-core.ts +3 -14
  189. package/src/api/server.ts +7 -3
  190. package/src/auto-continue.ts +8 -2
  191. package/src/code-execution/isolate/runner.ts +7 -5
  192. package/src/code-execution/isolate/scripts.ts +3 -2
  193. package/src/framework.ts +1132 -492
  194. package/src/gate/event-gate.ts +128 -47
  195. package/src/gate/index.ts +1 -1
  196. package/src/gate/types.ts +12 -2
  197. package/src/heartbeat/heartbeat.ts +48 -9
  198. package/src/held-writes.ts +3 -3
  199. package/src/host-presentation.ts +605 -0
  200. package/src/index.ts +2 -2
  201. package/src/line-reader.ts +58 -0
  202. package/src/mcpl/channel-registry.ts +137 -52
  203. package/src/mcpl/conversation-router.ts +2 -1
  204. package/src/mcpl/convert-block.ts +17 -4
  205. package/src/mcpl/hook-orchestrator.ts +4 -3
  206. package/src/mcpl/inbound-source.ts +3 -1
  207. package/src/mcpl/modern-connection.ts +11 -7
  208. package/src/mcpl/protocol-family.ts +24 -3
  209. package/src/mcpl/push-handler.ts +2 -2
  210. package/src/mcpl/references.ts +31 -23
  211. package/src/mcpl/server-connection.ts +20 -18
  212. package/src/mcpl/skip-reply-wake.ts +35 -0
  213. package/src/mcpl/tool-presence.ts +18 -17
  214. package/src/mcpl/tool-signature.ts +17 -16
  215. package/src/mcpl/toolset-host.ts +18 -6
  216. package/src/mcpl/toolsets-module.ts +142 -50
  217. package/src/mcpl/transport.ts +18 -14
  218. package/src/mcpl/types.ts +29 -12
  219. package/src/module-registry.ts +6 -7
  220. package/src/modules/api/index.ts +4 -2
  221. package/src/modules/discord/index.ts +37 -25
  222. package/src/modules/history/index.ts +8 -6
  223. package/src/modules/sender-name.ts +9 -0
  224. package/src/modules/workspace/answers.ts +20 -2
  225. package/src/modules/workspace/direct-write.ts +4 -3
  226. package/src/modules/workspace/effects.ts +50 -40
  227. package/src/modules/workspace/index.ts +58 -34
  228. package/src/modules/workspace/observe.ts +139 -88
  229. package/src/modules/workspace/reconcile.ts +159 -36
  230. package/src/notice-tags.ts +84 -0
  231. package/src/reasoning-loss-notice.ts +5 -3
  232. package/src/silent-heartbeat.ts +4 -3
  233. package/src/supplied-text.ts +154 -0
  234. package/src/tool-args.ts +9 -8
  235. package/src/tool-image-ledger.ts +2 -1
  236. package/src/tool-presentation.ts +5 -4
  237. package/src/tune-out/coordinator.ts +25 -27
  238. package/src/types/agent.ts +23 -9
  239. package/src/types/framework.ts +44 -0
  240. package/src/types/module.ts +23 -11
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Text the framework didn't write (what a participant or a server supplied),
3
+ * put beside structure of the framework's own: the reply judge's document,
4
+ * and notices the framework writes. A value that can end its line can start
5
+ * a line that reads as the framework's own.
6
+ */
7
+
8
+ import { CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS, NOTICE_TAG } from './notice-tags.js';
9
+
10
+ /** Every form of line break a reader might take for one: CR LF, CR, LF,
11
+ * vertical tab, form feed, NEL, and the Unicode line and paragraph separators. */
12
+ const LINE_BREAK = /\r\n|[\r\n\u000b\u000c\u0085\u2028\u2029]/g;
13
+
14
+ /** Text with each line break, in whatever form, as `\n`. */
15
+ export function normalizeLineBreaks(text: string): string {
16
+ return text.replace(LINE_BREAK, '\n');
17
+ }
18
+
19
+ /** A value kept to one line: each run of line breaks, in whatever form, as one
20
+ * space, so it can't end the line it's put in or start one of its own. */
21
+ export function oneLine(text: string): string {
22
+ return normalizeLineBreaks(text).replace(/\n+/g, ' ');
23
+ }
24
+
25
+ const INVISIBLE = '\\p{Cf}\\p{Default_Ignorable_Code_Point}';
26
+ const SKIPPED = `[\\s\\p{Cc}${INVISIBLE}]`;
27
+ const tagWord = (word: string): string =>
28
+ [...word].map((c) => (/[\\^$.*+?()[\]{}|/]/.test(c) ? `\\${c}` : c)).join(`${SKIPPED}*`);
29
+ const NOTICE_OPENING = new RegExp(
30
+ `\\[(?=${SKIPPED}*(?:${[...Object.values(NOTICE_TAG), ...CONTEXT_MANAGER_TAGS, ...MEMBRANE_TAGS].map((tag) => tag.split(' ').map(tagWord).join(`${SKIPPED}+`)).join('|')})(?![\\p{L}\\p{N}]))`,
31
+ 'giu',
32
+ );
33
+
34
+ /**
35
+ * Texts read as one string, joined by line breaks, with a backslash put
36
+ * before the bracket of each opening of a listed tag (NOTICE_TAG,
37
+ * CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS) that hasn't one before it in its own
38
+ * text. A text `marks` says false of is read and never changed.
39
+ *
40
+ * Every opening, not only one that starts a line: membrane's XML formatter
41
+ * writes `participant: ` before each message, so the framework's own
42
+ * openings stand mid-line there too. It joins a message's text blocks with
43
+ * line breaks and sets its images apart from that text, while the native
44
+ * formatter sends each block on its own, so an opening split across a
45
+ * message's text blocks is one either way. In any case, by Unicode's case folding, and
46
+ * through whitespace, control and invisible characters anywhere inside the
47
+ * bracket, which a model reads past.
48
+ */
49
+ export function markedTexts(texts: readonly string[], marks: readonly boolean[] = []): string[] {
50
+ const starts: number[] = [];
51
+ let at = 0;
52
+ for (const text of texts) {
53
+ starts.push(at);
54
+ at += text.length + 1;
55
+ }
56
+ const offsets: number[][] = texts.map(() => []);
57
+ for (const match of texts.join('\n').matchAll(NOTICE_OPENING)) {
58
+ let holder = texts.length - 1;
59
+ while (starts[holder]! > match.index) holder--;
60
+ const offset = match.index - starts[holder]!;
61
+ if (marks[holder] !== false && texts[holder]![offset - 1] !== '\\') offsets[holder]!.push(offset);
62
+ }
63
+ return texts.map((text, i) => withMarks(text, offsets[i]!));
64
+ }
65
+
66
+ function withMarks(text: string, offsets: readonly number[]): string {
67
+ let marked = '';
68
+ let from = 0;
69
+ for (const offset of offsets) {
70
+ marked += `${text.slice(from, offset)}\\`;
71
+ from = offset;
72
+ }
73
+ return marked + text.slice(from);
74
+ }
75
+
76
+ function closedWithin(text: string, at: number): boolean {
77
+ let depth = 0;
78
+ for (let i = at; i < text.length; i++) {
79
+ if (text[i] === '[') depth++;
80
+ else if (text[i] === ']' && --depth === 0) return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function unmarkedOpenings(text: string): number[] {
86
+ return [...text.matchAll(NOTICE_OPENING)].map((match) => match.index).filter((at) => text[at - 1] !== '\\');
87
+ }
88
+
89
+ export function markedText(text: string): string {
90
+ return markedTexts([text])[0]!;
91
+ }
92
+
93
+ export function markNoticeOpenings<T extends object>(blocks: readonly T[], marks: (index: number) => boolean = () => true): T[] {
94
+ const at: number[] = [];
95
+ blocks.forEach((block, index) => {
96
+ const { type, text } = (block ?? {}) as { type?: unknown; text?: unknown };
97
+ if (type === 'text' && typeof text === 'string') at.push(index);
98
+ });
99
+ const textOf = (index: number): string => (blocks[index] as unknown as { text: string }).text;
100
+ const marked = markedTexts(at.map(textOf), at.map(marks));
101
+ const out = [...blocks];
102
+ at.forEach((index, i) => {
103
+ if (marked[i] !== textOf(index)) out[index] = { ...blocks[index]!, text: marked[i] };
104
+ });
105
+ return out;
106
+ }
107
+
108
+ export function suppliedValue(value: unknown): string {
109
+ return markedText(oneLine(String(value)));
110
+ }
111
+
112
+ export function cutToFit(text: string, length: number): string {
113
+ let end = Math.max(0, Math.floor(length));
114
+ if (end >= text.length) return text;
115
+ for (;;) {
116
+ const last = text.charCodeAt(end - 1);
117
+ if (end < text.length && last >= 0xd800 && last <= 0xdbff) end--;
118
+ else if (text[end - 1] === '\\' && text[end] === '[') end--;
119
+ else {
120
+ const cut = text.slice(0, end);
121
+ const open = unmarkedOpenings(cut).find((at) => !closedWithin(cut, at));
122
+ if (open === undefined) return cut;
123
+ end = open;
124
+ }
125
+ }
126
+ }
127
+
128
+ export function showsAsItself(value: string): boolean {
129
+ return value !== '' && suppliedValue(value) === value && value.isWellFormed() && !EDGE_OR_QUOTE.test(value);
130
+ }
131
+
132
+ const EDGE_OR_QUOTE = /^\s|\s$|["\\]/;
133
+
134
+ export function quotedValue(value: unknown, space?: number): string {
135
+ return (JSON.stringify(value, null, space) ?? 'null').replace(/"(?:[^"\\]|\\.)*"/g, (literal) => quotedString(JSON.parse(literal)));
136
+ }
137
+
138
+ function quotedString(text: string): string {
139
+ let quoted = '';
140
+ let from = 0;
141
+ for (const match of text.matchAll(NOTICE_OPENING)) {
142
+ quoted += `${jsonText(text.slice(from, match.index))}\\u005b`;
143
+ from = match.index + 1;
144
+ }
145
+ return `"${quoted}${jsonText(text.slice(from))}"`;
146
+ }
147
+
148
+ // JSON.stringify leaves NEL, U+2028 and U+2029 as they are.
149
+ const jsonText = (text: string): string =>
150
+ JSON.stringify(text).slice(1, -1).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
151
+
152
+ export function nameToTypeBack(name: string): string {
153
+ return showsAsItself(name) ? name : quotedValue(name);
154
+ }
package/src/tool-args.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { nameToTypeBack, quotedValue } from './supplied-text.js';
1
2
  import type { ToolDefinition } from './types/index.js';
2
3
 
3
4
  /** A run of 16 or more digits, which membrane's XML parser passes as text for
@@ -79,20 +80,20 @@ export function markedInputRefusal(call: { name: string; unparseableInput?: stri
79
80
  // object. A block still open when a stream breaks the API's block order
80
81
  // is marked the same way, and one that had stopped with its object's JSON,
81
82
  // 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
+ if (/^[ \t\n\r]*$/.test(text)) return `Nothing was done: ${nameToTypeBack(call.name)} its arguments never arrived; call it again with them whole.`;
83
84
  let parsed: unknown;
84
85
  try {
85
86
  parsed = JSON.parse(text);
86
87
  } 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
+ return `Nothing was done: ${nameToTypeBack(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
  }
89
90
  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.`;
91
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) return `Nothing was done: ${nameToTypeBack(call.name)} ${notAnObject(parsed)}.`;
92
+ return `Nothing was done: ${nameToTypeBack(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
  }
93
94
 
94
95
  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
+ return `Nothing was done: ${nameToTypeBack(call.name)} didn't run, because the response it was in was cut off at the output limit. ` +
96
97
  'To make the call, send less in one response: shorter text before it, shorter arguments, or fewer calls at once.';
97
98
  }
98
99
 
@@ -216,7 +217,7 @@ export function toolArgsProblems(
216
217
  const kinds = admittedKinds(declared(key));
217
218
  if (value !== undefined && (value !== null || takesNull(declared(key)))) continue;
218
219
  missing.add(key);
219
- const wanted = kinds.length > 0 ? `"${key}" (${oneOf(kinds.map(withArticle))})` : `"${key}"`;
220
+ const wanted = kinds.length > 0 ? `${quotedValue(key)} (${oneOf(kinds.map(withArticle))})` : quotedValue(key);
220
221
  problems.push(`needs ${wanted}, and this call gave ${value === null ? 'null' : 'none'}`);
221
222
  }
222
223
  if (missing.size > 0 && declaresKeysPlainly(schema as Record<string, unknown>)) {
@@ -235,11 +236,11 @@ export function toolArgsProblems(
235
236
  const kind = kindOf(value);
236
237
  const largeInteger = kind === 'string' && kinds.includes('number') && LARGE_INTEGER_TEXT.test(value as string);
237
238
  if (kinds.length > 0 && !kinds.includes(kind) && !largeInteger) {
238
- problems.push(`"${key}" must be ${oneOf(kinds.map(withArticle))}, and this call gave ${withArticle(kind)}`);
239
+ problems.push(`${quotedValue(key)} must be ${oneOf(kinds.map(withArticle))}, and this call gave ${withArticle(kind)}`);
239
240
  continue;
240
241
  }
241
242
  if (Array.isArray(param.enum) && !param.enum.includes(value)) {
242
- problems.push(`"${key}" must be one of ${oneOf(param.enum.map(shown))}, and this call gave ${shown(value)}`);
243
+ problems.push(`${quotedValue(key)} must be one of ${oneOf(param.enum.map((e) => quotedValue(e)))}, and this call gave ${shown(value)}`);
243
244
  }
244
245
  }
245
246
  return problems;
@@ -26,6 +26,7 @@
26
26
  * of bytes from another surface is the failure this exists to end.
27
27
  */
28
28
 
29
+ import { NOTICE_TAG } from './notice-tags.js';
29
30
  import { createHash, randomBytes } from 'node:crypto';
30
31
  import { REFERENCE_STUB_MAX_CHARS, referenceTypeLabel } from './mcpl/references.js';
31
32
  import { safeSlice } from './safe-slice.js';
@@ -256,7 +257,7 @@ export type ParsedImagePlaceholder =
256
257
  const PLACEHOLDER_RE = /\[image: ([a-z0-9!#$&^_.+-]{1,127}\/[a-z0-9!#$&^_.+-]{1,127}), (~?[\d.]{1,16}(?:B|KB|MB))(?:, ref (img_[0-9a-z]{1,16}_\d{1,16}))?\]/g;
257
258
  /** `[ref_1_ab3d] cam.png — image/png, 1.2MB claimed — from tool result — …`
258
259
  * (one line, at most REFERENCE_STUB_MAX_CHARS long: see parseImagePlaceholders). */
259
- const REFERENCE_STUB_RE = /^\[(ref_[0-9a-z]+_[0-9a-z]+)\] ([^\n]*)$/gm;
260
+ const REFERENCE_STUB_RE = new RegExp(`^\\[(${NOTICE_TAG.referenceId}_[0-9a-z]+_[0-9a-z]+)\\] ([^\\n]*)$`, 'gm');
260
261
  const IMAGE_MIME_RE = /\bimage\/[a-z0-9!#$&^_.+-]+/i;
261
262
 
262
263
  /**
@@ -32,7 +32,8 @@ const MAX_BYTES = 256 * 1024;
32
32
  */
33
33
  const NONBLOCK = typeof constants.O_NONBLOCK === 'number' ? constants.O_NONBLOCK : 0;
34
34
  const MAX_DESCRIPTION_CHARS = 32768;
35
- const protectedNames = new Set(['set_tool_visibility', 'set_tool_description', 'workspace--read']);
35
+ // The toolsets meta-tools are how a hidden tool is reached again, so they stay.
36
+ const protectedNames = new Set(['set_tool_visibility', 'set_tool_description', 'workspace--read', 'toolsets--open', 'toolsets--act', 'toolsets--look', 'toolsets--close']);
36
37
  export const isPresentationTool = (name: string): boolean => name === 'set_tool_visibility' || name === 'set_tool_description';
37
38
  const hash = (value: string): string => createHash('sha256').update(value).digest('hex');
38
39
 
@@ -175,7 +176,7 @@ function lockLeftBehind(error: unknown): string {
175
176
  }
176
177
 
177
178
  export function presentationTools(path: string): ToolDefinition[] {
178
- const signpost = ` Read the generated catalogue with workspace--read {"path":${JSON.stringify(path)},"limit":140}. Hidden tools remain available; changes affect the next newly compiled request, not an in-flight stream.`;
179
+ const signpost = ` Read the generated catalogue with workspace--read {"path":${JSON.stringify(path)},"limit":140}. A hidden tool moves out of your tool list into the host's "settings" toolset (an MCPL server's tool, into that server's tab): toolsets--look lists it there, and toolsets--act calls it once that toolset is open. Changes affect the next newly compiled request, not an in-flight stream.`;
179
180
  return [
180
181
  {name:'set_tool_visibility', description:'Show or hide a tool definition. Hiding does not revoke permission.' + signpost,
181
182
  inputSchema:{type:'object',properties:{name:{type:'string'},visible:{type:'boolean'}},required:['name','visible'],additionalProperties:false}},
@@ -341,7 +342,7 @@ export class ToolPresentation {
341
342
  if (field === 'description' && typeof args.description === 'string' && characters(args.description) > MAX_DESCRIPTION_CHARS)
342
343
  throw new PresentationRefusal(`Nothing was changed: the description is ${count(characters(args.description))} characters, over the ${count(MAX_DESCRIPTION_CHARS)} allowed. Shorten it and call set_tool_description again.`);
343
344
  if (field === 'visible' && args.visible === false && protectedNames.has(name))
344
- throw new PresentationRefusal(`Nothing was changed: ${name} can't be hidden, since workspace--read, set_tool_visibility and set_tool_description are how you read the catalogue and undo these changes. Calling this again won't change that.`);
345
+ throw new PresentationRefusal(`Nothing was changed: ${name} can't be hidden, since workspace--read, set_tool_visibility, set_tool_description and the toolsets-- tools are how you read the catalogue, reach hidden tools and undo these changes. Calling this again won't change that.`);
345
346
  return name;
346
347
  }
347
348
  }
@@ -356,7 +357,7 @@ export function renderCatalogue(snapshot: PresentationSnapshot): string {
356
357
  '# Tool catalogue — generated from currently available tools', '',
357
358
  `Revision: ${snapshot.revision}`, '',
358
359
  `${snapshot.entries.length} tools: ${visible} visible, ${snapshot.entries.length-visible} hidden.`,
359
- 'Visible = supplied in new requests. Hidden = stored away, still available.',
360
+ 'Visible = not hidden by you: in your tool list, unless the host keeps it in its "settings" toolset or a server\'s tab (toolsets--look shows those). Hidden = moved into the "settings" toolset, or its MCPL server\'s tab: toolsets--look shows it, toolsets--act calls it once that toolset is open.',
360
361
  'These are your visibility choices, not fixed primary/secondary rankings.',
361
362
  'Copy exact names from the index; similar-sounding names may not exist.',
362
363
  'Show a hidden tool: set_tool_visibility {"name":"EXACT_NAME","visible":true}.',
@@ -15,6 +15,8 @@
15
15
  * lifecycle facts (the [Gate: …] precedent).
16
16
  */
17
17
 
18
+ import { NOTICE_TAG } from '../notice-tags.js';
19
+ import { cutToFit, suppliedValue } from '../supplied-text.js';
18
20
  import { randomUUID } from 'node:crypto';
19
21
  import type { ContentBlock } from '@animalabs/membrane';
20
22
  import type { StoredMessage } from '@animalabs/context-manager';
@@ -38,14 +40,14 @@ const WAKE_COALESCE_MS = 2_000;
38
40
  const EXPIRY_RETRY_MS = 60_000;
39
41
 
40
42
  /** Ends a backlog line cut to fit in one of the residents' messages. */
41
- const CUT_MARK = " [cut to fit; the whole message is in the channel's history]";
43
+ const CUT_MARK = ` [${NOTICE_TAG.cutToFit}; the whole message is in the channel's history]`;
42
44
 
43
45
  /**
44
46
  * tune_out's refusal of a channel already tuned out: its settings, then what
45
47
  * its transport is now where the repeat tried to open it again (`transport`,
46
48
  * from the framework's dispatch), then how to change the settings.
47
49
  */
48
- export function alreadyTunedOutText(shown: string, p: TuneOutParams, transport?: string): string {
50
+ export function alreadyTunedOutText(quoted: string, p: TuneOutParams, transport?: string): string {
49
51
  // A duration so long its end is past what a Date can hold has no time to show.
50
52
  const expires = p.expiresAtMs !== undefined ? new Date(p.expiresAtMs) : undefined;
51
53
  const until = expires && Number.isFinite(expires.getTime())
@@ -53,21 +55,13 @@ export function alreadyTunedOutText(shown: string, p: TuneOutParams, transport?:
53
55
  : '';
54
56
  // A thrown error's words may end without a stop.
55
57
  const now = transport === undefined ? '' : /[.!?]$/.test(transport) ? `${transport} ` : `${transport}. `;
56
- return `Nothing changed${transport !== undefined ? ' in the tune-out' : ''}: ${JSON.stringify(shown)} is already `
58
+ return `Nothing changed${transport !== undefined ? ' in the tune-out' : ''}: ${quoted} is already `
57
59
  + `tuned out, with cadenceSeconds ${p.cadenceSeconds}, backlogCap ${p.backlogCap} and maxWakes ${p.maxWakes}${until}. `
58
60
  + now
59
61
  + 'To change those, cancel it first with tune_out mode "cancel", which delivers its backlog to you now, then call '
60
62
  + 'tune_out again with the new settings.';
61
63
  }
62
64
 
63
- /** The first `length` code units of `text`, one fewer where the last would be
64
- * the first half of a surrogate pair. */
65
- function prefixOf(text: string, length: number): string {
66
- const cut = text.slice(0, Math.max(0, length));
67
- const last = cut.charCodeAt(cut.length - 1);
68
- return last >= 0xd800 && last <= 0xdbff ? cut.slice(0, -1) : cut;
69
- }
70
-
71
65
  /** What a thrown value says, without throwing: an Error's message, or the
72
66
  * value as text, which String() can't give for an object with no prototype. */
73
67
  function thrownReason(error: unknown): string {
@@ -153,6 +147,10 @@ export class TuneOutCoordinator {
153
147
  return this.channelRegistry.nameShown({ serverId, channelId });
154
148
  }
155
149
 
150
+ private quotedShown(serverId: string, channelId: string): string {
151
+ return this.channelRegistry.quotedShown({ serverId, channelId });
152
+ }
153
+
156
154
  /** Restart cadence + expiry timers and re-anchor after a process restart. */
157
155
  resumeActiveEpochs(): void {
158
156
  let oldestAnchor: number | null = null;
@@ -244,14 +242,14 @@ export class TuneOutCoordinator {
244
242
  if (this.hooks.isForkBound({ serverId, channelId })) {
245
243
  return {
246
244
  ok: false,
247
- error: `Nothing changed: a conversation is bound to ${JSON.stringify(this.shown(serverId, channelId))}, so its messages go to that `
245
+ error: `Nothing changed: a conversation is bound to ${this.quotedShown(serverId, channelId)}, so its messages go to that `
248
246
  + "conversation, and tune_out won't divert a channel while one is. Calling tune_out on it again won't change "
249
247
  + 'that until the conversation ends, which happens only once the channel has been quiet for a while. Tune '
250
248
  + 'out a different channel, or ask whoever runs this host if this one should be diverted.',
251
249
  };
252
250
  }
253
251
  const current = this.channelRegistry.getTuneOutState(serverId, channelId);
254
- if (current) return { ok: false, error: alreadyTunedOutText(this.shown(serverId, channelId), current.params), alreadyTunedOut: true };
252
+ if (current) return { ok: false, error: alreadyTunedOutText(this.quotedShown(serverId, channelId), current.params), alreadyTunedOut: true };
255
253
  // A tool's input reaches here as the model wrote it, and the store can't
256
254
  // keep a NaN or an Infinity: its append would throw after the registry
257
255
  // had marked the channel tuned out. So each option is read as a number
@@ -337,7 +335,7 @@ export class TuneOutCoordinator {
337
335
  const ended = this.channelRegistry.cancelTuneOut(serverId, channelId, 'open', source);
338
336
  if (!ended) {
339
337
  // Its reader is the resident's tune_out: cancel_tuneout finds the channel first.
340
- return { ok: false, error: this.notTunedOut(this.shown(serverId, channelId), 'you') };
338
+ return { ok: false, error: this.notTunedOut(this.quotedShown(serverId, channelId), 'you') };
341
339
  }
342
340
  this.clearTimers(serverId, channelId);
343
341
  this.divertedSinceInvocation.delete(this.key(serverId, channelId));
@@ -360,7 +358,7 @@ export class TuneOutCoordinator {
360
358
  // start at 0 and take them all.
361
359
  const cap = Math.floor(ended.params.backlogCap);
362
360
  const authorOf = (m: Pick<StoredMessage, 'participant' | 'metadata'>): string =>
363
- (m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant;
361
+ suppliedValue((m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant);
364
362
  const lineOf = (m: Pick<StoredMessage, 'participant' | 'content' | 'metadata'>): string => {
365
363
  const text = m.content
366
364
  .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
@@ -370,10 +368,10 @@ export class TuneOutCoordinator {
370
368
  };
371
369
  // The channel as the resident and the subconscious are shown it.
372
370
  const name = this.shown(serverId, channelId);
373
- const lifecycle = `[Tune-out: ${name} cancelled — ${reason}, ${ended.wakeCount} wake${ended.wakeCount === 1 ? '' : 's'}]`;
371
+ const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${ended.wakeCount} wake${ended.wakeCount === 1 ? '' : 's'}]`;
374
372
  const render = (lines: string[], truncated: number): string => {
375
373
  const attrs = [
376
- `channel="${name}"`,
374
+ `channel=${this.quotedShown(serverId, channelId)}`,
377
375
  `messages=${backlog.length}`,
378
376
  ...(truncated > 0 ? [`truncated=${truncated} (oldest; fetch_history covers the rest)`] : []),
379
377
  ];
@@ -417,7 +415,7 @@ export class TuneOutCoordinator {
417
415
  // What of the newest line's text fits beside the frame and the mark. A
418
416
  // cut shows some of it or none: a room under one character, or one
419
417
  // that would split a surrogate pair, leaves the head empty.
420
- const head = prefixOf(lines.at(-1) ?? '', room - frameShowing(1) - CUT_MARK.length);
418
+ const head = cutToFit(lines.at(-1) ?? '', room - frameShowing(1) - CUT_MARK.length);
421
419
  if (kept === 0 && head !== '') {
422
420
  lines = [head + CUT_MARK];
423
421
  shown = shown.slice(-1);
@@ -480,7 +478,7 @@ export class TuneOutCoordinator {
480
478
  : '';
481
479
  this.hooks.addMessage(
482
480
  'user',
483
- [{ type: 'text', text: `[Tune-out cancelled: ${this.shown(serverId, channelId)} — ${reason}.${truncNote}${heldNote}]` }],
481
+ [{ type: 'text', text: `[${NOTICE_TAG.tuneOutCancelled}: ${this.shown(serverId, channelId)} — ${reason}.${truncNote}${heldNote}]` }],
484
482
  { system: true, kind: 'tune-out-cancel-report', channelId },
485
483
  subName,
486
484
  );
@@ -664,8 +662,8 @@ export class TuneOutCoordinator {
664
662
 
665
663
  const shown = this.shown(serverId, channelId);
666
664
  const notice = trigger === 'wake'
667
- ? `[Tune-out wake: ${shown} — ${count} addressed message${count === 1 ? '' : 's'}]`
668
- : `[Tune-out cadence: ${shown} — ${count} diverted message${count === 1 ? '' : 's'} since your last look]`;
665
+ ? `[${NOTICE_TAG.tuneOutWake}: ${shown} — ${count} addressed message${count === 1 ? '' : 's'}]`
666
+ : `[${NOTICE_TAG.tuneOutCadence}: ${shown} — ${count} diverted message${count === 1 ? '' : 's'} since your last look]`;
669
667
  this.hooks.addMessage(
670
668
  'user',
671
669
  [{ type: 'text', text: notice }],
@@ -709,7 +707,7 @@ export class TuneOutCoordinator {
709
707
  position: 'system',
710
708
  content: [{
711
709
  type: 'text',
712
- text: `[Standing dispositions]\n${entries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}`,
710
+ text: `[${NOTICE_TAG.standingDispositions}]\n${entries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}`,
713
711
  }],
714
712
  };
715
713
  }
@@ -809,7 +807,7 @@ export class TuneOutCoordinator {
809
807
  if (shared) return { success: false, error: shared, isError: true };
810
808
  const channel = named[0];
811
809
  if (!channel || !this.channelRegistry.getTuneOutState(channel.serverId, channel.channelId)) {
812
- return { success: false, error: this.notTunedOut(name, 'the resident'), isError: true };
810
+ return { success: false, error: this.notTunedOut(JSON.stringify(name), 'the resident'), isError: true };
813
811
  }
814
812
  const note = typeof input.text === 'string' && input.text.trim() ? input.text.trim() : undefined;
815
813
  const result = this.cancel(channel.serverId, channel.channelId, 'subconscious', 'subconscious judgment', note);
@@ -874,7 +872,7 @@ export class TuneOutCoordinator {
874
872
  }
875
873
 
876
874
  /**
877
- * A cancel of a channel that isn't tuned out, by the name the caller gave
875
+ * A cancel of a channel that isn't tuned out, by the name, quoted, the caller gave
878
876
  * or knows it by: which channels are, so the next call can name one, and
879
877
  * how a tune-out ends without one. `backlogTo`
880
878
  * names the resident as the message's reader knows it: "you" to the
@@ -884,11 +882,11 @@ export class TuneOutCoordinator {
884
882
  * neither this list nor the name's lookup (channelsNamed, which reads the
885
883
  * registered channels too), and the text claims nothing about it.
886
884
  */
887
- private notTunedOut(name: string, backlogTo: 'you' | 'the resident'): string {
888
- const active = this.listActive().map((e) => JSON.stringify(this.shown(e.serverId, e.channelId)));
885
+ private notTunedOut(quoted: string, backlogTo: 'you' | 'the resident'): string {
886
+ const active = this.listActive().map((e) => this.quotedShown(e.serverId, e.channelId));
889
887
  const opening = active.length === 0
890
888
  ? 'no connected channel is tuned out now.'
891
- : `no connected channel ${JSON.stringify(name)} is tuned out. Connected channels tuned out now: `
889
+ : `no connected channel ${quoted} is tuned out. Connected channels tuned out now: `
892
890
  + `${active.join(', ')}.`;
893
891
  return `Nothing was cancelled: ${opening} A tune-out also ends by itself when its duration runs out, or at the `
894
892
  + `first wake past its wake budget, and its backlog goes to ${backlogTo} then.`;
@@ -55,13 +55,15 @@ export interface AgentConfig {
55
55
  * Enforced at dispatch for registered agent callers; omitted means 'all'.
56
56
  * - 'all': all available tools
57
57
  * - string[]: exact tool names, including module--utility names for utilities.
58
- * Utilities aren't tools of their own: an agent's model and its scripts are
59
- * offered them only through 'utils', whose run checks the utility's own
60
- * name. An agent that should use a utility lists both 'utils' and
61
- * 'module--utility'. The name alone passes the dispatch check for any call
62
- * by that name, whether the host makes it with executeToolCall on the
63
- * agent's behalf or the model or a script makes it without being offered
64
- * it. The slot is hidden when no utility is permitted.
58
+ * Utilities and the host's other rarely-used tools are in the host's
59
+ * built-in toolset ("settings"), out of the tool list: the model reaches
60
+ * them with toolsets--open {toolset:"settings"} and toolsets--act, each
61
+ * checked under the tool's own name. An agent that should use a utility
62
+ * from its model lists 'toolsets--open', 'toolsets--act' and
63
+ * 'module--utility' (an agent without the two meta-tools keeps every other
64
+ * tool in its list, but no utility). The name alone passes the dispatch
65
+ * check for any call by that name, whether the host makes it with
66
+ * executeToolCall on the agent's behalf or the model or a script makes it.
65
67
  * Trusted module calls are exempt. Genuinely unidentified public callers,
66
68
  * including unknown legacy names, remain unrestricted. Framework-created
67
69
  * identities have tool authority only while registered.
@@ -580,8 +582,9 @@ export interface InferenceRequest {
580
582
  * the turn's auto-published plain-text speech here so a single TRUNK agent
581
583
  * replies in the channel it is answering — not the process-global most-recent-
582
584
  * inbound locus, which a concurrent message elsewhere can hijack (item-3
583
- * redux). Undefined for non-channel wakes (heartbeat, timers, module events),
584
- * which correctly fall back to the global default channel, except an
585
+ * redux). Undefined for non-channel wakes (heartbeat, timers, module events).
586
+ * A heartbeat's turn takes no channel at all, and holds its plain speech as
587
+ * drafts; any other falls back to the global default channel, except an
585
588
  * ephemeral run's (a subagent's), which takes no global fallback.
586
589
  */
587
590
  channelId?: string;
@@ -604,6 +607,17 @@ export interface InferenceRequest {
604
607
  * it starts. Hosts don't set it.
605
608
  */
606
609
  autoContinueRun?: AutoContinueRun;
610
+ /**
611
+ * Framework-set: whether this wake's turn is a heartbeat's, which has no
612
+ * channel and holds its plain speech as drafts. True on a wake that
613
+ * continues a heartbeat's turn with no channel of its own (an autoContinue
614
+ * continuation, a bounce's resend wake, a ` !` continuation, and a re-arm,
615
+ * which copies its trigger), and on a merged batch whose causes include a
616
+ * heartbeat's wake and name no channel; false on a merged batch with none,
617
+ * so the batch's first request doesn't decide it. Unset, a wake is a
618
+ * heartbeat's when its `source` is `heartbeat`. Hosts don't set it.
619
+ */
620
+ heartbeatTurn?: boolean;
607
621
  /**
608
622
  * True when the triggering message explicitly addressed the agent (mention,
609
623
  * reply-to-bot, DM — `chat:addressed` in MCPL RFC-001 terms). When a wake
@@ -218,6 +218,50 @@ export interface FrameworkConfig {
218
218
  */
219
219
  providerHold?: ProviderHoldHook;
220
220
 
221
+ /**
222
+ * Delays before a turn whose retries ran out is sent again, in ms (default
223
+ * `[120000, 480000, 1920000]`: 2, 8 and 32 minutes), or `false` never to.
224
+ * When a persistent agent's turn ends exhausted on a transient failure (a
225
+ * server error, an overload, a rate limit, a network failure, or a timeout
226
+ * membrane marks retryable and not as a deadline it reports as run out, such
227
+ * as a 408 or a socket's ETIMEDOUT), its trigger is queued again after the
228
+ * first delay, as a wake of its own. It opens where the exhausted turn's
229
+ * plain speech was going: its locus, where a message mid-turn may have moved
230
+ * it, else its trigger channel. Carrying that channel, it ranks as addressed
231
+ * in a merged wake's choice of channel, so a newer ambient wake from
232
+ * elsewhere doesn't take it; an autoContinue continuation's still yields its
233
+ * channel to any wake's, as it does on main; a continuation's, such as a
234
+ * budget restart's, names no author. A re-arm that would have to guess a
235
+ * channel isn't sent again: one whose turn was woken for a channel id that
236
+ * names no one registered channel and never found one, and whose request
237
+ * names none of its own. A failure that states a wait is held for that wait
238
+ * first, as any turn's is, and a deadline ends the turn as before, whether
239
+ * the host set it or membrane marks one as run out: the runtime's or
240
+ * undici's on a sent request, the Anthropic adapter's watchdogs, or the
241
+ * Anthropic SDK's timeout, which covers its connect timeout too. Until one
242
+ * of the re-armed turn's rounds has run or had its tool calls cut off, its
243
+ * failure gets no retries from the error policy: on another transient
244
+ * failure the next delay follows, until the list runs out, and a failure of
245
+ * any other kind ends the re-arming. A wake for the agent while it waits
246
+ * starts its turn at once, with the re-armed trigger merged in. That turn is
247
+ * the wake's: the error policy retries its failures. A completed turn starts
248
+ * the count again, and so does a message said to the agent: not a system
249
+ * message or notice; not an edit, reaction or deletion, unless an edit is
250
+ * the first version of a message the agent sees; not a message its own view
251
+ * leaves out, such as one a tune-out holds from it; and not the agent's own
252
+ * schedule, the heartbeat's tick or reminder, a self-wake timer it set, or a
253
+ * script's wake. A notice a module or an MCPL server stores as a message
254
+ * counts. A message in the residents' shared slot counts for each resident
255
+ * that reads it. Any other wake leaves the count going on. A re-armed turn's
256
+ * transient failure writes no `[inference-failed]` marker unless a round of
257
+ * it ran or had its tool calls cut off; any other failure of it writes one.
258
+ * Each counts toward the hard-down streak. Ephemeral runs and conversation
259
+ * forks are never re-armed. The wait and the count are kept in memory:
260
+ * `stop()` drops them, and so does a restart. The wait isn't a turn, so
261
+ * `abortInference()` doesn't end it.
262
+ */
263
+ exhaustedTurnRearmMs?: number[] | false;
264
+
221
265
  /** Interval for periodic store sync in milliseconds (default: 1000ms, 0 to disable) */
222
266
  syncIntervalMs?: number;
223
267
 
@@ -72,10 +72,11 @@ export type ModuleMessage = StoredMessage & {
72
72
  * True on a message a tune-out holds from the residents: one that arrived
73
73
  * in a tuned-out channel, stamped `metadata.tuneOut` as it came in. The
74
74
  * store keeps it, but no resident is shown the row itself: no resident's
75
- * compiled view includes it, during the tune-out or after it, and a tool
76
- * boundary of a turn alive when it arrived stores it without injecting it
77
- * into the live turn. The subconscious reads it. Its text can still reach
78
- * a resident in rows of their own, which aren't marked, such as the
75
+ * compiled view includes it, during the tune-out or after it, and a
76
+ * resident's tool boundary of a turn alive when it arrived stores it
77
+ * without injecting it into the live turn. The subconscious reads it. Its
78
+ * text can still reach a resident in rows of their own, which aren't
79
+ * marked, such as the
79
80
  * cancel's `<tuned-out-backlog>` row, with the text of the tune-out's newest
80
81
  * messages, up to `backlogCap` of them and as many as fit in one of the
81
82
  * residents' messages, the stored ones first and then those still held;
@@ -142,16 +143,27 @@ export interface Module {
142
143
 
143
144
  /**
144
145
  * Utilities: agent-invocable operations that deliberately do NOT get a
145
- * first-class tool slot. Every tool schema taxes every inference; a
146
- * capability used twice a month shouldn't. Utilities are listed, described,
147
- * and invoked through the framework's single `utils` tool instead — same
148
- * ToolDefinition shape, same un-prefixed names, and dispatch arrives at the
149
- * SAME handleToolCall as getTools() output, so a tool migrates between
150
- * surfaces by moving its definition from one list to the other and nothing
151
- * else. Optional; omitting it means the module contributes no utilities.
146
+ * slot in the tool list. Every tool schema taxes every inference; a
147
+ * capability used twice a month shouldn't. Utilities belong to the host's
148
+ * built-in toolset ("settings"): an agent sees them with
149
+ * `toolsets--look {toolset:"settings"}` and calls them with
150
+ * `toolsets--act` once it has opened it. Same ToolDefinition shape, same
151
+ * un-prefixed names, and dispatch arrives at the SAME handleToolCall as
152
+ * getTools() output. Optional; omitting it means the module contributes no
153
+ * utilities.
152
154
  */
153
155
  getUtilities?(): ToolDefinition[];
154
156
 
157
+ /**
158
+ * Where one of this module's tools (a getTools() name, unprefixed) is
159
+ * presented: in the agent's tool list (`'header'`, the default), or in the
160
+ * host's built-in toolset (`'builtin'`), out of the tool list, opened with
161
+ * `toolsets--open {toolset:"settings"}` and called with `toolsets--act`.
162
+ * For tools an agent uses rarely: settings, administration, status reads.
163
+ * Optional; omitting it keeps every tool in the tool list.
164
+ */
165
+ toolPlacement?(toolName: string): 'header' | 'builtin' | undefined;
166
+
155
167
  /**
156
168
  * Handle a tool call.
157
169
  * Tool name is without module prefix.