@hydra-acp/cli 0.1.25 → 0.1.26
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/README.md +9 -9
- package/dist/cli.js +1100 -92
- package/dist/index.d.ts +87 -0
- package/dist/index.js +721 -66
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -380,8 +380,10 @@ async function ensureBinary(args) {
|
|
|
380
380
|
}
|
|
381
381
|
await downloadAndExtract({
|
|
382
382
|
agentId: args.agentId,
|
|
383
|
+
version: args.version,
|
|
383
384
|
archiveUrl: args.target.archive,
|
|
384
|
-
installDir
|
|
385
|
+
installDir,
|
|
386
|
+
onProgress: args.onProgress
|
|
385
387
|
});
|
|
386
388
|
if (!await fileExists(cmdPath)) {
|
|
387
389
|
throw new Error(
|
|
@@ -401,9 +403,16 @@ async function downloadAndExtract(args) {
|
|
|
401
403
|
const archivePath = await downloadTo({
|
|
402
404
|
url: args.archiveUrl,
|
|
403
405
|
dir: tempDir,
|
|
404
|
-
agentId: args.agentId
|
|
406
|
+
agentId: args.agentId,
|
|
407
|
+
version: args.version,
|
|
408
|
+
onProgress: args.onProgress
|
|
405
409
|
});
|
|
406
410
|
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
411
|
+
safeEmit(args.onProgress, {
|
|
412
|
+
phase: "extract",
|
|
413
|
+
agentId: args.agentId,
|
|
414
|
+
version: args.version
|
|
415
|
+
});
|
|
407
416
|
await extract(archivePath, tempDir);
|
|
408
417
|
await fsp.unlink(archivePath).catch(() => void 0);
|
|
409
418
|
try {
|
|
@@ -414,16 +423,35 @@ async function downloadAndExtract(args) {
|
|
|
414
423
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
415
424
|
() => void 0
|
|
416
425
|
);
|
|
426
|
+
safeEmit(args.onProgress, {
|
|
427
|
+
phase: "installed",
|
|
428
|
+
agentId: args.agentId,
|
|
429
|
+
version: args.version
|
|
430
|
+
});
|
|
417
431
|
return;
|
|
418
432
|
}
|
|
419
433
|
throw err;
|
|
420
434
|
}
|
|
421
435
|
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
436
|
+
safeEmit(args.onProgress, {
|
|
437
|
+
phase: "installed",
|
|
438
|
+
agentId: args.agentId,
|
|
439
|
+
version: args.version
|
|
440
|
+
});
|
|
422
441
|
} catch (err) {
|
|
423
442
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
424
443
|
throw err;
|
|
425
444
|
}
|
|
426
445
|
}
|
|
446
|
+
function safeEmit(cb, event) {
|
|
447
|
+
if (!cb) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
cb(event);
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
427
455
|
async function downloadTo(args) {
|
|
428
456
|
const filename = inferArchiveName(args.url);
|
|
429
457
|
const dest = path2.join(args.dir, filename);
|
|
@@ -436,17 +464,34 @@ async function downloadTo(args) {
|
|
|
436
464
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
437
465
|
const out = fs3.createWriteStream(dest);
|
|
438
466
|
const nodeStream = Readable.fromWeb(response.body);
|
|
467
|
+
safeEmit(args.onProgress, {
|
|
468
|
+
phase: "download_start",
|
|
469
|
+
agentId: args.agentId,
|
|
470
|
+
version: args.version,
|
|
471
|
+
totalBytes: total
|
|
472
|
+
});
|
|
439
473
|
let received = 0;
|
|
440
|
-
let
|
|
441
|
-
|
|
474
|
+
let lastLogEmit = Date.now();
|
|
475
|
+
let lastCbEmit = 0;
|
|
476
|
+
const LOG_INTERVAL_MS = 2e3;
|
|
477
|
+
const CB_INTERVAL_MS = 150;
|
|
442
478
|
nodeStream.on("data", (chunk) => {
|
|
443
479
|
received += chunk.length;
|
|
444
480
|
const now = Date.now();
|
|
445
|
-
if (now -
|
|
446
|
-
|
|
481
|
+
if (now - lastCbEmit >= CB_INTERVAL_MS) {
|
|
482
|
+
lastCbEmit = now;
|
|
483
|
+
safeEmit(args.onProgress, {
|
|
484
|
+
phase: "download_progress",
|
|
485
|
+
agentId: args.agentId,
|
|
486
|
+
version: args.version,
|
|
487
|
+
receivedBytes: received,
|
|
488
|
+
totalBytes: total
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (now - lastLogEmit >= LOG_INTERVAL_MS) {
|
|
492
|
+
lastLogEmit = now;
|
|
493
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
447
494
|
}
|
|
448
|
-
lastEmit = now;
|
|
449
|
-
logSink(formatProgress(args.agentId, received, total));
|
|
450
495
|
});
|
|
451
496
|
await new Promise((resolve3, reject) => {
|
|
452
497
|
nodeStream.on("error", reject);
|
|
@@ -461,6 +506,13 @@ async function downloadTo(args) {
|
|
|
461
506
|
/* done */
|
|
462
507
|
true
|
|
463
508
|
));
|
|
509
|
+
safeEmit(args.onProgress, {
|
|
510
|
+
phase: "download_done",
|
|
511
|
+
agentId: args.agentId,
|
|
512
|
+
version: args.version,
|
|
513
|
+
receivedBytes: received,
|
|
514
|
+
totalBytes: total
|
|
515
|
+
});
|
|
464
516
|
return dest;
|
|
465
517
|
}
|
|
466
518
|
function formatProgress(agentId, received, total, done = false) {
|
|
@@ -559,9 +611,11 @@ async function ensureNpmPackage(args) {
|
|
|
559
611
|
}
|
|
560
612
|
await installInto({
|
|
561
613
|
agentId: args.agentId,
|
|
614
|
+
version: args.version,
|
|
562
615
|
packageSpec: args.packageSpec,
|
|
563
616
|
installDir,
|
|
564
|
-
registry: args.registry
|
|
617
|
+
registry: args.registry,
|
|
618
|
+
onProgress: args.onProgress
|
|
565
619
|
});
|
|
566
620
|
if (!await fileExists2(binPath)) {
|
|
567
621
|
throw new Error(
|
|
@@ -577,6 +631,12 @@ async function installInto(args) {
|
|
|
577
631
|
logSink2(
|
|
578
632
|
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
579
633
|
);
|
|
634
|
+
safeEmit2(args.onProgress, {
|
|
635
|
+
phase: "install_start",
|
|
636
|
+
agentId: args.agentId,
|
|
637
|
+
version: args.version,
|
|
638
|
+
packageSpec: args.packageSpec
|
|
639
|
+
});
|
|
580
640
|
await runNpmInstall({
|
|
581
641
|
packageSpec: args.packageSpec,
|
|
582
642
|
cwd: tempDir,
|
|
@@ -590,11 +650,21 @@ async function installInto(args) {
|
|
|
590
650
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
591
651
|
() => void 0
|
|
592
652
|
);
|
|
653
|
+
safeEmit2(args.onProgress, {
|
|
654
|
+
phase: "installed",
|
|
655
|
+
agentId: args.agentId,
|
|
656
|
+
version: args.version
|
|
657
|
+
});
|
|
593
658
|
return;
|
|
594
659
|
}
|
|
595
660
|
throw err;
|
|
596
661
|
}
|
|
597
662
|
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
663
|
+
safeEmit2(args.onProgress, {
|
|
664
|
+
phase: "installed",
|
|
665
|
+
agentId: args.agentId,
|
|
666
|
+
version: args.version
|
|
667
|
+
});
|
|
598
668
|
} catch (err) {
|
|
599
669
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
600
670
|
() => void 0
|
|
@@ -602,44 +672,87 @@ async function installInto(args) {
|
|
|
602
672
|
throw err;
|
|
603
673
|
}
|
|
604
674
|
}
|
|
675
|
+
function safeEmit2(cb, event) {
|
|
676
|
+
if (!cb) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
cb(event);
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
var ETXTBSY_RETRIES = 5;
|
|
685
|
+
var ETXTBSY_BACKOFF_MS = 25;
|
|
605
686
|
function runNpmInstall(args) {
|
|
606
|
-
return
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
child.on("exit", (code, signal) => {
|
|
629
|
-
if (code === 0) {
|
|
630
|
-
resolve3();
|
|
687
|
+
return runNpmInstallOnce(args, 0);
|
|
688
|
+
}
|
|
689
|
+
async function runNpmInstallOnce(args, attempt) {
|
|
690
|
+
try {
|
|
691
|
+
await new Promise((resolve3, reject) => {
|
|
692
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
693
|
+
let child;
|
|
694
|
+
try {
|
|
695
|
+
child = spawn2(
|
|
696
|
+
"npm",
|
|
697
|
+
[
|
|
698
|
+
"install",
|
|
699
|
+
"--no-audit",
|
|
700
|
+
"--no-fund",
|
|
701
|
+
"--silent",
|
|
702
|
+
...registryArgs,
|
|
703
|
+
args.packageSpec
|
|
704
|
+
],
|
|
705
|
+
{ cwd: args.cwd, stdio: ["ignore", "pipe", "pipe"] }
|
|
706
|
+
);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
reject(err);
|
|
631
709
|
return;
|
|
632
710
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
711
|
+
let stderrTail = "";
|
|
712
|
+
child.stdout?.on("data", (chunk) => {
|
|
713
|
+
void chunk;
|
|
714
|
+
});
|
|
715
|
+
child.stderr?.setEncoding("utf8");
|
|
716
|
+
child.stderr?.on("data", (chunk) => {
|
|
717
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
718
|
+
});
|
|
719
|
+
child.on("error", (err) => {
|
|
720
|
+
const e = err;
|
|
721
|
+
if (e.code === "ENOENT") {
|
|
722
|
+
reject(
|
|
723
|
+
new Error(
|
|
724
|
+
`npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)`
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
reject(err);
|
|
730
|
+
});
|
|
731
|
+
child.on("exit", (code, signal) => {
|
|
732
|
+
if (code === 0) {
|
|
733
|
+
resolve3();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
737
|
+
const tail = stderrTail.trim();
|
|
738
|
+
reject(
|
|
739
|
+
new Error(
|
|
740
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
638
741
|
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
639
|
-
|
|
640
|
-
|
|
742
|
+
)
|
|
743
|
+
);
|
|
744
|
+
});
|
|
641
745
|
});
|
|
642
|
-
})
|
|
746
|
+
} catch (err) {
|
|
747
|
+
const code = err.code;
|
|
748
|
+
if (code === "ETXTBSY" && attempt < ETXTBSY_RETRIES) {
|
|
749
|
+
await new Promise(
|
|
750
|
+
(r) => setTimeout(r, ETXTBSY_BACKOFF_MS * (attempt + 1))
|
|
751
|
+
);
|
|
752
|
+
return runNpmInstallOnce(args, attempt + 1);
|
|
753
|
+
}
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
643
756
|
}
|
|
644
757
|
async function fileExists2(p) {
|
|
645
758
|
try {
|
|
@@ -837,12 +950,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
837
950
|
};
|
|
838
951
|
}
|
|
839
952
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
953
|
+
const npmCb = options.onInstallProgress;
|
|
840
954
|
const binPath = await ensureNpmPackage({
|
|
841
955
|
agentId: agent.id,
|
|
842
956
|
version,
|
|
843
957
|
packageSpec: npx.package,
|
|
844
958
|
bin,
|
|
845
|
-
registry: options.npmRegistry
|
|
959
|
+
registry: options.npmRegistry,
|
|
960
|
+
onProgress: npmCb ? (e) => npmCb({ source: "npm", ...e }) : void 0
|
|
846
961
|
});
|
|
847
962
|
return {
|
|
848
963
|
command: binPath,
|
|
@@ -858,10 +973,12 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
858
973
|
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
859
974
|
);
|
|
860
975
|
}
|
|
976
|
+
const binCb = options.onInstallProgress;
|
|
861
977
|
const cmdPath = await ensureBinary({
|
|
862
978
|
agentId: agent.id,
|
|
863
979
|
version,
|
|
864
|
-
target
|
|
980
|
+
target,
|
|
981
|
+
onProgress: binCb ? (e) => binCb({ source: "binary", ...e }) : void 0
|
|
865
982
|
});
|
|
866
983
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
867
984
|
return {
|
|
@@ -1094,6 +1211,29 @@ function extractHydraMeta(meta) {
|
|
|
1094
1211
|
out.availableModes = modes;
|
|
1095
1212
|
}
|
|
1096
1213
|
}
|
|
1214
|
+
if (Array.isArray(obj.availableModels)) {
|
|
1215
|
+
const models = [];
|
|
1216
|
+
for (const raw of obj.availableModels) {
|
|
1217
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const m = raw;
|
|
1221
|
+
if (typeof m.modelId !== "string") {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const model = { modelId: m.modelId };
|
|
1225
|
+
if (typeof m.name === "string") {
|
|
1226
|
+
model.name = m.name;
|
|
1227
|
+
}
|
|
1228
|
+
if (typeof m.description === "string") {
|
|
1229
|
+
model.description = m.description;
|
|
1230
|
+
}
|
|
1231
|
+
models.push(model);
|
|
1232
|
+
}
|
|
1233
|
+
if (models.length > 0) {
|
|
1234
|
+
out.availableModels = models;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1097
1237
|
return out;
|
|
1098
1238
|
}
|
|
1099
1239
|
function mergeMeta(passthrough, ours) {
|
|
@@ -1217,6 +1357,23 @@ var PromptAmendedParams = z3.object({
|
|
|
1217
1357
|
originator: PromptOriginatorSchema,
|
|
1218
1358
|
amendedAt: z3.number()
|
|
1219
1359
|
});
|
|
1360
|
+
var AgentInstallProgressParams = z3.object({
|
|
1361
|
+
agentId: z3.string(),
|
|
1362
|
+
version: z3.string(),
|
|
1363
|
+
source: z3.enum(["binary", "npm"]),
|
|
1364
|
+
phase: z3.enum([
|
|
1365
|
+
"download_start",
|
|
1366
|
+
"download_progress",
|
|
1367
|
+
"download_done",
|
|
1368
|
+
"extract",
|
|
1369
|
+
"install_start",
|
|
1370
|
+
"installed"
|
|
1371
|
+
]),
|
|
1372
|
+
receivedBytes: z3.number().optional(),
|
|
1373
|
+
totalBytes: z3.number().optional(),
|
|
1374
|
+
packageSpec: z3.string().optional()
|
|
1375
|
+
});
|
|
1376
|
+
var AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agent_install_progress";
|
|
1220
1377
|
var ProxyInitializeParams = z3.object({
|
|
1221
1378
|
protocolVersion: z3.number().optional(),
|
|
1222
1379
|
proxyInfo: z3.object({
|
|
@@ -1787,11 +1944,19 @@ var Session = class {
|
|
|
1787
1944
|
// Last available_modes_update we observed from the agent. Same
|
|
1788
1945
|
// pattern as commands: cache, persist, broadcast on change.
|
|
1789
1946
|
agentAdvertisedModes = [];
|
|
1947
|
+
// Last availableModels payload we observed (from current_model_update,
|
|
1948
|
+
// a session/new / session/load response, or — for opencode — a
|
|
1949
|
+
// config_option_update where configOptions[i].id === "model").
|
|
1950
|
+
// Cached so a mid-session attach can synthesize a model picker
|
|
1951
|
+
// snapshot, and so session/set_model can validate the requested id
|
|
1952
|
+
// against what the agent claims to support.
|
|
1953
|
+
agentAdvertisedModels = [];
|
|
1790
1954
|
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
1791
1955
|
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
1792
1956
|
// surface the latest snapshot via the attach response _meta.
|
|
1793
1957
|
agentCommandsHandlers = [];
|
|
1794
1958
|
agentModesHandlers = [];
|
|
1959
|
+
agentModelsHandlers = [];
|
|
1795
1960
|
modelHandlers = [];
|
|
1796
1961
|
modeHandlers = [];
|
|
1797
1962
|
usageHandlers = [];
|
|
@@ -1827,6 +1992,9 @@ var Session = class {
|
|
|
1827
1992
|
if (init.agentModes && init.agentModes.length > 0) {
|
|
1828
1993
|
this.agentAdvertisedModes = [...init.agentModes];
|
|
1829
1994
|
}
|
|
1995
|
+
if (init.agentModels && init.agentModels.length > 0) {
|
|
1996
|
+
this.agentAdvertisedModels = [...init.agentModels];
|
|
1997
|
+
}
|
|
1830
1998
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
1831
1999
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
1832
2000
|
this.logger = init.logger;
|
|
@@ -1864,6 +2032,23 @@ var Session = class {
|
|
|
1864
2032
|
}
|
|
1865
2033
|
});
|
|
1866
2034
|
}
|
|
2035
|
+
// Re-broadcast our cached availableModels via current_model_update.
|
|
2036
|
+
// Spec shape: { currentModel, availableModels } — we only include the
|
|
2037
|
+
// currentModel field when we know it, so this broadcast can also fire
|
|
2038
|
+
// model-list updates standalone before any current model is set.
|
|
2039
|
+
broadcastAvailableModels() {
|
|
2040
|
+
const update = {
|
|
2041
|
+
sessionUpdate: "current_model_update",
|
|
2042
|
+
availableModels: [...this.agentAdvertisedModels]
|
|
2043
|
+
};
|
|
2044
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
2045
|
+
update.currentModel = this.currentModel;
|
|
2046
|
+
}
|
|
2047
|
+
this.recordAndBroadcast("session/update", {
|
|
2048
|
+
sessionId: this.upstreamSessionId,
|
|
2049
|
+
update
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
1867
2052
|
// Register session/update, session/request_permission, and onExit
|
|
1868
2053
|
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
1869
2054
|
// the new agent is plumbed identically. The exit handler's identity
|
|
@@ -1894,6 +2079,10 @@ var Session = class {
|
|
|
1894
2079
|
this.recordAndBroadcast("session/update", params);
|
|
1895
2080
|
return;
|
|
1896
2081
|
}
|
|
2082
|
+
if (this.maybeApplyAgentConfigOption(params)) {
|
|
2083
|
+
this.recordAndBroadcast("session/update", params);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
1897
2086
|
if (this.maybeApplyAgentUsage(params)) {
|
|
1898
2087
|
this.recordAndBroadcast("session/update", params);
|
|
1899
2088
|
return;
|
|
@@ -2042,16 +2231,19 @@ var Session = class {
|
|
|
2042
2231
|
recordedAt
|
|
2043
2232
|
});
|
|
2044
2233
|
}
|
|
2045
|
-
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
2234
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0 || this.agentAdvertisedModels.length > 0) {
|
|
2235
|
+
const update = {
|
|
2236
|
+
sessionUpdate: "current_model_update"
|
|
2237
|
+
};
|
|
2238
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
2239
|
+
update.currentModel = this.currentModel;
|
|
2240
|
+
}
|
|
2241
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
2242
|
+
update.availableModels = [...this.agentAdvertisedModels];
|
|
2243
|
+
}
|
|
2046
2244
|
out.push({
|
|
2047
2245
|
method: "session/update",
|
|
2048
|
-
params: {
|
|
2049
|
-
sessionId,
|
|
2050
|
-
update: {
|
|
2051
|
-
sessionUpdate: "current_model_update",
|
|
2052
|
-
currentModel: this.currentModel
|
|
2053
|
-
}
|
|
2054
|
-
},
|
|
2246
|
+
params: { sessionId, update },
|
|
2055
2247
|
recordedAt
|
|
2056
2248
|
});
|
|
2057
2249
|
}
|
|
@@ -2686,6 +2878,18 @@ var Session = class {
|
|
|
2686
2878
|
onTitleChange(handler) {
|
|
2687
2879
|
this.titleHandlers.push(handler);
|
|
2688
2880
|
}
|
|
2881
|
+
// External entry point for retitling a live session from outside the
|
|
2882
|
+
// ACP slash-command path (e.g. PATCH /v1/sessions/:id from the picker).
|
|
2883
|
+
// Goes through the same enqueuePrompt path as /hydra title so it
|
|
2884
|
+
// serializes after any in-flight turn and shares broadcast/persistence.
|
|
2885
|
+
retitle(title) {
|
|
2886
|
+
return this.runTitleCommand(title);
|
|
2887
|
+
}
|
|
2888
|
+
// External entry point for the LLM-regen title path (T in the picker,
|
|
2889
|
+
// equivalent to bare /hydra title with no arg).
|
|
2890
|
+
retitleFromAgent() {
|
|
2891
|
+
return this.runTitleCommand("");
|
|
2892
|
+
}
|
|
2689
2893
|
// Update the canonical title and broadcast a session_info_update to
|
|
2690
2894
|
// every attached client. Clients that already speak the spec's
|
|
2691
2895
|
// session_info_update need no hydra-specific wiring to pick this up.
|
|
@@ -2733,12 +2937,19 @@ var Session = class {
|
|
|
2733
2937
|
// Apply an agent-emitted current_model_update. Returns true if the
|
|
2734
2938
|
// notification was a model update (caller still needs to broadcast
|
|
2735
2939
|
// it). Returns false otherwise so the caller can try the next kind.
|
|
2940
|
+
// current_model_update can carry availableModels in the same payload
|
|
2941
|
+
// (per ACP spec); we cache that list too so session/set_model can
|
|
2942
|
+
// validate against it.
|
|
2736
2943
|
maybeApplyAgentModel(params) {
|
|
2737
2944
|
const obj = params ?? {};
|
|
2738
2945
|
const update = obj.update ?? {};
|
|
2739
2946
|
if (update.sessionUpdate !== "current_model_update") {
|
|
2740
2947
|
return false;
|
|
2741
2948
|
}
|
|
2949
|
+
const models = parseModelsList(update.availableModels);
|
|
2950
|
+
if (models.length > 0) {
|
|
2951
|
+
this.setAgentAdvertisedModels(models);
|
|
2952
|
+
}
|
|
2742
2953
|
const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
|
|
2743
2954
|
if (raw === void 0) {
|
|
2744
2955
|
return true;
|
|
@@ -2756,6 +2967,55 @@ var Session = class {
|
|
|
2756
2967
|
}
|
|
2757
2968
|
return true;
|
|
2758
2969
|
}
|
|
2970
|
+
// Apply an opencode-style config_option_update. opencode emits this
|
|
2971
|
+
// (not the spec-shaped current_model_update / available_models_update)
|
|
2972
|
+
// to carry both the current model and the list of available models.
|
|
2973
|
+
// The payload is `configOptions: [{ id: "model", currentValue, options:
|
|
2974
|
+
// [{ value, name }] }, ...]`. We harvest only the entry whose id is
|
|
2975
|
+
// "model" — other ids ("mode", "effort", etc.) are opencode-internal
|
|
2976
|
+
// and not consumed by hydra. Returns true when we recognized and
|
|
2977
|
+
// handled the notification so the wireAgent loop can stop trying
|
|
2978
|
+
// further extractors (the broadcast still fires; clients that grok
|
|
2979
|
+
// config_option_update render it directly).
|
|
2980
|
+
maybeApplyAgentConfigOption(params) {
|
|
2981
|
+
const obj = params ?? {};
|
|
2982
|
+
const update = obj.update ?? {};
|
|
2983
|
+
if (update.sessionUpdate !== "config_option_update") {
|
|
2984
|
+
return false;
|
|
2985
|
+
}
|
|
2986
|
+
const list = update.configOptions;
|
|
2987
|
+
if (!Array.isArray(list)) {
|
|
2988
|
+
return true;
|
|
2989
|
+
}
|
|
2990
|
+
for (const raw of list) {
|
|
2991
|
+
if (!raw || typeof raw !== "object") {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
const opt = raw;
|
|
2995
|
+
if (opt.id !== "model") {
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
const models = parseModelsList(opt.options);
|
|
2999
|
+
if (models.length > 0) {
|
|
3000
|
+
this.setAgentAdvertisedModels(models);
|
|
3001
|
+
}
|
|
3002
|
+
const cv = opt.currentValue;
|
|
3003
|
+
if (typeof cv === "string") {
|
|
3004
|
+
const trimmed = cv.trim();
|
|
3005
|
+
if (trimmed && trimmed !== this.currentModel) {
|
|
3006
|
+
this.currentModel = trimmed;
|
|
3007
|
+
for (const handler of this.modelHandlers) {
|
|
3008
|
+
try {
|
|
3009
|
+
handler(trimmed);
|
|
3010
|
+
} catch {
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
return true;
|
|
3018
|
+
}
|
|
2759
3019
|
maybeApplyAgentMode(params) {
|
|
2760
3020
|
const obj = params ?? {};
|
|
2761
3021
|
const update = obj.update ?? {};
|
|
@@ -2854,6 +3114,20 @@ var Session = class {
|
|
|
2854
3114
|
}
|
|
2855
3115
|
this.broadcastAvailableModes();
|
|
2856
3116
|
}
|
|
3117
|
+
setAgentAdvertisedModels(models) {
|
|
3118
|
+
if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
|
|
3119
|
+
this.broadcastAvailableModels();
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
this.agentAdvertisedModels = models;
|
|
3123
|
+
for (const handler of this.agentModelsHandlers) {
|
|
3124
|
+
try {
|
|
3125
|
+
handler(models);
|
|
3126
|
+
} catch {
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
this.broadcastAvailableModels();
|
|
3130
|
+
}
|
|
2857
3131
|
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
2858
3132
|
// persist the new value into meta.json so cold resurrect can restore
|
|
2859
3133
|
// them via the attach response _meta.
|
|
@@ -2863,6 +3137,9 @@ var Session = class {
|
|
|
2863
3137
|
onAgentModesChange(handler) {
|
|
2864
3138
|
this.agentModesHandlers.push(handler);
|
|
2865
3139
|
}
|
|
3140
|
+
onAgentModelsChange(handler) {
|
|
3141
|
+
this.agentModelsHandlers.push(handler);
|
|
3142
|
+
}
|
|
2866
3143
|
onModelChange(handler) {
|
|
2867
3144
|
this.modelHandlers.push(handler);
|
|
2868
3145
|
}
|
|
@@ -2888,6 +3165,15 @@ var Session = class {
|
|
|
2888
3165
|
availableModes() {
|
|
2889
3166
|
return [...this.agentAdvertisedModes];
|
|
2890
3167
|
}
|
|
3168
|
+
// The agent's advertised models list. Used by acp-ws.ts's dedicated
|
|
3169
|
+
// session/set_model handler to validate the requested modelId before
|
|
3170
|
+
// forwarding to the agent (catches cross-agent set_model storms from
|
|
3171
|
+
// clients that assume a different agent is on the other end). When
|
|
3172
|
+
// the agent never advertised any models, returns [] and the
|
|
3173
|
+
// set_model handler falls back to pass-through.
|
|
3174
|
+
availableModels() {
|
|
3175
|
+
return [...this.agentAdvertisedModels];
|
|
3176
|
+
}
|
|
2891
3177
|
// Pick up an agent-emitted session_info_update and store its title
|
|
2892
3178
|
// as our canonical record. The notification is also forwarded to
|
|
2893
3179
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -3035,6 +3321,12 @@ var Session = class {
|
|
|
3035
3321
|
this.agentMeta = fresh.agentMeta;
|
|
3036
3322
|
this.agentAdvertisedCommands = [];
|
|
3037
3323
|
this.broadcastMergedCommands();
|
|
3324
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
3325
|
+
this.setAgentAdvertisedModels([]);
|
|
3326
|
+
}
|
|
3327
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
3328
|
+
this.setAgentAdvertisedModes([]);
|
|
3329
|
+
}
|
|
3038
3330
|
await oldAgent.kill().catch(() => void 0);
|
|
3039
3331
|
if (transcript) {
|
|
3040
3332
|
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
@@ -3601,6 +3893,42 @@ function sameAdvertisedModes(a, b) {
|
|
|
3601
3893
|
}
|
|
3602
3894
|
return true;
|
|
3603
3895
|
}
|
|
3896
|
+
function sameAdvertisedModels(a, b) {
|
|
3897
|
+
if (a.length !== b.length) {
|
|
3898
|
+
return false;
|
|
3899
|
+
}
|
|
3900
|
+
for (let i = 0; i < a.length; i++) {
|
|
3901
|
+
if (a[i]?.modelId !== b[i]?.modelId || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
3902
|
+
return false;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
return true;
|
|
3906
|
+
}
|
|
3907
|
+
function parseModelsList(list) {
|
|
3908
|
+
if (!Array.isArray(list)) {
|
|
3909
|
+
return [];
|
|
3910
|
+
}
|
|
3911
|
+
const out = [];
|
|
3912
|
+
for (const raw of list) {
|
|
3913
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
const r = raw;
|
|
3917
|
+
const modelId = typeof r.modelId === "string" && r.modelId.trim() || typeof r.value === "string" && r.value.trim() || typeof r.id === "string" && r.id.trim() || void 0;
|
|
3918
|
+
if (!modelId) {
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
const model = { modelId };
|
|
3922
|
+
if (typeof r.name === "string" && r.name.length > 0) {
|
|
3923
|
+
model.name = r.name;
|
|
3924
|
+
}
|
|
3925
|
+
if (typeof r.description === "string" && r.description.length > 0) {
|
|
3926
|
+
model.description = r.description;
|
|
3927
|
+
}
|
|
3928
|
+
out.push(model);
|
|
3929
|
+
}
|
|
3930
|
+
return out;
|
|
3931
|
+
}
|
|
3604
3932
|
function extractAdvertisedModes(params) {
|
|
3605
3933
|
const obj = params ?? {};
|
|
3606
3934
|
const update = obj.update ?? {};
|
|
@@ -3805,6 +4133,11 @@ var PersistedAgentMode = z4.object({
|
|
|
3805
4133
|
name: z4.string().optional(),
|
|
3806
4134
|
description: z4.string().optional()
|
|
3807
4135
|
});
|
|
4136
|
+
var PersistedAgentModel = z4.object({
|
|
4137
|
+
modelId: z4.string(),
|
|
4138
|
+
name: z4.string().optional(),
|
|
4139
|
+
description: z4.string().optional()
|
|
4140
|
+
});
|
|
3808
4141
|
var PersistedUsage = z4.object({
|
|
3809
4142
|
used: z4.number().optional(),
|
|
3810
4143
|
size: z4.number().optional(),
|
|
@@ -3851,6 +4184,7 @@ var SessionRecord = z4.object({
|
|
|
3851
4184
|
currentUsage: PersistedUsage.optional(),
|
|
3852
4185
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
3853
4186
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
4187
|
+
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
3854
4188
|
createdAt: z4.string(),
|
|
3855
4189
|
updatedAt: z4.string()
|
|
3856
4190
|
});
|
|
@@ -3970,6 +4304,7 @@ function recordFromMemorySession(args) {
|
|
|
3970
4304
|
currentUsage: args.currentUsage,
|
|
3971
4305
|
agentCommands: args.agentCommands,
|
|
3972
4306
|
agentModes: args.agentModes,
|
|
4307
|
+
agentModels: args.agentModels,
|
|
3973
4308
|
createdAt: args.createdAt ?? now,
|
|
3974
4309
|
updatedAt: args.updatedAt ?? now
|
|
3975
4310
|
};
|
|
@@ -4204,7 +4539,8 @@ var SessionManager = class {
|
|
|
4204
4539
|
cwd: params.cwd,
|
|
4205
4540
|
agentArgs: params.agentArgs,
|
|
4206
4541
|
mcpServers: params.mcpServers,
|
|
4207
|
-
model: params.model
|
|
4542
|
+
model: params.model,
|
|
4543
|
+
onInstallProgress: params.onInstallProgress
|
|
4208
4544
|
});
|
|
4209
4545
|
const session = new Session({
|
|
4210
4546
|
cwd: params.cwd,
|
|
@@ -4221,7 +4557,8 @@ var SessionManager = class {
|
|
|
4221
4557
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
4222
4558
|
currentModel: fresh.initialModel,
|
|
4223
4559
|
currentMode: fresh.initialMode,
|
|
4224
|
-
agentModes: fresh.initialModes
|
|
4560
|
+
agentModes: fresh.initialModes,
|
|
4561
|
+
agentModels: fresh.initialModels
|
|
4225
4562
|
});
|
|
4226
4563
|
await this.attachManagerHooks(session);
|
|
4227
4564
|
return session;
|
|
@@ -4266,7 +4603,10 @@ var SessionManager = class {
|
|
|
4266
4603
|
if (params.upstreamSessionId === "") {
|
|
4267
4604
|
return this.doResurrectFromImport(params);
|
|
4268
4605
|
}
|
|
4269
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4606
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4607
|
+
npmRegistry: this.npmRegistry,
|
|
4608
|
+
onInstallProgress: params.onInstallProgress
|
|
4609
|
+
});
|
|
4270
4610
|
const agent = this.spawner({
|
|
4271
4611
|
agentId: params.agentId,
|
|
4272
4612
|
cwd: params.cwd,
|
|
@@ -4324,6 +4664,7 @@ var SessionManager = class {
|
|
|
4324
4664
|
currentUsage: params.currentUsage,
|
|
4325
4665
|
agentCommands: params.agentCommands,
|
|
4326
4666
|
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
4667
|
+
agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
|
|
4327
4668
|
// Only gate the first-prompt title heuristic when we actually have
|
|
4328
4669
|
// a title to preserve. A title-less session (lost to a write race
|
|
4329
4670
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -4347,7 +4688,8 @@ var SessionManager = class {
|
|
|
4347
4688
|
agentId: params.agentId,
|
|
4348
4689
|
cwd,
|
|
4349
4690
|
agentArgs: params.agentArgs,
|
|
4350
|
-
mcpServers: []
|
|
4691
|
+
mcpServers: [],
|
|
4692
|
+
onInstallProgress: params.onInstallProgress
|
|
4351
4693
|
});
|
|
4352
4694
|
const session = new Session({
|
|
4353
4695
|
sessionId: params.hydraSessionId,
|
|
@@ -4370,6 +4712,7 @@ var SessionManager = class {
|
|
|
4370
4712
|
currentUsage: params.currentUsage,
|
|
4371
4713
|
agentCommands: params.agentCommands,
|
|
4372
4714
|
agentModes: params.agentModes ?? fresh.initialModes,
|
|
4715
|
+
agentModels: params.agentModels ?? fresh.initialModels,
|
|
4373
4716
|
firstPromptSeeded: !!params.title,
|
|
4374
4717
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
4375
4718
|
});
|
|
@@ -4399,7 +4742,10 @@ var SessionManager = class {
|
|
|
4399
4742
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
4400
4743
|
throw err;
|
|
4401
4744
|
}
|
|
4402
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4745
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4746
|
+
npmRegistry: this.npmRegistry,
|
|
4747
|
+
onInstallProgress: params.onInstallProgress
|
|
4748
|
+
});
|
|
4403
4749
|
const agent = this.spawner({
|
|
4404
4750
|
agentId: params.agentId,
|
|
4405
4751
|
cwd: params.cwd,
|
|
@@ -4425,15 +4771,25 @@ var SessionManager = class {
|
|
|
4425
4771
|
);
|
|
4426
4772
|
}
|
|
4427
4773
|
let initialModel = extractInitialModel(newResult);
|
|
4774
|
+
const initialModels = extractInitialModels(newResult);
|
|
4428
4775
|
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
4429
4776
|
if (desired && desired !== initialModel) {
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4777
|
+
const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
|
|
4778
|
+
if (validates) {
|
|
4779
|
+
try {
|
|
4780
|
+
await agent.connection.request("session/set_model", {
|
|
4781
|
+
sessionId: sessionIdRaw,
|
|
4782
|
+
modelId: desired
|
|
4783
|
+
});
|
|
4784
|
+
initialModel = desired;
|
|
4785
|
+
} catch {
|
|
4786
|
+
}
|
|
4787
|
+
} else {
|
|
4788
|
+
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
4789
|
+
process.stderr.write(
|
|
4790
|
+
`hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
|
|
4791
|
+
`
|
|
4792
|
+
);
|
|
4437
4793
|
}
|
|
4438
4794
|
}
|
|
4439
4795
|
const initialModes = extractInitialModes(newResult);
|
|
@@ -4443,6 +4799,7 @@ var SessionManager = class {
|
|
|
4443
4799
|
upstreamSessionId: sessionIdRaw,
|
|
4444
4800
|
agentMeta: newResult._meta,
|
|
4445
4801
|
initialModel,
|
|
4802
|
+
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
4446
4803
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
4447
4804
|
initialMode
|
|
4448
4805
|
};
|
|
@@ -4505,6 +4862,15 @@ var SessionManager = class {
|
|
|
4505
4862
|
}))
|
|
4506
4863
|
}).catch(() => void 0);
|
|
4507
4864
|
});
|
|
4865
|
+
session.onAgentModelsChange((models) => {
|
|
4866
|
+
void this.persistSnapshot(session.sessionId, {
|
|
4867
|
+
agentModels: models.map((m) => ({
|
|
4868
|
+
modelId: m.modelId,
|
|
4869
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
4870
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
4871
|
+
}))
|
|
4872
|
+
}).catch(() => void 0);
|
|
4873
|
+
});
|
|
4508
4874
|
this.sessions.set(session.sessionId, session);
|
|
4509
4875
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
4510
4876
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -4548,6 +4914,7 @@ var SessionManager = class {
|
|
|
4548
4914
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
4549
4915
|
agentCommands: record.agentCommands,
|
|
4550
4916
|
agentModes: record.agentModes,
|
|
4917
|
+
agentModels: record.agentModels,
|
|
4551
4918
|
createdAt: record.createdAt
|
|
4552
4919
|
};
|
|
4553
4920
|
}
|
|
@@ -4791,6 +5158,26 @@ var SessionManager = class {
|
|
|
4791
5158
|
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
4792
5159
|
return record !== void 0;
|
|
4793
5160
|
}
|
|
5161
|
+
// Public retitle entry point that works on live AND cold sessions.
|
|
5162
|
+
// - Live: routes through Session.retitle so attached clients receive
|
|
5163
|
+
// a session_info_update broadcast (and persistTitle fires from the
|
|
5164
|
+
// onTitleChange handler, just like /hydra title).
|
|
5165
|
+
// - Cold: writes the new title straight into meta.json — there's
|
|
5166
|
+
// nothing in memory to broadcast to, but a later resurrect / list
|
|
5167
|
+
// will pick up the new title.
|
|
5168
|
+
// Returns false when no record exists at all (live or on disk).
|
|
5169
|
+
async setTitle(sessionId, title) {
|
|
5170
|
+
const live = this.get(sessionId);
|
|
5171
|
+
if (live) {
|
|
5172
|
+
await live.retitle(title);
|
|
5173
|
+
return true;
|
|
5174
|
+
}
|
|
5175
|
+
if (!await this.hasRecord(sessionId)) {
|
|
5176
|
+
return false;
|
|
5177
|
+
}
|
|
5178
|
+
await this.persistTitle(sessionId, title);
|
|
5179
|
+
return true;
|
|
5180
|
+
}
|
|
4794
5181
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
4795
5182
|
// was written at create time; updating it here keeps the session
|
|
4796
5183
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -4843,6 +5230,7 @@ var SessionManager = class {
|
|
|
4843
5230
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
4844
5231
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
4845
5232
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
5233
|
+
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
4846
5234
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4847
5235
|
});
|
|
4848
5236
|
});
|
|
@@ -4944,6 +5332,18 @@ function mergeForPersistence(session, existing) {
|
|
|
4944
5332
|
return out;
|
|
4945
5333
|
}) : void 0;
|
|
4946
5334
|
const agentModes = persistedModes ?? existing?.agentModes;
|
|
5335
|
+
const sessionModels = session.availableModels();
|
|
5336
|
+
const persistedModels = sessionModels.length > 0 ? sessionModels.map((m) => {
|
|
5337
|
+
const out = { modelId: m.modelId };
|
|
5338
|
+
if (m.name !== void 0) {
|
|
5339
|
+
out.name = m.name;
|
|
5340
|
+
}
|
|
5341
|
+
if (m.description !== void 0) {
|
|
5342
|
+
out.description = m.description;
|
|
5343
|
+
}
|
|
5344
|
+
return out;
|
|
5345
|
+
}) : void 0;
|
|
5346
|
+
const agentModels = persistedModels ?? existing?.agentModels;
|
|
4947
5347
|
return recordFromMemorySession({
|
|
4948
5348
|
sessionId: session.sessionId,
|
|
4949
5349
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -4960,6 +5360,7 @@ function mergeForPersistence(session, existing) {
|
|
|
4960
5360
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
4961
5361
|
agentCommands,
|
|
4962
5362
|
agentModes,
|
|
5363
|
+
agentModels,
|
|
4963
5364
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
4964
5365
|
});
|
|
4965
5366
|
}
|
|
@@ -5025,6 +5426,40 @@ function asString(value) {
|
|
|
5025
5426
|
function nonEmptyOrUndefined(arr) {
|
|
5026
5427
|
return arr.length > 0 ? arr : void 0;
|
|
5027
5428
|
}
|
|
5429
|
+
function extractInitialModels(result) {
|
|
5430
|
+
const direct = parseModelsList(result.availableModels);
|
|
5431
|
+
if (direct.length > 0) {
|
|
5432
|
+
return direct;
|
|
5433
|
+
}
|
|
5434
|
+
const models = result.models;
|
|
5435
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
5436
|
+
const fromModelsObj = parseModelsList(
|
|
5437
|
+
models.availableModels
|
|
5438
|
+
);
|
|
5439
|
+
if (fromModelsObj.length > 0) {
|
|
5440
|
+
return fromModelsObj;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
const meta = result._meta;
|
|
5444
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
5445
|
+
for (const [key, value] of Object.entries(
|
|
5446
|
+
meta
|
|
5447
|
+
)) {
|
|
5448
|
+
if (key === "hydra-acp") {
|
|
5449
|
+
continue;
|
|
5450
|
+
}
|
|
5451
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
5452
|
+
const fromMeta = parseModelsList(
|
|
5453
|
+
value.availableModels
|
|
5454
|
+
);
|
|
5455
|
+
if (fromMeta.length > 0) {
|
|
5456
|
+
return fromMeta;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
return [];
|
|
5462
|
+
}
|
|
5028
5463
|
function extractInitialModes(result) {
|
|
5029
5464
|
const direct = parseModesList(result.availableModes);
|
|
5030
5465
|
if (direct.length > 0) {
|
|
@@ -6247,8 +6682,55 @@ function mapToolCallUpdate(u) {
|
|
|
6247
6682
|
if (status !== void 0) {
|
|
6248
6683
|
event.status = status;
|
|
6249
6684
|
}
|
|
6685
|
+
if (status === "failed") {
|
|
6686
|
+
const errorText = extractToolFailureText(u);
|
|
6687
|
+
if (errorText !== null) {
|
|
6688
|
+
event.errorText = errorText;
|
|
6689
|
+
}
|
|
6690
|
+
if (isUpstreamInterrupted(u, errorText)) {
|
|
6691
|
+
event.upstreamInterrupted = true;
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6250
6694
|
return event;
|
|
6251
6695
|
}
|
|
6696
|
+
function extractToolFailureText(u) {
|
|
6697
|
+
const content = u.content;
|
|
6698
|
+
if (Array.isArray(content)) {
|
|
6699
|
+
for (const block of content) {
|
|
6700
|
+
if (!block || typeof block !== "object") {
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6703
|
+
const b = block;
|
|
6704
|
+
const text = extractContentText(b.content);
|
|
6705
|
+
if (text !== null && text.length > 0) {
|
|
6706
|
+
return text;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
const rawOutput = u.rawOutput;
|
|
6711
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
6712
|
+
const err = rawOutput.error;
|
|
6713
|
+
if (typeof err === "string" && err.length > 0) {
|
|
6714
|
+
return sanitizeWireText(err);
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
return null;
|
|
6718
|
+
}
|
|
6719
|
+
function isUpstreamInterrupted(u, errorText) {
|
|
6720
|
+
const rawOutput = u.rawOutput;
|
|
6721
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
6722
|
+
const meta = rawOutput.metadata;
|
|
6723
|
+
if (meta && typeof meta === "object") {
|
|
6724
|
+
if (meta.interrupted === true) {
|
|
6725
|
+
return true;
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
if (errorText !== null && errorText.toLowerCase().includes("tool execution aborted")) {
|
|
6730
|
+
return true;
|
|
6731
|
+
}
|
|
6732
|
+
return false;
|
|
6733
|
+
}
|
|
6252
6734
|
function mapPlan(u) {
|
|
6253
6735
|
const entries = u.entries;
|
|
6254
6736
|
if (!Array.isArray(entries)) {
|
|
@@ -6629,6 +7111,35 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
6629
7111
|
}
|
|
6630
7112
|
reply.code(204).send();
|
|
6631
7113
|
});
|
|
7114
|
+
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
7115
|
+
const raw = request.params.id;
|
|
7116
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
7117
|
+
const body = request.body ?? {};
|
|
7118
|
+
if (body.regen === true) {
|
|
7119
|
+
const session = manager.get(id);
|
|
7120
|
+
if (!session) {
|
|
7121
|
+
reply.code(409).send({ error: "regen requires a live session" });
|
|
7122
|
+
return;
|
|
7123
|
+
}
|
|
7124
|
+
void session.retitleFromAgent().catch((err) => {
|
|
7125
|
+
app.log.warn(
|
|
7126
|
+
`title regen failed for ${id}: ${err.message}`
|
|
7127
|
+
);
|
|
7128
|
+
});
|
|
7129
|
+
reply.code(202).send();
|
|
7130
|
+
return;
|
|
7131
|
+
}
|
|
7132
|
+
if (typeof body.title !== "string" || body.title.trim().length === 0) {
|
|
7133
|
+
reply.code(400).send({ error: "title must be a non-empty string" });
|
|
7134
|
+
return;
|
|
7135
|
+
}
|
|
7136
|
+
const ok = await manager.setTitle(id, body.title);
|
|
7137
|
+
if (!ok) {
|
|
7138
|
+
reply.code(404).send({ error: "session not found" });
|
|
7139
|
+
return;
|
|
7140
|
+
}
|
|
7141
|
+
reply.code(204).send();
|
|
7142
|
+
});
|
|
6632
7143
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
6633
7144
|
const raw = request.params.id;
|
|
6634
7145
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -7216,7 +7727,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7216
7727
|
mcpServers: params.mcpServers,
|
|
7217
7728
|
title: hydraMeta.name,
|
|
7218
7729
|
agentArgs: hydraMeta.agentArgs,
|
|
7219
|
-
model: hydraMeta.model
|
|
7730
|
+
model: hydraMeta.model,
|
|
7731
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
7220
7732
|
});
|
|
7221
7733
|
const client = bindClientToSession(connection, session, state);
|
|
7222
7734
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -7232,6 +7744,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7232
7744
|
})();
|
|
7233
7745
|
});
|
|
7234
7746
|
const modesPayload = buildModesPayload(session);
|
|
7747
|
+
const modelsPayload = buildModelsPayload(session);
|
|
7235
7748
|
return {
|
|
7236
7749
|
sessionId: session.sessionId,
|
|
7237
7750
|
// session/new is implicitly an attach; mirror session/attach's
|
|
@@ -7240,6 +7753,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7240
7753
|
// events without an extra round-trip.
|
|
7241
7754
|
clientId: client.clientId,
|
|
7242
7755
|
...modesPayload ? { modes: modesPayload } : {},
|
|
7756
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
7243
7757
|
_meta: buildResponseMeta(session)
|
|
7244
7758
|
};
|
|
7245
7759
|
});
|
|
@@ -7275,7 +7789,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7275
7789
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
7276
7790
|
throw err;
|
|
7277
7791
|
}
|
|
7278
|
-
session = await deps.manager.resurrect(
|
|
7792
|
+
session = await deps.manager.resurrect({
|
|
7793
|
+
...resurrectParams,
|
|
7794
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
7795
|
+
});
|
|
7279
7796
|
}
|
|
7280
7797
|
const client = bindClientToSession(
|
|
7281
7798
|
connection,
|
|
@@ -7301,6 +7818,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7301
7818
|
}
|
|
7302
7819
|
session.replayPendingPermissions(client);
|
|
7303
7820
|
const modesPayload = buildModesPayload(session);
|
|
7821
|
+
const modelsPayload = buildModelsPayload(session);
|
|
7304
7822
|
return {
|
|
7305
7823
|
sessionId: session.sessionId,
|
|
7306
7824
|
clientId: client.clientId,
|
|
@@ -7312,6 +7830,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7312
7830
|
historyPolicy: appliedPolicy,
|
|
7313
7831
|
replayed: replay.length,
|
|
7314
7832
|
...modesPayload ? { modes: modesPayload } : {},
|
|
7833
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
7315
7834
|
_meta: buildResponseMeta(session)
|
|
7316
7835
|
};
|
|
7317
7836
|
});
|
|
@@ -7467,15 +7986,39 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7467
7986
|
}
|
|
7468
7987
|
session.replayPendingPermissions(client);
|
|
7469
7988
|
const modesPayload = buildModesPayload(session);
|
|
7989
|
+
const modelsPayload = buildModelsPayload(session);
|
|
7470
7990
|
return {
|
|
7471
7991
|
sessionId: session.sessionId,
|
|
7472
7992
|
// Same as session/new: include clientId so the deferred-echo
|
|
7473
7993
|
// path in queue-aware clients can recognize own broadcasts.
|
|
7474
7994
|
clientId: client.clientId,
|
|
7475
7995
|
...modesPayload ? { modes: modesPayload } : {},
|
|
7996
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
7476
7997
|
_meta: buildResponseMeta(session)
|
|
7477
7998
|
};
|
|
7478
7999
|
});
|
|
8000
|
+
connection.onRequest("session/set_model", async (rawParams) => {
|
|
8001
|
+
const decision = decideSetModel(rawParams, deps.manager);
|
|
8002
|
+
if (decision.kind === "error") {
|
|
8003
|
+
app.log.warn(decision.logMessage);
|
|
8004
|
+
const err = new Error(decision.message);
|
|
8005
|
+
err.code = decision.code;
|
|
8006
|
+
throw err;
|
|
8007
|
+
}
|
|
8008
|
+
if (decision.kind === "no_op") {
|
|
8009
|
+
app.log.warn(decision.logMessage);
|
|
8010
|
+
await connection.notify("session/update", {
|
|
8011
|
+
sessionId: decision.sessionId,
|
|
8012
|
+
update: {
|
|
8013
|
+
sessionUpdate: "current_model_update",
|
|
8014
|
+
currentModel: decision.currentModel
|
|
8015
|
+
}
|
|
8016
|
+
}).catch(() => void 0);
|
|
8017
|
+
return null;
|
|
8018
|
+
}
|
|
8019
|
+
app.log.info(decision.logMessage);
|
|
8020
|
+
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
8021
|
+
});
|
|
7479
8022
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
7480
8023
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
7481
8024
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -7498,6 +8041,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7498
8041
|
});
|
|
7499
8042
|
});
|
|
7500
8043
|
}
|
|
8044
|
+
function makeInstallProgressForwarder(connection) {
|
|
8045
|
+
return (event) => {
|
|
8046
|
+
const payload = {
|
|
8047
|
+
agentId: event.agentId,
|
|
8048
|
+
version: event.version,
|
|
8049
|
+
source: event.source,
|
|
8050
|
+
phase: event.phase
|
|
8051
|
+
};
|
|
8052
|
+
if ("receivedBytes" in event) {
|
|
8053
|
+
payload.receivedBytes = event.receivedBytes;
|
|
8054
|
+
}
|
|
8055
|
+
if ("totalBytes" in event) {
|
|
8056
|
+
payload.totalBytes = event.totalBytes;
|
|
8057
|
+
}
|
|
8058
|
+
if ("packageSpec" in event) {
|
|
8059
|
+
payload.packageSpec = event.packageSpec;
|
|
8060
|
+
}
|
|
8061
|
+
void connection.notify(AGENT_INSTALL_PROGRESS_METHOD, payload).catch(() => void 0);
|
|
8062
|
+
};
|
|
8063
|
+
}
|
|
7501
8064
|
function buildModesPayload(session) {
|
|
7502
8065
|
const modes = session.availableModes();
|
|
7503
8066
|
if (modes.length === 0) {
|
|
@@ -7518,6 +8081,94 @@ function buildModesPayload(session) {
|
|
|
7518
8081
|
const currentModeId = session.currentMode ?? modes[0].id;
|
|
7519
8082
|
return { currentModeId, availableModes };
|
|
7520
8083
|
}
|
|
8084
|
+
function buildModelsPayload(session) {
|
|
8085
|
+
const models = session.availableModels();
|
|
8086
|
+
if (models.length === 0) {
|
|
8087
|
+
return void 0;
|
|
8088
|
+
}
|
|
8089
|
+
const availableModels = models.map((m) => {
|
|
8090
|
+
const out = {
|
|
8091
|
+
modelId: m.modelId
|
|
8092
|
+
};
|
|
8093
|
+
if (m.name !== void 0) {
|
|
8094
|
+
out.name = m.name;
|
|
8095
|
+
}
|
|
8096
|
+
if (m.description !== void 0) {
|
|
8097
|
+
out.description = m.description;
|
|
8098
|
+
}
|
|
8099
|
+
return out;
|
|
8100
|
+
});
|
|
8101
|
+
const currentModelId = session.currentModel ?? models[0].modelId;
|
|
8102
|
+
return { currentModelId, availableModels };
|
|
8103
|
+
}
|
|
8104
|
+
function decideSetModel(rawParams, manager) {
|
|
8105
|
+
if (!rawParams || typeof rawParams !== "object") {
|
|
8106
|
+
return {
|
|
8107
|
+
kind: "error",
|
|
8108
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8109
|
+
message: "session/set_model requires params",
|
|
8110
|
+
logMessage: "session/set_model rejected: params not an object"
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
const params = rawParams;
|
|
8114
|
+
if (typeof params.sessionId !== "string") {
|
|
8115
|
+
return {
|
|
8116
|
+
kind: "error",
|
|
8117
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8118
|
+
message: "session/set_model requires string sessionId",
|
|
8119
|
+
logMessage: "session/set_model rejected: missing/non-string sessionId"
|
|
8120
|
+
};
|
|
8121
|
+
}
|
|
8122
|
+
if (typeof params.modelId !== "string") {
|
|
8123
|
+
return {
|
|
8124
|
+
kind: "error",
|
|
8125
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8126
|
+
message: "session/set_model requires string modelId",
|
|
8127
|
+
logMessage: `session/set_model rejected: missing/non-string modelId sessionId=${params.sessionId}`
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
const session = manager.get(params.sessionId);
|
|
8131
|
+
if (!session) {
|
|
8132
|
+
return {
|
|
8133
|
+
kind: "error",
|
|
8134
|
+
code: JsonRpcErrorCodes.SessionNotFound,
|
|
8135
|
+
message: `session ${params.sessionId} not found`,
|
|
8136
|
+
logMessage: `session/set_model rejected: session not found sessionId=${params.sessionId}`
|
|
8137
|
+
};
|
|
8138
|
+
}
|
|
8139
|
+
const advertised = session.availableModels();
|
|
8140
|
+
if (advertised.length === 0) {
|
|
8141
|
+
return {
|
|
8142
|
+
kind: "ok",
|
|
8143
|
+
session,
|
|
8144
|
+
logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
8145
|
+
};
|
|
8146
|
+
}
|
|
8147
|
+
const match = advertised.find((m) => m.modelId === params.modelId);
|
|
8148
|
+
if (!match) {
|
|
8149
|
+
const known = advertised.map((m) => m.modelId).join(", ");
|
|
8150
|
+
if (session.currentModel !== void 0 && session.currentModel.length > 0) {
|
|
8151
|
+
return {
|
|
8152
|
+
kind: "no_op",
|
|
8153
|
+
session,
|
|
8154
|
+
sessionId: params.sessionId,
|
|
8155
|
+
currentModel: session.currentModel,
|
|
8156
|
+
logMessage: `session/set_model no_op (resyncing client) sessionId=${params.sessionId} requested=${JSON.stringify(params.modelId)} actual=${JSON.stringify(session.currentModel)} agentId=${session.agentId} known=[${known}]`
|
|
8157
|
+
};
|
|
8158
|
+
}
|
|
8159
|
+
return {
|
|
8160
|
+
kind: "error",
|
|
8161
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8162
|
+
message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
|
|
8163
|
+
logMessage: `session/set_model rejected sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)} agentId=${session.agentId} known=[${known}] (no current model to fall back to)`
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
return {
|
|
8167
|
+
kind: "ok",
|
|
8168
|
+
session,
|
|
8169
|
+
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
8170
|
+
};
|
|
8171
|
+
}
|
|
7521
8172
|
function buildResponseMeta(session) {
|
|
7522
8173
|
const ours = {
|
|
7523
8174
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -7547,6 +8198,10 @@ function buildResponseMeta(session) {
|
|
|
7547
8198
|
if (modes.length > 0) {
|
|
7548
8199
|
ours.availableModes = modes;
|
|
7549
8200
|
}
|
|
8201
|
+
const models = session.availableModels();
|
|
8202
|
+
if (models.length > 0) {
|
|
8203
|
+
ours.availableModels = models;
|
|
8204
|
+
}
|
|
7550
8205
|
if (session.turnStartedAt !== void 0) {
|
|
7551
8206
|
ours.turnStartedAt = session.turnStartedAt;
|
|
7552
8207
|
}
|