macroclaw 0.27.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.
package/src/prompts.ts CHANGED
@@ -15,28 +15,161 @@ Use raw <b>, <i>, <code>, <pre> tags. Escape &, <, > in text content as &amp;, &
15
15
  Architecture: message bridge connecting chat interface and scheduled tasks. \
16
16
  Persistent session — conversation history carries across messages. Workspace persists across sessions.
17
17
 
18
- Context tags: messages may be prefixed with [Context: <type>]. Types:
19
- - cron/<name>automated scheduled task. Prefer action="silent" when nothing noteworthy.
20
- - button-clickuser tapped an inline keyboard button.
21
- - background-result/<name>output from a background agent you spawned. Decide whether to relay or handle silently.
22
- - background-agent/<name>you are a background agent. Complete task, return result.
23
- - previous task "<prompt>" moved to background a long-running task was demoted. Mention briefly if relevant.
18
+ Event format: every incoming message is wrapped in an <event> XML block. Attributes:
19
+ - name — short identifier for this event (e.g. "check-logs", "cron-daily").
20
+ - typewhat triggered this event. One of:
21
+ - user-messagedirect user message. Content in <text>, optional <files>.
22
+ - button-clickuser tapped an inline button. Label in <button>.
23
+ - schedule-trigger — automated scheduled task. Contains <schedule> with name and optional missed-by/scheduled-at attributes. Prefer action="silent" when nothing noteworthy.
24
+ - background-agent-start — you are a background agent. Complete the task in <text> and return a result.
25
+ - background-agent-result — a background agent has finished. Contains <original-event name="..." /> linking to the agent that produced it, and a <result> block with <text> and optional <files>. Always use action="send" — the user expects to see the outcome. Summarize, relay, or add additional context from the conversation as appropriate.
26
+ - background-agent-progress — interim progress update from a still-running background agent. Contains <original-event name="..." /> and a <progress> element. This is NOT a final result. Do not report to the user unless it contains exceptionally important information (errors, blockers, urgent findings). Keep this context in mind — if the user later asks about progress of a background task, use the latest progress update to answer.
27
+ - peek — status check on a running session. Contains <target-event name="..." /> identifying the event being peeked at. Only consider progress since that event started. Respond with a brief status update (2-3 sentences): what has been done, what's happening now, what's remaining. Return plain text, not structured output.
28
+ - health-check — automated status check on a background agent. Contains <target-event name="..." />. Report whether the task is complete or still in progress.
29
+ - session — "main" (primary conversation) or "background" (background agent).
30
+
31
+ Backgrounded events: when a new message arrives while a previous task is still running, \
32
+ the running task is automatically moved to a background session. The new event will contain \
33
+ a <backgrounded-event name="..." /> element referencing the task that was moved. \
34
+ This is informational — the backgrounded task continues running independently. \
35
+ Do not re-execute or act on the backgrounded task; focus on the new event's content.
36
+
37
+ Inner elements:
38
+ - <text> — the message text or task description.
39
+ - <files> — list of <file path="..." /> attachments. Read/view at those paths.
40
+ - <button> — the label of the tapped button.
41
+ - <schedule> — cron job metadata (name, missed-by, scheduled-at attributes).
42
+ - <backgrounded-event name="..." /> — a previously running task moved to background (see above).
43
+ - <original-event name="..." /> — in background-agent-result, links to the agent that produced the result.
44
+ - <target-event name="..." /> — in peek, identifies the event being checked on.
45
+ - <progress> — interim status from a still-running background agent.
46
+ - <result> — wraps the output from a completed background agent. Contains <text> and optional <files>.
47
+ - <instructions> — inline guidance for how to handle this specific event. Always follow these instructions.
24
48
 
25
49
  Background agents: spawn alongside any response via backgroundAgents array:
26
50
  backgroundAgents: [{ name: "label", prompt: "task", model: "haiku" }]
27
- Each runs in same workspace, forked session. Result fed back as [Context: background-result/<name>].
51
+ Each runs in same workspace, forked session. Result fed back as background-agent-result event.
28
52
  Models: haiku (fast/cheap), sonnet (balanced, default), opus (complex reasoning).
