@iamoberlin/chorus 2.0.0 → 2.2.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/{target/idl → idl}/chorus_prayers.json +387 -42
- package/index.ts +4 -1
- package/package.json +6 -5
- package/src/choirs.ts +12 -8
- package/src/prayers/cli.ts +231 -84
- package/src/prayers/crypto.ts +132 -0
- package/src/prayers/solana.ts +329 -52
- package/src/scheduler.ts +84 -36
package/src/scheduler.ts
CHANGED
|
@@ -14,6 +14,17 @@ import { join } from "path";
|
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { spawn } from "child_process";
|
|
16
16
|
|
|
17
|
+
// Type for the plugin API's runAgentTurn method
|
|
18
|
+
interface AgentTurnResult {
|
|
19
|
+
text?: string;
|
|
20
|
+
payloads?: Array<{ text?: string; mediaUrl?: string | null }>;
|
|
21
|
+
meta?: { durationMs?: number };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Delivery ─────────────────────────────────────────────────
|
|
25
|
+
// Choir agents handle their own delivery via OpenClaw messaging tools.
|
|
26
|
+
// The scheduler's job is execution and scheduling — not routing messages.
|
|
27
|
+
|
|
17
28
|
interface ChoirContext {
|
|
18
29
|
choirId: string;
|
|
19
30
|
output: string;
|
|
@@ -95,6 +106,52 @@ export function createChoirScheduler(
|
|
|
95
106
|
// Load persisted state instead of starting fresh
|
|
96
107
|
const runState = loadRunState(log);
|
|
97
108
|
|
|
109
|
+
// CLI fallback for executing choirs when plugin API is unavailable
|
|
110
|
+
async function executeChoirViaCli(choir: Choir, prompt: string): Promise<string> {
|
|
111
|
+
const result = await new Promise<{ status: number | null; stdout: string; stderr: string }>((resolve) => {
|
|
112
|
+
const child = spawn('openclaw', [
|
|
113
|
+
'agent',
|
|
114
|
+
'--session-id', `chorus:${choir.id}`,
|
|
115
|
+
'--message', prompt,
|
|
116
|
+
'--json',
|
|
117
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
118
|
+
|
|
119
|
+
let stdout = '';
|
|
120
|
+
let stderr = '';
|
|
121
|
+
const maxBuffer = 1024 * 1024;
|
|
122
|
+
child.stdout.on('data', (d: Buffer) => { if (stdout.length < maxBuffer) stdout += d.toString(); });
|
|
123
|
+
child.stderr.on('data', (d: Buffer) => { if (stderr.length < maxBuffer) stderr += d.toString(); });
|
|
124
|
+
|
|
125
|
+
const timer = setTimeout(() => { child.kill('SIGTERM'); }, 300000); // 5 min
|
|
126
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ status: code, stdout, stderr }); });
|
|
127
|
+
child.on('error', (err) => { clearTimeout(timer); resolve({ status: 1, stdout: '', stderr: String(err) }); });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (result.status === 0 && result.stdout) {
|
|
131
|
+
const stdout = result.stdout;
|
|
132
|
+
// Find the last top-level JSON object (skip plugin log noise)
|
|
133
|
+
for (let i = stdout.length - 1; i >= 0; i--) {
|
|
134
|
+
if (stdout[i] === '{') {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(stdout.slice(i));
|
|
137
|
+
return parsed.response ||
|
|
138
|
+
parsed.content ||
|
|
139
|
+
parsed.result?.payloads?.slice(-1)?.[0]?.text ||
|
|
140
|
+
parsed.result?.text ||
|
|
141
|
+
(typeof parsed.result === "string" ? parsed.result : null) ||
|
|
142
|
+
stdout;
|
|
143
|
+
} catch { /* keep searching */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return stdout;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (result.stderr) {
|
|
150
|
+
log.warn(`[chorus] ${choir.name} CLI stderr: ${result.stderr.slice(0, 200)}`);
|
|
151
|
+
}
|
|
152
|
+
return "(no response)";
|
|
153
|
+
}
|
|
154
|
+
|
|
98
155
|
// Build the prompt with context injected
|
|
99
156
|
function buildPrompt(choir: Choir): string {
|
|
100
157
|
let prompt = choir.prompt;
|
|
@@ -110,7 +167,7 @@ export function createChoirScheduler(
|
|
|
110
167
|
return prompt;
|
|
111
168
|
}
|
|
112
169
|
|
|
113
|
-
// Execute a choir using
|
|
170
|
+
// Execute a choir using the plugin API (fast, in-process) with CLI fallback
|
|
114
171
|
async function executeChoir(choir: Choir): Promise<void> {
|
|
115
172
|
const state = runState.get(choir.id) || { runCount: 0 };
|
|
116
173
|
const startTime = Date.now();
|
|
@@ -127,49 +184,40 @@ export function createChoirScheduler(
|
|
|
127
184
|
|
|
128
185
|
try {
|
|
129
186
|
const prompt = buildPrompt(choir);
|
|
187
|
+
let output = "(no response)";
|
|
130
188
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let stdout = '';
|
|
141
|
-
let stderr = '';
|
|
142
|
-
const maxBuffer = 1024 * 1024;
|
|
143
|
-
child.stdout.on('data', (d: Buffer) => { if (stdout.length < maxBuffer) stdout += d.toString(); });
|
|
144
|
-
child.stderr.on('data', (d: Buffer) => { if (stderr.length < maxBuffer) stderr += d.toString(); });
|
|
145
|
-
|
|
146
|
-
const timer = setTimeout(() => { child.kill('SIGTERM'); }, 300000); // 5 min
|
|
147
|
-
child.on('close', (code) => { clearTimeout(timer); resolve({ status: code, stdout, stderr }); });
|
|
148
|
-
child.on('error', (err) => { clearTimeout(timer); resolve({ status: 1, stdout: '', stderr: String(err) }); });
|
|
149
|
-
});
|
|
189
|
+
// Prefer plugin API (in-process, no CLI spawn overhead)
|
|
190
|
+
if (typeof api.runAgentTurn === 'function') {
|
|
191
|
+
try {
|
|
192
|
+
const result: AgentTurnResult = await api.runAgentTurn({
|
|
193
|
+
sessionLabel: `chorus:${choir.id}`,
|
|
194
|
+
message: prompt,
|
|
195
|
+
isolated: true,
|
|
196
|
+
timeoutSeconds: 300,
|
|
197
|
+
});
|
|
150
198
|
|
|
151
|
-
|
|
199
|
+
// Extract text from payloads — concatenate all payload texts
|
|
200
|
+
const payloadTexts = (result?.payloads || [])
|
|
201
|
+
.map((p: any) => p?.text || '')
|
|
202
|
+
.filter((t: string) => t.length > 0);
|
|
152
203
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
const parsed = JSON.parse(stdout.slice(jsonStart));
|
|
160
|
-
output = parsed.response || parsed.content || stdout;
|
|
161
|
-
} catch {
|
|
162
|
-
output = stdout;
|
|
204
|
+
if (payloadTexts.length > 0) {
|
|
205
|
+
// Use the last substantive payload (earlier ones are often thinking-out-loud)
|
|
206
|
+
output = payloadTexts[payloadTexts.length - 1];
|
|
207
|
+
} else if (result?.text) {
|
|
208
|
+
output = result.text;
|
|
163
209
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
210
|
+
} catch (apiErr) {
|
|
211
|
+
log.warn(`[chorus] API runAgentTurn failed for ${choir.name}, falling back to CLI: ${apiErr}`);
|
|
212
|
+
output = await executeChoirViaCli(choir, prompt);
|
|
166
213
|
}
|
|
167
|
-
} else
|
|
168
|
-
|
|
214
|
+
} else {
|
|
215
|
+
// Fallback: spawn CLI process
|
|
216
|
+
output = await executeChoirViaCli(choir, prompt);
|
|
169
217
|
}
|
|
170
218
|
|
|
171
219
|
execution.durationMs = Date.now() - startTime;
|
|
172
|
-
execution.success =
|
|
220
|
+
execution.success = output !== "(no response)";
|
|
173
221
|
execution.outputLength = output.length;
|
|
174
222
|
execution.tokensUsed = estimateTokens(output);
|
|
175
223
|
|