@animalabs/agent-framework 0.22.4 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +105 -3
  2. package/dist/src/agent.d.ts +36 -10
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +76 -32
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/code-execution/index.d.ts +1 -1
  7. package/dist/src/code-execution/index.d.ts.map +1 -1
  8. package/dist/src/code-execution/index.js.map +1 -1
  9. package/dist/src/code-execution/py-runner.d.ts +34 -2
  10. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  11. package/dist/src/code-execution/py-runner.js +72 -16
  12. package/dist/src/code-execution/py-runner.js.map +1 -1
  13. package/dist/src/code-execution/runtime-py.d.ts +3 -2
  14. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  15. package/dist/src/code-execution/runtime-py.js +47 -7
  16. package/dist/src/code-execution/runtime-py.js.map +1 -1
  17. package/dist/src/code-execution/tool-definition.d.ts +12 -7
  18. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  19. package/dist/src/code-execution/tool-definition.js +59 -11
  20. package/dist/src/code-execution/tool-definition.js.map +1 -1
  21. package/dist/src/config-checks.d.ts +2 -2
  22. package/dist/src/config-checks.d.ts.map +1 -1
  23. package/dist/src/config-checks.js +6 -2
  24. package/dist/src/config-checks.js.map +1 -1
  25. package/dist/src/framework.d.ts +744 -120
  26. package/dist/src/framework.d.ts.map +1 -1
  27. package/dist/src/framework.js +3546 -906
  28. package/dist/src/framework.js.map +1 -1
  29. package/dist/src/gate/event-gate.d.ts.map +1 -1
  30. package/dist/src/gate/event-gate.js +13 -6
  31. package/dist/src/gate/event-gate.js.map +1 -1
  32. package/dist/src/held-writes.d.ts +16 -0
  33. package/dist/src/held-writes.d.ts.map +1 -0
  34. package/dist/src/held-writes.js +56 -0
  35. package/dist/src/held-writes.js.map +1 -0
  36. package/dist/src/index.d.ts +7 -3
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +8 -4
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/mcpl/capability-grant.d.ts +4 -0
  41. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  42. package/dist/src/mcpl/capability-grant.js +1 -1
  43. package/dist/src/mcpl/capability-grant.js.map +1 -1
  44. package/dist/src/mcpl/channel-registry.d.ts +95 -18
  45. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  46. package/dist/src/mcpl/channel-registry.js +578 -150
  47. package/dist/src/mcpl/channel-registry.js.map +1 -1
  48. package/dist/src/mcpl/convert-block.d.ts +2 -2
  49. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  50. package/dist/src/mcpl/convert-block.js +6 -7
  51. package/dist/src/mcpl/convert-block.js.map +1 -1
  52. package/dist/src/mcpl/index.d.ts +5 -1
  53. package/dist/src/mcpl/index.d.ts.map +1 -1
  54. package/dist/src/mcpl/index.js +3 -1
  55. package/dist/src/mcpl/index.js.map +1 -1
  56. package/dist/src/mcpl/inference-router.d.ts +4 -2
  57. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  58. package/dist/src/mcpl/inference-router.js +6 -7
  59. package/dist/src/mcpl/inference-router.js.map +1 -1
  60. package/dist/src/mcpl/log-value.d.ts +7 -0
  61. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  62. package/dist/src/mcpl/log-value.js +9 -0
  63. package/dist/src/mcpl/log-value.js.map +1 -1
  64. package/dist/src/mcpl/modern-connection.d.ts +227 -0
  65. package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
  66. package/dist/src/mcpl/modern-connection.js +1088 -0
  67. package/dist/src/mcpl/modern-connection.js.map +1 -0
  68. package/dist/src/mcpl/prefix-rule.d.ts +18 -0
  69. package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
  70. package/dist/src/mcpl/prefix-rule.js +25 -0
  71. package/dist/src/mcpl/prefix-rule.js.map +1 -0
  72. package/dist/src/mcpl/protocol-family.d.ts +179 -0
  73. package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
  74. package/dist/src/mcpl/protocol-family.js +697 -0
  75. package/dist/src/mcpl/protocol-family.js.map +1 -0
  76. package/dist/src/mcpl/push-coalescer.d.ts +38 -19
  77. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  78. package/dist/src/mcpl/push-coalescer.js +52 -23
  79. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  80. package/dist/src/mcpl/reference-fetcher.d.ts +66 -15
  81. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  82. package/dist/src/mcpl/reference-fetcher.js +243 -76
  83. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  84. package/dist/src/mcpl/references.d.ts +63 -18
  85. package/dist/src/mcpl/references.d.ts.map +1 -1
  86. package/dist/src/mcpl/references.js +117 -32
  87. package/dist/src/mcpl/references.js.map +1 -1
  88. package/dist/src/mcpl/server-connection.d.ts +326 -30
  89. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  90. package/dist/src/mcpl/server-connection.js +995 -212
  91. package/dist/src/mcpl/server-connection.js.map +1 -1
  92. package/dist/src/mcpl/server-registry.d.ts +25 -5
  93. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  94. package/dist/src/mcpl/server-registry.js +51 -12
  95. package/dist/src/mcpl/server-registry.js.map +1 -1
  96. package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
  97. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
  98. package/dist/src/mcpl/tool-result-normalize.js +232 -0
  99. package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
  100. package/dist/src/mcpl/transport.d.ts +56 -13
  101. package/dist/src/mcpl/transport.d.ts.map +1 -1
  102. package/dist/src/mcpl/transport.js +206 -70
  103. package/dist/src/mcpl/transport.js.map +1 -1
  104. package/dist/src/mcpl/types.d.ts +66 -20
  105. package/dist/src/mcpl/types.d.ts.map +1 -1
  106. package/dist/src/mcpl/types.js.map +1 -1
  107. package/dist/src/mcpl/visible-content.d.ts +2 -1
  108. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  109. package/dist/src/mcpl/visible-content.js +2 -1
  110. package/dist/src/mcpl/visible-content.js.map +1 -1
  111. package/dist/src/module-registry.d.ts +16 -6
  112. package/dist/src/module-registry.d.ts.map +1 -1
  113. package/dist/src/module-registry.js +65 -6
  114. package/dist/src/module-registry.js.map +1 -1
  115. package/dist/src/modules/api/index.d.ts +1 -1
  116. package/dist/src/modules/api/index.d.ts.map +1 -1
  117. package/dist/src/modules/api/index.js +3 -2
  118. package/dist/src/modules/api/index.js.map +1 -1
  119. package/dist/src/modules/discord/discord-js-client.d.ts +42 -2
  120. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  121. package/dist/src/modules/discord/discord-js-client.js +268 -65
  122. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  123. package/dist/src/modules/discord/index.d.ts +27 -5
  124. package/dist/src/modules/discord/index.d.ts.map +1 -1
  125. package/dist/src/modules/discord/index.js +366 -76
  126. package/dist/src/modules/discord/index.js.map +1 -1
  127. package/dist/src/modules/discord/refusal.d.ts +14 -0
  128. package/dist/src/modules/discord/refusal.d.ts.map +1 -0
  129. package/dist/src/modules/discord/refusal.js +14 -0
  130. package/dist/src/modules/discord/refusal.js.map +1 -0
  131. package/dist/src/modules/discord/types.d.ts +17 -7
  132. package/dist/src/modules/discord/types.d.ts.map +1 -1
  133. package/dist/src/modules/health/index.d.ts.map +1 -1
  134. package/dist/src/modules/health/index.js +6 -2
  135. package/dist/src/modules/health/index.js.map +1 -1
  136. package/dist/src/modules/index.d.ts +1 -1
  137. package/dist/src/modules/index.d.ts.map +1 -1
  138. package/dist/src/modules/index.js +1 -1
  139. package/dist/src/modules/index.js.map +1 -1
  140. package/dist/src/modules/workspace/disk-agreement.d.ts +25 -2
  141. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  142. package/dist/src/modules/workspace/disk-agreement.js +45 -5
  143. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  144. package/dist/src/modules/workspace/effects.d.ts +2 -1
  145. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/effects.js +19 -9
  147. package/dist/src/modules/workspace/effects.js.map +1 -1
  148. package/dist/src/modules/workspace/index.d.ts +214 -42
  149. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  150. package/dist/src/modules/workspace/index.js +667 -113
  151. package/dist/src/modules/workspace/index.js.map +1 -1
  152. package/dist/src/modules/workspace/observe.d.ts +240 -8
  153. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  154. package/dist/src/modules/workspace/observe.js +554 -24
  155. package/dist/src/modules/workspace/observe.js.map +1 -1
  156. package/dist/src/modules/workspace/types.d.ts +10 -1
  157. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  158. package/dist/src/modules/workspace/types.js.map +1 -1
  159. package/dist/src/modules/workspace/watcher.d.ts +42 -0
  160. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/watcher.js +130 -25
  162. package/dist/src/modules/workspace/watcher.js.map +1 -1
  163. package/dist/src/silent-heartbeat.d.ts +112 -39
  164. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  165. package/dist/src/silent-heartbeat.js +100 -29
  166. package/dist/src/silent-heartbeat.js.map +1 -1
  167. package/dist/src/tool-args.d.ts +40 -0
  168. package/dist/src/tool-args.d.ts.map +1 -0
  169. package/dist/src/tool-args.js +207 -0
  170. package/dist/src/tool-args.js.map +1 -0
  171. package/dist/src/tool-presentation.d.ts.map +1 -1
  172. package/dist/src/tool-presentation.js +9 -2
  173. package/dist/src/tool-presentation.js.map +1 -1
  174. package/dist/src/types/agent.d.ts +27 -17
  175. package/dist/src/types/agent.d.ts.map +1 -1
  176. package/dist/src/types/events.d.ts +8 -0
  177. package/dist/src/types/events.d.ts.map +1 -1
  178. package/dist/src/types/framework.d.ts +17 -2
  179. package/dist/src/types/framework.d.ts.map +1 -1
  180. package/dist/src/types/module.d.ts +54 -10
  181. package/dist/src/types/module.d.ts.map +1 -1
  182. package/dist/src/types/trace.d.ts +2 -2
  183. package/dist/src/types/trace.d.ts.map +1 -1
  184. package/package.json +3 -4
  185. package/src/agent.ts +99 -36
  186. package/src/code-execution/index.ts +1 -0
  187. package/src/code-execution/py-runner.ts +104 -17
  188. package/src/code-execution/runtime-py.ts +47 -7
  189. package/src/code-execution/tool-definition.ts +65 -11
  190. package/src/config-checks.ts +4 -2
  191. package/src/framework.ts +3633 -956
  192. package/src/gate/event-gate.ts +14 -6
  193. package/src/held-writes.ts +59 -0
  194. package/src/index.ts +19 -4
  195. package/src/mcpl/capability-grant.ts +1 -1
  196. package/src/mcpl/channel-registry.ts +613 -160
  197. package/src/mcpl/convert-block.ts +15 -7
  198. package/src/mcpl/index.ts +14 -1
  199. package/src/mcpl/inference-router.ts +8 -7
  200. package/src/mcpl/log-value.ts +10 -0
  201. package/src/mcpl/modern-connection.ts +1182 -0
  202. package/src/mcpl/prefix-rule.ts +32 -0
  203. package/src/mcpl/protocol-family.ts +691 -0
  204. package/src/mcpl/push-coalescer.ts +67 -29
  205. package/src/mcpl/reference-fetcher.ts +288 -82
  206. package/src/mcpl/references.ts +123 -33
  207. package/src/mcpl/server-connection.ts +1050 -221
  208. package/src/mcpl/server-registry.ts +50 -12
  209. package/src/mcpl/tool-result-normalize.ts +294 -0
  210. package/src/mcpl/transport.ts +204 -62
  211. package/src/mcpl/types.ts +67 -20
  212. package/src/mcpl/visible-content.ts +2 -1
  213. package/src/module-registry.ts +68 -13
  214. package/src/modules/api/index.ts +3 -2
  215. package/src/modules/discord/discord-js-client.ts +288 -72
  216. package/src/modules/discord/index.ts +380 -77
  217. package/src/modules/discord/refusal.ts +13 -0
  218. package/src/modules/discord/types.ts +17 -9
  219. package/src/modules/health/index.ts +6 -2
  220. package/src/modules/index.ts +1 -1
  221. package/src/modules/workspace/disk-agreement.ts +46 -5
  222. package/src/modules/workspace/effects.ts +18 -7
  223. package/src/modules/workspace/index.ts +722 -124
  224. package/src/modules/workspace/observe.ts +591 -22
  225. package/src/modules/workspace/types.ts +10 -1
  226. package/src/modules/workspace/watcher.ts +126 -25
  227. package/src/silent-heartbeat.ts +184 -59
  228. package/src/tool-args.ts +213 -0
  229. package/src/tool-presentation.ts +9 -2
  230. package/src/types/agent.ts +27 -17
  231. package/src/types/events.ts +8 -0
  232. package/src/types/framework.ts +17 -2
  233. package/src/types/module.ts +55 -10
  234. package/src/types/trace.ts +2 -2