29
53
  User can spawn directly with /bg command. Use for long-running tasks that shouldn't block.
30
54
 
31
55
  Session routing: if a new message arrives while your session is busy for over 1 minute, \
32
56
  the running task is automatically moved to background and a new session is forked. \
33
- You may see a [Context: previous task "..." moved to background] prefix when this happens.
57
+ The new event will contain a <backgrounded-event> element (see above).
34
58
 
35
- Files: attachments listed as [File: /path] prefixes. Read/view at those paths. \
36
- Send files via files array (absolute paths). Images (.png/.jpg/.jpeg/.gif/.webp) as photos, rest as documents. 50MB limit.
59
+ Files: send files via files array (absolute paths). \
60
+ Images (.png/.jpg/.jpeg/.gif/.webp) as photos, rest as documents. 50MB limit.
37
61
 
38
62
  Cron: jobs in data/schedule.json (hot-reloaded). Cron jobs always run as background sessions. \
39
63
  Use "silent" when check finds nothing new, "send" when noteworthy.
40
64
 
41
65
  MessageButtons: include a buttons field (flat array of label strings) to attach inline buttons below your message. \
42
66
  Each button gets its own row. Max 27 characters per label — if options need more detail, describe them in the message and use short labels on buttons.`;
67
+
68
+ // --- Event builder ---
69
+
70
+ export function escapeXml(text: string): string {
71
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
72
+ }
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;
90
+ text?: string;
91
+ files?: string[];
92
+ button?: string;
93
+ schedule?: { name: string; missedBy?: string; scheduledAt?: string };
94
+ backgroundedEvent?: string;
95
+ originalEvent?: string;
96
+ targetEvent?: string;
97
+ instructions?: string;
98
+ progress?: string;
99
+ result?: { text: string; files?: string[] };
100
+ }
101
+
102
+ export function buildEvent(input: EventInput): string {
103
+ const lines: string[] = [
104
+ `<event name="${escapeXml(input.name)}" type="${input.type}" session="${input.session}">`,
105
+ ];
106
+
107
+ // Backgrounded event (always before content for visibility)
108
+ if (input.backgroundedEvent) {
109
+ lines.push(`<backgrounded-event name="${escapeXml(input.backgroundedEvent)}" />`);
110
+ }
111
+
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)}"`);
117
+ lines.push(`<schedule ${attrs.join(" ")} />`);
118
+ }
119
+
120
+ // Original event reference (for background-agent-result)
121
+ if (input.originalEvent) {
122
+ lines.push(`<original-event name="${escapeXml(input.originalEvent)}" />`);
123
+ }
124
+
125
+ // Target event reference (for peek)
126
+ if (input.targetEvent) {
127
+ lines.push(`<target-event name="${escapeXml(input.targetEvent)}" />`);
128
+ }
129
+
130
+ // Progress (for background-agent-progress)
131
+ if (input.progress) {
132
+ lines.push(`<progress>${escapeXml(input.progress)}</progress>`);
133
+ }
134
+
135
+ // Result block (for background-agent-result)
136
+ if (input.result) {
137
+ lines.push("<result>");
138
+ lines.push(`<text>${escapeXml(input.result.text)}</text>`);
139
+ if (input.result.files?.length) {
140
+ lines.push("<files>");
141
+ for (const f of input.result.files) {
142
+ lines.push(` <file path="${escapeXml(f)}" />`);
143
+ }
144
+ lines.push("</files>");
145
+ }
146
+ lines.push("</result>");
147
+ }
148
+
149
+ // Button label
150
+ if (input.button) {
151
+ lines.push(`<button>${escapeXml(input.button)}</button>`);
152
+ }
153
+
154
+ // Text content
155
+ if (input.text) {
156
+ lines.push(`<text>${escapeXml(input.text)}</text>`);
157
+ }
158
+
159
+ // Files
160
+ if (input.files?.length) {
161
+ lines.push("<files>");
162
+ for (const f of input.files) {
163
+ lines.push(` <file path="${escapeXml(f)}" />`);
164
+ }
165
+ lines.push("</files>");
166
+ }
167
+
168
+ // Instructions (inline guidance for long sessions)
169
+ if (input.instructions) {
170
+ lines.push(`<instructions>${escapeXml(input.instructions)}</instructions>`);
171
+ }
172
+
173
+ lines.push("</event>");
174
+ return lines.join("\n");
175
+ }
@@ -17,7 +17,7 @@ function readScheduleConfig() {
17
17
  }
