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 +1 -1
- package/src/orchestrator.ts +41 -70
- package/src/prompts.test.ts +102 -162
- package/src/prompts.ts +62 -53
package/package.json
CHANGED
package/src/orchestrator.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
session:
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
+
return backgroundAgentResultEvent(
|
|
390
386
|
name,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
393
|
+
return backgroundAgentProgressEvent(
|
|
404
394
|
name,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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 {
|
package/src/prompts.test.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
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("
|
|
54
|
-
it("escapes &, <, >, \"", () => {
|
|
55
|
-
expect(escapeXml('a & b < c > d "e"')).toBe("a & b < c > d "e"");
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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("
|
|
128
|
-
const result =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
99
|
+
it("escapes XML in text content", () => {
|
|
100
|
+
const result = userMessageEvent("test", "a < b & c > d");
|
|
101
|
+
expect(result).toContain("<text>a < b & c > d</text>");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("escapes XML in name attribute", () => {
|
|
105
|
+
const result = userMessageEvent('a & "b"', "test");
|
|
106
|
+
expect(result).toContain('name="a & "b""');
|
|
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 & "stuff""');
|
|
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 =
|
|
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 & "b"</button>");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("scheduleTriggerEvent", () => {
|
|
151
140
|
it("builds schedule trigger event", () => {
|
|
152
|
-
const result =
|
|
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 =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 =
|
|
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 =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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("
|
|
221
|
-
const result =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
name: 'a & "b"',
|
|
278
|
-
type: "user-message",
|
|
279
|
-
session: "main",
|
|
280
|
-
text: "test",
|
|
281
|
-
});
|
|
282
|
-
expect(result).toContain('name="a & "b""');
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
expect(result).toContain("<button>a & "b"</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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
text: "hello",
|
|
302
|
-
});
|
|
303
|
-
expect(result).toContain('backgrounded-event name="task & "stuff""');
|
|
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
|
-
|
|
70
|
+
function escapeXml(text: string): string {
|
|
71
71
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
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
|
-
|
|
87
|
+
function buildXml(name: string, type: string, session: string, fields: BuildXmlFields): string {
|
|
103
88
|
const lines: string[] = [
|
|
104
|
-
`<event name="${escapeXml(
|
|
89
|
+
`<event name="${escapeXml(name)}" type="${type}" session="${session}">`,
|
|
105
90
|
];
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
if (input.result) {
|
|
115
|
+
if (fields.result) {
|
|
137
116
|
lines.push("<result>");
|
|
138
|
-
lines.push(`<text>${escapeXml(
|
|
139
|
-
if (
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
160
|
-
if (input.files?.length) {
|
|
136
|
+
if (fields.files?.length) {
|
|
161
137
|
lines.push("<files>");
|
|
162
|
-
for (const f of
|
|
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
|
-
|
|
169
|
-
|
|
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
|
+
}
|