orchestrar 0.3.2 → 0.3.4
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/orchestrator.js +142 -62
- package/package.json +1 -1
package/orchestrator.js
CHANGED
|
@@ -50,23 +50,23 @@ async function runWorkInstance(createOpencode, docs, root) {
|
|
|
50
50
|
const sessionID = extractSessionID(session, "session.create (work instance)");
|
|
51
51
|
const promptPaths = buildPromptPaths(docs, root);
|
|
52
52
|
|
|
53
|
-
await sendPrompt(
|
|
53
|
+
const workMessageID = await sendPrompt(
|
|
54
54
|
client,
|
|
55
55
|
sessionID,
|
|
56
56
|
buildMilestonePrompt(promptPaths),
|
|
57
57
|
root
|
|
58
58
|
);
|
|
59
|
-
await
|
|
59
|
+
await waitForMessageComplete(client, sessionID, workMessageID, root);
|
|
60
60
|
|
|
61
|
-
await runReviewLoop(
|
|
61
|
+
await runReviewLoop(client, sessionID, root);
|
|
62
62
|
|
|
63
|
-
await sendPrompt(
|
|
63
|
+
const markTasksMessageID = await sendPrompt(
|
|
64
64
|
client,
|
|
65
65
|
sessionID,
|
|
66
66
|
buildMarkTasksPrompt(promptPaths.plan),
|
|
67
67
|
root
|
|
68
68
|
);
|
|
69
|
-
await
|
|
69
|
+
await waitForMessageComplete(client, sessionID, markTasksMessageID, root);
|
|
70
70
|
} finally {
|
|
71
71
|
await disposeInstance(client, server, root);
|
|
72
72
|
}
|
|
@@ -86,7 +86,7 @@ async function runCommitInstance(createOpencode, root) {
|
|
|
86
86
|
);
|
|
87
87
|
|
|
88
88
|
const sessionID = extractSessionID(session, "session.create (commit instance)");
|
|
89
|
-
await sendPrompt(
|
|
89
|
+
const commitMessageID = await sendPrompt(
|
|
90
90
|
client,
|
|
91
91
|
sessionID,
|
|
92
92
|
buildCommitPrompt(),
|
|
@@ -94,13 +94,13 @@ async function runCommitInstance(createOpencode, root) {
|
|
|
94
94
|
COMMIT_MODEL,
|
|
95
95
|
COMMIT_AGENT
|
|
96
96
|
);
|
|
97
|
-
await
|
|
97
|
+
await waitForMessageComplete(client, sessionID, commitMessageID, root);
|
|
98
98
|
} finally {
|
|
99
99
|
await disposeInstance(client, server, root);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async function runReviewLoop(
|
|
103
|
+
async function runReviewLoop(client, sessionID, root) {
|
|
104
104
|
const maxIterations = parseNumber(
|
|
105
105
|
process.env.ORCHESTRATOR_MAX_REVIEW_ITERATIONS,
|
|
106
106
|
DEFAULT_MAX_REVIEW_ITERATIONS
|
|
@@ -108,7 +108,7 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
|
|
|
108
108
|
|
|
109
109
|
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
110
110
|
logStep(`Running review (${iteration}/${maxIterations})`);
|
|
111
|
-
const reviewResult = await runReviewCommand(
|
|
111
|
+
const reviewResult = await runReviewCommand(client, root);
|
|
112
112
|
if (isFindingsEmpty(reviewResult)) {
|
|
113
113
|
logStep("Review clean; no findings.");
|
|
114
114
|
return;
|
|
@@ -118,13 +118,13 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
|
|
|
118
118
|
? reviewResult.findings.length
|
|
119
119
|
: "unknown";
|
|
120
120
|
logStep(`Review found ${findingsCount} issues; requesting fixes.`);
|
|
121
|
-
await sendPrompt(
|
|
121
|
+
const fixMessageID = await sendPrompt(
|
|
122
122
|
client,
|
|
123
123
|
sessionID,
|
|
124
124
|
buildFindingsPrompt(reviewResult),
|
|
125
125
|
root
|
|
126
126
|
);
|
|
127
|
-
await
|
|
127
|
+
await waitForMessageComplete(client, sessionID, fixMessageID, root);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
throw new Error(
|
|
@@ -132,7 +132,7 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
|
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
async function runReviewCommand(
|
|
135
|
+
async function runReviewCommand(client, root) {
|
|
136
136
|
const commandName =
|
|
137
137
|
process.env.ORCHESTRATOR_REVIEW_COMMAND || DEFAULT_REVIEW_COMMAND_NAME;
|
|
138
138
|
const commandArguments =
|
|
@@ -142,58 +142,56 @@ async function runReviewCommand(createOpencode, root) {
|
|
|
142
142
|
process.env.ORCHESTRATOR_REVIEW_TIMEOUT_MS,
|
|
143
143
|
DEFAULT_REVIEW_TIMEOUT_MS
|
|
144
144
|
);
|
|
145
|
+
const session = await unwrap(
|
|
146
|
+
client.session.create({
|
|
147
|
+
query: { directory: root },
|
|
148
|
+
body: { title: "Review" },
|
|
149
|
+
}),
|
|
150
|
+
"session.create"
|
|
151
|
+
);
|
|
145
152
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
const sessionID = extractSessionID(session, "session.create (review instance)");
|
|
154
|
+
const commandResult = await unwrap(
|
|
155
|
+
client.session.command({
|
|
156
|
+
path: { id: sessionID },
|
|
157
|
+
query: { directory: root },
|
|
158
|
+
body: {
|
|
159
|
+
command: commandName,
|
|
160
|
+
arguments: commandArguments,
|
|
161
|
+
agent: DEFAULT_AGENT,
|
|
162
|
+
model: DEFAULT_MODEL,
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
"session.command"
|
|
166
|
+
);
|
|
149
167
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
const commandMessageID = extractMessageID(commandResult);
|
|
169
|
+
await waitForMessageComplete(
|
|
170
|
+
client,
|
|
171
|
+
sessionID,
|
|
172
|
+
commandMessageID,
|
|
173
|
+
root,
|
|
174
|
+
timeoutMs
|
|
175
|
+
);
|
|
158
176
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
let parts = commandResult?.parts ?? [];
|
|
178
|
+
const messageID = extractMessageID(commandResult);
|
|
179
|
+
if (messageID) {
|
|
180
|
+
const message = await unwrap(
|
|
181
|
+
client.session.message({
|
|
182
|
+
path: { id: sessionID, messageID },
|
|
163
183
|
query: { directory: root },
|
|
164
|
-
body: {
|
|
165
|
-
command: commandName,
|
|
166
|
-
arguments: commandArguments,
|
|
167
|
-
agent: DEFAULT_AGENT,
|
|
168
|
-
model: DEFAULT_MODEL,
|
|
169
|
-
},
|
|
170
184
|
}),
|
|
171
|
-
"session.
|
|
185
|
+
"session.message"
|
|
172
186
|
);
|
|
187
|
+
parts = message?.parts ?? parts;
|
|
188
|
+
}
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const messageID = commandResult?.info?.id;
|
|
178
|
-
if (messageID) {
|
|
179
|
-
const message = await unwrap(
|
|
180
|
-
client.session.message({
|
|
181
|
-
path: { id: sessionID, messageID },
|
|
182
|
-
query: { directory: root },
|
|
183
|
-
}),
|
|
184
|
-
"session.message"
|
|
185
|
-
);
|
|
186
|
-
parts = message?.parts ?? parts;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const output = collectCommandOutput(parts);
|
|
190
|
-
if (!output.trim()) {
|
|
191
|
-
throw new Error("Review command produced no output.");
|
|
192
|
-
}
|
|
193
|
-
return extractReviewJson(output);
|
|
194
|
-
} finally {
|
|
195
|
-
await disposeInstance(client, server, root);
|
|
190
|
+
const output = collectCommandOutput(parts);
|
|
191
|
+
if (!output.trim()) {
|
|
192
|
+
throw new Error("Review command produced no output.");
|
|
196
193
|
}
|
|
194
|
+
return extractReviewJson(output);
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
async function sendPrompt(
|
|
@@ -205,7 +203,7 @@ async function sendPrompt(
|
|
|
205
203
|
agentSpec = DEFAULT_AGENT
|
|
206
204
|
) {
|
|
207
205
|
const model = parseModelSpec(modelSpec);
|
|
208
|
-
await unwrap(
|
|
206
|
+
const response = await unwrap(
|
|
209
207
|
client.session.prompt({
|
|
210
208
|
path: { id: sessionID },
|
|
211
209
|
query: { directory: root },
|
|
@@ -217,6 +215,8 @@ async function sendPrompt(
|
|
|
217
215
|
}),
|
|
218
216
|
"session.prompt"
|
|
219
217
|
);
|
|
218
|
+
|
|
219
|
+
return extractMessageID(response);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
|
|
@@ -232,26 +232,79 @@ async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
|
|
|
232
232
|
);
|
|
233
233
|
|
|
234
234
|
const start = Date.now();
|
|
235
|
+
let lastKnownSessions = [];
|
|
235
236
|
while (Date.now() - start < timeoutMs) {
|
|
236
237
|
const statusMap = await unwrap(
|
|
237
238
|
client.session.status({ query: { directory: root } }),
|
|
238
239
|
"session.status"
|
|
239
240
|
);
|
|
241
|
+
if (statusMap && typeof statusMap === "object") {
|
|
242
|
+
lastKnownSessions = Object.keys(statusMap);
|
|
243
|
+
}
|
|
240
244
|
const status = statusMap?.[sessionID];
|
|
241
245
|
if (!status) {
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
await delay(pollIntervalMs);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (status.type === "idle") {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
await delay(pollIntervalMs);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const knownList = lastKnownSessions.length
|
|
256
|
+
? lastKnownSessions.join(", ")
|
|
257
|
+
: "none";
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Timed out waiting for session ${sessionID} to go idle. Known sessions: ${knownList}.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function waitForMessageComplete(
|
|
264
|
+
client,
|
|
265
|
+
sessionID,
|
|
266
|
+
messageID,
|
|
267
|
+
root,
|
|
268
|
+
timeoutOverrideMs
|
|
269
|
+
) {
|
|
270
|
+
if (!messageID) {
|
|
271
|
+
await waitForSessionIdle(client, sessionID, root, timeoutOverrideMs);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const timeoutMs =
|
|
276
|
+
timeoutOverrideMs ??
|
|
277
|
+
parseNumber(
|
|
278
|
+
process.env.ORCHESTRATOR_SESSION_TIMEOUT_MS,
|
|
279
|
+
DEFAULT_SESSION_TIMEOUT_MS
|
|
280
|
+
);
|
|
281
|
+
const pollIntervalMs = parseNumber(
|
|
282
|
+
process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
|
|
283
|
+
DEFAULT_STATUS_POLL_INTERVAL_MS
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const start = Date.now();
|
|
287
|
+
while (Date.now() - start < timeoutMs) {
|
|
288
|
+
const message = await unwrap(
|
|
289
|
+
client.session.message({
|
|
290
|
+
path: { id: sessionID, messageID },
|
|
291
|
+
query: { directory: root },
|
|
292
|
+
}),
|
|
293
|
+
"session.message"
|
|
294
|
+
);
|
|
295
|
+
const info = message?.info ?? message;
|
|
296
|
+
if (info?.error) {
|
|
244
297
|
throw new Error(
|
|
245
|
-
`Session
|
|
298
|
+
`Session message ${messageID} failed: ${formatError(info.error)}`
|
|
246
299
|
);
|
|
247
300
|
}
|
|
248
|
-
if (
|
|
301
|
+
if (info?.time?.completed || info?.finish) {
|
|
249
302
|
return;
|
|
250
303
|
}
|
|
251
304
|
await delay(pollIntervalMs);
|
|
252
305
|
}
|
|
253
306
|
|
|
254
|
-
throw new Error(`Timed out waiting for
|
|
307
|
+
throw new Error(`Timed out waiting for message ${messageID} to complete.`);
|
|
255
308
|
}
|
|
256
309
|
|
|
257
310
|
async function resolveDocs(root) {
|
|
@@ -506,6 +559,33 @@ function extractSessionID(session, context) {
|
|
|
506
559
|
);
|
|
507
560
|
}
|
|
508
561
|
|
|
562
|
+
function extractMessageID(message) {
|
|
563
|
+
if (!message || typeof message !== "object") {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const candidates = [
|
|
568
|
+
message.info?.id,
|
|
569
|
+
message.info?.messageID,
|
|
570
|
+
message.id,
|
|
571
|
+
message.messageID,
|
|
572
|
+
message.data?.info?.id,
|
|
573
|
+
message.data?.info?.messageID,
|
|
574
|
+
message.data?.id,
|
|
575
|
+
message.data?.messageID,
|
|
576
|
+
message.properties?.info?.id,
|
|
577
|
+
message.properties?.info?.messageID,
|
|
578
|
+
message.properties?.id,
|
|
579
|
+
message.properties?.messageID,
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
for (const candidate of candidates) {
|
|
583
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
584
|
+
return candidate;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
509
589
|
function safeStringify(value, maxLength = 1000) {
|
|
510
590
|
try {
|
|
511
591
|
const json = JSON.stringify(value);
|