regent-code 3.0.6 → 3.0.7

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.
@@ -6,7 +6,7 @@ If the package is published to npm, both the plugin and the MCP server install
6
6
  with a single command against any existing `opencode.json` / `opencode.jsonc`:
7
7
 
8
8
  ```bash
9
- npx -y regent-code@3.0.6 install
9
+ npx -y regent-code@3.0.7 install
10
10
  ```
11
11
 
12
12
  Patches the project config (or the global `~/.config/opencode/` config) to add
@@ -19,7 +19,7 @@ Patches the project config (or the global `~/.config/opencode/` config) to add
19
19
  ```jsonc
20
20
  {
21
21
  "$schema": "https://opencode.ai/config.json",
22
- "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.6"],
22
+ "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.7"],
23
23
  }
24
24
  ```
25
25
 
@@ -32,7 +32,7 @@ Patches the project config (or the global `~/.config/opencode/` config) to add
32
32
  }
33
33
  ```
34
34
 
35
- The pinned version is recommended. Use the unpinned branch only when you intentionally want the latest changes. The `v3.0.6` git tag must be pushed to GitHub before the pinned spec resolves.
35
+ The pinned version is recommended. Use the unpinned branch only when you intentionally want the latest changes. The `v3.0.7` git tag must be pushed to GitHub before the pinned spec resolves.
36
36
 
37
37
  ## Single-source rule (duplicate plugin ID)
38
38
 
package/README.md CHANGED
@@ -51,7 +51,7 @@ Add Regent to your OpenCode configuration:
51
51
  ```jsonc
52
52
  {
53
53
  "$schema": "https://opencode.ai/config.json",
54
- "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.6"],
54
+ "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.7"],
55
55
  }
56
56
  ```
57
57
 
@@ -69,7 +69,7 @@ are read from the persisted transcript when the service stores turns, otherwise
69
69
  from the synchronous `session.generate` text; agent catalogs are queried with
70
70
  and without an explicit location scope; and the caller-authorization guardrail
71
71
  degrades OPEN when the runtime agent API is unresolvable (only recursion from
72
- known worker sessions stays hard-blocked). The `v3.0.6` git tag must be pushed
72
+ known worker sessions stays hard-blocked). The `v3.0.7` git tag must be pushed
73
73
  to GitHub before this pinned spec resolves.
74
74
 
75
75
  > **Windows dev-machine warning (single-source rule):** when this repository is open as an OpenCode project, its own `.opencode/plugins/regent.js` is auto-loaded as a project plugin. Do NOT also pin regent in `opencode.jsonc` on the same machine — two active sources make host plugin reloads fail with `Duplicate plugin ID: regent`, leaving sessions with a torn tool surface and blocking live skill/plugin edits. Either develop unpinned (project plugin only) or pin the repo file directly: `"plugins": ["file:///Q:/PROJECTS/PERSONAL/regent-code/.opencode/plugins/regent.js"]`. One source of truth, always.
@@ -79,13 +79,13 @@ to GitHub before this pinned spec resolves.
79
79
  The fastest way to get **both** the plugin and the MCP server on any machine — no cloning, no manual config edits, no local files:
80
80
 
81
81
  ```bash