@@ -0,0 +1,213 @@
1
+ import type { ToolDefinition } from './types/index.js';
2
+
3
+ /** A run of 16 or more digits, which membrane's XML parser passes as text for
4
+ * a number or integer parameter: some such runs would lose precision as a
5
+ * JavaScript number, so membrane keeps every one as text, and so does this
6
+ * check. */
7
+ const LARGE_INTEGER_TEXT = /^\d{16,}$/;
8
+
9
+ /** The JSON kind of a value, as a schema's `type` names it. */
10
+ function kindOf(value: unknown): string {
11
+ if (Array.isArray(value)) return 'array';
12
+ if (value === null) return 'null';
13
+ return typeof value;
14
+ }
15
+
16
+ /** A kind with its article, for a sentence. */
17
+ function withArticle(kind: string): string {
18
+ if (kind === 'null') return 'null';
19
+ return /^[aeiou]/.test(kind) ? `an ${kind}` : `a ${kind}`;
20
+ }
21
+
22
+ /** The types JSON Schema names. A `type` naming anything else, such as
23
+ * `any`, is one the check can't read. */
24
+ const JSON_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
25
+
26
+ /** The kinds a declaration's type admits, each once, or none when it declares
27
+ * none it can read. A type list naming anything JSON Schema doesn't have is
28
+ * one it can't read as a whole, as membrane reads it, rather than the names
29
+ * in it that it knows. OpenAPI's `nullable: true`, the form Gemini's dialect
30
+ * uses, admits `null` beside its type, as a type list naming `null` does. */
31
+ function admittedKinds(param: { type?: unknown; nullable?: unknown } | undefined): string[] {
32
+ const type = param?.type;
33
+ const types = Array.isArray(type) ? type : type === undefined ? [] : [type];
34
+ if (!types.every((t) => typeof t === 'string' && JSON_TYPES.has(t))) return [];
35
+ const kinds = new Set((types as string[]).map((t) => (t === 'integer' ? 'number' : t)));
36
+ if (kinds.size > 0 && param?.nullable === true) kinds.add('null');
37
+ return [...kinds];
38
+ }
39
+
40
+ /** Whether `null` is a value for a key so declared: where the declaration
41
+ * admits it (a type list naming it, or `nullable: true`), or declares no
42
+ * type at all. A type the check can't read says nothing about `null`, so
43
+ * there `null` is no value unless the declaration names it. */
44
+ function takesNull(param: { type?: unknown; nullable?: unknown } | undefined): boolean {
45
+ const type = param?.type;
46
+ if (type === undefined || param?.nullable === true) return true;
47
+ return Array.isArray(type) ? type.includes('null') : type === 'null';
48
+ }
49
+
50
+ /** A value as the call gave it, briefly: the first 80 characters of its JSON
51
+ * text, then an ellipsis when there are more, cut between characters rather
52
+ * than inside one. The whole value is serialized before the cut. */
53
+ function shown(value: unknown): string {
54
+ let text: string;
55
+ try {
56
+ text = JSON.stringify(value) ?? String(value);
57
+ } catch {
58
+ text = '(a value that could not be shown)';
59
+ }
60
+ let head = '';
61
+ let count = 0;
62
+ for (const char of text) {
63
+ if (count === 80) return `${head}…`;
64
+ head += char;
65
+ count++;
66
+ }
67
+ return head;
68
+ }
69
+
70
+ /** A list in prose: `a`, `a or b`, `a, b or c`. */
71
+ function oneOf(items: string[]): string {
72
+ return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} or ${items[items.length - 1]}`;
73
+ }
74
+
75
+ /** A list in prose: `a`, `a and b`, `a, b and c`. */
76
+ function allOf(items: string[]): string {
77
+ return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
78
+ }
79
+
80
+ /** At most this many keys a call gave that its schema doesn't declare are named. */
81
+ const UNDECLARED_SHOWN = 5;
82
+
83
+ /** An own property, so a key such as `constructor` isn't found on the prototype. */
84
+ function own(obj: Record<string, unknown>, key: string): unknown {
85
+ return Object.hasOwn(obj, key) ? obj[key] : undefined;
86
+ }
87
+
88
+ /** An object that isn't a list: a map of keys, as `properties` should be. */
89
+ function isMap(value: unknown): value is Record<string, unknown> {
90
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
91
+ }
92
+
93
+ /** Every root keyword in JSON Schema's drafts 4 to 2020-12, besides
94
+ * `properties` and `required`, that can accept, require or name an object's
95
+ * keys, except the two read by declaresKeysPlainly (`additionalProperties`
96
+ * and `unevaluatedProperties`, which count when `true` or a schema object). The
97
+ * reading is cautious: a root with any of them names no key, whatever its
98
+ * subschemas hold, since naming a key the schema takes would mislead the
99
+ * agent more than a hint left out. So `not` is here too, since a negation
100
+ * can require a key (`not: {properties: {y: {type: 'string'}}}` refuses an
101
+ * object without a non-string `y`), and so is `if` with or without `then`
102
+ * and `else`. `then` and `else` act only beside `if`, and a keyword JSON
103
+ * Schema doesn't have declares nothing. */
104
+ const OTHER_KEY_DECLARERS = [
105
+ 'allOf', 'anyOf', 'oneOf', 'not', 'if', '$ref', '$dynamicRef', '$recursiveRef',
106
+ 'dependentSchemas', 'dependencies', 'dependentRequired', 'patternProperties',
107
+ 'propertyNames', 'enum', 'const',
108
+ ];
109
+
110
+ /** Whether a schema's root declares its keys only in `properties` and
111
+ * `required`, so that a key in neither isn't one of its parameters. A root
112
+ * with any keyword in OTHER_KEY_DECLARERS can declare keys another way. A
113
+ * schema object for `additionalProperties` or `unevaluatedProperties`, `{}`
114
+ * included, or `true`, which means the same there, says the root takes keys
115
+ * `properties` doesn't list, as a map's input does, so a key in neither list
116
+ * may be meant. `false` there says it takes none, so it names. Left out,
117
+ * the keyword is JSON Schema's default, which says nothing of what the
118
+ * author meant, and most schemas leave it out without welcoming other keys,
119
+ * so a root without it names them too. */
120
+ function declaresKeysPlainly(schema: Record<string, unknown>): boolean {
121
+ return !OTHER_KEY_DECLARERS.some((key) => Object.hasOwn(schema, key))
122
+ && !takesOtherKeys(schema.additionalProperties)
123
+ && !takesOtherKeys(schema.unevaluatedProperties);
124
+ }
125
+
126
+ function takesOtherKeys(value: unknown): boolean {
127
+ return value === true || isMap(value);
128
+ }
129
+
130
+ /**
131
+ * What's wrong with a call's input against its tool's declared inputSchema,
132
+ * checked shallowly: the input is an object, and each root `required` key has
133
+ * a value. With `values`, also each declared key's value: a type and an enum
134
+ * value its declaration admits. It isn't a JSON-Schema engine: nested shapes,
135
+ * unions and references are the handler's to check, as they always were.
136
+ * Every problem is reported at once, each naming the key, what the schema
137
+ * wants and what the call gave. An empty list means the call can go to its
138
+ * handler.
139
+ *
140
+ * - An absent input (`undefined` or `null`) gives no arguments: it's refused
141
+ * only for the keys the schema requires.
142
+ * - A required key given as `null` has no value, the same as one left out,
143
+ * unless its declaration admits `null` (a type list naming it, or
144
+ * `nullable: true`), or has no `type` at all: a key with no `type` takes
145
+ * `null` as a value, as it takes any other. One whose `type` the check
146
+ * can't read, such as Gemini's `STRING`, or a list naming one, takes
147
+ * `null` only where the declaration names it, and its values aren't
148
+ * checked against the type. An empty string is a value.
149
+ * - A schema whose `required` isn't a list requires nothing here, and a key
150
+ * it lists twice is reported once.
151
+ * - Where a required key is missing and the call gave keys the schema
152
+ * declares neither in its `properties` nor in its `required` list, those
153
+ * are named too, since a key sent under another name is one reason a
154
+ * required key goes missing. They're never refused for themselves. A schema
155
+ * whose root can declare keys another way names none
156
+ * (declaresKeysPlainly).
157
+ *
158
+ * A module tool's call is checked without `values`: a handler may take more
159
+ * than its schema can say on every provider, such as one name where a list
160
+ * is declared, and refusing that would refuse calls that work. The `utils`
161
+ * tool checks a utility's values too, as it always has. `field` names what
162
+ * the input is to the caller ("input" unless given).
163
+ */
164
+ export function toolArgsProblems(
165
+ input: unknown,
166
+ schema: ToolDefinition['inputSchema'] | undefined,
167
+ options: { values?: boolean; field?: string } = {},
168
+ ): string[] {
169
+ if (!schema) return [];
170
+ const absent = input === undefined || input === null;
171
+ 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))}`];
173
+ }
174
+ const obj = (absent ? {} : input) as Record<string, unknown>;
175
+ const properties = (isMap(schema.properties) ? schema.properties : {}) as Record<string, { type?: unknown; nullable?: unknown; enum?: unknown } | undefined>;
176
+ const declared = (key: string) => (Object.hasOwn(properties, key) ? properties[key] : undefined);
177
+ const problems: string[] = [];
178
+ const missing = new Set<string>();
179
+ const required: unknown[] = Array.isArray(schema.required) ? schema.required : [];
180
+ for (const key of required) {
181
+ if (typeof key !== 'string' || missing.has(key)) continue;
182
+ const value = own(obj, key);
183
+ const kinds = admittedKinds(declared(key));
184
+ if (value !== undefined && (value !== null || takesNull(declared(key)))) continue;
185
+ missing.add(key);
186
+ const wanted = kinds.length > 0 ? `"${key}" (${oneOf(kinds.map(withArticle))})` : `"${key}"`;
187
+ problems.push(`needs ${wanted}, and this call gave ${value === null ? 'null' : 'none'}`);
188
+ }
189
+ if (missing.size > 0 && declaresKeysPlainly(schema as Record<string, unknown>)) {
190
+ const undeclared = Object.keys(obj).filter((key) => !Object.hasOwn(properties, key) && !required.includes(key));
191
+ if (undeclared.length > 0) {
192
+ const named = undeclared.slice(0, UNDECLARED_SHOWN).map(shown);
193
+ if (undeclared.length > UNDECLARED_SHOWN) named.push(`${undeclared.length - UNDECLARED_SHOWN} more`);
194
+ problems.push(undeclared.length === 1 ? `${named[0]} isn't one of its parameters` : `${allOf(named)} aren't among its parameters`);
195
+ }
196
+ }
197
+ if (!options.values) return problems;
198
+ for (const [key, value] of Object.entries(obj)) {
199
+ const param = declared(key);
200
+ if (!param || value === undefined || missing.has(key)) continue;
201
+ const kinds = admittedKinds(param);
202
+ const kind = kindOf(value);
203
+ const largeInteger = kind === 'string' && kinds.includes('number') && LARGE_INTEGER_TEXT.test(value as string);
204
+ if (kinds.length > 0 && !kinds.includes(kind) && !largeInteger) {
205
+ problems.push(`"${key}" must be ${oneOf(kinds.map(withArticle))}, and this call gave ${withArticle(kind)}`);
206
+ continue;
207
+ }
208
+ if (Array.isArray(param.enum) && !param.enum.includes(value)) {
209
+ problems.push(`"${key}" must be one of ${oneOf(param.enum.map(shown))}, and this call gave ${shown(value)}`);
210
+ }
211
+ }
212
+ return problems;
213
+ }
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { fchmodSync, closeSync, fstatSync, lstatSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { constants, fchmodSync, closeSync, fstatSync, lstatSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { closestNames } from './closest-names.js';
4
4
  import { boundedErrorMessage, errorMessage } from './error-message.js';
5
5
  import type { ToolDefinition, ToolResult } from './types/events.js';
@@ -24,6 +24,13 @@ export interface PresentationSnapshot {
24
24
  entries: { name: string; source: string; visible: boolean; originalDescription: string; description: string; descriptionSource?: string; inputSchema: ToolDefinition['inputSchema'] }[];
25
25
  }
26
26
  const MAX_BYTES = 256 * 1024;
27
+ /**
28
+ * Opened with each read of a presentation file, as observe.ts opens workspace
29
+ * files: a FIFO at the operator's path opens at once and fails the regular-file
30
+ * check, where a blocking open would hold the whole process (it's synchronous)
31
+ * until something opened the FIFO for writing.
32
+ */
33
+ const NONBLOCK = typeof constants.O_NONBLOCK === 'number' ? constants.O_NONBLOCK : 0;
27
34
  const MAX_DESCRIPTION_CHARS = 32768;
28
35
  const protectedNames = new Set(['set_tool_visibility', 'set_tool_description', 'workspace--read']);
29
36
  export const isPresentationTool = (name: string): boolean => name === 'set_tool_visibility' || name === 'set_tool_description';
@@ -173,7 +180,7 @@ export class ToolPresentation {
173
180
  constructor(readonly config: ToolPresentationConfig) {}
174
181
  private read(path = this.config.path, allowMissing = true): string {
175
182
  let fd: number;
176
- try { fd = openSync(path, 'r'); }
183
+ try { fd = openSync(path, constants.O_RDONLY | NONBLOCK); }
177
184
  catch (error) {
178
185
  const code = (error as NodeJS.ErrnoException).code;
179
186
  if (code === 'ENOENT') {
@@ -14,7 +14,12 @@ export type SameRoundThinkTextPolicySource =
14
14
  */
15
15
  export interface AgentConfig {
16
16
  toolPresentation?: import("../tool-presentation.js").ToolPresentationConfig;
17
- /** Unique name for this agent */
17
+ /**
18
+ * Unique name for this agent: create() refuses two configured agents of one
19
+ * name. Names compare exactly, so `Dup` beside `dup` is two agents, though
20
+ * the history tool's author filter matches their own turns without letter
21
+ * case and can't tell them apart there.
22
+ */
18
23
  name: string;
19
24
 
20
25
  /** Model to use (e.g., 'claude-sonnet-4-20250514') */
@@ -27,9 +32,19 @@ export interface AgentConfig {
27
32
  strategy?: ContextStrategy;
28
33
 
29
34
  /**
30
- * Which tools this agent can use.
35
+ * Enforced at dispatch for registered agent callers; omitted means 'all'.
31
36
  * - 'all': all available tools
32
- * - string[]: specific tool names (with module prefix)
37
+ * - string[]: exact tool names, including module--utility names for utilities.
38
+ * Utilities aren't tools of their own: an agent's model and its scripts are
39
+ * offered them only through 'utils', whose run checks the utility's own
40
+ * name. An agent that should use a utility lists both 'utils' and
41
+ * 'module--utility'. The name alone passes the dispatch check for any call
42
+ * by that name, whether the host makes it with executeToolCall on the
43
+ * agent's behalf or the model or a script makes it without being offered
44
+ * it. The slot is hidden when no utility is permitted.
45
+ * Trusted module calls are exempt. Genuinely unidentified public callers,
46
+ * including unknown legacy names, remain unrestricted. Framework-created
47
+ * identities have tool authority only while registered.
33
48
  */
34
49
  allowedTools?: 'all' | string[];
35
50
 
@@ -172,12 +187,6 @@ export interface AgentConfig {
172
187
  autoRewind?: boolean;
173
188
  /** Max consecutive rewinds before giving up a turn. Default 3. */
174
189
  maxRewinds?: number;
175
- /**
176
- * When the rewound turn is a *human* message (not a machine tool result),
177
- * announce the withholding on the conversational surface (Discord) rather
178
- * than dropping it silently. Default true.
179
- */
180
- announceHumanTurns?: boolean;
181
190
  };
182
191
 
183
192
  /** Opt-in tool-output admission and one guarded retry after a provider
@@ -197,7 +206,8 @@ export interface AgentConfig {
197
206
  * that publication envelope through the same authorized resolver.
198
207
  * See docs/hybrid-prose-routing.md.
199
208
  * - 'disabled': generated prose is never published. Only explicit tools may
200
- * send externally; prose remains in Chronicle with a private suppression receipt.
209
+ * send externally; prose remains in Chronicle, and a turn that wrote plain
210
+ * speech ends with a private `[delivered] plain speech → no channel` receipt.
201
211
  */
202
212
  proseRouting?: 'locus' | 'explicit' | 'hybrid' | 'disabled';
203
213
 
@@ -372,7 +382,8 @@ export interface InferenceRequest {
372
382
  * replies in the channel it is answering — not the process-global most-recent-
373
383
  * inbound locus, which a concurrent message elsewhere can hijack (item-3
374
384
  * redux). Undefined for non-channel wakes (heartbeat, timers, module events),
375
- * which correctly fall back to the global default channel.
385
+ * which correctly fall back to the global default channel, except an
386
+ * ephemeral run's (a subagent's), which takes no global fallback.
376
387
  */
377
388
  channelId?: string;
378
389
  /**
@@ -406,11 +417,10 @@ export interface InferenceRequest {
406
417
  /** Suppress every automatic plain-prose delivery for this logical turn.
407
418
  * Explicit tool calls remain available. Used by authenticated silent wakes. */
408
419
  suppressProse?: boolean;
409
- /** Ephemeral system-position prompt for this turn only. Never written to
410
- * Chronicle; callers must supply bounded non-secret control text. */
411
- ephemeralSystemPrompt?: string;
412
- /** The authenticated silent heartbeat tick this turn answers. Every row the
413
- * turn stores is stamped `metadata.silentHeartbeat` with it, and request
414
- * builds render a request-only separator before the tick's first row. */
420
+ /** The authenticated silent heartbeat tick this turn answers. The rows the
421
+ * turn's reply stores (its assistant responses and tool results) are
422
+ * stamped `metadata.silentHeartbeat` with it, and request builds render a
423
+ * request-only separator carrying the tick's instruction before the tick's
424
+ * first surviving row (`SILENT_HEARTBEAT_SEPARATOR`). */
415
425
  silentHeartbeat?: SilentHeartbeatTick;
416
426
  }
@@ -192,6 +192,14 @@ export interface ToolResult {
192
192
  error?: string;
193
193
  /** Whether this was an error (for LLM) */
194
194
  isError?: boolean;
195
+ /**
196
+ * An MCP tool's `structuredContent`: its machine-readable result, beside
197
+ * `data`, which is the content view. Present exactly when the server sent
198
+ * one, so `false`, `0` and `null` are values here, not absence. Programs
199
+ * read it (scripts get it in their result object); the model sees content,
200
+ * and sees this rendered as JSON only when the content carries no text.
201
+ */
202
+ structured?: unknown;
195
203
  /**
196
204
  * When true, the framework saves tool_use + tool_result messages to context,
197
205
  * cancels the active stream, and resets the agent to idle.
@@ -24,8 +24,6 @@ export interface ModuleProcessResponse {
24
24
  export interface ProcessLoggingConfig {
25
25
  /** Persist process logs to Chronicle (default: false) */
26
26
  persist?: boolean;
27
- /** Broadcast process:completed trace events (default: false) */
28
- broadcast?: boolean;
29
27
  }
30
28
 
31
29
  /**
@@ -76,6 +74,23 @@ export interface CodeExecutionConfig {
76
74
  /** Max wake_agent() calls per background script (default 100); further
77
75
  * calls raise RuntimeError inside the script. */
78
76
  maxWakesPerScript?: number;
77
+ /** Variables for the python interpreter beyond the operating allowlist it
78
+ * inherits (see CHILD_ENV_ALLOWLIST), such as PYTHONPATH. A recipe can
79
+ * `${VAR}`-substitute them from .env. Other host variables aren't
80
+ * inherited, which limits what model-authored code inherits, not what a
81
+ * process running as the same user can read. The runner sets
82
+ * PYTHONUNBUFFERED and PYTHONIOENCODING last, over any value given here,
83
+ * since its protocol needs both. The interpreter's working directory is
84
+ * its own runtime directory, so a relative path given here, as in
85
+ * PYTHONPATH, resolves there: give absolute paths. */
86
+ env?: Record<string, string>;
87
+ /** The interpreter inherits the host's whole environment instead of the
88
+ * allowlist: every host variable, provider keys included, reaches
89
+ * model-authored code and every subprocess a script starts. Listing what
90
+ * scripts need in `env` is the safer route. With code execution enabled,
91
+ * `create()` refuses any value but true, false or null, which counts as
92
+ * not set. */
93
+ inheritEnv?: boolean;
79
94
  }
80
95
 
81
96
  /** See `FrameworkConfig.providerHold`. */
@@ -19,15 +19,39 @@ export interface MessageQuery {
19
19
  externalIds?: string[];
20
20
  /** Filter by participant name */
21
21
  participant?: string;
22
- /** Filter by metadata fields (shallow match) */
22
+ /** Filter by metadata fields: each key a path, dot-separated for a nested
23
+ * field (such as 'external.channelId'), its value compared by strict
24
+ * equality */
23
25
  metadata?: Record<string, unknown>;
24
26
  }
25
27
 
28
+ /**
29
+ * A message as a module context's reads answer it: a stored row, or a row
30
+ * for a write the framework holds (`held`).
31
+ */
32
+ export type ModuleMessage = StoredMessage & {
33
+ /**
34
+ * True while the framework holds the message's write, as it does while
35
+ * the write's agent has a turn alive or the host is quiesced, until a
36
+ * later boundary stores it. The row's `id` is then a handle, which
37
+ * getMessage, editMessage and removeMessage take as they take a stored
38
+ * id, and which names the stored row once the write lands. Its `sequence`
39
+ * and `timestamp` are provisional: the sequence comes after every stored
40
+ * row's, the timestamp is when the framework held the write (for a write
41
+ * an older release held, when its queue was read back), and both change
42
+ * when it lands. Absent on a stored row.
43
+ */
44
+ held?: true;
45
+ };
46
+
26
47
  /**
27
48
  * Result of a message query.
28
49
  */
29
50
  export interface MessageQueryResult {
30
- messages: StoredMessage[];
51
+ /** The matching stored rows, then, as the newest, the matching rows of
52
+ * writes the framework holds (ModuleMessage.held): before the stored rows
53
+ * in a reversed query */
54
+ messages: ModuleMessage[];
31
55
  totalCount: number;
32
56
  }
33
57
 
@@ -249,7 +273,14 @@ export interface ModuleContext {
249
273
  getModule<T extends Module>(name: string): T | null;
250
274
 
251
275
  /**
252
- * Add a message to the conversation.
276
+ * Add a message to the conversation, and answer its id. While the
277
+ * framework holds the write (the agent's turn is alive, or the host is
278
+ * quiesced), the id is a handle, `held:` and the write's id, which
279
+ * getMessage, editMessage and removeMessage take as they take a stored
280
+ * id, and which names the stored row once the write lands
281
+ * (ModuleMessage.held). The writes a process held, and keeps across a
282
+ * restart, show to the next process's modules once create() has restored
283
+ * them, which is after every module's start().
253
284
  */
254
285
  addMessage(
255
286
  participant: string,
@@ -258,28 +289,40 @@ export interface ModuleContext {
258
289
  ): MessageId;
259
290
 
260
291
  /**
261
- * Edit a message.
292
+ * Edit a message, by its id or a held write's handle. A held write is
293
+ * stored with the new content.
262
294
  */
263
295
  editMessage(id: MessageId, content: ContentBlock[]): void;
264
296
 
265
297
  /**
266
- * Remove a message.
298
+ * Remove a message, by its id or a held write's handle. A held write is
299
+ * dropped, so the agent never reads it.
267
300
  */
268
301
  removeMessage(id: MessageId): void;
269
302
 
270
303
  /**
271
- * Find a message by external ID.
304
+ * Find the message for an external ID this context added a message under:
305
+ * the row it added, while the store holds it. For a write the framework
306
+ * held, whose id this context doesn't keep, the first row the store shows
307
+ * under that external ID, held rows included, a held row by its handle.
308
+ * Null when this context added none under it, when the row it added is
309
+ * gone, or when the store shows none for a held write, such as one the
310
+ * last process held, looked up in start().
272
311
  */
273
312
  findMessageByExternalId(source: string, externalId: string): MessageId | null;
274
313
 
275
314
  /**
276
- * Get a message by ID.
315
+ * Get a message by its id or a held write's handle.
277
316
  */
278
- getMessage(id: MessageId): StoredMessage | null;
317
+ getMessage(id: MessageId): ModuleMessage | null;
279
318
 
280
319
  /**
281
320
  * Query messages by filter criteria.
282
321
  * Useful for finding messages from external sources, by participant, etc.
322
+ * The rows of writes the framework holds come after the stored rows, as
323
+ * the newest, or before them in a reversed query, marked `held`
324
+ * (ModuleMessage). Those the last process held show once create() has
325
+ * restored them, after every module's start() (addMessage).
283
326
  */
284
327
  queryMessages(filter: MessageQuery): MessageQueryResult;
285
328
 
@@ -447,12 +490,14 @@ export interface EventResponse {
447
490
  addMessages?: NewMessage[];
448
491
 
449
492
  /**
450
- * Messages to edit.
493
+ * Messages to edit, each by its id or a held write's handle
494
+ * (ModuleContext.editMessage).
451
495
  */
452
496
  editMessages?: MessageEdit[];
453
497
 
454
498
  /**
455
- * Messages to remove.
499
+ * Messages to remove, each by its id or a held write's handle
500
+ * (ModuleContext.removeMessage).
456
501
  */
457
502
  removeMessages?: MessageId[];
458
503
 
@@ -632,7 +632,7 @@ export type TraceEvent =
632
632
  | (TraceEventBase & {
633
633
  type: 'mcpl:orphaned-response';
634
634
  serverId: string;
635
- /** The JSON-RPC id of the late response whose pending request already timed out. */
635
+ /** The JSON-RPC id of the late response, whose request had already timed out or been ended by its transport's close. */
636
636
  responseId: string | number;
637
637
  /** Original request method when retained for mandatory-deadline visibility. */
638
638
  method?: string;
@@ -674,7 +674,7 @@ export type TraceEvent =
674
674
  parkedAdmissions?: number;
675
675
  /** True when undrained turns were force-cancelled (abandon). */
676
676
  abandoned?: boolean;
677
- /** Agents whose turn abandon could not cancel (token held, no stream). */
677
+ /** 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. */
678
678
  unabandonable?: string[];
679
679
  })
680
680
  | (TraceEventBase & {