@flue/client 0.0.27 → 0.0.29
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/dist/index.d.mts +4 -0
- package/dist/index.mjs +56 -12
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -101,6 +101,8 @@ interface SkillOptions<S extends v.GenericSchema | undefined = undefined> {
|
|
|
101
101
|
providerID: string;
|
|
102
102
|
modelID: string;
|
|
103
103
|
};
|
|
104
|
+
/** Max time to wait for the skill to complete (ms). Defaults to 60 minutes. */
|
|
105
|
+
timeout?: number;
|
|
104
106
|
}
|
|
105
107
|
interface PromptOptions<S extends v.GenericSchema | undefined = undefined> {
|
|
106
108
|
/** Valibot schema for structured result extraction. */
|
|
@@ -110,6 +112,8 @@ interface PromptOptions<S extends v.GenericSchema | undefined = undefined> {
|
|
|
110
112
|
providerID: string;
|
|
111
113
|
modelID: string;
|
|
112
114
|
};
|
|
115
|
+
/** Max time to wait for the prompt to complete (ms). Defaults to 60 minutes. */
|
|
116
|
+
timeout?: number;
|
|
113
117
|
}
|
|
114
118
|
interface ShellOptions {
|
|
115
119
|
/** Environment variables scoped to this subprocess only. */
|
package/dist/index.mjs
CHANGED
|
@@ -181,7 +181,6 @@ function buildResultInstructions(schema) {
|
|
|
181
181
|
const { $schema: _, ...schemaWithoutMeta } = toJsonSchema(schema, { errorMode: "ignore" });
|
|
182
182
|
return [
|
|
183
183
|
"",
|
|
184
|
-
"When complete, you MUST output your result between these exact delimiters conforming to this schema:",
|
|
185
184
|
"```json",
|
|
186
185
|
JSON.stringify(schemaWithoutMeta, null, 2),
|
|
187
186
|
"```",
|
|
@@ -205,6 +204,20 @@ function buildProxyInstructions(instructions) {
|
|
|
205
204
|
return "\n\n" + instructions.join("\n");
|
|
206
205
|
}
|
|
207
206
|
/**
|
|
207
|
+
* Build a standalone follow-up prompt that asks the LLM to return its result.
|
|
208
|
+
*
|
|
209
|
+
* Used when the initial response is missing the ---RESULT_START--- /
|
|
210
|
+
* ---RESULT_END--- block. Sent as a second message in the same session so the
|
|
211
|
+
* format instructions are fresh in context.
|
|
212
|
+
*/
|
|
213
|
+
function buildResultExtractionPrompt(schema) {
|
|
214
|
+
return [
|
|
215
|
+
"Your task is complete. Now respond with ONLY your final result.",
|
|
216
|
+
"No explanation, no preamble — just the result in the following format, conforming to this schema:",
|
|
217
|
+
buildResultInstructions(schema)
|
|
218
|
+
].join("\n");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
208
221
|
* Build the prompt text for a skill invocation.
|
|
209
222
|
*
|
|
210
223
|
* If `name` looks like a file path (contains '/' or ends with '.md'), the
|
|
@@ -220,7 +233,10 @@ function buildSkillPrompt(name, args, schema, proxyInstructions) {
|
|
|
220
233
|
];
|
|
221
234
|
if (args && Object.keys(args).length > 0) parts.push(`\nArguments:\n${JSON.stringify(args, null, 2)}`);
|
|
222
235
|
if (proxyInstructions && proxyInstructions.length > 0) parts.push(buildProxyInstructions(proxyInstructions));
|
|
223
|
-
if (schema)
|
|
236
|
+
if (schema) {
|
|
237
|
+
parts.push("When complete, you MUST output your result between these exact delimiters conforming to this schema:");
|
|
238
|
+
parts.push(buildResultInstructions(schema));
|
|
239
|
+
}
|
|
224
240
|
return parts.join("\n");
|
|
225
241
|
}
|
|
226
242
|
|
|
@@ -291,8 +307,8 @@ function extractLastResultBlock(text) {
|
|
|
291
307
|
const POLL_INTERVAL = 15e3;
|
|
292
308
|
/** Max times we'll see 0 assistant messages before giving up. */
|
|
293
309
|
const MAX_EMPTY_POLLS = 20;
|
|
294
|
-
/**
|
|
295
|
-
const
|
|
310
|
+
/** Default max time to poll before timing out (ms) - 60 minutes. */
|
|
311
|
+
const DEFAULT_POLL_TIMEOUT = 3600 * 1e3;
|
|
296
312
|
/**
|
|
297
313
|
* Low-level primitive: send a fully-formed prompt to OpenCode, poll until
|
|
298
314
|
* idle, and optionally extract a typed result.
|
|
@@ -301,7 +317,7 @@ const MAX_POLL_TIME = 2700 * 1e3;
|
|
|
301
317
|
* constructing their own prompt text.
|
|
302
318
|
*/
|
|
303
319
|
async function runPrompt(client, workdir, label, prompt, options) {
|
|
304
|
-
const { result: schema, model } = options ?? {};
|
|
320
|
+
const { result: schema, model, timeout } = options ?? {};
|
|
305
321
|
console.log(`[flue] ${label}: starting`);
|
|
306
322
|
console.log(`[flue] ${label}: creating session`);
|
|
307
323
|
const session = await client.session.create({
|
|
@@ -336,22 +352,46 @@ async function runPrompt(client, workdir, label, prompt, options) {
|
|
|
336
352
|
if (asyncResult.error) throw new Error(`Failed to send prompt for "${label}" (session ${sessionId}): ${JSON.stringify(asyncResult.error)}`);
|
|
337
353
|
await confirmSessionStarted(client, sessionId, workdir, label);
|
|
338
354
|
console.log(`[flue] ${label}: starting polling`);
|
|
339
|
-
const parts = await pollUntilIdle(client, sessionId, workdir, label, promptStart);
|
|
355
|
+
const parts = await pollUntilIdle(client, sessionId, workdir, label, promptStart, timeout);
|
|
340
356
|
const promptElapsed = ((Date.now() - promptStart) / 1e3).toFixed(1);
|
|
341
357
|
console.log(`[flue] ${label}: completed (${promptElapsed}s)`);
|
|
342
358
|
if (!schema) return;
|
|
343
|
-
|
|
359
|
+
try {
|
|
360
|
+
return extractResult(parts, schema, sessionId);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (!(error instanceof SkillOutputError)) throw error;
|
|
363
|
+
if (!error.message.includes("---RESULT_START---")) throw error;
|
|
364
|
+
console.log(`[flue] ${label}: result extraction failed, sending follow-up prompt to request result`);
|
|
365
|
+
const followUpResult = await client.session.promptAsync({
|
|
366
|
+
path: { id: sessionId },
|
|
367
|
+
query: { directory: workdir },
|
|
368
|
+
body: {
|
|
369
|
+
...model ? { model } : {},
|
|
370
|
+
parts: [{
|
|
371
|
+
type: "text",
|
|
372
|
+
text: buildResultExtractionPrompt(schema)
|
|
373
|
+
}]
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
if (followUpResult.error) {
|
|
377
|
+
if (followUpResult.error instanceof Error) followUpResult.error.cause = error;
|
|
378
|
+
throw followUpResult.error;
|
|
379
|
+
}
|
|
380
|
+
await confirmSessionStarted(client, sessionId, workdir, label);
|
|
381
|
+
return extractResult(await pollUntilIdle(client, sessionId, workdir, label, Date.now(), timeout), schema, sessionId);
|
|
382
|
+
}
|
|
344
383
|
}
|
|
345
384
|
/**
|
|
346
385
|
* Run a named skill: builds the skill prompt from the name + args + schema,
|
|
347
386
|
* then delegates to runPrompt().
|
|
348
387
|
*/
|
|
349
388
|
async function runSkill(client, workdir, name, options, proxyInstructions) {
|
|
350
|
-
const { args, result: schema, model } = options ?? {};
|
|
389
|
+
const { args, result: schema, model, timeout } = options ?? {};
|
|
351
390
|
const prompt = buildSkillPrompt(name, args, schema, proxyInstructions);
|
|
352
391
|
return runPrompt(client, workdir, `skill("${name}")`, prompt, {
|
|
353
392
|
result: schema,
|
|
354
|
-
model
|
|
393
|
+
model,
|
|
394
|
+
timeout
|
|
355
395
|
});
|
|
356
396
|
}
|
|
357
397
|
/**
|
|
@@ -378,14 +418,15 @@ async function confirmSessionStarted(client, sessionId, workdir, label) {
|
|
|
378
418
|
}
|
|
379
419
|
throw new Error(`"${label}" failed to start: session ${sessionId} has no messages after 15s.\nThe prompt was accepted but OpenCode never began processing it.\nThis usually means no model is configured. Pass --model to the flue CLI or set "model" in opencode.json.`);
|
|
380
420
|
}
|
|
381
|
-
async function pollUntilIdle(client, sessionId, workdir, label, startTime) {
|
|
421
|
+
async function pollUntilIdle(client, sessionId, workdir, label, startTime, timeout) {
|
|
422
|
+
const maxPollTime = timeout ?? DEFAULT_POLL_TIMEOUT;
|
|
382
423
|
let emptyPolls = 0;
|
|
383
424
|
let pollCount = 0;
|
|
384
425
|
for (;;) {
|
|
385
426
|
await sleep(POLL_INTERVAL);
|
|
386
427
|
pollCount++;
|
|
387
428
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(0);
|
|
388
|
-
if (Date.now() - startTime >
|
|
429
|
+
if (Date.now() - startTime > maxPollTime) throw new Error(`"${label}" timed out after ${elapsed}s. Session never went idle. This may indicate a stuck session or OpenCode bug.`);
|
|
389
430
|
const statusResult = await client.session.status({ query: { directory: workdir } });
|
|
390
431
|
const sessionStatus = statusResult.data?.[sessionId];
|
|
391
432
|
if (!sessionStatus || sessionStatus.type === "idle") {
|
|
@@ -472,7 +513,10 @@ var FlueClient = class {
|
|
|
472
513
|
promptText
|
|
473
514
|
];
|
|
474
515
|
if (this.proxyInstructions.length > 0) parts.push(buildProxyInstructions(this.proxyInstructions));
|
|
475
|
-
if (schema)
|
|
516
|
+
if (schema) {
|
|
517
|
+
parts.push("When complete, you MUST output your result between these exact delimiters conforming to this schema:");
|
|
518
|
+
parts.push(buildResultInstructions(schema));
|
|
519
|
+
}
|
|
476
520
|
const fullPrompt = parts.join("\n");
|
|
477
521
|
const label = `prompt("${promptText.length > 40 ? promptText.slice(0, 40) + "…" : promptText}")`;
|
|
478
522
|
return runPrompt(this.client, this.workdir, label, fullPrompt, {
|