@khorsheed/dsh-local-agent 0.2.0-rc.3 → 0.2.0
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/CHANGELOG.md +19 -1
- package/README.en.md +23 -164
- package/README.i18n.yaml +2 -2
- package/README.md +23 -164
- package/lib/client.js +373 -133
- package/lib/index.js +483 -14
- package/lib/member-bridge.js +195 -25
- package/lib/typert.host.js +107 -15
- package/lib/typert.remote-client.js +4 -4
- package/lib/types/client/HarnessModelPicker.d.ts +21 -1
- package/lib/types/client/HarnessModelPicker.js +31 -3
- package/lib/types/client/MemberLiveNode.js +38 -4
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +16 -2
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/model-directory-scope.d.ts +14 -0
- package/lib/types/client/model-directory-scope.js +20 -0
- package/lib/types/index.d.ts +18 -2
- package/lib/types/index.js +81 -14
- package/lib/types/live-stream.d.ts +11 -0
- package/lib/types/live-stream.js +26 -0
- package/lib/types/member-bridge.d.ts +11 -1
- package/lib/types/member-bridge.js +81 -13
- package/lib/types/member-channel.d.ts +44 -1
- package/lib/types/member-channel.js +65 -2
- package/lib/types/mode-skills.d.ts +18 -0
- package/lib/types/mode-skills.js +56 -0
- package/lib/types/preset-tool-host.d.ts +70 -0
- package/lib/types/preset-tool-host.js +210 -0
- package/lib/types/types.d.ts +27 -2
- package/package.json +5 -1
package/lib/index.js
CHANGED
|
@@ -4,9 +4,11 @@ import { appendFileSync, chmodSync, closeSync, fsyncSync, mkdirSync, openSync, r
|
|
|
4
4
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import z from "@deepseek-ai/schemastery";
|
|
7
|
+
import * as sessionModule from "@deepseek-ai/dsh-session";
|
|
7
8
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
8
9
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
10
|
import { createServer } from "node:net";
|
|
11
|
+
import { createScope } from "@deepseek-ai/dsh-scope";
|
|
10
12
|
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
11
13
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
12
14
|
//#region lib/types/cli-version.js
|
|
@@ -1260,6 +1262,65 @@ let LocalAgentGateway = (() => {
|
|
|
1260
1262
|
};
|
|
1261
1263
|
})();
|
|
1262
1264
|
//#endregion
|
|
1265
|
+
//#region lib/types/mode-skills.js
|
|
1266
|
+
/**
|
|
1267
|
+
* Mode skills, installed the CLI's own way: a member with a mode gets the
|
|
1268
|
+
* skills that mode contributes (e.g. the 剪辑 mode's `video-studio`) written
|
|
1269
|
+
* into its harness home's skill directory — `skills/<name>/SKILL.md`, the
|
|
1270
|
+
* layout Codex (`$CODEX_HOME`), Claude Code (`$CLAUDE_CONFIG_DIR`) and Kimi
|
|
1271
|
+
* (`$KIMI_CODE_HOME`, its auto-discovered user skill directory) load. The home is shared by every member of that harness, so each skill is
|
|
1272
|
+
* named `dsh-<name>` and says it applies only alongside the mode's tools.
|
|
1273
|
+
* Harnesses without a known skill directory get none. Best-effort:
|
|
1274
|
+
* a failed write never fails the member's tool listing.
|
|
1275
|
+
* @module @khorsheed/dsh-local-agent/mode-skills
|
|
1276
|
+
*/
|
|
1277
|
+
/** Harnesses whose CLI loads `<home>/skills/<name>/SKILL.md`. */
|
|
1278
|
+
const SKILL_HOMES = /* @__PURE__ */ new Set([
|
|
1279
|
+
"codex",
|
|
1280
|
+
"claude-code",
|
|
1281
|
+
"kimi"
|
|
1282
|
+
]);
|
|
1283
|
+
/** The skill directory of a harness home, or undefined when its CLI has none we know. */
|
|
1284
|
+
function harnessSkillsDir(harness, home) {
|
|
1285
|
+
return SKILL_HOMES.has(harness) ? join(home, "skills") : void 0;
|
|
1286
|
+
}
|
|
1287
|
+
/** The SKILL.md a mode skill installs as. */
|
|
1288
|
+
function modeSkillFile(skill, mode) {
|
|
1289
|
+
const name = `dsh-${skill.name}`;
|
|
1290
|
+
const description = `${skill.description} (dsh mode "${mode}": use only when your tools include this mode's tools.)`.replace(/\s+/g, " ");
|
|
1291
|
+
return {
|
|
1292
|
+
name,
|
|
1293
|
+
text: `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n\n${skill.content.trim()}\n`
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Write the mode's skills into the harness's skill directory, skipping files
|
|
1298
|
+
* already up to date.
|
|
1299
|
+
* @param dir - the harness skill directory.
|
|
1300
|
+
* @param mode - the member's mode id.
|
|
1301
|
+
* @param skills - the mode's skills.
|
|
1302
|
+
* @returns the installed skill names.
|
|
1303
|
+
*/
|
|
1304
|
+
function installModeSkills(dir, mode, skills) {
|
|
1305
|
+
const installed = [];
|
|
1306
|
+
for (const skill of skills) {
|
|
1307
|
+
const file = modeSkillFile(skill, mode);
|
|
1308
|
+
const target = join(dir, file.name, "SKILL.md");
|
|
1309
|
+
let current;
|
|
1310
|
+
try {
|
|
1311
|
+
current = readFileSync(target, "utf8");
|
|
1312
|
+
} catch {
|
|
1313
|
+
current = void 0;
|
|
1314
|
+
}
|
|
1315
|
+
if (current !== file.text) {
|
|
1316
|
+
mkdirSync(join(dir, file.name), { recursive: true });
|
|
1317
|
+
writeFileSync(target, file.text);
|
|
1318
|
+
}
|
|
1319
|
+
installed.push(file.name);
|
|
1320
|
+
}
|
|
1321
|
+
return installed;
|
|
1322
|
+
}
|
|
1323
|
+
//#endregion
|
|
1263
1324
|
//#region lib/types/member-channel.js
|
|
1264
1325
|
/**
|
|
1265
1326
|
* Member channel, host side: the loopback listener the bridge MCP servers call
|
|
@@ -1288,6 +1349,8 @@ let LocalAgentGateway = (() => {
|
|
|
1288
1349
|
* `busy`; else the facade `resume` with a provenance-tagged prompt.
|
|
1289
1350
|
* @module @khorsheed/dsh-local-agent/member-channel
|
|
1290
1351
|
*/
|
|
1352
|
+
/** How long one bridged mode-tool call may run before it is cancelled. */
|
|
1353
|
+
const MODE_TOOL_TIMEOUT_MS = 18e5;
|
|
1291
1354
|
/**
|
|
1292
1355
|
* The member-channel host: owns the loopback unix-socket listener and the
|
|
1293
1356
|
* delivery chain. Mounted by the plugin's `apply` and disposed with its fiber.
|
|
@@ -1296,11 +1359,13 @@ var MemberChannel = class {
|
|
|
1296
1359
|
ctx;
|
|
1297
1360
|
registry;
|
|
1298
1361
|
socketPath;
|
|
1362
|
+
modeTools;
|
|
1299
1363
|
server;
|
|
1300
|
-
constructor(ctx, registry, socketPath) {
|
|
1364
|
+
constructor(ctx, registry, socketPath, modeTools) {
|
|
1301
1365
|
this.ctx = ctx;
|
|
1302
1366
|
this.registry = registry;
|
|
1303
1367
|
this.socketPath = socketPath;
|
|
1368
|
+
this.modeTools = modeTools;
|
|
1304
1369
|
}
|
|
1305
1370
|
/**
|
|
1306
1371
|
* Start listening. A listener failure (e.g. a socket path beyond the
|
|
@@ -1360,7 +1425,8 @@ var MemberChannel = class {
|
|
|
1360
1425
|
async answer(socket, line) {
|
|
1361
1426
|
let outcome;
|
|
1362
1427
|
try {
|
|
1363
|
-
|
|
1428
|
+
const request = JSON.parse(line);
|
|
1429
|
+
outcome = request !== null && typeof request === "object" && "modeTools" in request ? await this.handleModeTools(request) : await this.handle(request);
|
|
1364
1430
|
} catch (error) {
|
|
1365
1431
|
outcome = {
|
|
1366
1432
|
ok: false,
|
|
@@ -1458,6 +1524,75 @@ var MemberChannel = class {
|
|
|
1458
1524
|
receipt: "sent"
|
|
1459
1525
|
};
|
|
1460
1526
|
}
|
|
1527
|
+
/**
|
|
1528
|
+
* The member's mode tools: the room says which mode the member has; the
|
|
1529
|
+
* preset tool host lists or runs that mode's plugin tools. No room, no mode
|
|
1530
|
+
* or no host = an empty list.
|
|
1531
|
+
* @param request - the bridge callback.
|
|
1532
|
+
* @returns the list or the call result.
|
|
1533
|
+
*/
|
|
1534
|
+
async handleModeTools(request) {
|
|
1535
|
+
const run = typeof request.token === "string" ? this.registry.resolveMemberRun(request.token) : void 0;
|
|
1536
|
+
if (run === void 0) return {
|
|
1537
|
+
ok: false,
|
|
1538
|
+
error: "localAgent: unknown or expired member token"
|
|
1539
|
+
};
|
|
1540
|
+
const mode = this.modeTools === void 0 ? void 0 : await this.probeRoom()?.memberMode?.(run);
|
|
1541
|
+
if ("list" in request.modeTools) {
|
|
1542
|
+
if (mode === void 0) {
|
|
1543
|
+
this.modeTools?.note({
|
|
1544
|
+
childSessionId: run.childSessionId,
|
|
1545
|
+
provider: run.provider,
|
|
1546
|
+
mode: false
|
|
1547
|
+
});
|
|
1548
|
+
return {
|
|
1549
|
+
ok: true,
|
|
1550
|
+
tools: []
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
const tools = await this.modeTools.list(run.childSessionId, mode.preset, mode.cwd);
|
|
1555
|
+
await this.installSkills(run, mode.preset);
|
|
1556
|
+
return {
|
|
1557
|
+
ok: true,
|
|
1558
|
+
tools
|
|
1559
|
+
};
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
this.ctx.logger.warn(`localAgent: mode tools for ${run.childSessionId} unavailable: ${String(error)}`);
|
|
1562
|
+
return {
|
|
1563
|
+
ok: true,
|
|
1564
|
+
tools: []
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (mode === void 0) return {
|
|
1569
|
+
ok: false,
|
|
1570
|
+
error: "This member has no mode tools"
|
|
1571
|
+
};
|
|
1572
|
+
const { name, arguments: args } = request.modeTools.call;
|
|
1573
|
+
try {
|
|
1574
|
+
return {
|
|
1575
|
+
ok: true,
|
|
1576
|
+
result: await this.modeTools.call(run.childSessionId, mode.preset, name, args ?? {}, AbortSignal.timeout(MODE_TOOL_TIMEOUT_MS), mode.cwd)
|
|
1577
|
+
};
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
return {
|
|
1580
|
+
ok: false,
|
|
1581
|
+
error: String(error)
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
/** Install the mode's skills into the member's harness skill directory (best-effort). */
|
|
1586
|
+
async installSkills(run, preset) {
|
|
1587
|
+
try {
|
|
1588
|
+
const harness = this.registry.list().map((name) => this.registry.get(name)).find((candidate) => candidate?.delegationProvider === run.provider);
|
|
1589
|
+
const dir = harness === void 0 ? void 0 : harnessSkillsDir(harness.name, this.registry.homeDir(harness.name));
|
|
1590
|
+
if (dir === void 0) return;
|
|
1591
|
+
installModeSkills(dir, preset, await this.modeTools.skills(run.childSessionId, preset));
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
this.ctx.logger.warn(`localAgent: mode skills for ${run.childSessionId} not installed: ${String(error)}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1461
1596
|
/** Probe the optional room gate without importing any room package. */
|
|
1462
1597
|
probeRoom() {
|
|
1463
1598
|
const room = this.ctx.get.call(this.ctx, "room");
|
|
@@ -1479,6 +1614,10 @@ var MemberChannel = class {
|
|
|
1479
1614
|
to: request.to,
|
|
1480
1615
|
content: request.text,
|
|
1481
1616
|
parentSessionId: from.parentSessionId,
|
|
1617
|
+
...request.kind === "task" || request.kind === "notify" ? { kind: request.kind } : {},
|
|
1618
|
+
...typeof request.inReplyTo === "string" ? { inReplyTo: request.inReplyTo } : {},
|
|
1619
|
+
...typeof request.note === "string" ? { note: request.note } : {},
|
|
1620
|
+
...typeof request.task === "string" ? { task: request.task } : {},
|
|
1482
1621
|
provenance: {
|
|
1483
1622
|
childSessionId: from.childSessionId,
|
|
1484
1623
|
provider: from.provider,
|
|
@@ -1507,6 +1646,251 @@ var MemberChannel = class {
|
|
|
1507
1646
|
}
|
|
1508
1647
|
};
|
|
1509
1648
|
//#endregion
|
|
1649
|
+
//#region lib/types/preset-tool-host.js
|
|
1650
|
+
/**
|
|
1651
|
+
* Preset tool host: lets an external CLI member use the plugin tools of a
|
|
1652
|
+
* host agent preset (a mode), through its member bridge, ON THE MEMBER'S OWN
|
|
1653
|
+
* SESSION. The tool registry keys scoped tools by an agent identity, and
|
|
1654
|
+
* plugin tools read only `agent.id` and `agent.session` from it — so the
|
|
1655
|
+
* member gets a model-less stand-in: `{ id, session }` over its own child
|
|
1656
|
+
* session, given a scope (`createScope`) with the preset mounted. Calls run
|
|
1657
|
+
* through the registry's normal pipeline (policies, approvals, timeouts) as
|
|
1658
|
+
* that stand-in, so plugin state (a video project) belongs to the member's
|
|
1659
|
+
* session and the mode's panels show there; the session also records the mode
|
|
1660
|
+
* (`agent-preset/selected`), so per-session plugin visibility follows it.
|
|
1661
|
+
* Only PLUGIN tools are offered (`Symbol.for('dsh.tool.origin')` channel
|
|
1662
|
+
* `plugin`; the CLI brings its own shell, files and todo).
|
|
1663
|
+
*
|
|
1664
|
+
* Everything host-side is probed, never injected: without `sessions`,
|
|
1665
|
+
* `agentPresets` or `tools` the member simply gets no extra tools.
|
|
1666
|
+
* @module @khorsheed/dsh-local-agent/preset-tool-host
|
|
1667
|
+
*/
|
|
1668
|
+
/** Owners whose tools the bridge already serves itself (room), or that delegate from dsh (the local-agent family). */
|
|
1669
|
+
const skippedOwner = (owner) => owner === "@khorsheed/dsh-room" || owner.startsWith("@khorsheed/dsh-local-agent");
|
|
1670
|
+
/** How long a freshly mounted preset may take to finish registering its tools, and how often to look. */
|
|
1671
|
+
const SETTLE_MS = 3e3;
|
|
1672
|
+
const SETTLE_STEP_MS = 150;
|
|
1673
|
+
const ORIGIN = Symbol.for("dsh.tool.origin");
|
|
1674
|
+
/** The mode a session records: its header, overridden by the latest selection. */
|
|
1675
|
+
function sessionPreset(session) {
|
|
1676
|
+
let preset = session.header.agentPreset ?? void 0;
|
|
1677
|
+
for (const event of session.snapshotEvents()) if (event.type === "agent-preset/selected") preset = event.data?.agentPreset ?? preset;
|
|
1678
|
+
return preset;
|
|
1679
|
+
}
|
|
1680
|
+
/** Whether a definition is a plugin tool the bridge may forward. */
|
|
1681
|
+
function isPluginTool(definition) {
|
|
1682
|
+
const origin = definition?.[ORIGIN];
|
|
1683
|
+
return origin?.channel === "plugin" && !skippedOwner(String(origin.owner));
|
|
1684
|
+
}
|
|
1685
|
+
/** Map registry content blocks to MCP content (text and images; anything else as text). */
|
|
1686
|
+
function mcpContent(blocks) {
|
|
1687
|
+
return blocks.map((block) => {
|
|
1688
|
+
if (block.type === "text") return {
|
|
1689
|
+
type: "text",
|
|
1690
|
+
text: block.text ?? ""
|
|
1691
|
+
};
|
|
1692
|
+
if (block.type === "image" && typeof block.data === "string") return {
|
|
1693
|
+
type: "image",
|
|
1694
|
+
data: block.data,
|
|
1695
|
+
mimeType: block.mediaType ?? block.mimeType ?? "image/png"
|
|
1696
|
+
};
|
|
1697
|
+
return {
|
|
1698
|
+
type: "text",
|
|
1699
|
+
text: JSON.stringify(block)
|
|
1700
|
+
};
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
/** The per-member preset agents and the list/call operations the bridge forwards. */
|
|
1704
|
+
var PresetToolHost = class {
|
|
1705
|
+
ctx;
|
|
1706
|
+
diagnostics;
|
|
1707
|
+
hosts = /* @__PURE__ */ new Map();
|
|
1708
|
+
/** Stand-in → the member's child session, for routing approvals its calls raise. */
|
|
1709
|
+
standIns = /* @__PURE__ */ new WeakMap();
|
|
1710
|
+
/** The member a stand-in agent acts for, if it is one of ours. */
|
|
1711
|
+
memberOf(agent) {
|
|
1712
|
+
return typeof agent === "object" && agent !== null ? this.standIns.get(agent) : void 0;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* @param ctx - the local-agent plugin context.
|
|
1716
|
+
* @param diagnostics - optional JSONL file each listing appends a summary to
|
|
1717
|
+
* (what the preset agent's scope saw and which tools passed the filter).
|
|
1718
|
+
*/
|
|
1719
|
+
constructor(ctx, diagnostics) {
|
|
1720
|
+
this.ctx = ctx;
|
|
1721
|
+
this.diagnostics = diagnostics;
|
|
1722
|
+
}
|
|
1723
|
+
/** Append one diagnostics line (best-effort). */
|
|
1724
|
+
note(entry) {
|
|
1725
|
+
if (this.diagnostics === void 0) return;
|
|
1726
|
+
try {
|
|
1727
|
+
appendFileSync(this.diagnostics, `${JSON.stringify({
|
|
1728
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1729
|
+
...entry
|
|
1730
|
+
})}\n`);
|
|
1731
|
+
} catch {}
|
|
1732
|
+
}
|
|
1733
|
+
probe(name) {
|
|
1734
|
+
return this.ctx.get.call(this.ctx, name);
|
|
1735
|
+
}
|
|
1736
|
+
/** The member's stand-in, scoped with its preset on first use (and re-scoped when the preset changed). */
|
|
1737
|
+
async agentFor(childSessionId, preset, _cwd) {
|
|
1738
|
+
const sessions = this.probe("sessions");
|
|
1739
|
+
const presets = this.probe("agentPresets");
|
|
1740
|
+
const session = sessions?.get(SessionId(childSessionId));
|
|
1741
|
+
if (presets?.mount === void 0 || session === void 0) return void 0;
|
|
1742
|
+
const current = this.hosts.get(childSessionId);
|
|
1743
|
+
if (current !== void 0 && current.preset === preset && (await current.agent).agent.session === session) return (await current.agent).agent;
|
|
1744
|
+
if (current !== void 0) {
|
|
1745
|
+
this.hosts.delete(childSessionId);
|
|
1746
|
+
current.agent.then((handle) => handle.dispose()).catch(() => {});
|
|
1747
|
+
}
|
|
1748
|
+
const agent = (async () => {
|
|
1749
|
+
const resolved = await presets.resolve(preset);
|
|
1750
|
+
const member = {
|
|
1751
|
+
id: childSessionId,
|
|
1752
|
+
session
|
|
1753
|
+
};
|
|
1754
|
+
this.standIns.set(member, childSessionId);
|
|
1755
|
+
const scope = createScope(this.ctx, member);
|
|
1756
|
+
member.ctx = scope.ctx;
|
|
1757
|
+
try {
|
|
1758
|
+
await presets.mount(scope.ctx, resolved.id);
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
await scope.dispose();
|
|
1761
|
+
throw error;
|
|
1762
|
+
}
|
|
1763
|
+
if (sessionPreset(session) !== resolved.id) session.append("agent-preset/selected", { agentPreset: resolved.id });
|
|
1764
|
+
return {
|
|
1765
|
+
agent: member,
|
|
1766
|
+
dispose: () => scope.dispose()
|
|
1767
|
+
};
|
|
1768
|
+
})();
|
|
1769
|
+
this.hosts.set(childSessionId, {
|
|
1770
|
+
preset,
|
|
1771
|
+
agent
|
|
1772
|
+
});
|
|
1773
|
+
try {
|
|
1774
|
+
return (await agent).agent;
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
this.hosts.delete(childSessionId);
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* The preset's plugin tools, for a member's `tools/list`.
|
|
1782
|
+
* @param childSessionId - the member's child session.
|
|
1783
|
+
* @param preset - the member's mode.
|
|
1784
|
+
* @param cwd - the member's working directory.
|
|
1785
|
+
* @returns the tools, empty when the host lacks the capabilities.
|
|
1786
|
+
*/
|
|
1787
|
+
async list(childSessionId, preset, cwd) {
|
|
1788
|
+
const tools = this.probe("tools");
|
|
1789
|
+
const fresh = this.hosts.get(childSessionId)?.preset !== preset;
|
|
1790
|
+
const agent = await this.agentFor(childSessionId, preset, cwd);
|
|
1791
|
+
if (tools === void 0 || agent === void 0) {
|
|
1792
|
+
this.note({
|
|
1793
|
+
childSessionId,
|
|
1794
|
+
preset,
|
|
1795
|
+
tools: tools !== void 0,
|
|
1796
|
+
agent: agent !== void 0
|
|
1797
|
+
});
|
|
1798
|
+
return [];
|
|
1799
|
+
}
|
|
1800
|
+
let schemas = tools.schemas(agent);
|
|
1801
|
+
if (fresh) for (let waited = 0; waited < SETTLE_MS; waited += SETTLE_STEP_MS) {
|
|
1802
|
+
await new Promise((resolve) => setTimeout(resolve, SETTLE_STEP_MS));
|
|
1803
|
+
const next = tools.schemas(agent);
|
|
1804
|
+
if (next.length === schemas.length && waited >= 450) break;
|
|
1805
|
+
schemas = next;
|
|
1806
|
+
}
|
|
1807
|
+
const listed = schemas.filter((schema) => isPluginTool(tools.get(schema.name, agent)));
|
|
1808
|
+
this.note({
|
|
1809
|
+
childSessionId,
|
|
1810
|
+
preset,
|
|
1811
|
+
agent: agent.id,
|
|
1812
|
+
scope: schemas.map((schema) => {
|
|
1813
|
+
const origin = tools.get(schema.name, agent)?.[ORIGIN];
|
|
1814
|
+
return origin === void 0 ? schema.name : `${schema.name}<${String(origin.channel)}:${String(origin.owner)}>`;
|
|
1815
|
+
}),
|
|
1816
|
+
listed: listed.map((schema) => schema.name)
|
|
1817
|
+
});
|
|
1818
|
+
return listed.map((schema) => ({
|
|
1819
|
+
name: schema.name,
|
|
1820
|
+
description: schema.description ?? "",
|
|
1821
|
+
inputSchema: schema.parameters ?? {
|
|
1822
|
+
type: "object",
|
|
1823
|
+
properties: {}
|
|
1824
|
+
}
|
|
1825
|
+
}));
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Run one plugin tool for a member through the registry's execution
|
|
1829
|
+
* pipeline (policies, approvals, timeouts), as the member's preset agent.
|
|
1830
|
+
* @param childSessionId - the member's child session.
|
|
1831
|
+
* @param preset - the member's mode.
|
|
1832
|
+
* @param name - the tool name.
|
|
1833
|
+
* @param args - the tool arguments.
|
|
1834
|
+
* @param signal - cancellation.
|
|
1835
|
+
* @param cwd - the member's working directory.
|
|
1836
|
+
* @returns the MCP result.
|
|
1837
|
+
*/
|
|
1838
|
+
async call(childSessionId, preset, name, args, signal, cwd) {
|
|
1839
|
+
const tools = this.probe("tools");
|
|
1840
|
+
const agent = await this.agentFor(childSessionId, preset, cwd);
|
|
1841
|
+
if (tools === void 0 || agent === void 0) return {
|
|
1842
|
+
content: [{
|
|
1843
|
+
type: "text",
|
|
1844
|
+
text: "Mode tools are unavailable on this host"
|
|
1845
|
+
}],
|
|
1846
|
+
isError: true
|
|
1847
|
+
};
|
|
1848
|
+
if (!isPluginTool(tools.get(name, agent))) return {
|
|
1849
|
+
content: [{
|
|
1850
|
+
type: "text",
|
|
1851
|
+
text: `Unknown tool ${name} in mode ${preset}`
|
|
1852
|
+
}],
|
|
1853
|
+
isError: true
|
|
1854
|
+
};
|
|
1855
|
+
const result = await tools.execute({
|
|
1856
|
+
callId: `bridge-${randomUUID()}`,
|
|
1857
|
+
name,
|
|
1858
|
+
arguments: args,
|
|
1859
|
+
agent,
|
|
1860
|
+
signal
|
|
1861
|
+
});
|
|
1862
|
+
return {
|
|
1863
|
+
content: mcpContent(result.content),
|
|
1864
|
+
isError: result.isError
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* The skills the member's mode contributes: visible in its scope, absent
|
|
1869
|
+
* from the global view (installed skills every CLI already sees stay out).
|
|
1870
|
+
* @param childSessionId - the member's child session.
|
|
1871
|
+
* @param preset - the member's mode.
|
|
1872
|
+
* @returns the mode's skills with their content; empty without a skill registry.
|
|
1873
|
+
*/
|
|
1874
|
+
async skills(childSessionId, preset) {
|
|
1875
|
+
const registry = this.probe("skills");
|
|
1876
|
+
const agent = await this.agentFor(childSessionId, preset, void 0);
|
|
1877
|
+
if (registry?.list === void 0 || agent === void 0) return [];
|
|
1878
|
+
const global = new Set((await registry.list()).map((skill) => skill.name));
|
|
1879
|
+
const own = (await registry.list({ scope: agent })).filter((skill) => !global.has(skill.name));
|
|
1880
|
+
return (await Promise.all(own.map((skill) => registry.get(skill.name, { scope: agent })))).flatMap((skill) => skill === void 0 ? [] : [{
|
|
1881
|
+
name: skill.name,
|
|
1882
|
+
description: skill.description,
|
|
1883
|
+
content: skill.content
|
|
1884
|
+
}]);
|
|
1885
|
+
}
|
|
1886
|
+
/** Dispose every preset agent. */
|
|
1887
|
+
async dispose() {
|
|
1888
|
+
const hosts = [...this.hosts.values()];
|
|
1889
|
+
this.hosts.clear();
|
|
1890
|
+
await Promise.all(hosts.map((host) => host.agent.then((handle) => handle.dispose()).catch(() => {})));
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
//#endregion
|
|
1510
1894
|
//#region lib/types/member-inbox.js
|
|
1511
1895
|
/** Durable human input; the provider FIFO remains the sole execution order. */
|
|
1512
1896
|
var MemberInbox = class {
|
|
@@ -2793,6 +3177,8 @@ var LiveStreamPublisher = class {
|
|
|
2793
3177
|
persist;
|
|
2794
3178
|
onError;
|
|
2795
3179
|
states = /* @__PURE__ */ new Map();
|
|
3180
|
+
/** Items shown live but not yet in the log: their step cannot open before the earlier steps close. */
|
|
3181
|
+
previews = /* @__PURE__ */ new Map();
|
|
2796
3182
|
constructor(streams, session, turn, persist, onError) {
|
|
2797
3183
|
this.streams = streams;
|
|
2798
3184
|
this.session = session;
|
|
@@ -2800,8 +3186,29 @@ var LiveStreamPublisher = class {
|
|
|
2800
3186
|
this.persist = persist;
|
|
2801
3187
|
this.onError = onError;
|
|
2802
3188
|
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Show an item live without writing it to the session: a step may open in
|
|
3191
|
+
* the log only after every earlier step has closed, so a later item that
|
|
3192
|
+
* streams first waits here (browser-only) until the driver can open it.
|
|
3193
|
+
*/
|
|
3194
|
+
preview(step, kind, text, options = {}) {
|
|
3195
|
+
const id = `${this.turn}:${step}${options.itemId === void 0 ? "" : `:${options.itemId}`}`;
|
|
3196
|
+
const previous = this.previews.get(id);
|
|
3197
|
+
if (previous?.text === text && previous.kind === kind) return;
|
|
3198
|
+
const item = {
|
|
3199
|
+
id,
|
|
3200
|
+
turn: this.turn,
|
|
3201
|
+
step,
|
|
3202
|
+
kind,
|
|
3203
|
+
text,
|
|
3204
|
+
receivedAt: previous?.receivedAt ?? options.receivedAt ?? Date.now()
|
|
3205
|
+
};
|
|
3206
|
+
this.previews.set(id, item);
|
|
3207
|
+
this.streams.publish(String(this.session.id), item);
|
|
3208
|
+
}
|
|
2803
3209
|
update(step, kind, text, options = {}) {
|
|
2804
3210
|
const id = `${this.turn}:${step}${options.itemId === void 0 ? "" : `:${options.itemId}`}`;
|
|
3211
|
+
this.previews.delete(id);
|
|
2805
3212
|
let state = this.states.get(id);
|
|
2806
3213
|
if (state?.item.text === text && state.item.kind === kind) return;
|
|
2807
3214
|
const receivedAt = options.receivedAt ?? Date.now();
|
|
@@ -2860,6 +3267,11 @@ var LiveStreamPublisher = class {
|
|
|
2860
3267
|
}
|
|
2861
3268
|
/** Call only after the corresponding native final message has been appended. */
|
|
2862
3269
|
finish(step) {
|
|
3270
|
+
for (const [id, item] of this.previews) {
|
|
3271
|
+
if (item.step !== step) continue;
|
|
3272
|
+
this.previews.delete(id);
|
|
3273
|
+
this.streams.finish(String(this.session.id), id);
|
|
3274
|
+
}
|
|
2863
3275
|
for (const [id, state] of this.states) {
|
|
2864
3276
|
if (state.item.step !== step) continue;
|
|
2865
3277
|
if (state.timer !== void 0) clearTimeout(state.timer);
|
|
@@ -2879,6 +3291,8 @@ var LiveStreamPublisher = class {
|
|
|
2879
3291
|
}
|
|
2880
3292
|
/** A producer failure keeps a durable partial, without claiming completion. */
|
|
2881
3293
|
dispose() {
|
|
3294
|
+
for (const id of this.previews.keys()) this.streams.finish(String(this.session.id), id);
|
|
3295
|
+
this.previews.clear();
|
|
2882
3296
|
for (const [id, state] of this.states) {
|
|
2883
3297
|
this.checkpoint(id);
|
|
2884
3298
|
if (state.timer !== void 0) clearTimeout(state.timer);
|
|
@@ -3536,6 +3950,11 @@ function parseScopeFlag(input) {
|
|
|
3536
3950
|
...scope === void 0 ? {} : { scope }
|
|
3537
3951
|
};
|
|
3538
3952
|
}
|
|
3953
|
+
/** A delegation record without its requested model. */
|
|
3954
|
+
function withoutModel(record) {
|
|
3955
|
+
const { model: _model, ...rest } = record;
|
|
3956
|
+
return rest;
|
|
3957
|
+
}
|
|
3539
3958
|
/** Map a login failure to a command error, naming the swallowed signal. */
|
|
3540
3959
|
function loginFailure(harness, exitCode, signal) {
|
|
3541
3960
|
return `${harness.name} login exited with ${signal ?? `code ${exitCode ?? "unknown"}`}; try /${harness.name} login again.`;
|
|
@@ -4142,7 +4561,8 @@ var LocalAgentRegistry = class {
|
|
|
4142
4561
|
recordDelegation(record) {
|
|
4143
4562
|
const binding = this.memberControls.binding(record.childSessionId);
|
|
4144
4563
|
if (binding !== void 0) record = {
|
|
4145
|
-
...record,
|
|
4564
|
+
...withoutModel(record),
|
|
4565
|
+
...binding.model === void 0 ? {} : { model: binding.model },
|
|
4146
4566
|
...binding.effort === void 0 ? {} : { effort: binding.effort },
|
|
4147
4567
|
...binding.configurationLock === void 0 ? {} : { configurationLock: binding.configurationLock }
|
|
4148
4568
|
};
|
|
@@ -4667,8 +5087,12 @@ var LocalAgentRegistry = class {
|
|
|
4667
5087
|
* const handle = await ctx.sessionPersistence.open(SessionId(childSessionId), 'write')
|
|
4668
5088
|
* try {
|
|
4669
5089
|
* const cold = await handle.read(0)
|
|
5090
|
+
* // Close a turn a restart interrupted, or the next round's turn/start
|
|
5091
|
+
* // makes the stored log unreadable.
|
|
5092
|
+
* const closers = interruptedTurnClosers(cold.events)
|
|
5093
|
+
* if (closers.length > 0) await handle.append(closers)
|
|
4670
5094
|
* const session = ctx.sessions.prepare(SessionId(childSessionId), {
|
|
4671
|
-
* seed: [...cold.events],
|
|
5095
|
+
* seed: [...cold.events, ...closers],
|
|
4672
5096
|
* meta: structuredClone(handle.header),
|
|
4673
5097
|
* inheritedEventCount: handle.inheritedEventCount,
|
|
4674
5098
|
* eventState: cold.eventState,
|
|
@@ -4676,7 +5100,7 @@ var LocalAgentRegistry = class {
|
|
|
4676
5100
|
* // The seeded constructor's session/end-seed marker lands before enter()
|
|
4677
5101
|
* // installs publication hooks — push the unstored suffix through the
|
|
4678
5102
|
* // handle directly, or the writer's contiguous cursor diverges.
|
|
4679
|
-
* const unstored = session.snapshotEvents().slice(cold.events.length)
|
|
5103
|
+
* const unstored = session.snapshotEvents().slice(cold.events.length + closers.length)
|
|
4680
5104
|
* if (unstored.length > 0) await handle.append(unstored)
|
|
4681
5105
|
* const detach = ctx.sessions.enter(session)
|
|
4682
5106
|
* try { ctx.sessions.announce(session) }
|
|
@@ -4904,13 +5328,16 @@ var LocalAgentRegistry = class {
|
|
|
4904
5328
|
const handle = await this.acquireChildWriteHandle(childSessionId);
|
|
4905
5329
|
try {
|
|
4906
5330
|
const cold = await handle.read(0);
|
|
5331
|
+
const closeOpenTurn = sessionModule.interruptedTurnClosers;
|
|
5332
|
+
const closers = closeOpenTurn === void 0 ? [] : closeOpenTurn(cold.events);
|
|
5333
|
+
if (closers.length > 0) await handle.append(closers);
|
|
4907
5334
|
const session = sessions.prepare(SessionId(childSessionId), {
|
|
4908
|
-
seed: [...cold.events],
|
|
5335
|
+
seed: [...cold.events, ...closers],
|
|
4909
5336
|
meta: structuredClone(handle.header),
|
|
4910
5337
|
inheritedEventCount: handle.inheritedEventCount,
|
|
4911
5338
|
eventState: cold.eventState
|
|
4912
5339
|
});
|
|
4913
|
-
const unstored = session.snapshotEvents().slice(cold.events.length);
|
|
5340
|
+
const unstored = session.snapshotEvents().slice(cold.events.length + closers.length);
|
|
4914
5341
|
if (unstored.length > 0) await handle.append(unstored);
|
|
4915
5342
|
const detach = sessions.enter(session);
|
|
4916
5343
|
try {
|
|
@@ -4940,18 +5367,41 @@ var LocalAgentRegistry = class {
|
|
|
4940
5367
|
* @param session - the live child session to sync.
|
|
4941
5368
|
*/
|
|
4942
5369
|
async syncChildSession(session, strict = false) {
|
|
5370
|
+
const id = String(session.id);
|
|
4943
5371
|
try {
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
5372
|
+
try {
|
|
5373
|
+
await this.syncChildSessionOnce(session);
|
|
5374
|
+
} catch (error) {
|
|
5375
|
+
if (!/append seq mismatch/.test(error instanceof Error ? error.message : String(error))) throw error;
|
|
5376
|
+
await this.releaseChildWriteHandle(id);
|
|
5377
|
+
await this.syncChildSessionOnce(session);
|
|
5378
|
+
}
|
|
4949
5379
|
} catch (error) {
|
|
4950
5380
|
if (strict) throw error;
|
|
4951
|
-
this.ctx.logger.warn(`localAgent: syncing child session ${
|
|
5381
|
+
this.ctx.logger.warn(`localAgent: syncing child session ${id} to persistence failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4952
5382
|
}
|
|
4953
5383
|
}
|
|
4954
5384
|
/**
|
|
5385
|
+
* One sync pass. While the write handle is open the host routes every live
|
|
5386
|
+
* append into the writer's buffer, so the routed events are drained first
|
|
5387
|
+
* (`sessions.flush`: drain + flush) and only what is STILL missing — events
|
|
5388
|
+
* from before the handle opened, or a host that routes nothing — goes in
|
|
5389
|
+
* explicitly, bounded to the events that existed before the drain. Appending
|
|
5390
|
+
* routed-but-undrained events explicitly as well would land them twice: the
|
|
5391
|
+
* explicit write wins, the buffered copy then fails contiguity forever, and
|
|
5392
|
+
* every later flush — the host's tool-execution checkpoint included —
|
|
5393
|
+
* throws "append seq mismatch".
|
|
5394
|
+
*/
|
|
5395
|
+
async syncChildSessionOnce(session) {
|
|
5396
|
+
const handle = await this.acquireChildWriteHandle(String(session.id), session.header);
|
|
5397
|
+
const known = session.snapshotEvents().length;
|
|
5398
|
+
await this.ctx.get("sessions")?.flush(session);
|
|
5399
|
+
const stored = await handle.read(0);
|
|
5400
|
+
const missing = session.snapshotEvents().slice(stored.events.length, known);
|
|
5401
|
+
if (missing.length > 0) await handle.append(missing);
|
|
5402
|
+
await handle.flush();
|
|
5403
|
+
}
|
|
5404
|
+
/**
|
|
4955
5405
|
* Get or open the child session's registry-owned WRITE handle (the
|
|
4956
5406
|
* {@link childWriteHandles} cache). When the caller carries the session
|
|
4957
5407
|
* header (a sync), a missing stored session is created from it; a create
|
|
@@ -5543,10 +5993,29 @@ async function apply(ctx, config) {
|
|
|
5543
5993
|
const registry = new LocalAgentRegistry(ctx, config.homesRoot, config.loginPromptTimeoutMs ?? 1e4);
|
|
5544
5994
|
ctx.provide(LOCAL_AGENT_SERVICE, registry);
|
|
5545
5995
|
new LocalAgentGateway(ctx);
|
|
5546
|
-
const
|
|
5996
|
+
const modeTools = new PresetToolHost(ctx, join(config.homesRoot, "mode-tools.jsonl"));
|
|
5997
|
+
const memberChannel = new MemberChannel(ctx, registry, registry.memberBridgeSocketPath(), modeTools);
|
|
5547
5998
|
memberChannel.start();
|
|
5999
|
+
ctx.on("approval/request", async (request, next) => {
|
|
6000
|
+
const child = modeTools.memberOf(request.agent);
|
|
6001
|
+
const delegation = child === void 0 ? void 0 : registry.getDelegation(child);
|
|
6002
|
+
const room = ctx.get.call(ctx, "room");
|
|
6003
|
+
if (child === void 0 || delegation === void 0 || room?.askMemberApproval === void 0) return next();
|
|
6004
|
+
const reason = request.displayReason ?? request.reason;
|
|
6005
|
+
const outcome = await room.askMemberApproval({
|
|
6006
|
+
childSessionId: child,
|
|
6007
|
+
parentSessionId: delegation.parentSessionId,
|
|
6008
|
+
provider: delegation.provider
|
|
6009
|
+
}, {
|
|
6010
|
+
toolName: request.toolName,
|
|
6011
|
+
...reason === void 0 ? {} : { reason },
|
|
6012
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
6013
|
+
});
|
|
6014
|
+
return outcome === "unavailable" ? next() : outcome;
|
|
6015
|
+
}, { prepend: true });
|
|
5548
6016
|
ctx.effect(() => () => {
|
|
5549
6017
|
memberChannel.dispose();
|
|
6018
|
+
modeTools.dispose();
|
|
5550
6019
|
});
|
|
5551
6020
|
ctx.commands.register({
|
|
5552
6021
|
name: "local-agent",
|