@khorsheed/dsh-local-agent 0.2.0-rc.1 → 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 +31 -0
- package/README.en.md +23 -164
- package/README.i18n.yaml +2 -2
- package/README.md +23 -164
- package/lib/client.js +23707 -4768
- package/lib/index.js +827 -62
- package/lib/member-bridge.js +195 -25
- package/lib/tool-factory.js +52 -19
- package/lib/typert.host.js +1440 -353
- package/lib/typert.remote-client.d.ts +43 -3
- package/lib/typert.remote-client.js +973 -335
- package/lib/types/client/FamilyCard.d.ts +37 -0
- package/lib/types/client/FamilyCard.js +55 -0
- 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/ModelListSection.d.ts +49 -0
- package/lib/types/client/ModelListSection.js +132 -0
- package/lib/types/client/ProviderAuthBlock.d.ts +10 -2
- package/lib/types/client/ProviderAuthBlock.js +13 -6
- package/lib/types/client/auth-status.d.ts +6 -0
- package/lib/types/client/auth-status.js +8 -0
- package/lib/types/client/harness-cards.d.ts +72 -0
- package/lib/types/client/harness-cards.js +57 -0
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.d.ts +32 -7
- package/lib/types/client/index.js +66 -9
- package/lib/types/client/locales.d.ts +25 -1
- package/lib/types/client/locales.js +50 -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/gateway.d.ts +49 -1
- package/lib/types/gateway.js +89 -0
- package/lib/types/index.d.ts +91 -38
- package/lib/types/index.js +190 -58
- 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/member-controls.d.ts +8 -0
- package/lib/types/member-controls.js +15 -2
- package/lib/types/mode-skills.d.ts +18 -0
- package/lib/types/mode-skills.js +56 -0
- package/lib/types/model-list.d.ts +55 -0
- package/lib/types/model-list.js +101 -0
- package/lib/types/preset-tool-host.d.ts +70 -0
- package/lib/types/preset-tool-host.js +210 -0
- package/lib/types/tool-factory.d.ts +9 -3
- package/lib/types/tool-factory.js +55 -18
- package/lib/types/types.d.ts +62 -5
- package/package.json +27 -23
- package/lib/invariant.js +0 -85
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -105
package/lib/index.js
CHANGED
|
@@ -4,10 +4,13 @@ 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";
|
|
13
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
14
|
//#region lib/types/cli-version.js
|
|
12
15
|
/**
|
|
13
16
|
* The family's CLI version probe: one bounded `<cli> --version` spawn whose
|
|
@@ -503,6 +506,11 @@ let LocalAgentGateway = (() => {
|
|
|
503
506
|
let _sessions_decorators;
|
|
504
507
|
let _memberOf_decorators;
|
|
505
508
|
let _harnessModel_decorators;
|
|
509
|
+
let _checkEndpoint_decorators;
|
|
510
|
+
let _endpointAuth_decorators;
|
|
511
|
+
let _configureEndpoint_decorators;
|
|
512
|
+
let _modelList_decorators;
|
|
513
|
+
let _configureModelList_decorators;
|
|
506
514
|
let _memberModel_decorators;
|
|
507
515
|
let _setMemberModel_decorators;
|
|
508
516
|
let _memberConfiguration_decorators;
|
|
@@ -529,6 +537,11 @@ let LocalAgentGateway = (() => {
|
|
|
529
537
|
_sessions_decorators = [Remote("sessions")];
|
|
530
538
|
_memberOf_decorators = [Remote("memberOf")];
|
|
531
539
|
_harnessModel_decorators = [Remote("harnessModel")];
|
|
540
|
+
_checkEndpoint_decorators = [Remote("checkEndpoint")];
|
|
541
|
+
_endpointAuth_decorators = [Remote("endpointAuth")];
|
|
542
|
+
_configureEndpoint_decorators = [Remote("configureEndpoint")];
|
|
543
|
+
_modelList_decorators = [Remote("modelList")];
|
|
544
|
+
_configureModelList_decorators = [Remote("configureModelList")];
|
|
532
545
|
_memberModel_decorators = [Remote("memberModel")];
|
|
533
546
|
_setMemberModel_decorators = [Remote("setMemberModel")];
|
|
534
547
|
_memberConfiguration_decorators = [Remote("memberConfiguration")];
|
|
@@ -672,6 +685,61 @@ let LocalAgentGateway = (() => {
|
|
|
672
685
|
},
|
|
673
686
|
metadata: _metadata
|
|
674
687
|
}, null, _instanceExtraInitializers);
|
|
688
|
+
__esDecorate(this, null, _checkEndpoint_decorators, {
|
|
689
|
+
kind: "method",
|
|
690
|
+
name: "checkEndpoint",
|
|
691
|
+
static: false,
|
|
692
|
+
private: false,
|
|
693
|
+
access: {
|
|
694
|
+
has: (obj) => "checkEndpoint" in obj,
|
|
695
|
+
get: (obj) => obj.checkEndpoint
|
|
696
|
+
},
|
|
697
|
+
metadata: _metadata
|
|
698
|
+
}, null, _instanceExtraInitializers);
|
|
699
|
+
__esDecorate(this, null, _endpointAuth_decorators, {
|
|
700
|
+
kind: "method",
|
|
701
|
+
name: "endpointAuth",
|
|
702
|
+
static: false,
|
|
703
|
+
private: false,
|
|
704
|
+
access: {
|
|
705
|
+
has: (obj) => "endpointAuth" in obj,
|
|
706
|
+
get: (obj) => obj.endpointAuth
|
|
707
|
+
},
|
|
708
|
+
metadata: _metadata
|
|
709
|
+
}, null, _instanceExtraInitializers);
|
|
710
|
+
__esDecorate(this, null, _configureEndpoint_decorators, {
|
|
711
|
+
kind: "method",
|
|
712
|
+
name: "configureEndpoint",
|
|
713
|
+
static: false,
|
|
714
|
+
private: false,
|
|
715
|
+
access: {
|
|
716
|
+
has: (obj) => "configureEndpoint" in obj,
|
|
717
|
+
get: (obj) => obj.configureEndpoint
|
|
718
|
+
},
|
|
719
|
+
metadata: _metadata
|
|
720
|
+
}, null, _instanceExtraInitializers);
|
|
721
|
+
__esDecorate(this, null, _modelList_decorators, {
|
|
722
|
+
kind: "method",
|
|
723
|
+
name: "modelList",
|
|
724
|
+
static: false,
|
|
725
|
+
private: false,
|
|
726
|
+
access: {
|
|
727
|
+
has: (obj) => "modelList" in obj,
|
|
728
|
+
get: (obj) => obj.modelList
|
|
729
|
+
},
|
|
730
|
+
metadata: _metadata
|
|
731
|
+
}, null, _instanceExtraInitializers);
|
|
732
|
+
__esDecorate(this, null, _configureModelList_decorators, {
|
|
733
|
+
kind: "method",
|
|
734
|
+
name: "configureModelList",
|
|
735
|
+
static: false,
|
|
736
|
+
private: false,
|
|
737
|
+
access: {
|
|
738
|
+
has: (obj) => "configureModelList" in obj,
|
|
739
|
+
get: (obj) => obj.configureModelList
|
|
740
|
+
},
|
|
741
|
+
metadata: _metadata
|
|
742
|
+
}, null, _instanceExtraInitializers);
|
|
675
743
|
__esDecorate(this, null, _memberModel_decorators, {
|
|
676
744
|
kind: "method",
|
|
677
745
|
name: "memberModel",
|
|
@@ -981,6 +1049,75 @@ let LocalAgentGateway = (() => {
|
|
|
981
1049
|
});
|
|
982
1050
|
}
|
|
983
1051
|
/**
|
|
1052
|
+
* Verify one harness's saved endpoint from the host — the settings card's
|
|
1053
|
+
* verify button. The answer is the harness's human-readable verdict text.
|
|
1054
|
+
* Session-less by design: the commands channel needs a main agent session,
|
|
1055
|
+
* which a settings card may not have. Null when the harness is unknown or
|
|
1056
|
+
* declares no probe (the card then hides the button — declare-and-degrade).
|
|
1057
|
+
* @param name - the harness name.
|
|
1058
|
+
* @returns the verdict text, or null.
|
|
1059
|
+
*/
|
|
1060
|
+
async checkEndpoint(name) {
|
|
1061
|
+
const harness = this.ctx.localAgent.get(name);
|
|
1062
|
+
if (typeof harness?.checkEndpoint !== "function") return null;
|
|
1063
|
+
return harness.checkEndpoint(this.ctx.localAgent.homeDir(name));
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* The settings card's read of one harness's resolved endpoint state —
|
|
1067
|
+
* baseUrl verbatim, credentials as presence booleans (secrets never cross).
|
|
1068
|
+
* Session-less like the rest of the gateway. Null when the harness is
|
|
1069
|
+
* unknown or declares no endpoint face (the card then hides the form).
|
|
1070
|
+
* @param name - the harness name.
|
|
1071
|
+
* @returns the resolved per-field state, or null.
|
|
1072
|
+
*/
|
|
1073
|
+
async endpointAuth(name) {
|
|
1074
|
+
const harness = this.ctx.localAgent.get(name);
|
|
1075
|
+
if (harness?.endpointAuth === void 0) return null;
|
|
1076
|
+
return harness.endpointAuth.read(this.ctx.localAgent.homeDir(name));
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Apply one endpoint-credential edit into the harness's own credential
|
|
1080
|
+
* store (a string sets, null clears, absent leaves), answering the
|
|
1081
|
+
* post-write resolved state so the card can flip immediately. The write
|
|
1082
|
+
* deliberately bypasses the volatile row-config channel — a credential
|
|
1083
|
+
* never lands in the profile patch document. Null when the harness is
|
|
1084
|
+
* unknown or declares no endpoint face.
|
|
1085
|
+
* @param name - the harness name.
|
|
1086
|
+
* @param fields - the field edits.
|
|
1087
|
+
* @returns the post-write resolved state, or null.
|
|
1088
|
+
*/
|
|
1089
|
+
async configureEndpoint(name, fields) {
|
|
1090
|
+
const harness = this.ctx.localAgent.get(name);
|
|
1091
|
+
if (harness?.endpointAuth === void 0) return null;
|
|
1092
|
+
await harness.endpointAuth.write(this.ctx.localAgent.homeDir(name), fields);
|
|
1093
|
+
return harness.endpointAuth.read(this.ctx.localAgent.homeDir(name));
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* The settings card's read of one harness's curated model list — ordered
|
|
1097
|
+
* identifiers, first is the default. Null when the harness is unknown or
|
|
1098
|
+
* declares no list face (the card then hides the editor).
|
|
1099
|
+
* @param name - the harness name.
|
|
1100
|
+
* @returns the ordered list, or null.
|
|
1101
|
+
*/
|
|
1102
|
+
async modelList(name) {
|
|
1103
|
+
const harness = this.ctx.localAgent.get(name);
|
|
1104
|
+
if (harness?.modelList === void 0) return null;
|
|
1105
|
+
return harness.modelList.read(this.ctx.localAgent.homeDir(name));
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Replace one harness's curated model list wholesale (reorders, removals
|
|
1109
|
+
* and additions all ride the same write), answering the list as persisted.
|
|
1110
|
+
* Null when the harness is unknown or declares no list face.
|
|
1111
|
+
* @param name - the harness name.
|
|
1112
|
+
* @param models - the new ordered list.
|
|
1113
|
+
* @returns the persisted list, or null.
|
|
1114
|
+
*/
|
|
1115
|
+
async configureModelList(name, models) {
|
|
1116
|
+
const harness = this.ctx.localAgent.get(name);
|
|
1117
|
+
if (harness?.modelList === void 0) return null;
|
|
1118
|
+
return harness.modelList.write(this.ctx.localAgent.homeDir(name), models);
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
984
1121
|
* The model surface of one member — the composer's picker read. The
|
|
985
1122
|
* delegation record's requested model rides in from the core so the broker
|
|
986
1123
|
* can rank it between the override and the settings layer; the record's
|
|
@@ -1125,6 +1262,65 @@ let LocalAgentGateway = (() => {
|
|
|
1125
1262
|
};
|
|
1126
1263
|
})();
|
|
1127
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
|
|
1128
1324
|
//#region lib/types/member-channel.js
|
|
1129
1325
|
/**
|
|
1130
1326
|
* Member channel, host side: the loopback listener the bridge MCP servers call
|
|
@@ -1153,6 +1349,8 @@ let LocalAgentGateway = (() => {
|
|
|
1153
1349
|
* `busy`; else the facade `resume` with a provenance-tagged prompt.
|
|
1154
1350
|
* @module @khorsheed/dsh-local-agent/member-channel
|
|
1155
1351
|
*/
|
|
1352
|
+
/** How long one bridged mode-tool call may run before it is cancelled. */
|
|
1353
|
+
const MODE_TOOL_TIMEOUT_MS = 18e5;
|
|
1156
1354
|
/**
|
|
1157
1355
|
* The member-channel host: owns the loopback unix-socket listener and the
|
|
1158
1356
|
* delivery chain. Mounted by the plugin's `apply` and disposed with its fiber.
|
|
@@ -1161,11 +1359,13 @@ var MemberChannel = class {
|
|
|
1161
1359
|
ctx;
|
|
1162
1360
|
registry;
|
|
1163
1361
|
socketPath;
|
|
1362
|
+
modeTools;
|
|
1164
1363
|
server;
|
|
1165
|
-
constructor(ctx, registry, socketPath) {
|
|
1364
|
+
constructor(ctx, registry, socketPath, modeTools) {
|
|
1166
1365
|
this.ctx = ctx;
|
|
1167
1366
|
this.registry = registry;
|
|
1168
1367
|
this.socketPath = socketPath;
|
|
1368
|
+
this.modeTools = modeTools;
|
|
1169
1369
|
}
|
|
1170
1370
|
/**
|
|
1171
1371
|
* Start listening. A listener failure (e.g. a socket path beyond the
|
|
@@ -1225,7 +1425,8 @@ var MemberChannel = class {
|
|
|
1225
1425
|
async answer(socket, line) {
|
|
1226
1426
|
let outcome;
|
|
1227
1427
|
try {
|
|
1228
|
-
|
|
1428
|
+
const request = JSON.parse(line);
|
|
1429
|
+
outcome = request !== null && typeof request === "object" && "modeTools" in request ? await this.handleModeTools(request) : await this.handle(request);
|
|
1229
1430
|
} catch (error) {
|
|
1230
1431
|
outcome = {
|
|
1231
1432
|
ok: false,
|
|
@@ -1323,6 +1524,75 @@ var MemberChannel = class {
|
|
|
1323
1524
|
receipt: "sent"
|
|
1324
1525
|
};
|
|
1325
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
|
+
}
|
|
1326
1596
|
/** Probe the optional room gate without importing any room package. */
|
|
1327
1597
|
probeRoom() {
|
|
1328
1598
|
const room = this.ctx.get.call(this.ctx, "room");
|
|
@@ -1344,6 +1614,10 @@ var MemberChannel = class {
|
|
|
1344
1614
|
to: request.to,
|
|
1345
1615
|
content: request.text,
|
|
1346
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 } : {},
|
|
1347
1621
|
provenance: {
|
|
1348
1622
|
childSessionId: from.childSessionId,
|
|
1349
1623
|
provider: from.provider,
|
|
@@ -1372,6 +1646,251 @@ var MemberChannel = class {
|
|
|
1372
1646
|
}
|
|
1373
1647
|
};
|
|
1374
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
|
|
1375
1894
|
//#region lib/types/member-inbox.js
|
|
1376
1895
|
/** Durable human input; the provider FIFO remains the sole execution order. */
|
|
1377
1896
|
var MemberInbox = class {
|
|
@@ -2059,9 +2578,20 @@ var MemberControls = class {
|
|
|
2059
2578
|
adapter;
|
|
2060
2579
|
onError;
|
|
2061
2580
|
admitted = /* @__PURE__ */ new WeakMap();
|
|
2581
|
+
/**
|
|
2582
|
+
* Latest admitted configuration per child session id. Host 0.2.1 note: the
|
|
2583
|
+
* activation manager wraps the provider's run, so a facade/tool caller never
|
|
2584
|
+
* holds the exact object {@link admitted} keys — its run adapter names the
|
|
2585
|
+
* child session id instead. Rounds of one child serialize through this
|
|
2586
|
+
* class's queue, so the latest entry at read time is the reader's own round.
|
|
2587
|
+
*/
|
|
2588
|
+
admittedByChild = /* @__PURE__ */ new Map();
|
|
2062
2589
|
configurationOf(run) {
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2590
|
+
const exact = this.admitted.get(run);
|
|
2591
|
+
if (exact !== void 0) return structuredClone(exact);
|
|
2592
|
+
const id = run.id;
|
|
2593
|
+
const byChild = typeof id === "string" ? this.admittedByChild.get(id) : void 0;
|
|
2594
|
+
return byChild === void 0 ? void 0 : structuredClone(byChild);
|
|
2065
2595
|
}
|
|
2066
2596
|
members = /* @__PURE__ */ new Map();
|
|
2067
2597
|
constructor(storage, adapter, onError) {
|
|
@@ -2176,6 +2706,7 @@ var MemberControls = class {
|
|
|
2176
2706
|
nativeStarted = true;
|
|
2177
2707
|
const run = await start(lease.configuration);
|
|
2178
2708
|
this.admitted.set(run, lease.configuration);
|
|
2709
|
+
this.admittedByChild.set(binding.childSessionId, lease.configuration);
|
|
2179
2710
|
const release = () => {
|
|
2180
2711
|
try {
|
|
2181
2712
|
lease.release();
|
|
@@ -2394,6 +2925,99 @@ function delegationEnv(extra) {
|
|
|
2394
2925
|
};
|
|
2395
2926
|
}
|
|
2396
2927
|
//#endregion
|
|
2928
|
+
//#region lib/types/model-list.js
|
|
2929
|
+
/**
|
|
2930
|
+
* A harness's curated model list: `<scopedHome>/models.json` holds an
|
|
2931
|
+
* ORDERED array of model identifiers the user picked from the endpoint's
|
|
2932
|
+
* served catalog. The list's FIRST entry is the harness's default model —
|
|
2933
|
+
* it answers wherever the plugin-config `model` key answered before (round
|
|
2934
|
+
* resolution, the broker's settings layer, the eval snapshot's model
|
|
2935
|
+
* field), ahead of that key; an absent or empty list leaves the
|
|
2936
|
+
* pre-existing chain untouched (settings key → the scoped config file →
|
|
2937
|
+
* the CLI's own default). One store shape serves every harness in the
|
|
2938
|
+
* family (claude-code, codex, kimi): the file is keyed by the scoped home
|
|
2939
|
+
* alone, so a scope carries its own list exactly like its own endpoint
|
|
2940
|
+
* credential.
|
|
2941
|
+
*
|
|
2942
|
+
* Why a file and not the settings channel: the credential form's
|
|
2943
|
+
* endpoint-auth.json proved the pattern — the harness owns the store, the
|
|
2944
|
+
* volatile row-config channel rejects writes a deployment cannot explain,
|
|
2945
|
+
* and the file keeps every scope free to carry its own list. The settings
|
|
2946
|
+
* card edits the default scope's file through the gateway's
|
|
2947
|
+
* `modelList`/`configureModelList` Remotes; nothing here is secret (0600 is
|
|
2948
|
+
* for uniformity with the credential file, not necessity).
|
|
2949
|
+
* @module @khorsheed/dsh-local-agent/model-list
|
|
2950
|
+
*/
|
|
2951
|
+
/** The scoped-home model list file. */
|
|
2952
|
+
const MODEL_LIST_FILE = "models.json";
|
|
2953
|
+
/** Normalize one list: trimmed, blank-dropped, deduplicated keeping first occurrence. */
|
|
2954
|
+
function normalizeModelList(models) {
|
|
2955
|
+
const out = [];
|
|
2956
|
+
for (const candidate of models) {
|
|
2957
|
+
if (typeof candidate !== "string") continue;
|
|
2958
|
+
const trimmed = candidate.trim();
|
|
2959
|
+
if (trimmed !== "" && !out.includes(trimmed)) out.push(trimmed);
|
|
2960
|
+
}
|
|
2961
|
+
return out;
|
|
2962
|
+
}
|
|
2963
|
+
/** Fold a parsed file payload into a list; any other shape reads as empty. */
|
|
2964
|
+
function fold(parsed) {
|
|
2965
|
+
if (Array.isArray(parsed)) return normalizeModelList(parsed);
|
|
2966
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.models)) return normalizeModelList(parsed.models);
|
|
2967
|
+
return [];
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Read the scope's model list. Absent, unreadable, or malformed content all
|
|
2971
|
+
* read as an empty list — the fallback chain then decides, exactly as
|
|
2972
|
+
* before the file existed.
|
|
2973
|
+
* @param homeDir - the harness's scoped home.
|
|
2974
|
+
* @returns the ordered model identifiers.
|
|
2975
|
+
*/
|
|
2976
|
+
async function readModelList(homeDir) {
|
|
2977
|
+
try {
|
|
2978
|
+
return fold(JSON.parse(await readFile(join(homeDir, MODEL_LIST_FILE), "utf8")));
|
|
2979
|
+
} catch {
|
|
2980
|
+
return [];
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* The sync twin of {@link readModelList} for the per-round model resolver —
|
|
2985
|
+
* one small file read per resolution, no caching, so a card write always
|
|
2986
|
+
* reaches the very next round.
|
|
2987
|
+
* @param homeDir - the scope's scoped home.
|
|
2988
|
+
* @returns the ordered model identifiers.
|
|
2989
|
+
*/
|
|
2990
|
+
function readModelListSync(homeDir) {
|
|
2991
|
+
try {
|
|
2992
|
+
return fold(JSON.parse(readFileSync(join(homeDir, MODEL_LIST_FILE), "utf8")));
|
|
2993
|
+
} catch {
|
|
2994
|
+
return [];
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
/** The list's default model: its first entry, or undefined when empty. */
|
|
2998
|
+
function firstModelOf(models) {
|
|
2999
|
+
return models.length === 0 ? void 0 : models[0];
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Persist the scope's model list (normalized; an empty list DELETES the
|
|
3003
|
+
* file rather than shadowing the fallback chain with nothing). Answers the
|
|
3004
|
+
* normalized list as written, so the writer can flip its surface without a
|
|
3005
|
+
* re-read.
|
|
3006
|
+
* @param homeDir - the scope's scoped home.
|
|
3007
|
+
* @param models - the new ordered list.
|
|
3008
|
+
* @returns the normalized list.
|
|
3009
|
+
*/
|
|
3010
|
+
async function writeModelList(homeDir, models) {
|
|
3011
|
+
const normalized = normalizeModelList(models);
|
|
3012
|
+
if (normalized.length === 0) {
|
|
3013
|
+
await rm(join(homeDir, MODEL_LIST_FILE), { force: true });
|
|
3014
|
+
return [];
|
|
3015
|
+
}
|
|
3016
|
+
await mkdir(homeDir, { recursive: true });
|
|
3017
|
+
await writeFile(join(homeDir, MODEL_LIST_FILE), JSON.stringify({ models: normalized }, void 0, 2) + "\n", { mode: 384 });
|
|
3018
|
+
return normalized;
|
|
3019
|
+
}
|
|
3020
|
+
//#endregion
|
|
2397
3021
|
//#region lib/types/live-flush.js
|
|
2398
3022
|
/** Maximum batching delay for a live mirror; independent of persistence cadence. */
|
|
2399
3023
|
const LIVE_FLUSH_INTERVAL_MS = 50;
|
|
@@ -2553,6 +3177,8 @@ var LiveStreamPublisher = class {
|
|
|
2553
3177
|
persist;
|
|
2554
3178
|
onError;
|
|
2555
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();
|
|
2556
3182
|
constructor(streams, session, turn, persist, onError) {
|
|
2557
3183
|
this.streams = streams;
|
|
2558
3184
|
this.session = session;
|
|
@@ -2560,8 +3186,29 @@ var LiveStreamPublisher = class {
|
|
|
2560
3186
|
this.persist = persist;
|
|
2561
3187
|
this.onError = onError;
|
|
2562
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
|
+
}
|
|
2563
3209
|
update(step, kind, text, options = {}) {
|
|
2564
3210
|
const id = `${this.turn}:${step}${options.itemId === void 0 ? "" : `:${options.itemId}`}`;
|
|
3211
|
+
this.previews.delete(id);
|
|
2565
3212
|
let state = this.states.get(id);
|
|
2566
3213
|
if (state?.item.text === text && state.item.kind === kind) return;
|
|
2567
3214
|
const receivedAt = options.receivedAt ?? Date.now();
|
|
@@ -2620,6 +3267,11 @@ var LiveStreamPublisher = class {
|
|
|
2620
3267
|
}
|
|
2621
3268
|
/** Call only after the corresponding native final message has been appended. */
|
|
2622
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
|
+
}
|
|
2623
3275
|
for (const [id, state] of this.states) {
|
|
2624
3276
|
if (state.item.step !== step) continue;
|
|
2625
3277
|
if (state.timer !== void 0) clearTimeout(state.timer);
|
|
@@ -2639,6 +3291,8 @@ var LiveStreamPublisher = class {
|
|
|
2639
3291
|
}
|
|
2640
3292
|
/** A producer failure keeps a durable partial, without claiming completion. */
|
|
2641
3293
|
dispose() {
|
|
3294
|
+
for (const id of this.previews.keys()) this.streams.finish(String(this.session.id), id);
|
|
3295
|
+
this.previews.clear();
|
|
2642
3296
|
for (const [id, state] of this.states) {
|
|
2643
3297
|
this.checkpoint(id);
|
|
2644
3298
|
if (state.timer !== void 0) clearTimeout(state.timer);
|
|
@@ -3094,12 +3748,49 @@ function delegationIntentKey(parentSessionId, provider) {
|
|
|
3094
3748
|
* `cancel()` and the caller's own abort channel both reach the run.
|
|
3095
3749
|
* `AbortSignal.any` is the harness's own fusion primitive (agent-loop's resume
|
|
3096
3750
|
* uses it); either source aborts the run.
|
|
3751
|
+
*
|
|
3752
|
+
* Host 0.2.1 note: the fused signal reaches `startActivation`'s `spec.signal`,
|
|
3753
|
+
* which owns cancellation only up to publication — after the activation is
|
|
3754
|
+
* published the manager unlinks it and `dispose()` is the sole cancel channel.
|
|
3755
|
+
* {@link activationRun} therefore re-wires the caller signal to disposal.
|
|
3097
3756
|
*/
|
|
3098
3757
|
function fusedSignal(controller, caller) {
|
|
3099
3758
|
if (caller === void 0) return controller.signal;
|
|
3100
3759
|
return AbortSignal.any([controller.signal, caller]);
|
|
3101
3760
|
}
|
|
3102
3761
|
/**
|
|
3762
|
+
* Adapt a 0.2.1 managed activation to the family's published `SubagentRun`
|
|
3763
|
+
* shape. The adapter's `id` is the dsh child session id — the family's
|
|
3764
|
+
* tracking key, resume handle, and client-facing identity — NOT the
|
|
3765
|
+
* activation's `childId`, which for an external (CLI) provider is the
|
|
3766
|
+
* provider's per-execution id and never names the mirrored live Session.
|
|
3767
|
+
* `dispose()` is the activation's exact-epoch cancellation: it settles the
|
|
3768
|
+
* result with an `aborted`-class stop reason and runs teardown, so a caller
|
|
3769
|
+
* signal aborted mid-run is re-wired to it (the start signal the facade fused
|
|
3770
|
+
* is unlinked by the manager at publication). The listener detaches when the
|
|
3771
|
+
* result settles, so a long-lived caller signal never accumulates them.
|
|
3772
|
+
*/
|
|
3773
|
+
function activationRun(activation, childSessionId, caller, warn) {
|
|
3774
|
+
const dispose = () => activation.dispose();
|
|
3775
|
+
if (caller !== void 0) {
|
|
3776
|
+
const onAbort = () => {
|
|
3777
|
+
dispose().catch((error) => {
|
|
3778
|
+
warn(`localAgent: caller-signal disposal of ${childSessionId} failed: ${String(error)}`);
|
|
3779
|
+
});
|
|
3780
|
+
};
|
|
3781
|
+
caller.addEventListener("abort", onAbort, { once: true });
|
|
3782
|
+
const detach = () => {
|
|
3783
|
+
caller.removeEventListener("abort", onAbort);
|
|
3784
|
+
};
|
|
3785
|
+
activation.result.then(detach, detach);
|
|
3786
|
+
}
|
|
3787
|
+
return {
|
|
3788
|
+
id: SessionId(childSessionId),
|
|
3789
|
+
result: activation.result,
|
|
3790
|
+
dispose
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3103
3794
|
* The per-harness append-only delegation-mapping log inside the harness's
|
|
3104
3795
|
* scoped home (`<homeDir>/<harness>/delegations.jsonl`): one JSON record per
|
|
3105
3796
|
* line, the last line per childSessionId wins — the same replace semantics as
|
|
@@ -3259,6 +3950,11 @@ function parseScopeFlag(input) {
|
|
|
3259
3950
|
...scope === void 0 ? {} : { scope }
|
|
3260
3951
|
};
|
|
3261
3952
|
}
|
|
3953
|
+
/** A delegation record without its requested model. */
|
|
3954
|
+
function withoutModel(record) {
|
|
3955
|
+
const { model: _model, ...rest } = record;
|
|
3956
|
+
return rest;
|
|
3957
|
+
}
|
|
3262
3958
|
/** Map a login failure to a command error, naming the swallowed signal. */
|
|
3263
3959
|
function loginFailure(harness, exitCode, signal) {
|
|
3264
3960
|
return `${harness.name} login exited with ${signal ?? `code ${exitCode ?? "unknown"}`}; try /${harness.name} login again.`;
|
|
@@ -3428,9 +4124,9 @@ var LocalAgentRegistry = class {
|
|
|
3428
4124
|
/**
|
|
3429
4125
|
* Per-(parent session, provider) FIFO of delegation intents. The family
|
|
3430
4126
|
* resume tool stages exactly one intent per call before awaiting
|
|
3431
|
-
* `ctx.subagents.
|
|
3432
|
-
* start, so fresh and resume rounds stay paired even under
|
|
3433
|
-
* delegations from one parent.
|
|
4127
|
+
* `ctx.subagents.startActivation()`, and the owning provider consumes
|
|
4128
|
+
* exactly one per start, so fresh and resume rounds stay paired even under
|
|
4129
|
+
* parallel delegations from one parent.
|
|
3434
4130
|
*/
|
|
3435
4131
|
delegationIntents = /* @__PURE__ */ new Map();
|
|
3436
4132
|
/**
|
|
@@ -3483,10 +4179,12 @@ var LocalAgentRegistry = class {
|
|
|
3483
4179
|
* - **facade-tracked** ({@link trackRun}): runs started through
|
|
3484
4180
|
* {@link LocalAgentRegistry.start} / {@link LocalAgentRegistry.resume},
|
|
3485
4181
|
* carrying the fused AbortController whose signal reached
|
|
3486
|
-
* `ctx.subagents.
|
|
4182
|
+
* `ctx.subagents.startActivation`; cancel aborts the pre-publication
|
|
4183
|
+
* signal and disposes the published activation.
|
|
3487
4184
|
* - **tool-registered** ({@link trackDelegationRun}): runs the family tool
|
|
3488
|
-
* starts directly through `ctx.subagents.
|
|
3489
|
-
* cancel lever (its fused controller
|
|
4185
|
+
* starts directly through `ctx.subagents.startActivation`, carrying an
|
|
4186
|
+
* explicit cancel lever (its fused controller plus the activation's
|
|
4187
|
+
* dispose); cancel calls it.
|
|
3490
4188
|
*
|
|
3491
4189
|
* Every entry clears itself when the run's result settles (any stop
|
|
3492
4190
|
* reason), and facade entries also stop their heartbeat timer then.
|
|
@@ -3863,7 +4561,8 @@ var LocalAgentRegistry = class {
|
|
|
3863
4561
|
recordDelegation(record) {
|
|
3864
4562
|
const binding = this.memberControls.binding(record.childSessionId);
|
|
3865
4563
|
if (binding !== void 0) record = {
|
|
3866
|
-
...record,
|
|
4564
|
+
...withoutModel(record),
|
|
4565
|
+
...binding.model === void 0 ? {} : { model: binding.model },
|
|
3867
4566
|
...binding.effort === void 0 ? {} : { effort: binding.effort },
|
|
3868
4567
|
...binding.configurationLock === void 0 ? {} : { configurationLock: binding.configurationLock }
|
|
3869
4568
|
};
|
|
@@ -3899,9 +4598,10 @@ var LocalAgentRegistry = class {
|
|
|
3899
4598
|
/**
|
|
3900
4599
|
* Stage one delegation intent for a provider's next `start()`. The family
|
|
3901
4600
|
* resume tool stages exactly one intent per call before awaiting
|
|
3902
|
-
* `ctx.subagents.
|
|
3903
|
-
* {@link takeDelegationIntent}, so the per-(parent, provider)
|
|
3904
|
-
* fresh and resume rounds even under parallel delegations from
|
|
4601
|
+
* `ctx.subagents.startActivation()`; the provider consumes exactly one per
|
|
4602
|
+
* start via {@link takeDelegationIntent}, so the per-(parent, provider)
|
|
4603
|
+
* FIFO pairs fresh and resume rounds even under parallel delegations from
|
|
4604
|
+
* one parent.
|
|
3905
4605
|
* @param parentSessionId - the delegating parent session id.
|
|
3906
4606
|
* @param provider - the `ctx.subagents` provider name.
|
|
3907
4607
|
* @param intent - the fresh or resume intent for the next start.
|
|
@@ -3933,7 +4633,7 @@ var LocalAgentRegistry = class {
|
|
|
3933
4633
|
}
|
|
3934
4634
|
/**
|
|
3935
4635
|
* Remove one previously staged intent by reference, returning whether it was
|
|
3936
|
-
* still queued. The delegation facade calls this when `ctx.subagents.
|
|
4636
|
+
* still queued. The delegation facade calls this when `ctx.subagents.startActivation()`
|
|
3937
4637
|
* throws: an intent the provider never consumed must not linger in the FIFO,
|
|
3938
4638
|
* where the NEXT same-(parent, provider) start would misconsume it as its own
|
|
3939
4639
|
* (orphan-intent crosstalk). When the provider already consumed the intent
|
|
@@ -3956,9 +4656,7 @@ var LocalAgentRegistry = class {
|
|
|
3956
4656
|
return true;
|
|
3957
4657
|
}
|
|
3958
4658
|
/**
|
|
3959
|
-
* Every recorded delegation, in recording order.
|
|
3960
|
-
* cross-checks each record's provider against the mounted subagent
|
|
3961
|
-
* providers so a provider rename cannot leave a stale resume mapping.
|
|
4659
|
+
* Every recorded delegation, in recording order.
|
|
3962
4660
|
* @returns the recorded delegations.
|
|
3963
4661
|
*/
|
|
3964
4662
|
listDelegations() {
|
|
@@ -4276,7 +4974,9 @@ var LocalAgentRegistry = class {
|
|
|
4276
4974
|
* {@link unstageDelegationIntent} (a no-op once the provider consumed it).
|
|
4277
4975
|
*
|
|
4278
4976
|
* The returned run is tracked under its `run.id` (== the new dsh child
|
|
4279
|
-
* session id
|
|
4977
|
+
* session id, minted here and carried to the provider through the staged
|
|
4978
|
+
* intent — host 0.2.1's `startActivation` never surfaces it on the
|
|
4979
|
+
* activation) until `run.result` settles; {@link cancel} aborts it by that
|
|
4280
4980
|
* id. Note on trust: cordis plugins share one process, so the ownership
|
|
4281
4981
|
* checks here and in {@link resume} guard against caller MISTAKES, not
|
|
4282
4982
|
* against a malicious plugin — their real protection target is untrusted
|
|
@@ -4314,10 +5014,12 @@ var LocalAgentRegistry = class {
|
|
|
4314
5014
|
const subagents = this.requireSubagents();
|
|
4315
5015
|
this.requireProvider(subagents, provider);
|
|
4316
5016
|
const parent = this.requireLiveParent(parentSessionId);
|
|
5017
|
+
const childSessionId = prepared?.binding.childSessionId ?? randomUUID();
|
|
4317
5018
|
const intent = {
|
|
4318
5019
|
kind: "fresh",
|
|
4319
5020
|
...options?.onAdmitted === void 0 ? {} : { onAdmitted: options.onAdmitted },
|
|
4320
5021
|
...options?.preparedMemberId === void 0 ? {} : { preparedMemberId: options.preparedMemberId },
|
|
5022
|
+
...prepared === void 0 ? { childSessionId } : {},
|
|
4321
5023
|
...options?.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
4322
5024
|
...options?.exec === void 0 ? {} : { exec: options.exec },
|
|
4323
5025
|
...options?.scope === void 0 ? {} : { scope: options.scope },
|
|
@@ -4329,17 +5031,21 @@ var LocalAgentRegistry = class {
|
|
|
4329
5031
|
const controller = new AbortController();
|
|
4330
5032
|
let run;
|
|
4331
5033
|
try {
|
|
4332
|
-
run = await subagents.
|
|
4333
|
-
|
|
4334
|
-
|
|
5034
|
+
run = activationRun(await subagents.startActivation({
|
|
5035
|
+
provider,
|
|
5036
|
+
label: options?.label ?? "",
|
|
5037
|
+
request: {
|
|
5038
|
+
prompt,
|
|
5039
|
+
parent
|
|
5040
|
+
},
|
|
4335
5041
|
signal: fusedSignal(controller, options?.signal),
|
|
4336
|
-
|
|
4337
|
-
});
|
|
5042
|
+
delivery: "caller"
|
|
5043
|
+
}), childSessionId, options?.signal, (message) => this.ctx.logger.warn(message));
|
|
4338
5044
|
} catch (error) {
|
|
4339
5045
|
this.unstageDelegationIntent(parentSessionId, provider, intent);
|
|
4340
5046
|
throw error;
|
|
4341
5047
|
}
|
|
4342
|
-
this.trackRun(
|
|
5048
|
+
this.trackRun(childSessionId, controller, run, options?.onProgress);
|
|
4343
5049
|
if (prepared !== void 0) try {
|
|
4344
5050
|
this.preparedMembers.write({
|
|
4345
5051
|
...prepared,
|
|
@@ -4363,12 +5069,12 @@ var LocalAgentRegistry = class {
|
|
|
4363
5069
|
* without staging; the provider still takes the authoritative lock inside
|
|
4364
5070
|
* its `start()`;
|
|
4365
5071
|
* 4. live parent agent resolution (`ctx.agents.get`) — a harness hard
|
|
4366
|
-
* constraint, since `ctx.subagents.
|
|
5072
|
+
* constraint, since `ctx.subagents.startActivation` requires a live parent;
|
|
4367
5073
|
* 5. child-session REATTACH when `ctx.sessions.get(childSessionId)` is
|
|
4368
5074
|
* undefined (recipe below; `opts.reattach === false` disables this step
|
|
4369
5075
|
* and fails loud on a non-live child instead);
|
|
4370
5076
|
* 6. stage `{ kind: 'resume', childSessionId, cliSessionId }` and, in the
|
|
4371
|
-
* same synchronous flow, `await ctx.subagents.
|
|
5077
|
+
* same synchronous flow, `await ctx.subagents.startActivation({…})`;
|
|
4372
5078
|
* 7. on start failure, roll the intent back via
|
|
4373
5079
|
* {@link unstageDelegationIntent} (no-op once consumed);
|
|
4374
5080
|
* 8. on success, track the run under `childSessionId` for {@link cancel}
|
|
@@ -4381,8 +5087,12 @@ var LocalAgentRegistry = class {
|
|
|
4381
5087
|
* const handle = await ctx.sessionPersistence.open(SessionId(childSessionId), 'write')
|
|
4382
5088
|
* try {
|
|
4383
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)
|
|
4384
5094
|
* const session = ctx.sessions.prepare(SessionId(childSessionId), {
|
|
4385
|
-
* seed: [...cold.events],
|
|
5095
|
+
* seed: [...cold.events, ...closers],
|
|
4386
5096
|
* meta: structuredClone(handle.header),
|
|
4387
5097
|
* inheritedEventCount: handle.inheritedEventCount,
|
|
4388
5098
|
* eventState: cold.eventState,
|
|
@@ -4390,7 +5100,7 @@ var LocalAgentRegistry = class {
|
|
|
4390
5100
|
* // The seeded constructor's session/end-seed marker lands before enter()
|
|
4391
5101
|
* // installs publication hooks — push the unstored suffix through the
|
|
4392
5102
|
* // handle directly, or the writer's contiguous cursor diverges.
|
|
4393
|
-
* const unstored = session.snapshotEvents().slice(cold.events.length)
|
|
5103
|
+
* const unstored = session.snapshotEvents().slice(cold.events.length + closers.length)
|
|
4394
5104
|
* if (unstored.length > 0) await handle.append(unstored)
|
|
4395
5105
|
* const detach = ctx.sessions.enter(session)
|
|
4396
5106
|
* try { ctx.sessions.announce(session) }
|
|
@@ -4448,12 +5158,16 @@ var LocalAgentRegistry = class {
|
|
|
4448
5158
|
const controller = new AbortController();
|
|
4449
5159
|
let run;
|
|
4450
5160
|
try {
|
|
4451
|
-
run = await subagents.
|
|
4452
|
-
|
|
4453
|
-
|
|
5161
|
+
run = activationRun(await subagents.startActivation({
|
|
5162
|
+
provider,
|
|
5163
|
+
label: options?.label ?? "",
|
|
5164
|
+
request: {
|
|
5165
|
+
prompt,
|
|
5166
|
+
parent
|
|
5167
|
+
},
|
|
4454
5168
|
signal: fusedSignal(controller, options?.signal),
|
|
4455
|
-
|
|
4456
|
-
});
|
|
5169
|
+
delivery: "caller"
|
|
5170
|
+
}), childSessionId, options?.signal, (message) => this.ctx.logger.warn(message));
|
|
4457
5171
|
} catch (error) {
|
|
4458
5172
|
this.unstageDelegationIntent(parentSessionId, provider, intent);
|
|
4459
5173
|
throw error;
|
|
@@ -4462,11 +5176,12 @@ var LocalAgentRegistry = class {
|
|
|
4462
5176
|
return run;
|
|
4463
5177
|
}
|
|
4464
5178
|
/**
|
|
4465
|
-
* Cancel an in-flight facade-started run by its dsh child session id:
|
|
4466
|
-
* the internal controller
|
|
4467
|
-
*
|
|
4468
|
-
*
|
|
4469
|
-
* reason. A caller holding the `SubagentRun` may also
|
|
5179
|
+
* Cancel an in-flight facade-started run by its dsh child session id:
|
|
5180
|
+
* aborts the internal controller (the pre-publication channel fused into
|
|
5181
|
+
* the activation's start signal) and disposes the activation — host 0.2.1's
|
|
5182
|
+
* canonical POST-publication cancellation, which settles the run with an
|
|
5183
|
+
* `aborted`-class stop reason. A caller holding the `SubagentRun` may also
|
|
5184
|
+
* `run.dispose()`.
|
|
4470
5185
|
* @param childSessionId - the dsh child session id of the run to cancel.
|
|
4471
5186
|
* @returns whether an in-flight run was found and signalled (false on a
|
|
4472
5187
|
* miss — never a silent success, never a throw).
|
|
@@ -4474,24 +5189,29 @@ var LocalAgentRegistry = class {
|
|
|
4474
5189
|
cancel(childSessionId) {
|
|
4475
5190
|
const entry = this.runs.get(childSessionId);
|
|
4476
5191
|
if (entry === void 0) return this.memberInbox.cancelStarting(childSessionId);
|
|
4477
|
-
if (entry.controller !== void 0)
|
|
4478
|
-
|
|
5192
|
+
if (entry.controller !== void 0) {
|
|
5193
|
+
entry.controller.abort();
|
|
5194
|
+
entry.run.dispose().catch((error) => {
|
|
5195
|
+
this.ctx.logger.warn(`localAgent: cancel disposal of ${childSessionId} failed: ${String(error)}`);
|
|
5196
|
+
});
|
|
5197
|
+
} else entry.cancel?.();
|
|
4479
5198
|
return true;
|
|
4480
5199
|
}
|
|
4481
5200
|
/**
|
|
4482
5201
|
* Register a NON-facade in-flight delegation run with the active-delegation
|
|
4483
5202
|
* registry, keyed by the dsh child session id, so the `/local-agent stop`
|
|
4484
5203
|
* command and {@link cancel} can reach it. This is the family TOOL's entry
|
|
4485
|
-
* point: the tool starts its runs through `ctx.subagents.
|
|
4486
|
-
* (not the facade), so without this registration its in-flight
|
|
4487
|
-
* be invisible to the stop command. The caller owns the cancel
|
|
4488
|
-
*
|
|
4489
|
-
*
|
|
4490
|
-
* result settles (any stop reason); an
|
|
4491
|
-
* child session wins (it already owns
|
|
4492
|
-
*
|
|
4493
|
-
* @param
|
|
4494
|
-
* @param
|
|
5204
|
+
* point: the tool starts its runs through `ctx.subagents.startActivation`
|
|
5205
|
+
* directly (not the facade), so without this registration its in-flight
|
|
5206
|
+
* runs would be invisible to the stop command. The caller owns the cancel
|
|
5207
|
+
* lever — aborting the fused controller (pre-publication) and disposing the
|
|
5208
|
+
* activation (post-publication), the same channels the facade drives. The
|
|
5209
|
+
* entry clears itself when the run's result settles (any stop reason); an
|
|
5210
|
+
* existing facade entry for the same child session wins (it already owns
|
|
5211
|
+
* cancellation and progress).
|
|
5212
|
+
* @param childSessionId - the dsh child session id.
|
|
5213
|
+
* @param run - the published subagent run (the tool's activation adapter).
|
|
5214
|
+
* @param cancel - the lever that cancels the run.
|
|
4495
5215
|
*/
|
|
4496
5216
|
trackDelegationRun(childSessionId, run, cancel) {
|
|
4497
5217
|
if (this.runs.has(childSessionId)) return;
|
|
@@ -4581,7 +5301,7 @@ var LocalAgentRegistry = class {
|
|
|
4581
5301
|
* from persistence when it is not — a host restart evicts every child from
|
|
4582
5302
|
* the live store. This is the public entry of the facade resume's reattach
|
|
4583
5303
|
* recipe for callers that start resume rounds WITHOUT the facade (the
|
|
4584
|
-
* family subagent tool starts runs directly through `ctx.subagents.
|
|
5304
|
+
* family subagent tool starts runs directly through `ctx.subagents.startActivation`;
|
|
4585
5305
|
* without this call the provider refuses the resume as "not live").
|
|
4586
5306
|
* Idempotent; a child that was never persisted fails through the
|
|
4587
5307
|
* persistence layer's own not-found error.
|
|
@@ -4608,13 +5328,16 @@ var LocalAgentRegistry = class {
|
|
|
4608
5328
|
const handle = await this.acquireChildWriteHandle(childSessionId);
|
|
4609
5329
|
try {
|
|
4610
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);
|
|
4611
5334
|
const session = sessions.prepare(SessionId(childSessionId), {
|
|
4612
|
-
seed: [...cold.events],
|
|
5335
|
+
seed: [...cold.events, ...closers],
|
|
4613
5336
|
meta: structuredClone(handle.header),
|
|
4614
5337
|
inheritedEventCount: handle.inheritedEventCount,
|
|
4615
5338
|
eventState: cold.eventState
|
|
4616
5339
|
});
|
|
4617
|
-
const unstored = session.snapshotEvents().slice(cold.events.length);
|
|
5340
|
+
const unstored = session.snapshotEvents().slice(cold.events.length + closers.length);
|
|
4618
5341
|
if (unstored.length > 0) await handle.append(unstored);
|
|
4619
5342
|
const detach = sessions.enter(session);
|
|
4620
5343
|
try {
|
|
@@ -4644,18 +5367,41 @@ var LocalAgentRegistry = class {
|
|
|
4644
5367
|
* @param session - the live child session to sync.
|
|
4645
5368
|
*/
|
|
4646
5369
|
async syncChildSession(session, strict = false) {
|
|
5370
|
+
const id = String(session.id);
|
|
4647
5371
|
try {
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
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
|
+
}
|
|
4653
5379
|
} catch (error) {
|
|
4654
5380
|
if (strict) throw error;
|
|
4655
|
-
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)}`);
|
|
4656
5382
|
}
|
|
4657
5383
|
}
|
|
4658
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
|
+
/**
|
|
4659
5405
|
* Get or open the child session's registry-owned WRITE handle (the
|
|
4660
5406
|
* {@link childWriteHandles} cache). When the caller carries the session
|
|
4661
5407
|
* header (a sync), a missing stored session is created from it; a create
|
|
@@ -5247,10 +5993,29 @@ async function apply(ctx, config) {
|
|
|
5247
5993
|
const registry = new LocalAgentRegistry(ctx, config.homesRoot, config.loginPromptTimeoutMs ?? 1e4);
|
|
5248
5994
|
ctx.provide(LOCAL_AGENT_SERVICE, registry);
|
|
5249
5995
|
new LocalAgentGateway(ctx);
|
|
5250
|
-
const
|
|
5996
|
+
const modeTools = new PresetToolHost(ctx, join(config.homesRoot, "mode-tools.jsonl"));
|
|
5997
|
+
const memberChannel = new MemberChannel(ctx, registry, registry.memberBridgeSocketPath(), modeTools);
|
|
5251
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 });
|
|
5252
6016
|
ctx.effect(() => () => {
|
|
5253
6017
|
memberChannel.dispose();
|
|
6018
|
+
modeTools.dispose();
|
|
5254
6019
|
});
|
|
5255
6020
|
ctx.commands.register({
|
|
5256
6021
|
name: "local-agent",
|
|
@@ -5278,4 +6043,4 @@ async function apply(ctx, config) {
|
|
|
5278
6043
|
});
|
|
5279
6044
|
}
|
|
5280
6045
|
//#endregion
|
|
5281
|
-
export { CLI_VERSION_FAILURE_TTL_MS, CLI_VERSION_PROBE_TIMEOUT_MS, Config, DELEGATIONS_FILENAME, FileMemberControlStorage, LIVE_CHECKPOINT_INTERVAL_MS, LIVE_FLUSH_INTERVAL_MS, LOCAL_AGENT_SERVICE, LiveFlush, LiveStreamPublisher, LocalAgentRegistry, LocalAgentStreams, MANUAL_LOGIN_LIMIT_MS, MANUAL_LOGIN_POLL_MS, MEMBER_BRIDGE_SOCKET_FILENAME, MemberConfigurationController, ModelDirectoryCache, REPLACE_LOGIN_GRACE_MS, RUN_PROGRESS_HEARTBEAT_MS, RUN_PROGRESS_SETTLE_GRACE_MS, SCOPE_NAME_RE, apply, assertModelExecOnly, assertResumeCwdUnchanged, assertResumeScopeUnchanged, assertScopeExecOnly, clearCliVersionCache, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, inject, modelDirectoryContextKey, name, parseCliVersion, parseScopeFlag, persistChildSession, probeCliVersion, resolveChildCwd, resolveRoundModel, scopedHomeName, settingsFace, stripAnsi, subagentDelegationLabel, vol };
|
|
6046
|
+
export { CLI_VERSION_FAILURE_TTL_MS, CLI_VERSION_PROBE_TIMEOUT_MS, Config, DELEGATIONS_FILENAME, FileMemberControlStorage, LIVE_CHECKPOINT_INTERVAL_MS, LIVE_FLUSH_INTERVAL_MS, LOCAL_AGENT_SERVICE, LiveFlush, LiveStreamPublisher, LocalAgentRegistry, LocalAgentStreams, MANUAL_LOGIN_LIMIT_MS, MANUAL_LOGIN_POLL_MS, MEMBER_BRIDGE_SOCKET_FILENAME, MODEL_LIST_FILE, MemberConfigurationController, ModelDirectoryCache, REPLACE_LOGIN_GRACE_MS, RUN_PROGRESS_HEARTBEAT_MS, RUN_PROGRESS_SETTLE_GRACE_MS, SCOPE_NAME_RE, apply, assertModelExecOnly, assertResumeCwdUnchanged, assertResumeScopeUnchanged, assertScopeExecOnly, clearCliVersionCache, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, firstModelOf, inject, modelDirectoryContextKey, name, normalizeModelList, parseCliVersion, parseScopeFlag, persistChildSession, probeCliVersion, readModelList, readModelListSync, resolveChildCwd, resolveRoundModel, scopedHomeName, settingsFace, stripAnsi, subagentDelegationLabel, vol, writeModelList };
|