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.
- package/.opencode/INSTALL.md +3 -3
- package/README.md +4 -4
- package/mcp/index.js +48 -10
- package/mcp/shared.js +33 -21
- package/package.json +1 -1
- package/regent-code-3.0.7.tgz +0 -0
- package/regent-code-3.0.6.tgz +0 -0
package/.opencode/INSTALL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
)
|
|
292
|
-
|
|
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
|
-
|
|
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
|
|
348
|
-
* skipped. Handles
|
|
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
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
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 =
|
|
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 (
|
|
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
|
Binary file
|
package/regent-code-3.0.6.tgz
DELETED
|
Binary file
|