@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
package/src/mcpl/types.ts CHANGED
@@ -193,8 +193,8 @@ export interface McplHostCapabilities {
193
193
  modelInfo?: boolean;
194
194
  featureSets?: boolean;
195
195
  channels?: boolean | McplChannelCapabilities;
196
- /** RFC-014 §6.3: this host presents tool hints. Sent only to a server
197
- * whose config sets `toolHints: true`. */
196
+ /** RFC-014 §6.3: this host presents tool hints. Sent to every server
197
+ * with hints (McplServerConfig.toolHints: on unless false). */
198
198
  toolHints?: boolean;
199
199
  }
200
200
 
@@ -508,18 +508,35 @@ export interface McplServerConfig {
508
508
  toolLifecycle?: import('./tool-lifecycle.js').ToolLifecycleConfig;
509
509
 
510
510
  /**
511
- * MCPL RFC-014 (tool presence, toolsets and surfaces). Default false: the
512
- * host advertises nothing and treats every tool as static, byte for byte as
513
- * before. When true, the host advertises `experimental.mcpl.toolHints`, and
514
- * honours hints only from a server that advertised `toolHints` too. Then
515
- * the server's static tools stay in the tool list as before, and its
516
- * dynamic tools (toolset tools, `mcpl/presence: "dynamic"`) leave it: they
517
- * are announced in the conversation as they become visible and invisible,
518
- * and called through the `toolsets` module's `toolsets--act`, opened with
519
- * `toolsets--open`, inspected with `toolsets--look` and closed with
520
- * `toolsets--close`. Only `true` turns it on, and null counts as not set.
511
+ * MCPL RFC-014 (tool presence, toolsets and surfaces). On unless set to
512
+ * `false`: the host advertises `experimental.mcpl.toolHints` and honours
513
+ * hints (through the `toolsets` meta-tools) only from a server that
514
+ * advertised `toolHints` too. Such a server's static tools stay in the tool
515
+ * list as before, and its dynamic tools (toolset tools, `mcpl/presence:
516
+ * "dynamic"`) leave it: they are announced in the conversation as they
517
+ * become visible and invisible, and called through `toolsets--act`, opened
518
+ * with `toolsets--open`, inspected with `toolsets--look` and closed with
519
+ * `toolsets--close`. A server that doesn't advertise `toolHints` lists every
520
+ * tool as before. `false` advertises nothing and treats every tool as
521
+ * static. Omitted and null are on unless a module given to `create()`
522
+ * holds the name `toolsets` (or a restart is moving a server to that
523
+ * prefix, which is refused); `true` with such a module is refused.
521
524
  */
522
525
  toolHints?: boolean;
526
+
527
+ /**
528
+ * How this server is presented to agents at first (the host's own
529
+ * toolsets, src/host-presentation.ts): `'full'` (the default) puts its
530
+ * tools in the tool list, as before; `'tab'` keeps it connected, its events
531
+ * still arriving, with none of its tools in the tool list until an agent
532
+ * opens its tab (`toolsets--open {server}`) and calls them through
533
+ * `toolsets--act`; `'closed-tab'` leaves it disconnected until an agent
534
+ * opens it. Each agent can switch it later (`toolsets--close {server}`,
535
+ * with `disconnect: true` for a closed tab; `toolsets--open {server,
536
+ * full: true}` back into the tool list), and that choice is kept across
537
+ * restarts beside the store, over this default.
538
+ */
539
+ presentation?: 'full' | 'tab' | 'closed-tab';
523
540
  }
524
541
 
525
542
  // ============================================================================
