macroclaw 0.31.0 → 0.32.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macroclaw",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,17 @@ import {
9
9
  import { writeHistoryPrompt, writeHistoryResult } from "./history";
10
10
  import { createLogger } from "./logger";
11
11
  import { generateName } from "./naming";
12
- import { buildEvent, type EventInput, SYSTEM_PROMPT } from "./prompts";
12
+ import {
13
+ backgroundAgentProgressEvent,
14
+ backgroundAgentResultEvent,
15
+ backgroundAgentStartEvent,
16
+ buttonClickEvent,
17
+ healthCheckEvent,
18
+ peekEvent,
19
+ SYSTEM_PROMPT,
20
+ scheduleTriggerEvent,
21
+ userMessageEvent,
22
+ } from "./prompts";
13
23
  import { Queue } from "./queue";
14
24
  import { loadSessions, saveSessions } from "./sessions";
15
25
 
@@ -136,13 +146,11 @@ export class Orchestrator {
136
146
 
137
147
  handleCron(name: string, prompt: string, model?: string, missed?: { missedBy: string; scheduledAt: string }): void {
138
148
  const cronName = `cron-${name}`;
139
- const formatted = buildEvent({
140
- name: cronName,
141
- type: "schedule-trigger",
142
- session: "background",
143
- schedule: { name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
144
- text: prompt,
145
- });
149
+ const formatted = scheduleTriggerEvent(
150
+ cronName,
151
+ { name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
152
+ prompt,
153
+ );
146
154
  this.#spawnBackgroundRaw(cronName, prompt, formatted, model ?? this.#config.model);
147
155
  }
148
156
 
@@ -209,13 +217,11 @@ export class Orchestrator {
209
217
  this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(session.name)}</b>...` });
210
218
 
211
219
  try {
212
- const prompt = buildEvent({
213
- name: `peek-${session.name}`,
214
- type: "peek",
215
- session: "background",
216
- targetEvent: session.name,
217
- instructions: `Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
218
- });
220
+ const prompt = peekEvent(
221
+ `peek-${session.name}`,
222
+ session.name,
223
+ `Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
224
+ );
219
225
  const query = this.#claude.forkSession(
220
226
  sessionId,
221
227
  prompt,
@@ -372,56 +378,28 @@ export class Orchestrator {
372
378
  }
373
379
 
374
380
  #formatPrompt(request: OrchestratorRequest, name: string, backgroundedEvent?: string): string {
375
- let input: EventInput;
376
-
377
381
  switch (request.type) {
378
382
  case "user":
379
- input = {
380
- name,
381
- type: "user-message",
382
- session: "main",
383
- text: request.message || undefined,
384
- files: request.files,
385
- backgroundedEvent,
386
- };
387
- break;
383
+ return userMessageEvent(name, request.message || "", { files: request.files, backgroundedEvent });
388
384
  case "background-agent-result":
389
- input = {
385
+ return backgroundAgentResultEvent(
390
386
  name,
391
- type: "background-agent-result",
392
- session: "main",
393
- originalEvent: request.name,
394
- result: {
395
- text: request.response.message || "[No output]",
396
- files: request.response.files,
397
- },
398
- backgroundedEvent,
399
- instructions: "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
400
- };
401
- break;
387
+ request.name,
388
+ { text: request.response.message || "[No output]", files: request.response.files },
389
+ "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
390
+ { backgroundedEvent },
391
+ );
402
392
  case "background-agent-progress":
403
- input = {
393
+ return backgroundAgentProgressEvent(
404
394
  name,
405
- type: "background-agent-progress",
406
- session: "main",
407
- originalEvent: request.name,
408
- progress: request.progress,
409
- instructions: "This is an interim progress update, not a final result. Do not report to the user unless it contains exceptionally important information.",
410
- backgroundedEvent,
411
- };
412
- break;
395
+ request.name,
396
+ request.progress,
397
+ "This is an interim progress update, not a final result. Do not report to the user unless it contains exceptionally important information.",
398
+ { backgroundedEvent },
399
+ );
413
400
  case "button":
414
- input = {
415
- name,
416
- type: "button-click",
417
- session: "main",
418
- button: request.label,
419
- backgroundedEvent,
420
- };
421
- break;
401
+ return buttonClickEvent(name, request.label, { backgroundedEvent });
422
402
  }
423
-
424
- return buildEvent(input);
425
403
  }
426
404
 
427
405
  static #requestLabel(request: OrchestratorRequest): string {
@@ -455,12 +433,7 @@ export class Orchestrator {
455
433
  // --- Background management ---
456
434
 
457
435
  #spawnBackground(name: string, prompt: string, model: string | undefined) {
458
- const formatted = buildEvent({
459
- name,
460
- type: "background-agent-start",
461
- session: "background",
462
- text: prompt,
463
- });
436
+ const formatted = backgroundAgentStartEvent(name, prompt);
464
437
  this.#spawnBackgroundRaw(name, prompt, formatted, model);
465
438
  }
466
439
 
@@ -523,13 +496,11 @@ export class Orchestrator {
523
496
 
524
497
  log.debug({ name: info.name, sessionId }, "Running health check");
525
498
 
526
- const prompt = buildEvent({
527
- name: `health-check-${info.name}`,
528
- type: "health-check",
529
- session: "background",
530
- targetEvent: info.name,
531
- instructions: "Report your current status. If your task is complete, set finished=true and provide the full output. If still working, set finished=false and describe current progress in one sentence.",
532
- });
499
+ const prompt = healthCheckEvent(
500
+ `health-check-${info.name}`,
501
+ info.name,
502
+ "Report your current status. If your task is complete, set finished=true and provide the full output. If still working, set finished=false and describe current progress in one sentence.",
503
+ );
533
504
 
534
505
  let query: RunningQuery<z.infer<typeof healthCheckSchema>>;
535
506
  try {
@@ -1,5 +1,15 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { buildEvent, escapeXml, SYSTEM_PROMPT } from "./prompts";
2
+ import {
3
+ backgroundAgentProgressEvent,
4
+ backgroundAgentResultEvent,
5
+ backgroundAgentStartEvent,
6
+ buttonClickEvent,
7
+ healthCheckEvent,
8
+ peekEvent,
9
+ SYSTEM_PROMPT,
10
+ scheduleTriggerEvent,
11
+ userMessageEvent,
12
+ } from "./prompts";
3
13
 
4
14
  describe("SYSTEM_PROMPT", () => {
5
15
  it("contains key sections", () => {
@@ -50,35 +60,16 @@ describe("SYSTEM_PROMPT", () => {
50
60
  });
51
61
  });
52
62
 
53
- describe("escapeXml", () => {
54
- it("escapes &, <, >, \"", () => {
55
- expect(escapeXml('a & b < c > d "e"')).toBe("a &amp; b &lt; c &gt; d &quot;e&quot;");
56
- });
57
-
58
- it("returns plain text unchanged", () => {
59
- expect(escapeXml("hello world")).toBe("hello world");
60
- });
61
- });
62
-
63
- describe("buildEvent", () => {
63
+ describe("userMessageEvent", () => {
64
64
  it("builds user message event", () => {
65
- const result = buildEvent({
66
- name: "check-logs",
67
- type: "user-message",
68
- session: "main",
69
- text: "hello",
70
- });
65
+ const result = userMessageEvent("check-logs", "hello");
71
66
  expect(result).toStartWith('<event name="check-logs" type="user-message" session="main">');
72
67
  expect(result).toContain("<text>hello</text>");
73
68
  expect(result).toEndWith("</event>");
74
69
  });
75
70
 
76
71
  it("builds user message with files", () => {
77
- const result = buildEvent({
78
- name: "analyze-photo",
79
- type: "user-message",
80
- session: "main",
81
- text: "what's in this image?",
72
+ const result = userMessageEvent("analyze-photo", "what's in this image?", {
82
73
  files: ["/tmp/photo.jpg", "/tmp/doc.pdf"],
83
74
  });
84
75
  expect(result).toContain("<text>what's in this image?</text>");
@@ -88,74 +79,66 @@ describe("buildEvent", () => {
88
79
  expect(result).toContain("</files>");
89
80
  });
90
81
 
91
- it("builds user message with files only (no text)", () => {
92
- const result = buildEvent({
93
- name: "task",
94
- type: "user-message",
95
- session: "main",
96
- files: ["/tmp/photo.jpg"],
97
- });
98
- expect(result).not.toContain("<text>");
99
- expect(result).toContain('<file path="/tmp/photo.jpg" />');
100
- });
101
-
102
82
  it("builds user message with backgrounded event", () => {
103
- const result = buildEvent({
104
- name: "check-logs",
105
- type: "user-message",
106
- session: "main",
83
+ const result = userMessageEvent("check-logs", "check the logs", {
107
84
  backgroundedEvent: "deploy-cluster",
108
- text: "check the logs",
109
85
  });
110
86
  expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
111
87
  expect(result).toContain("<text>check the logs</text>");
112
88
  });
113
89
 
114
90
  it("places backgrounded-event before text", () => {
115
- const result = buildEvent({
116
- name: "check-logs",
117
- type: "user-message",
118
- session: "main",
91
+ const result = userMessageEvent("check-logs", "hello", {
119
92
  backgroundedEvent: "deploy",
120
- text: "hello",
121
93
  });
122
94
  const bgIdx = result.indexOf("backgrounded-event");
123
95
  const textIdx = result.indexOf("<text>");
124
96
  expect(bgIdx).toBeLessThan(textIdx);
125
97
  });
126
98
 
127
- it("builds button click event", () => {
128
- const result = buildEvent({
129
- name: "btn-yes",
130
- type: "button-click",
131
- session: "main",
132
- button: "Yes",
99
+ it("escapes XML in text content", () => {
100
+ const result = userMessageEvent("test", "a < b & c > d");
101
+ expect(result).toContain("<text>a &lt; b &amp; c &gt; d</text>");
102
+ });
103
+
104
+ it("escapes XML in name attribute", () => {
105
+ const result = userMessageEvent('a & "b"', "test");
106
+ expect(result).toContain('name="a &amp; &quot;b&quot;"');
107
+ });
108
+
109
+ it("escapes XML in backgrounded event name", () => {
110
+ const result = userMessageEvent("test", "hello", {
111
+ backgroundedEvent: 'task & "stuff"',
133
112
  });
113
+ expect(result).toContain('backgrounded-event name="task &amp; &quot;stuff&quot;"');
114
+ });
115
+ });
116
+
117
+ describe("buttonClickEvent", () => {
118
+ it("builds button click event", () => {
119
+ const result = buttonClickEvent("btn-yes", "Yes");
134
120
  expect(result).toContain('type="button-click"');
135
121
  expect(result).toContain("<button>Yes</button>");
136
122
  expect(result).not.toContain("<text>");
137
123
  });
138
124
 
139
125
  it("builds button click with backgrounded event", () => {
140
- const result = buildEvent({
141
- name: "btn-yes",
142
- type: "button-click",
143
- session: "main",
144
- button: "Yes",
126
+ const result = buttonClickEvent("btn-yes", "Yes", {
145
127
  backgroundedEvent: "deploy-cluster",
146
128
  });
147
129
  expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
148
130
  expect(result).toContain("<button>Yes</button>");
149
131
  });
150
132
 
133
+ it("escapes XML in button label", () => {
134
+ const result = buttonClickEvent("btn", 'a & "b"');
135
+ expect(result).toContain("<button>a &amp; &quot;b&quot;</button>");
136
+ });
137
+ });
138
+
139
+ describe("scheduleTriggerEvent", () => {
151
140
  it("builds schedule trigger event", () => {
152
- const result = buildEvent({
153
- name: "cron-daily",
154
- type: "schedule-trigger",
155
- session: "background",
156
- schedule: { name: "daily" },
157
- text: "check updates",
158
- });
141
+ const result = scheduleTriggerEvent("cron-daily", { name: "daily" }, "check updates");
159
142
  expect(result).toContain('type="schedule-trigger"');
160
143
  expect(result).toContain('session="background"');
161
144
  expect(result).toContain('<schedule name="daily" />');
@@ -163,38 +146,34 @@ describe("buildEvent", () => {
163
146
  });
164
147
 
165
148
  it("builds missed schedule trigger with attributes", () => {
166
- const result = buildEvent({
167
- name: "cron-reminder",
168
- type: "schedule-trigger",
169
- session: "background",
170
- schedule: { name: "reminder", missedBy: "15m", scheduledAt: "2026-03-20T06:00:00Z" },
171
- text: "buy milk",
172
- });
149
+ const result = scheduleTriggerEvent(
150
+ "cron-reminder",
151
+ { name: "reminder", missedBy: "15m", scheduledAt: "2026-03-20T06:00:00Z" },
152
+ "buy milk",
153
+ );
173
154
  expect(result).toContain('missed-by="15m"');
174
155
  expect(result).toContain('scheduled-at="2026-03-20T06:00:00Z"');
175
156
  expect(result).toContain("<text>buy milk</text>");
176
157
  });
158
+ });
177
159
 
160
+ describe("backgroundAgentStartEvent", () => {
178
161
  it("builds background agent start event", () => {
179
- const result = buildEvent({
180
- name: "research",
181
- type: "background-agent-start",
182
- session: "background",
183
- text: "find papers about transformers",
184
- });
162
+ const result = backgroundAgentStartEvent("research", "find papers about transformers");
185
163
  expect(result).toContain('type="background-agent-start"');
186
164
  expect(result).toContain('session="background"');
187
165
  expect(result).toContain("<text>find papers about transformers</text>");
188
166
  });
167
+ });
189
168
 
169
+ describe("backgroundAgentResultEvent", () => {
190
170
  it("builds background agent result (text only)", () => {
191
- const result = buildEvent({
192
- name: "bg-research",
193
- type: "background-agent-result",
194
- session: "main",
195
- originalEvent: "research",
196
- result: { text: "found 3 papers" },
197
- });
171
+ const result = backgroundAgentResultEvent(
172
+ "bg-research",
173
+ "research",
174
+ { text: "found 3 papers" },
175
+ "Forward to user.",
176
+ );
198
177
  expect(result).toContain('type="background-agent-result"');
199
178
  expect(result).toContain('<original-event name="research" />');
200
179
  expect(result).toContain("<result>");
@@ -204,102 +183,63 @@ describe("buildEvent", () => {
204
183
  });
205
184
 
206
185
  it("builds background agent result with files", () => {
207
- const result = buildEvent({
208
- name: "bg-research",
209
- type: "background-agent-result",
210
- session: "main",
211
- originalEvent: "research",
212
- result: { text: "here are the screenshots", files: ["/tmp/screenshot.png"] },
213
- });
186
+ const result = backgroundAgentResultEvent(
187
+ "bg-research",
188
+ "research",
189
+ { text: "here are the screenshots", files: ["/tmp/screenshot.png"] },
190
+ "Forward to user.",
191
+ );
214
192
  expect(result).toContain("<result>");
215
193
  expect(result).toContain("<text>here are the screenshots</text>");
216
194
  expect(result).toContain('<file path="/tmp/screenshot.png" />');
217
195
  expect(result).toContain("</result>");
218
196
  });
219
197
 
220
- it("builds peek event with instructions", () => {
221
- const result = buildEvent({
222
- name: "peek-deploy",
223
- type: "peek",
224
- session: "background",
225
- targetEvent: "deploy",
226
- instructions: "Brief status update.",
227
- });
228
- expect(result).toContain('type="peek"');
229
- expect(result).toContain('<target-event name="deploy" />');
230
- expect(result).toContain("<instructions>Brief status update.</instructions>");
231
- expect(result).not.toContain("<text>");
232
- });
233
-
234
- it("builds progress event with progress tag", () => {
235
- const result = buildEvent({
236
- name: "progress-research",
237
- type: "background-agent-progress",
238
- session: "main",
239
- originalEvent: "research",
240
- progress: "indexing 500 documents",
241
- });
242
- expect(result).toContain('type="background-agent-progress"');
243
- expect(result).toContain('<original-event name="research" />');
244
- expect(result).toContain("<progress>indexing 500 documents</progress>");
245
- expect(result).not.toContain("<result>");
246
- });
247
-
248
- it("includes instructions in event", () => {
249
- const result = buildEvent({
250
- name: "bg-research",
251
- type: "background-agent-result",
252
- session: "main",
253
- originalEvent: "research",
254
- result: { text: "done" },
255
- instructions: "Forward to user.",
256
- });
198
+ it("includes instructions after result", () => {
199
+ const result = backgroundAgentResultEvent(
200
+ "bg-research",
201
+ "research",
202
+ { text: "done" },
203
+ "Forward to user.",
204
+ );
257
205
  expect(result).toContain("<instructions>Forward to user.</instructions>");
258
- // instructions come last, before </event>
259
206
  const instrIdx = result.indexOf("<instructions>");
260
207
  const closeIdx = result.indexOf("</event>");
261
208
  expect(instrIdx).toBeLessThan(closeIdx);
262
209
  expect(instrIdx).toBeGreaterThan(result.indexOf("</result>"));
263
210
  });
211
+ });
264
212
 
265
- it("escapes XML in text content", () => {
266
- const result = buildEvent({
267
- name: "test",
268
- type: "user-message",
269
- session: "main",
270
- text: "a < b & c > d",
271
- });
272
- expect(result).toContain("<text>a &lt; b &amp; c &gt; d</text>");
273
- });
274
-
275
- it("escapes XML in name attribute", () => {
276
- const result = buildEvent({
277
- name: 'a & "b"',
278
- type: "user-message",
279
- session: "main",
280
- text: "test",
281
- });
282
- expect(result).toContain('name="a &amp; &quot;b&quot;"');
213
+ describe("backgroundAgentProgressEvent", () => {
214
+ it("builds progress event with progress tag", () => {
215
+ const result = backgroundAgentProgressEvent(
216
+ "progress-research",
217
+ "research",
218
+ "indexing 500 documents",
219
+ "Do not report unless important.",
220
+ );
221
+ expect(result).toContain('type="background-agent-progress"');
222
+ expect(result).toContain('<original-event name="research" />');
223
+ expect(result).toContain("<progress>indexing 500 documents</progress>");
224
+ expect(result).not.toContain("<result>");
283
225
  });
226
+ });
284
227
 
285
- it("escapes XML in button label", () => {
286
- const result = buildEvent({
287
- name: "btn",
288
- type: "button-click",
289
- session: "main",
290
- button: 'a & "b"',
291
- });
292
- expect(result).toContain("<button>a &amp; &quot;b&quot;</button>");
228
+ describe("peekEvent", () => {
229
+ it("builds peek event with instructions", () => {
230
+ const result = peekEvent("peek-deploy", "deploy", "Brief status update.");
231
+ expect(result).toContain('type="peek"');
232
+ expect(result).toContain('<target-event name="deploy" />');
233
+ expect(result).toContain("<instructions>Brief status update.</instructions>");
234
+ expect(result).not.toContain("<text>");
293
235
  });
236
+ });
294
237
 
295
- it("escapes XML in backgrounded event name", () => {
296
- const result = buildEvent({
297
- name: "test",
298
- type: "user-message",
299
- session: "main",
300
- backgroundedEvent: 'task & "stuff"',
301
- text: "hello",
302
- });
303
- expect(result).toContain('backgrounded-event name="task &amp; &quot;stuff&quot;"');
238
+ describe("healthCheckEvent", () => {
239
+ it("builds health check event with instructions", () => {
240
+ const result = healthCheckEvent("health-check-deploy", "deploy", "Report status.");
241
+ expect(result).toContain('type="health-check"');
242
+ expect(result).toContain('<target-event name="deploy" />');
243
+ expect(result).toContain("<instructions>Report status.</instructions>");
304
244
  });
305
245
  });
package/src/prompts.ts CHANGED
@@ -67,26 +67,11 @@ Each button gets its own row. Max 27 characters per label — if options need mo
67
67
 
68
68
  // --- Event builder ---
69
69
 
70
- export function escapeXml(text: string): string {
70
+ function escapeXml(text: string): string {
71
71
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
72
72
  }
73
73
 
74
- export type SessionType = "main" | "background";
75
-
76
- export type EventType =
77
- | "user-message"
78
- | "button-click"
79
- | "schedule-trigger"
80
- | "background-agent-start"
81
- | "background-agent-result"
82
- | "background-agent-progress"
83
- | "peek"
84
- | "health-check";
85
-
86
- export interface EventInput {
87
- name: string;
88
- type: EventType;
89
- session: SessionType;
74
+ interface BuildXmlFields {
90
75
  text?: string;
91
76
  files?: string[];
92
77
  button?: string;
@@ -99,46 +84,40 @@ export interface EventInput {
99
84
  result?: { text: string; files?: string[] };
100
85
  }
101
86
 
102
- export function buildEvent(input: EventInput): string {
87
+ function buildXml(name: string, type: string, session: string, fields: BuildXmlFields): string {
103
88
  const lines: string[] = [
104
- `<event name="${escapeXml(input.name)}" type="${input.type}" session="${input.session}">`,
89
+ `<event name="${escapeXml(name)}" type="${type}" session="${session}">`,
105
90
  ];
106
91
 
107
- // Backgrounded event (always before content for visibility)
108
- if (input.backgroundedEvent) {
109
- lines.push(`<backgrounded-event name="${escapeXml(input.backgroundedEvent)}" />`);
92
+ if (fields.backgroundedEvent) {
93
+ lines.push(`<backgrounded-event name="${escapeXml(fields.backgroundedEvent)}" />`);
110
94
  }
111
95
 
112
- // Schedule metadata
113
- if (input.schedule) {
114
- const attrs = [`name="${escapeXml(input.schedule.name)}"`];
115
- if (input.schedule.missedBy) attrs.push(`missed-by="${escapeXml(input.schedule.missedBy)}"`);
116
- if (input.schedule.scheduledAt) attrs.push(`scheduled-at="${escapeXml(input.schedule.scheduledAt)}"`);
96
+ if (fields.schedule) {
97
+ const attrs = [`name="${escapeXml(fields.schedule.name)}"`];
98
+ if (fields.schedule.missedBy) attrs.push(`missed-by="${escapeXml(fields.schedule.missedBy)}"`);
99
+ if (fields.schedule.scheduledAt) attrs.push(`scheduled-at="${escapeXml(fields.schedule.scheduledAt)}"`);
117
100
  lines.push(`<schedule ${attrs.join(" ")} />`);
118
101
  }
119
102
 
120
- // Original event reference (for background-agent-result)
121
- if (input.originalEvent) {
122
- lines.push(`<original-event name="${escapeXml(input.originalEvent)}" />`);
103
+ if (fields.originalEvent) {
104
+ lines.push(`<original-event name="${escapeXml(fields.originalEvent)}" />`);
123
105
  }
124
106
 
125
- // Target event reference (for peek)
126
- if (input.targetEvent) {
127
- lines.push(`<target-event name="${escapeXml(input.targetEvent)}" />`);
107
+ if (fields.targetEvent) {
108
+ lines.push(`<target-event name="${escapeXml(fields.targetEvent)}" />`);
128
109
  }
129
110
 
130
- // Progress (for background-agent-progress)
131
- if (input.progress) {
132
- lines.push(`<progress>${escapeXml(input.progress)}</progress>`);
111
+ if (fields.progress) {
112
+ lines.push(`<progress>${escapeXml(fields.progress)}</progress>`);
133
113
  }
134
114
 
135
- // Result block (for background-agent-result)
136
- if (input.result) {
115
+ if (fields.result) {
137
116
  lines.push("<result>");
138
- lines.push(`<text>${escapeXml(input.result.text)}</text>`);
139
- if (input.result.files?.length) {
117
+ lines.push(`<text>${escapeXml(fields.result.text)}</text>`);
118
+ if (fields.result.files?.length) {
140
119
  lines.push("<files>");
141
- for (const f of input.result.files) {
120
+ for (const f of fields.result.files) {
142
121
  lines.push(` <file path="${escapeXml(f)}" />`);
143
122
  }
144
123
  lines.push("</files>");
@@ -146,30 +125,60 @@ export function buildEvent(input: EventInput): string {
146
125
  lines.push("</result>");
147
126
  }
148
127
 
149
- // Button label
150
- if (input.button) {
151
- lines.push(`<button>${escapeXml(input.button)}</button>`);
128
+ if (fields.button) {
129
+ lines.push(`<button>${escapeXml(fields.button)}</button>`);
152
130
  }
153
131
 
154
- // Text content
155
- if (input.text) {
156
- lines.push(`<text>${escapeXml(input.text)}</text>`);
132
+ if (fields.text) {
133
+ lines.push(`<text>${escapeXml(fields.text)}</text>`);
157
134
  }
158
135
 
159
- // Files
160
- if (input.files?.length) {
136
+ if (fields.files?.length) {
161
137
  lines.push("<files>");
162
- for (const f of input.files) {
138
+ for (const f of fields.files) {
163
139
  lines.push(` <file path="${escapeXml(f)}" />`);
164
140
  }
165
141
  lines.push("</files>");
166
142
  }
167
143
 
168
- // Instructions (inline guidance for long sessions)
169
- if (input.instructions) {
170
- lines.push(`<instructions>${escapeXml(input.instructions)}</instructions>`);
144
+ if (fields.instructions) {
145
+ lines.push(`<instructions>${escapeXml(fields.instructions)}</instructions>`);
171
146
  }
172
147
 
173
148
  lines.push("</event>");
174
149
  return lines.join("\n");
175
150
  }
151
+
152
+ // --- Per-type event builders ---
153
+
154
+ export function userMessageEvent(name: string, text: string, opts?: { files?: string[]; backgroundedEvent?: string }): string {
155
+ return buildXml(name, "user-message", "main", { text, files: opts?.files, backgroundedEvent: opts?.backgroundedEvent });
156
+ }
157
+
158
+ export function buttonClickEvent(name: string, button: string, opts?: { backgroundedEvent?: string }): string {
159
+ return buildXml(name, "button-click", "main", { button, backgroundedEvent: opts?.backgroundedEvent });
160
+ }
161
+
162
+ export function scheduleTriggerEvent(name: string, schedule: { name: string; missedBy?: string; scheduledAt?: string }, text: string): string {
163
+ return buildXml(name, "schedule-trigger", "background", { schedule, text });
164
+ }
165
+
166
+ export function backgroundAgentStartEvent(name: string, text: string): string {
167
+ return buildXml(name, "background-agent-start", "background", { text });
168
+ }
169
+
170
+ export function backgroundAgentResultEvent(name: string, originalEvent: string, result: { text: string; files?: string[] }, instructions: string, opts?: { backgroundedEvent?: string }): string {
171
+ return buildXml(name, "background-agent-result", "main", { originalEvent, result, instructions, backgroundedEvent: opts?.backgroundedEvent });
172
+ }
173
+
174
+ export function backgroundAgentProgressEvent(name: string, originalEvent: string, progress: string, instructions: string, opts?: { backgroundedEvent?: string }): string {
175
+ return buildXml(name, "background-agent-progress", "main", { originalEvent, progress, instructions, backgroundedEvent: opts?.backgroundedEvent });
176
+ }
177
+
178
+ export function peekEvent(name: string, targetEvent: string, instructions: string): string {
179
+ return buildXml(name, "peek", "background", { targetEvent, instructions });
180
+ }
181
+
182
+ export function healthCheckEvent(name: string, targetEvent: string, instructions: string): string {
183
+ return buildXml(name, "health-check", "background", { targetEvent, instructions });
184
+ }