18
18
 
19
19
  function makeOnJob() {
20
- return mock((_name: string, _prompt: string, _model?: string) => {});
20
+ return mock((_name: string, _prompt: string, _model?: string, _missed?: { missedBy: string; scheduledAt: string }) => {});
21
21
  }
22
22
 
23
23
  // Build a cron expression that matches the current minute
@@ -169,6 +169,7 @@ describe("Scheduler — fireAt jobs", () => {
169
169
  s.stop();
170
170
 
171
171
  expect(onJob).toHaveBeenCalledWith("now", "do it", undefined);
172
+ expect(onJob.mock.calls[0][3]).toBeUndefined();
172
173
  });
173
174
 
174
175
  it("removes one-shot job after firing", () => {
@@ -220,9 +221,10 @@ describe("Scheduler — fireAt jobs", () => {
220
221
  expect(onJob).toHaveBeenCalledTimes(1);
221
222
  const call = onJob.mock.calls[0];
222
223
  expect(call[0]).toBe("reminder");
223
- expect(call[1]).toContain("[missed event, should have fired");
224
- expect(call[1]).toContain("min ago at");
225
- expect(call[1]).toContain("buy milk");
224
+ expect(call[1]).toBe("buy milk");
225
+ expect(call[3]).toBeDefined();
226
+ expect(call[3]!.missedBy).toMatch(/^\d+m$/);
227
+ expect(call[3]!.scheduledAt).toBeDefined();
226
228
  });
227
229
 
228
230
  it("removes missed one-shot job after firing", () => {
@@ -257,8 +259,9 @@ describe("Scheduler — fireAt jobs", () => {
257
259
  expect(onJob).toHaveBeenCalledTimes(1);
258
260
  const call = onJob.mock.calls[0];
259
261
  expect(call[0]).toBe("recent");
260
- expect(call[1]).toContain("[missed event, should have fired");
261
- expect(call[1]).toContain("still valid");
262
+ expect(call[1]).toBe("still valid");
263
+ expect(call[3]).toBeDefined();
264
+ expect(call[3]!.missedBy).toMatch(/^\d+m$/);
262
265
  });
263
266
 
264
267
  it("discards stale one-shot job (older than a week) without firing", () => {
package/src/scheduler.ts CHANGED
@@ -21,8 +21,13 @@ const scheduleConfigSchema = z.object({
21
21
  type ScheduleConfig = z.infer<typeof scheduleConfigSchema>;
22
22
  type Job = z.infer<typeof jobSchema>;
23
23
 
24
+ export interface MissedInfo {
25
+ missedBy: string;
26
+ scheduledAt: string;
27
+ }
28
+
24
29
  export interface SchedulerConfig {
25
- onJob: (name: string, prompt: string, model?: string) => void;
30
+ onJob: (name: string, prompt: string, model?: string, missed?: MissedInfo) => void;
26
31
  }
27
32
 
28
33
  const TICK_INTERVAL = 10_000; // 10 seconds
@@ -148,9 +153,11 @@ export class Scheduler {
148
153
 
149
154
  if (diff <= MAX_MISSED_MS) {
150
155
  const missedMinutes = Math.round(diff / 60_000);
151
- const missedPrompt = `[missed event, should have fired ${missedMinutes} min ago at ${job.fireAt}] ${job.prompt}`;
152
156
  log.info({ name: job.name, missedMinutes, fireAt: job.fireAt }, "Firing missed one-shot job");
153
- this.#config.onJob(job.name, missedPrompt, job.model);
157
+ this.#config.onJob(job.name, job.prompt, job.model, {
158
+ missedBy: `${missedMinutes}m`,
159
+ scheduledAt: job.fireAt,
160
+ });
154
161
  return "remove";
155
162
  }
156
163