@@ -282,8 +282,8 @@ export class ModuleRegistry {
282
282
  /**
283
283
  * All utilities from all modules, namespaced exactly like tools
284
284
  * (`module--name`). See Module.getUtilities — these are deliberately kept
285
- * out of the model-facing tool list and surfaced through the framework's
286
- * `utils` meta-tool; dispatch goes through the same handleToolCall.
285
+ * out of the model-facing tool list and presented in the host's built-in
286
+ * toolset; dispatch goes through the same handleToolCall.
287
287
  */
288
288
  getAllUtilities(): ToolDefinition[] {
289
289
  const utilities: ToolDefinition[] = [];
@@ -300,8 +300,8 @@ export class ModuleRegistry {
300
300
 
301
301
  /**
302
302
  * A module tool's call for a path that stores or sends its result: the
303
- * model's dispatch, a script's inner call, the `utils` meta-tool for a
304
- * model. The result is checked and read once, in its JSON form
303
+ * model's dispatch (a `toolsets--act` call of it included), a script's
304
+ * inner call. The result is checked and read once, in its JSON form
305
305
  * ({@link storableToolResult}).
306
306
  */
307
307
  async storableToolCall(call: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
@@ -320,9 +320,8 @@ export class ModuleRegistry {
320
320
  * So is a call membrane marked `unparseableInput`, unless its text is JSON
321
321
  * `null`, which reaches the module as a call with no arguments
322
322
  * (markedInputRefusal).
323
- * A caller that has already resolved the tool's definition, as the `utils`
324
- * tool has a utility's, passes it as `definition`, and it's checked against
325
- * that one.
323
+ * A caller that has already resolved the tool's definition passes it as
324
+ * `definition`, and it's checked against that one.
326
325
  */
327
326
  async handleToolCall(given: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
328
327
  const marked = markedInputRefusal(given);
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { unknownToolMessage } from '../unknown-tool.js';
9
+ import { outsideSenderName } from '../sender-name.js';
9
10
  import type {
10
11
  Module,
11
12
  ModuleContext,
@@ -62,12 +63,13 @@ export class ApiModule implements Module {
62
63
  }
63
64
 
64
65
  private handleMessage(event: ApiMessageEvent): EventResponse {
66
+ const carried = event.metadata?.author;
65
67
  const response: EventResponse = {
66
68
  addMessages: [
67
69
  {
68
- participant: event.participant,
70
+ participant: outsideSenderName(event.participant, 'API'),
69
71
  content: [{ type: 'text', text: event.content }],
70
- metadata: event.metadata,
72
+ metadata: { ...event.metadata, author: typeof carried === 'object' && carried !== null ? carried : { name: event.participant } },
71
73
  },
72
74
  ],
73
75
  };
@@ -7,6 +7,8 @@
7
7
  * - Tools for explicit Discord operations (send, DM, react, etc.)
8
8
  */
9
9
 
10
+ import { NOTICE_TAG } from '../../notice-tags.js';
11
+ import { markedText, suppliedValue } from '../../supplied-text.js';
10
12
  import type { ContentBlock } from '@animalabs/membrane';
11
13
  import { continuesBody, type StoredMessage } from '@animalabs/context-manager';
12
14
  import type {
@@ -47,6 +49,7 @@ import type {
47
49
  import { formatZonedDateTime, resolveTimeZone } from '../../timezone.js';
48
50
  import { logValue } from '../../mcpl/log-value.js';
49
51
  import { unknownToolMessage } from '../unknown-tool.js';
52
+ import { outsideSenderName } from '../sender-name.js';
50
53
  import { DiscordRefusal } from './refusal.js';
51
54
 
52
55
  // Re-export event types for consumers who want to handle them
@@ -119,10 +122,11 @@ const NOT_RUNNING = "Speech handling wasn't changed: the Discord module hasn't s
119
122
  /** What the agent reads for a text attachment whose contents couldn't be
120
123
  * fetched: by an error, or by an answer that wasn't OK. */
121
124
  function attachmentNotFetched(filename: string): { type: 'text'; text: string } {
122
- return {
123
- type: 'text',
124
- text: `[Attachment: ${filename} couldn't be fetched, so its text isn't in your context. If you need it, ask the sender in the channel to paste it.]`,
125
- };
125
+ return { type: 'text', text: notFetchedText(suppliedValue(filename)) };
126
+ }
127
+
128
+ function notFetchedText(filename: string): string {
129
+ return `[${NOTICE_TAG.attachment}: ${filename} couldn't be fetched, so its text isn't in your context. If you need it, ask the sender in the channel to paste it.]`;
126
130
  }
127
131
 
128
132
  /**
@@ -165,7 +169,7 @@ function seconds(n: number): string {
165
169
 
166
170
  /** What a stored message reads when it has no text of its own and nothing the
167
171
  * host added, so the agent still sees that a message was there. */
168
- const EMPTY_MESSAGE = '[Empty message]';
172
+ const EMPTY_MESSAGE = `[${NOTICE_TAG.emptyMessage}]`;
169
173
 
170
174
  /**
171
175
  * Metadata on a stored Discord message whose first block is its own text,
@@ -184,6 +188,14 @@ function ownText(content: string): string {
184
188
  return content.trim() ? content : '';
185
189
  }
186
190
 
191
+ function storedOwnText(content: string): string {
192
+ return markedText(ownText(content));
193
+ }
194
+
195
+ function holdsOwnText(text: string, own: string): boolean {
196
+ return text === own || text === markedText(own);
197
+ }
198
+
187
199
  const isEmptyMarker = (block: ContentBlock): boolean => block.type === 'text' && block.text === EMPTY_MESSAGE;
188
200
  const isBlankText = (block: ContentBlock): boolean => block.type === 'text' && !block.text.trim();
189
201
 
@@ -198,7 +210,7 @@ function textFirst(text: string, after: ContentBlock[] = []): ContentBlock[] {
198
210
  /** A text-first row's content with `text` as the message's own text, in
199
211
  * place of its first block, keeping what the host added after it. */
200
212
  function withOwnText(content: ContentBlock[], text: string): ContentBlock[] {
201
- return textFirst(ownText(text), content.slice(1).filter((block) => !isEmptyMarker(block)));
213
+ return textFirst(storedOwnText(text), content.slice(1).filter((block) => !isEmptyMarker(block)));
202
214
  }
203
215
 
204
216
  /**
@@ -221,11 +233,11 @@ function contentForText(stored: StoredMessage, discordText: string): ContentBloc
221
233
  const text = ownText(discordText);
222
234
  const [first, ...rest] = stored.content;
223
235
  if (stored.metadata?.[TEXT_FIRST] === true) {
224
- if (first?.type === 'text' && first.text === text) return null;
236
+ if (first?.type === 'text' && holdsOwnText(first.text, text)) return null;
225
237
  return withOwnText(stored.content, text);
226
238
  }
227
- if (!text || (first?.type === 'text' && first.text === discordText)) return null;
228
- return [{ type: 'text', text: discordText }, ...(first?.type === 'text' ? rest : stored.content).filter((block) => !isEmptyMarker(block))];
239
+ if (!text || (first?.type === 'text' && holdsOwnText(first.text, discordText))) return null;
240
+ return [{ type: 'text', text: markedText(discordText) }, ...(first?.type === 'text' ? rest : stored.content).filter((block) => !isEmptyMarker(block))];
229
241
  }
230
242
 
231
243
  /**
@@ -255,7 +267,7 @@ function needsRebuild(stored: StoredMessage, message: HistoryMessage): boolean {
255
267
  const [first, ...rest] = stored.content;
256
268
  if (!own && first?.type === 'text' && !isBlankText(first) && !isEmptyMarker(first)) return true;
257
269
  const expected = (own ? 1 : 0) + attachments;
258
- const counted = own && first?.type === 'text' && first.text === own ? 1 + held(rest) : held(stored.content);
270
+ const counted = own && first?.type === 'text' && holdsOwnText(first.text, own) ? 1 + held(rest) : held(stored.content);
259
271
  return counted !== expected || (expected === 0 && !stored.content.some(isEmptyMarker));
260
272
  }
261
273
 
@@ -268,14 +280,14 @@ function needsRebuild(stored: StoredMessage, message: HistoryMessage): boolean {
268
280
  function holdsOtherText(stored: StoredMessage, message: HistoryMessage): boolean {
269
281
  const own = ownText(message.content);
270
282
  const [first, ...rest] = stored.content;
271
- const others = own && first?.type === 'text' && first.text === own ? rest : stored.content;
283
+ const others = own && first?.type === 'text' && holdsOwnText(first.text, own) ? rest : stored.content;
272
284
  return others.some((block) => block.type === 'text' && !isBlankText(block) && !isEmptyMarker(block));
273
285
  }
274
286
 
275
287
  /** Whether a message's conversion holds, after the message's own text, the
276
288
  * note for an attachment of its whose text wasn't fetched. */
277
289
  function holdsNotFetched(content: ContentBlock[], message: HistoryMessage): boolean {
278
- const notes = new Set((message.attachments ?? []).map((attachment) => attachmentNotFetched(attachment.filename).text));
290
+ const notes = new Set((message.attachments ?? []).flatMap((attachment) => [attachmentNotFetched(attachment.filename).text, notFetchedText(attachment.filename)]));
279
291
  return content.slice(1).some((block) => block.type === 'text' && notes.has(block.text));
280
292
  }
281
293
 
@@ -302,9 +314,10 @@ function forGoneAttachment(block: ContentBlock, attachments: readonly DiscordAtt
302
314
  if (block.type !== 'text') return false;
303
315
  const { text } = block;
304
316
  const opening = text.startsWith('\n--- File: ') ? (filename: string) => `\n--- File: ${filename} ---\n`
305
- : text.startsWith('[Attachment: ') ? (filename: string) => `[Attachment: ${filename} `
317
+ : text.startsWith(`[${NOTICE_TAG.attachment}: `) ? (filename: string) => `[${NOTICE_TAG.attachment}: ${filename} `
306
318
  : null;
307
- return opening !== null && !attachments.some((attachment) => text.startsWith(opening(attachment.filename)));
319
+ return opening !== null && !attachments.some((attachment) =>
320
+ text.startsWith(opening(suppliedValue(attachment.filename))) || text.startsWith(opening(attachment.filename)));
308
321
  }
309
322
 
310
323
  /**
@@ -818,7 +831,7 @@ export class DiscordModule implements Module {
818
831
 
819
832
  // Built from the message and its attachments, or from its text alone
820
833
  // when an event carries no blocks; text first, as TEXT_FIRST marks.
821
- const contentBlocks = msg.contentBlocks ?? textFirst(ownText(msg.content));
834
+ const contentBlocks = msg.contentBlocks ?? textFirst(storedOwnText(msg.content));
822
835
  this.noteLive(msg.discordMessageId);
823
836
 
824
837
  // A message a history sync wrote first, stored or held, keeps that row:
@@ -833,9 +846,10 @@ export class DiscordModule implements Module {
833
846
  ...(stored ? {} : {
834
847
  addMessages: [
835
848
  {
836
- participant: msg.authorName,
849
+ participant: outsideSenderName(msg.authorName, 'Discord'),
837
850
  content: contentBlocks,
838
851
  metadata: {
852
+ author: { id: msg.authorId, name: msg.authorName },
839
853
  external: { source: 'discord', id: msg.discordMessageId },
840
854
  channelId: msg.channelId,
841
855
  timestamp: msg.timestamp,
@@ -1144,14 +1158,12 @@ export class DiscordModule implements Module {
1144
1158
  // For bot's own messages (from previous sessions), use the agent name
1145
1159
  // This ensures continuity of conversation history
1146
1160
  const isBotMessage = discordMsg.authorId === this.state.botUserId;
1147
- const agentName = ctx.getAgents()[0]?.name;
1148
- const participantName = isBotMessage
1149
- ? (agentName ?? discordMsg.authorName)
1150
- : discordMsg.authorName;
1161
+ const agentName = isBotMessage ? ctx.getAgents()[0]?.name : undefined;
1151
1162
  ctx.addMessage(
1152
- participantName,
1163
+ agentName ?? outsideSenderName(discordMsg.authorName, 'Discord'),
1153
1164
  content,
1154
1165
  {
1166
+ ...(agentName === undefined ? { author: { id: discordMsg.authorId, name: discordMsg.authorName } } : {}),
1155
1167
  external: { source: 'discord', id: discordMsg.id },
1156
1168
  channelId,
1157
1169
  timestamp: discordMsg.timestamp.getTime(),
@@ -1371,7 +1383,7 @@ export class DiscordModule implements Module {
1371
1383
  if (isTextFile && attachment.size >= TEXT_FILE_LIMIT_BYTES) {
1372
1384
  blocks.push({
1373
1385
  type: 'text',
1374
- text: `[Attachment: ${attachment.filename} (${attachment.size.toLocaleString('en-US')} bytes): this host reads text files under ${TEXT_FILE_LIMIT_BYTES.toLocaleString('en-US')} bytes into your context, so its text isn't here. If you need it, ask the sender to paste the part that matters.]`,
1386
+ text: `[${NOTICE_TAG.attachment}: ${suppliedValue(attachment.filename)} (${attachment.size.toLocaleString('en-US')} bytes): this host reads text files under ${TEXT_FILE_LIMIT_BYTES.toLocaleString('en-US')} bytes into your context, so its text isn't here. If you need it, ask the sender to paste the part that matters.]`,
1375
1387
  });
1376
1388
  continue;
1377
1389
  }
@@ -1383,7 +1395,7 @@ export class DiscordModule implements Module {
1383
1395
  const textContent = await response.text();
1384
1396
  blocks.push({
1385
1397
  type: 'text',
1386
- text: `\n--- File: ${attachment.filename} ---\n${textContent}\n--- End of ${attachment.filename} ---\n`,
1398
+ text: `\n--- File: ${suppliedValue(attachment.filename)} ---\n${markedText(textContent)}\n--- End of ${suppliedValue(attachment.filename)} ---\n`,
1387
1399
  });
1388
1400
  } else {
1389
1401
  // An answer whose body is never read holds its connection.
@@ -1401,11 +1413,11 @@ export class DiscordModule implements Module {
1401
1413
  // For unsupported attachments, add a note
1402
1414
  blocks.push({
1403
1415
  type: 'text',
1404
- text: `[Attachment: ${attachment.filename} (${contentType || 'unknown type'}): this host didn't read its contents in, so they aren't in your context. If you need them, ask the sender for them in a form you can read, such as pasted text.]`,
1416
+ text: `[${NOTICE_TAG.attachment}: ${suppliedValue(attachment.filename)} (${contentType ? suppliedValue(contentType) : 'unknown type'}): this host didn't read its contents in, so they aren't in your context. If you need them, ask the sender for them in a form you can read, such as pasted text.]`,
1405
1417
  });
1406
1418
  }
1407
1419
 
1408
- return textFirst(ownText(message.content), blocks);
1420
+ return textFirst(storedOwnText(message.content), blocks);
1409
1421
  }
1410
1422
 
1411
1423
  /**
@@ -2588,9 +2588,11 @@ function inTextUnits(text: string, hit: MatchHit): MatchHit {
2588
2588
  // author filtering
2589
2589
  // ============================================================================
2590
2590
 
2591
- /** metadata.author as written by MCPL channel ingestion ({id, name}). Every
2592
- * incoming channel message is stored with participant "user" — the real
2593
- * author lives only here. */
2591
+ /** metadata.author as MCPL channel ingestion and DiscordModule write it
2592
+ * ({id, name}), and ApiModule ({name}, or the author object an api:message
2593
+ * event carries). Their participant is "user", or the sender's name marked
2594
+ * with its source unless it's the generic "user", so the plain author
2595
+ * name is read here. */
2594
2596
  function authorOf(msg: StoredMessage): { id?: string; name?: string } | undefined {
2595
2597
  const a = (msg.metadata as { author?: unknown } | undefined)?.author;
2596
2598
  if (!a || typeof a !== 'object') return undefined;
@@ -2657,9 +2659,9 @@ function toAuthorSet(spec: AuthorSpec | undefined, field: string): Set<string> |
2657
2659
 
2658
2660
  /**
2659
2661
  * Predicate for `author`/`excludeAuthor`, or null when neither is given.
2660
- * A message's identities are its metadata.author name and id plus its
2661
- * stored participant — the last is what identifies the agent's own turns
2662
- * (and anything else ingested without author metadata). Exact match after
2662
+ * A message's identities are its metadata.author name and id when it has
2663
+ * one, and otherwise its stored participant, which is what identifies the
2664
+ * agent's own turns (and anything else ingested without author metadata). Exact match after
2663
2665
  * normalization, never substring: "ann" must not pull in "joanne".
2664
2666
  */
2665
2667
  function buildAuthorFilter(include: AuthorSpec | undefined, exclude: AuthorSpec | undefined): ((m: StoredMessage) => boolean) | null {
@@ -0,0 +1,9 @@
1
+ /** The participant a module stores for a sender's name from outside the
2
+ * framework: on one line, and marked with its source, so it isn't an
3
+ * agent's name unless a host names an agent in that form. The generic
4
+ * `user` stays as given, since context-manager places injections by the
5
+ * last message whose participant is `user`, in any capitalization. */
6
+ export function outsideSenderName(name: string, source: string): string {
7
+ const line = name.replace(/[\p{Cc}\u2028\u2029]+/gu, ' ');
8
+ return line.toLowerCase() === 'user' ? line : `${line} (${source})`;
9
+ }
@@ -81,6 +81,18 @@ export function errnoWayOn(code: string | undefined, otherwise = 'Calling again
81
81
  return otherwise;
82
82
  }
83
83
 
84
+ export function errnoClause(code: string | undefined, again: string, then: string): string {
85
+ const lasting = `${again} won't change that; `;
86
+ if (code === 'ENAMETOOLONG') return `${lasting}a shorter name would get past it`;
87
+ const fix = hostFix(code);
88
+ if (fix !== undefined) {
89
+ return code !== undefined && PASSING.has(code)
90
+ ? `${again} may get past it; if it fails the same way, whoever runs this host can ${fix}`
91
+ : `${lasting}whoever runs this host can ${fix}, and ${then}`;
92
+ }
93
+ return `${again} checks it afresh; if the same reason comes back, whoever runs this host can fix it on disk`;
94
+ }
95
+
84
96
  const OS_ERRNO = new Map<number, string>();
85
97
  for (const [name, number] of Object.entries(constants.errno)) if (!OS_ERRNO.has(number)) OS_ERRNO.set(number, name);
86
98
 
@@ -100,9 +112,15 @@ export function changesFiles(tool: string): boolean {
100
112
  return CHANGING.has(tool);
101
113
  }
102
114
 
103
- export const ROOT_RECHECK = "Calling again may work; if it fails the same way, whoever runs this host can check the mount's directory and its path.";
115
+ export const UNFOLLOWED_STEP = 'whoever runs this host can set followSymlinks: true on this mount if it leads inside the mount, mount what it leads to, or a directory that holds it, if it leads outside, or remove it if it leads nowhere';
116
+
117
+ export const MOUNT_IT = 'whoever runs this host can mount it, or a directory that holds it';
118
+
119
+ export const OUTSIDE_STEP = `nothing done through this mount reaches what it leads to, and ${MOUNT_IT}`;
104
120
 
105
- export const ROOT_UNCHECKED = "Calling again won't change that while the mount's directory is gone from its path, and checks it afresh otherwise; whoever runs this host can check the directory and its path.";
121
+ export const RESOLVED_OUTSIDE_STEP = 'nothing done through this mount reaches where it resolves, and whoever runs this host can mount that place';
122
+
123
+ export const ROOT_RECHECK = "Calling again may work; if it fails the same way, whoever runs this host can check the mount's directory and its path.";
106
124
 
107
125
  export const AWAITS_REOPEN = "The workspace can't check its files against disk until its store is reopened, so calling again won't change that; whoever runs this host can reopen it with a restart.";
108
126
 
@@ -56,7 +56,7 @@ import type { BigIntStats } from 'node:fs';
56
56
  import { lstat, open, realpath } from 'node:fs/promises';
57
57
  import { relative, sep } from 'node:path';
58
58
  import { refusedLanding } from './effects.js';
59
- import { ROOT_RECHECK, errnoWayOn, errnoWords, onlyTheHost, systemWords } from './answers.js';
59
+ import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, ROOT_RECHECK, UNFOLLOWED_STEP, errnoWayOn, errnoWords, onlyTheHost, systemWords } from './answers.js';
60
60
  import {
61
61
  type CheckedDir,
62
62
  type MountView,
@@ -135,7 +135,7 @@ export class WriteRefused extends Error {
135
135
 
136
136
  const AFRESH = 'Calling again checks it afresh.';
137
137
 
138
- const NAME_ITS_OWN = "Calling again won't change that; if you know the file it leads to, name that file by its own path.";
138
+ const NAME_ITS_OWN = `Calling again won't change that; ${UNFOLLOWED_STEP}.`;
139
139
 
140
140
  function rootWayOn(code: string | undefined): string {
141
141
  return code === 'ENOENT' || code === 'ENOTDIR' ? onlyTheHost("put the mount's directory back at its path") : errnoWayOn(code, ROOT_RECHECK, true);
@@ -145,7 +145,8 @@ function refusedWayOn(reason: string, code?: string, hostsName = false): string
145
145
  if (reason === ROOT_UNAVAILABLE) return rootWayOn(code);
146
146
  if (code !== undefined) return errnoWayOn(code, undefined, hostsName);
147
147
  if (reason === NOT_FOLLOWED) return NAME_ITS_OWN;
148
- if (reason === OUTSIDE_PARENT || reason === LEAVES_MOUNT) return "Calling again won't change that; the workspace writes only inside its mounts.";
148
+ if (reason === LEAVES_MOUNT) return `Calling again won't change that; ${OUTSIDE_STEP}.`;
149
+ if (reason === OUTSIDE_PARENT) return `Calling again won't change that; ${RESOLVED_OUTSIDE_STEP}.`;
149
150
  if (reason === NOT_A_DIRECTORY) return "Calling again won't change that while something other than a directory stands where the path needs one; another path, or removing what stands there, would.";
150
151
  return null;
151
152
  }