regent-code 3.0.5 → 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 +58 -15
- 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.5.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' };
|
|
@@ -227,7 +237,11 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
|
227
237
|
}
|
|
228
238
|
|
|
229
239
|
const key = 'mcp';
|
|
230
|
-
|
|
240
|
+
// Worker sessions attach to the service's CURRENT project by default —
|
|
241
|
+
// explicit foreign locations resolve to unloaded scopes with empty agent
|
|
242
|
+
// catalogs ("Agent not found"). Override only via REGENT_WORKER_LOCATION.
|
|
243
|
+
const workerLocation = (process.env.REGENT_WORKER_LOCATION || '').trim();
|
|
244
|
+
const directory = workerLocation || process.cwd();
|
|
231
245
|
let session;
|
|
232
246
|
|
|
233
247
|
try {
|
|
@@ -243,7 +257,8 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
|
243
257
|
for (let index = 0; index < selection.agents.length; index++) {
|
|
244
258
|
const agent = selection.agents[index];
|
|
245
259
|
try {
|
|
246
|
-
const createInput = { title, agent
|
|
260
|
+
const createInput = { title, agent };
|
|
261
|
+
if (workerLocation) createInput.location = { directory: workerLocation };
|
|
247
262
|
const sessionResult = await withRetry(() => client.session.create(createInput));
|
|
248
263
|
session = unwrapData(sessionResult);
|
|
249
264
|
if (session?.id) break;
|
|
@@ -281,10 +296,18 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
|
281
296
|
'If you cannot complete the task, say BLOCKED and explain why.',
|
|
282
297
|
].join('\n');
|
|
283
298
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
287
|
-
|
|
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)) : '';
|
|
288
311
|
|
|
289
312
|
// Collect the final answer once the turn has actually ended (generate
|
|
290
313
|
// resolves with the first chunk on current service semantics).
|
|
@@ -307,7 +330,21 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
|
307
330
|
};
|
|
308
331
|
}
|
|
309
332
|
const parsed = parseSubagentTextResponse(output);
|
|
310
|
-
|
|
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
|
+
}
|
|
311
348
|
|
|
312
349
|
if (filesChanged.length > 0) {
|
|
313
350
|
sessionFileChanges.set(session.id, {
|
|
@@ -319,6 +356,9 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
|
319
356
|
recordEvidence(session.id, filesChanged, directory);
|
|
320
357
|
}
|
|
321
358
|
|
|
359
|
+
if (process.env.REGENT_TRACE_TURN === '1') {
|
|
360
|
+
console.error(`[trace] dispatch returning ${session.id.slice(-8)}`);
|
|
361
|
+
}
|
|
322
362
|
return { status, output, concerns, files_changed: filesChanged, session_id: session.id };
|
|
323
363
|
} catch (err) {
|
|
324
364
|
const message = redactSecrets(safeErrorMessage(err)).slice(0, 500);
|
|
@@ -634,7 +674,7 @@ export function createRegentServer() {
|
|
|
634
674
|
const client = await getClient();
|
|
635
675
|
const resolveWorker = await createWorkerResolver(client, {
|
|
636
676
|
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
637
|
-
location: process.
|
|
677
|
+
location: process.env.REGENT_WORKER_LOCATION || '',
|
|
638
678
|
});
|
|
639
679
|
const result = await dispatchSubagent(client, resolveWorker, /** @type {any} */ (args));
|
|
640
680
|
return toContent(result);
|
|
@@ -659,7 +699,7 @@ export function createRegentServer() {
|
|
|
659
699
|
const client = await getClient();
|
|
660
700
|
const resolveWorker = await createWorkerResolver(client, {
|
|
661
701
|
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
662
|
-
location: process.
|
|
702
|
+
location: process.env.REGENT_WORKER_LOCATION || '',
|
|
663
703
|
});
|
|
664
704
|
const queue = [...args.tasks];
|
|
665
705
|
const results = [];
|
|
@@ -679,6 +719,9 @@ export function createRegentServer() {
|
|
|
679
719
|
}
|
|
680
720
|
const workerCount = Math.min(queue.length, 10);
|
|
681
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
|
+
}
|
|
682
725
|
const failed = results.filter((r) => r.status === 'blocked').length;
|
|
683
726
|
recordCircuitResult('mcp', results.length > 0 && failed === 0);
|
|
684
727
|
return toContent({
|
|
@@ -704,7 +747,7 @@ export function createRegentServer() {
|
|
|
704
747
|
const client = await getClient();
|
|
705
748
|
const resolveWorker = await createWorkerResolver(client, {
|
|
706
749
|
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
707
|
-
location: process.
|
|
750
|
+
location: process.env.REGENT_WORKER_LOCATION || '',
|
|
708
751
|
});
|
|
709
752
|
const results = await Promise.all(
|
|
710
753
|
args.questions.map(async (q) => {
|
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.5.tgz
DELETED
|
Binary file
|