82
- npx -y regent-code@3.0.6 install
82
+ npx -y regent-code@3.0.7 install
83
83
  ```
84
84
 
85
85
  The installer finds an existing `opencode.json` / `opencode.jsonc` (project config in the current directory first, then the global `~/.config/opencode/` config) and adds both entries:
86
86
 
87
87
  - **MCP server**: `mcp.servers.regent` → runs `["npx", "-y", "regent-code"]`. That single command spawns the server through the package's `bin` — no absolute paths, no global install, no per-machine shims.
88
- - **Plugin**: `plugins` → `regent-code@3.0.6`
88
+ - **Plugin**: `plugins` → `regent-code@3.0.7`
89
89
 
90
90
  It is **idempotent and non-destructive** — it only adds or updates regent entries, preserving comments, trailing commas, and every unrelated setting in the file. Re-run it to upgrade the pinned version. Flags: `--global` forces the user config, `--file <path>` targets an exact file, `--help` explains all options. Restart the OpenCode session afterwards — plugin load and MCP connection happen on config load.
91
91
 
package/mcp/index.js CHANGED
@@ -176,18 +176,28 @@ async function createWorkerResolver(client, options = {}) {
176
176
  }
177
177
 
178
178
  const candidates = [];
179
+ // Built-in subagents are last-resort fallbacks; project-defined and
180
+ // third-party child-capable agents are the preferred workers.
181
+ const BUILTIN_SUBAGENTS = new Set(['general', 'explore']);
179
182
  if (isChildCapableAgent(catalog.find((agent) => agent.id === 'regent-general'))) {
180
183
  candidates.push('regent-general');
181
184
  }
182
- const configuredGeneral = catalog.find((agent) => agent.id === 'general');
183
- if (!configuredGeneral || isChildCapableAgent(configuredGeneral)) {
184
- candidates.push('general');
185
- }
186
185
  for (const agent of catalog) {
187
- if (isChildCapableAgent(agent) && !candidates.includes(agent.id)) {
186
+ if (
187
+ isChildCapableAgent(agent) &&
188
+ !BUILTIN_SUBAGENTS.has(agent.id) &&
189
+ !candidates.includes(agent.id)
190
+ ) {
188
191
  candidates.push(agent.id);
189
192
  }
190
193
  }
194
+ if (isChildCapableAgent(catalog.find((agent) => agent.id === 'explore'))) {
195
+ candidates.push('explore');
196
+ }
197
+ const configuredGeneral = catalog.find((agent) => agent.id === 'general');
198
+ if (!configuredGeneral || isChildCapableAgent(configuredGeneral)) {
199
+ candidates.push('general');
200
+ }
191
201
 
192
202
  if (candidates.length === 0) {
193
203
  return { ok: false, error: 'No visible child-capable worker agent is available' };
@@ -286,10 +296,18 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
286
296
  'If you cannot complete the task, say BLOCKED and explain why.',
287
297
  ].join('\n');
288
298
 
289
- const result = await withRetry(() =>
290
- client.session.generate({ sessionID: session.id, prompt }),
291
- );
292
- const seed = extractGenerationText(unwrapData(result));
299
+ // Dispatch the turn as a real user message so the agent executes with its
300
+ // tools and permissions. `session.generate` on current services is a
301
+ // sessionless text completion (no agent run, zero tokens), so it is only
302
+ // the fallback for older services that expose generate-only dispatch.
303
+ let result = null;
304
+ try {
305
+ await withRetry(() => client.session.prompt({ sessionID: session.id, text: prompt }));
306
+ } catch (err) {
307
+ if (isUnavailableAgentError(err)) throw err;
308
+ result = await withRetry(() => client.session.generate({ sessionID: session.id, prompt }));
309
+ }
310
+ const seed = result ? extractGenerationText(unwrapData(result)) : '';
293
311
 
294
312
  // Collect the final answer once the turn has actually ended (generate
295
313
  // resolves with the first chunk on current service semantics).
@@ -312,7 +330,21 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
312
330
  };
313
331
  }
314
332
  const parsed = parseSubagentTextResponse(output);
315
- const { status, concerns, filesChanged } = parsed;
333
+ let { status, concerns, filesChanged } = parsed;
334
+
335
+ // The completion ceremony (- summary/- status lines) is a soft contract:
336
+ // a wait-verified ended turn with a real answer that skipped it is still
337
+ // done. NEVER applied to explicit NEEDS_CONTEXT/BLOCKED markers or
338
+ // concerns; mid-turn fragments never reach here (they return empty and
339
+ // hit the blocked path above).
340
+ if (
341
+ status === 'needs_context' &&
342
+ concerns.length === 0 &&
343
+ !/\bNEEDS_CONTEXT\b/i.test(output) &&
344
+ output.trim()
345
+ ) {
346
+ status = 'done';
347
+ }
316
348
 
317
349
  if (filesChanged.length > 0) {
318
350
  sessionFileChanges.set(session.id, {
@@ -324,6 +356,9 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
324
356
  recordEvidence(session.id, filesChanged, directory);
325
357
  }
326
358
 
359
+ if (process.env.REGENT_TRACE_TURN === '1') {
360
+ console.error(`[trace] dispatch returning ${session.id.slice(-8)}`);
361
+ }
327
362
  return { status, output, concerns, files_changed: filesChanged, session_id: session.id };
328
363
  } catch (err) {
329
364
  const message = redactSecrets(safeErrorMessage(err)).slice(0, 500);
@@ -684,6 +719,9 @@ export function createRegentServer() {
684
719
  }
685
720
  const workerCount = Math.min(queue.length, 10);
686
721
  await Promise.all(Array.from({ length: workerCount }, () => worker()));
722
+ if (process.env.REGENT_TRACE_TURN === '1') {
723
+ console.error(`[trace] all workers done (${results.length} results)`);
724
+ }
687
725
  const failed = results.filter((r) => r.status === 'blocked').length;
688
726
  recordCircuitResult('mcp', results.length > 0 && failed === 0);
689
727
  return toContent({
package/mcp/shared.js CHANGED
@@ -342,10 +342,23 @@ export function extractGenerationText(result) {
342
342
  return '';
343
343
  }
344
344
 
345
+ /**
346
+ * A message is still streaming when the service stamps `time.streamed` but
347
+ * no `time.completed` yet. Mid-stream reads must never be treated as final.
348
+ * @param {any} message
349
+ * @returns {boolean}
350
+ */
351
+ function isStillStreaming(message) {
352
+ const time = message?.time;
353
+ if (!time || time.completed != null) return false;
354
+ return time.streamed != null;
355
+ }
356
+
345
357
  /**
346
358
  * Join the assistant text of a session transcript (SessionMessageInfo[]).
347
- * Only `type: "assistant"` messages contribute; reasoning/tool parts are
348
- * skipped. Handles both chronological and reverse (desc) orderings.
359
+ * Only `type: "assistant"` messages that finished streaming contribute;
360
+ * reasoning/tool parts are skipped. Handles chronological and reverse (desc)
361
+ * orderings.
349
362
  * @param {any[]} messages
350
363
  * @returns {string}
351
364
  */
@@ -354,6 +367,7 @@ export function joinAssistantText(messages) {
354
367
  const chunks = [];
355
368
  for (const message of messages) {
356
369
  if (!message || message.type !== 'assistant') continue;
370
+ if (isStillStreaming(message)) continue;
357
371
  const content = message.content;
358
372
  if (typeof content === 'string') {
359
373
  chunks.push(content);
@@ -414,13 +428,15 @@ export async function readTurnTranscript(sessionApi, sessionID) {
414
428
  return [];
415
429
  }
416
430
 
431
+ const COMPLETION_BLOCK = /(?:^|\n)\s*-\s*status\s*[:=]|\bBLOCKED\b|\bsummary\s*[:=]/i;
432
+
417
433
  /**
418
- * Wait for a dispatched turn to end. The service resolves `session.generate`
419
- * with the FIRST text chunk while the turn continues asynchronously, so the
420
- * final answer only becomes read-consistent once the turn settles. Completion
421
- * is signalled by `session.wait` (blocks until the session is idle) when the
422
- * client exposes it; otherwise by transcript settlement (two consecutive
423
- * identical non-empty reads). Bounded by timeoutMs; never hangs.
434
+ * Wait for a dispatched turn to end by polling the transcript. Session waits
435
+ * are deliberately NOT used: long-poll `wait` calls accumulating on the
436
+ * service stalled the server's response path (HTTP/1.1 head-of-line
437
+ * blocking). Turns complete in seconds; a transcript read that carries the
438
+ * completion block ends immediately, and two consecutive identical non-empty
439
+ * reads settle the no-ceremony case. Bounded by timeoutMs; never hangs.
424
440
  * @param {any} sessionApi session or client handle
425
441
  * @param {string} sessionID
426
442
  * @param {{ timeoutMs?: number, intervalMs?: number }} [options]
@@ -429,26 +445,22 @@ export async function readTurnTranscript(sessionApi, sessionID) {
429
445
  export async function waitForTurnEnd(
430
446
  sessionApi,
431
447
  sessionID,
432
- { timeoutMs = 120000, intervalMs = 1500 } = {},
448
+ { timeoutMs = 120000, intervalMs = 1200 } = {},
433
449
  ) {
434
- if (typeof sessionApi?.session?.wait === 'function') {
435
- try {
436
- await Promise.race([
437
- sessionApi.session.wait({ sessionID }),
438
- new Promise((resolve) => setTimeout(resolve, timeoutMs)),
439
- ]);
440
- return;
441
- } catch {
442
- /* fall through to transcript settlement */
443
- }
444
- }
445
450
  const deadline = Date.now() + timeoutMs;
446
451
  let previous = '';
447
452
  let stableReads = 0;
448
453
  while (Date.now() < deadline) {
449
454
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
450
455
  const text = joinAssistantText(await readTurnTranscript(sessionApi, sessionID));
451
- if (text && text === previous) {
456
+ if (process.env.REGENT_TRACE_TURN === '1') {
457
+ console.error(
458
+ `[turn-trace] ${sessionID.slice(-8)} iter=${stableReads} text=${JSON.stringify(text.slice(0, 60))}`,
459
+ );
460
+ }
461
+ if (!text) continue;
462
+ if (COMPLETION_BLOCK.test(text)) return;
463
+ if (text === previous) {
452
464
  stableReads += 1;
453
465
  if (stableReads >= 2) return;
454
466
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "regent-code",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "description": "Agent orchestration for OpenCode. From idea to shipped — zero ceremony. Plugin + MCP server.",
5
5
  "type": "module",
6
6
  "main": ".opencode/plugins/regent.js",
Binary file
Binary file