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/package.json +1 -1
- package/src/app.test.ts +7 -7
- package/src/app.ts +2 -2
- package/src/claude.ts +1 -1
- package/src/index.ts +4 -0
- package/src/naming.test.ts +44 -0
- package/src/naming.ts +54 -0
- package/src/orchestrator.test.ts +162 -11
- package/src/orchestrator.ts +208 -35
- package/src/prompts.test.ts +268 -6
- package/src/prompts.ts +143 -10
- package/src/scheduler.test.ts +9 -6
- package/src/scheduler.ts +10 -3
package/src/prompts.ts
CHANGED
|
@@ -15,28 +15,161 @@ Use raw <b>, <i>, <code>, <pre> tags. Escape &, <, > in text content as &, &
|
|
|
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
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
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
|
+
- type — what triggered this event. One of:
|
|
21
|
+
- user-message — direct user message. Content in <text>, optional <files>.
|
|
22
|
+
- button-click — user 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
|
|
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
|
-
|
|
57
|
+
The new event will contain a <backgrounded-event> element (see above).
|
|
34
58
|
|
|
35
|
-
Files:
|
|
36
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
+
}
|
package/src/scheduler.test.ts
CHANGED
|
@@ -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]).
|
|
224
|
-
expect(call[
|
|
225
|
-
expect(call[
|
|
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]).
|
|
261
|
-
expect(call[
|
|
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,
|
|
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
|
|