@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 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) parts.push(buildResultInstructions(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
- /** Max time to poll before timing out (ms) - 45 minutes. */
295
- const MAX_POLL_TIME = 2700 * 1e3;
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
- return extractResult(parts, schema, sessionId);
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 > MAX_POLL_TIME) throw new Error(`"${label}" timed out after ${elapsed}s. Session never went idle. This may indicate a stuck session or OpenCode bug.`);
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) parts.push(buildResultInstructions(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, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flue/client",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {