@hydra-acp/cli 0.1.45 → 0.1.47
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 +4 -4
- package/dist/cli.js +526 -108
- package/dist/index.d.ts +22 -1
- package/dist/index.js +402 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -173,7 +173,7 @@ var DaemonConfig = z.object({
|
|
|
173
173
|
// Compaction trims to this many on a periodic basis; reads also slice
|
|
174
174
|
// to the tail at this length as a defensive measure against older
|
|
175
175
|
// daemons that may have written unbounded files.
|
|
176
|
-
sessionHistoryMaxEntries: z.number().int().positive().default(
|
|
176
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e4),
|
|
177
177
|
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
178
178
|
// daemon can include it in the diagnostic message when a spawn fails.
|
|
179
179
|
// Bump if your agents emit large tracebacks you want surfaced.
|
|
@@ -233,7 +233,13 @@ var TuiConfig = z.object({
|
|
|
233
233
|
// streaming lines beneath the live thinking block. Set false to
|
|
234
234
|
// suppress them — the TUI hotkey ^T toggles this at runtime without
|
|
235
235
|
// persisting back to config.
|
|
236
|
-
showThoughts: z.boolean().default(true)
|
|
236
|
+
showThoughts: z.boolean().default(true),
|
|
237
|
+
// Cap on entries kept in the cross-session global prompt-history file
|
|
238
|
+
// (~/.hydra-acp/prompt-history). This is the ^P / ^R recall list
|
|
239
|
+
// shared across all sessions; it's append-only on disk, so long-lived
|
|
240
|
+
// installs can grow past this — it's enforced at load time and per
|
|
241
|
+
// append in memory.
|
|
242
|
+
promptHistoryMaxEntries: z.number().int().positive().default(2e3)
|
|
237
243
|
});
|
|
238
244
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
239
245
|
var ExtensionBody = z.object({
|
|
@@ -287,7 +293,8 @@ var HydraConfig = z.object({
|
|
|
287
293
|
cwdColumnMaxWidth: 24,
|
|
288
294
|
progressIndicator: true,
|
|
289
295
|
defaultEnterAction: "amend",
|
|
290
|
-
showThoughts: true
|
|
296
|
+
showThoughts: true,
|
|
297
|
+
promptHistoryMaxEntries: 2e3
|
|
291
298
|
})
|
|
292
299
|
});
|
|
293
300
|
function extensionList(config) {
|
|
@@ -2284,6 +2291,11 @@ var HYDRA_COMMANDS = [
|
|
|
2284
2291
|
verb: "kill",
|
|
2285
2292
|
name: "hydra kill",
|
|
2286
2293
|
description: "Close this session (kills the agent; record is kept so it can be resumed later)"
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
verb: "restart",
|
|
2297
|
+
name: "hydra restart",
|
|
2298
|
+
description: "Restart the agent with a fresh session/new while preserving conversation history (useful when the proxy has changed available models)"
|
|
2287
2299
|
}
|
|
2288
2300
|
];
|
|
2289
2301
|
var VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
@@ -2370,7 +2382,9 @@ var Session = class {
|
|
|
2370
2382
|
// stale-prone for snapshot-shaped events).
|
|
2371
2383
|
currentModel;
|
|
2372
2384
|
currentMode;
|
|
2373
|
-
|
|
2385
|
+
// Raw per-agent-life usage. Never read directly outside this class —
|
|
2386
|
+
// always access via the currentUsage getter which adds cumulativeCost.
|
|
2387
|
+
_currentUsage;
|
|
2374
2388
|
updatedAt;
|
|
2375
2389
|
createdAt;
|
|
2376
2390
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -2436,6 +2450,7 @@ var Session = class {
|
|
|
2436
2450
|
// and noisy state churn keep a quiet session alive forever.
|
|
2437
2451
|
lastRecordedAt;
|
|
2438
2452
|
spawnReplacementAgent;
|
|
2453
|
+
listSessions;
|
|
2439
2454
|
logger;
|
|
2440
2455
|
transformChain;
|
|
2441
2456
|
// Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
|
|
@@ -2467,6 +2482,23 @@ var Session = class {
|
|
|
2467
2482
|
modeHandlers = [];
|
|
2468
2483
|
usageHandlers = [];
|
|
2469
2484
|
cumulativeCost = 0;
|
|
2485
|
+
// Total cost across all agent lives. costAmount in the returned snapshot
|
|
2486
|
+
// is cumulativeCost + the current agent's raw amount so every consumer
|
|
2487
|
+
// gets the right figure without knowing about the internal split.
|
|
2488
|
+
// cumulativeCost is stripped from the return value so it never leaks
|
|
2489
|
+
// into persistence paths via session.currentUsage.
|
|
2490
|
+
get currentUsage() {
|
|
2491
|
+
if (!this._currentUsage && !this.cumulativeCost) {
|
|
2492
|
+
return void 0;
|
|
2493
|
+
}
|
|
2494
|
+
const base = this._currentUsage ?? {};
|
|
2495
|
+
const total = this.cumulativeCost + (base.costAmount ?? 0);
|
|
2496
|
+
return {
|
|
2497
|
+
...base,
|
|
2498
|
+
costAmount: total || void 0,
|
|
2499
|
+
cumulativeCost: void 0
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2470
2502
|
// Set by amendPrompt at the start of a cancel-and-resubmit dance.
|
|
2471
2503
|
// broadcastTurnComplete reads it to attach the _meta.amended marker
|
|
2472
2504
|
// to the cancelled turn's turn_complete notification, and to fire the
|
|
@@ -2499,7 +2531,7 @@ var Session = class {
|
|
|
2499
2531
|
this.title = init.title;
|
|
2500
2532
|
this.currentModel = init.currentModel;
|
|
2501
2533
|
this.currentMode = init.currentMode;
|
|
2502
|
-
this.
|
|
2534
|
+
this._currentUsage = init.currentUsage;
|
|
2503
2535
|
this.cumulativeCost = init.currentUsage?.cumulativeCost ?? 0;
|
|
2504
2536
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
2505
2537
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
@@ -2513,6 +2545,7 @@ var Session = class {
|
|
|
2513
2545
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
2514
2546
|
this.idleEventTimeoutMs = init.idleEventTimeoutMs ?? 3e4;
|
|
2515
2547
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
2548
|
+
this.listSessions = init.listSessions;
|
|
2516
2549
|
this.logger = init.logger;
|
|
2517
2550
|
this.transformChain = init.transformChain ?? [];
|
|
2518
2551
|
if (init.firstPromptSeeded) {
|
|
@@ -2531,6 +2564,9 @@ var Session = class {
|
|
|
2531
2564
|
broadcastMergedCommands() {
|
|
2532
2565
|
const merged = [
|
|
2533
2566
|
...hydraCommandsAsAdvertised(),
|
|
2567
|
+
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
2568
|
+
{ name: "sessions", description: "List all sessions" },
|
|
2569
|
+
{ name: "help", description: "Show available commands" },
|
|
2534
2570
|
...this.agentAdvertisedCommands
|
|
2535
2571
|
];
|
|
2536
2572
|
this.recordAndBroadcast("session/update", {
|
|
@@ -2595,7 +2631,8 @@ var Session = class {
|
|
|
2595
2631
|
// then recordAndBroadcast. All state mutation happens after the chain exits.
|
|
2596
2632
|
// See forwardRequest for originatedBy / startIdx semantics.
|
|
2597
2633
|
async runResponseChain(params, originatedBy = /* @__PURE__ */ new Set(), startIdx = 0) {
|
|
2598
|
-
|
|
2634
|
+
const rawParams = params;
|
|
2635
|
+
let envelope = this.injectCumulativeCost(params);
|
|
2599
2636
|
for (let i = startIdx; i < this.transformChain.length; i++) {
|
|
2600
2637
|
const t = this.transformChain[i];
|
|
2601
2638
|
if (originatedBy.has(t.name)) {
|
|
@@ -2683,8 +2720,8 @@ var Session = class {
|
|
|
2683
2720
|
this.recordAndBroadcast("session/update", envelope);
|
|
2684
2721
|
return;
|
|
2685
2722
|
}
|
|
2686
|
-
if (this.maybeApplyAgentUsage(
|
|
2687
|
-
this.recordAndBroadcast("session/update",
|
|
2723
|
+
if (this.maybeApplyAgentUsage(rawParams)) {
|
|
2724
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2688
2725
|
return;
|
|
2689
2726
|
}
|
|
2690
2727
|
this.maybeApplyAgentSessionInfo(envelope);
|
|
@@ -2877,8 +2914,8 @@ var Session = class {
|
|
|
2877
2914
|
recordedAt
|
|
2878
2915
|
});
|
|
2879
2916
|
}
|
|
2880
|
-
if (this.currentUsage !== void 0
|
|
2881
|
-
const u = this.currentUsage
|
|
2917
|
+
if (this.currentUsage !== void 0) {
|
|
2918
|
+
const u = this.currentUsage;
|
|
2882
2919
|
const update = {
|
|
2883
2920
|
sessionUpdate: "usage_update"
|
|
2884
2921
|
};
|
|
@@ -2890,8 +2927,8 @@ var Session = class {
|
|
|
2890
2927
|
}
|
|
2891
2928
|
if (typeof u.costAmount === "number" || typeof u.costCurrency === "string") {
|
|
2892
2929
|
const cost = {};
|
|
2893
|
-
if (typeof u.costAmount === "number"
|
|
2894
|
-
cost.amount =
|
|
2930
|
+
if (typeof u.costAmount === "number") {
|
|
2931
|
+
cost.amount = u.costAmount;
|
|
2895
2932
|
}
|
|
2896
2933
|
if (typeof u.costCurrency === "string") {
|
|
2897
2934
|
cost.currency = u.costCurrency;
|
|
@@ -3430,6 +3467,23 @@ var Session = class {
|
|
|
3430
3467
|
sessionId: this.upstreamSessionId
|
|
3431
3468
|
});
|
|
3432
3469
|
}
|
|
3470
|
+
// Add a transformer to this session's chain retroactively. No-ops if it's
|
|
3471
|
+
// already present. Fires session.opened on the new transformer so it gets
|
|
3472
|
+
// the same lifecycle signal it would have received at session creation.
|
|
3473
|
+
addTransformer(ref) {
|
|
3474
|
+
const existing = this.transformChain.findIndex((t) => t.name === ref.name);
|
|
3475
|
+
if (existing >= 0) {
|
|
3476
|
+
this.transformChain[existing] = ref;
|
|
3477
|
+
} else {
|
|
3478
|
+
this.transformChain.push(ref);
|
|
3479
|
+
}
|
|
3480
|
+
if (ref.intercepts.has("lifecycle:session.opened")) {
|
|
3481
|
+
void ref.connection.notify("transformer/session_event", {
|
|
3482
|
+
event: "session.opened",
|
|
3483
|
+
sessionId: this.sessionId
|
|
3484
|
+
}).catch(() => void 0);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3433
3487
|
// Walk the request-side chain then forward to the agent.
|
|
3434
3488
|
// originatedBy: transformer names already in the lineage — skipped for loop
|
|
3435
3489
|
// prevention and to implement resume-routing on re-entry from emit_message.
|
|
@@ -3754,6 +3808,9 @@ var Session = class {
|
|
|
3754
3808
|
if (!trimmed || trimmed === this.currentMode) {
|
|
3755
3809
|
return true;
|
|
3756
3810
|
}
|
|
3811
|
+
this.logger?.info(
|
|
3812
|
+
`current_mode_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3813
|
+
);
|
|
3757
3814
|
this.currentMode = trimmed;
|
|
3758
3815
|
for (const handler of this.modeHandlers) {
|
|
3759
3816
|
try {
|
|
@@ -3773,7 +3830,7 @@ var Session = class {
|
|
|
3773
3830
|
if (update.sessionUpdate !== "usage_update") {
|
|
3774
3831
|
return false;
|
|
3775
3832
|
}
|
|
3776
|
-
const next = { ...this.
|
|
3833
|
+
const next = { ...this._currentUsage ?? {} };
|
|
3777
3834
|
let changed = false;
|
|
3778
3835
|
if (typeof update.used === "number" && next.used !== update.used) {
|
|
3779
3836
|
next.used = update.used;
|
|
@@ -3797,10 +3854,11 @@ var Session = class {
|
|
|
3797
3854
|
if (!changed) {
|
|
3798
3855
|
return true;
|
|
3799
3856
|
}
|
|
3800
|
-
this.
|
|
3857
|
+
this._currentUsage = next;
|
|
3858
|
+
const total = this.currentUsage ?? next;
|
|
3801
3859
|
for (const handler of this.usageHandlers) {
|
|
3802
3860
|
try {
|
|
3803
|
-
handler(
|
|
3861
|
+
handler(total);
|
|
3804
3862
|
} catch {
|
|
3805
3863
|
}
|
|
3806
3864
|
}
|
|
@@ -3810,16 +3868,16 @@ var Session = class {
|
|
|
3810
3868
|
// next agent life starts accumulating from $0. Fires usageHandlers so
|
|
3811
3869
|
// meta.json is updated before the new agent starts emitting.
|
|
3812
3870
|
accumulateAndResetCost() {
|
|
3813
|
-
const amount = this.
|
|
3871
|
+
const amount = this._currentUsage?.costAmount;
|
|
3814
3872
|
if (!amount)
|
|
3815
3873
|
return;
|
|
3816
3874
|
this.cumulativeCost += amount;
|
|
3817
3875
|
const next = {
|
|
3818
|
-
...this.
|
|
3876
|
+
...this._currentUsage ?? {},
|
|
3819
3877
|
cumulativeCost: this.cumulativeCost,
|
|
3820
3878
|
costAmount: void 0
|
|
3821
3879
|
};
|
|
3822
|
-
this.
|
|
3880
|
+
this._currentUsage = next;
|
|
3823
3881
|
for (const handler of this.usageHandlers) {
|
|
3824
3882
|
try {
|
|
3825
3883
|
handler(next);
|
|
@@ -3854,16 +3912,9 @@ var Session = class {
|
|
|
3854
3912
|
}
|
|
3855
3913
|
};
|
|
3856
3914
|
}
|
|
3857
|
-
//
|
|
3858
|
-
// session/list so list rows show the accumulated figure.
|
|
3915
|
+
// Deprecated alias — currentUsage already returns the total.
|
|
3859
3916
|
get totalUsage() {
|
|
3860
|
-
|
|
3861
|
-
return void 0;
|
|
3862
|
-
const base = this.currentUsage ?? {};
|
|
3863
|
-
return {
|
|
3864
|
-
...base,
|
|
3865
|
-
costAmount: this.cumulativeCost + (this.currentUsage?.costAmount ?? 0)
|
|
3866
|
-
};
|
|
3917
|
+
return this.currentUsage;
|
|
3867
3918
|
}
|
|
3868
3919
|
// Update the cached agent command list, fire persist handlers, and
|
|
3869
3920
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
@@ -3929,6 +3980,26 @@ var Session = class {
|
|
|
3929
3980
|
onModeChange(handler) {
|
|
3930
3981
|
this.modeHandlers.push(handler);
|
|
3931
3982
|
}
|
|
3983
|
+
// Apply a mode change initiated by a client request (session/set_mode)
|
|
3984
|
+
// when the agent doesn't emit a current_mode_update notification on its
|
|
3985
|
+
// own. Fires modeHandlers so the persistence hook and any other listeners
|
|
3986
|
+
// see the change, identical to the agent-notification path.
|
|
3987
|
+
applyModeChange(modeId) {
|
|
3988
|
+
const trimmed = modeId.trim();
|
|
3989
|
+
if (!trimmed || trimmed === this.currentMode) {
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
this.logger?.info(
|
|
3993
|
+
`applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3994
|
+
);
|
|
3995
|
+
this.currentMode = trimmed;
|
|
3996
|
+
for (const handler of this.modeHandlers) {
|
|
3997
|
+
try {
|
|
3998
|
+
handler(trimmed);
|
|
3999
|
+
} catch {
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
3932
4003
|
onUsageChange(handler) {
|
|
3933
4004
|
this.usageHandlers.push(handler);
|
|
3934
4005
|
}
|
|
@@ -4011,6 +4082,8 @@ var Session = class {
|
|
|
4011
4082
|
return this.runAgentCommand(arg);
|
|
4012
4083
|
case "kill":
|
|
4013
4084
|
return this.runKillCommand();
|
|
4085
|
+
case "restart":
|
|
4086
|
+
return this.runRestartCommand();
|
|
4014
4087
|
default: {
|
|
4015
4088
|
const err = new Error(
|
|
4016
4089
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -4020,6 +4093,92 @@ var Session = class {
|
|
|
4020
4093
|
}
|
|
4021
4094
|
}
|
|
4022
4095
|
}
|
|
4096
|
+
async handleSessionsCommand() {
|
|
4097
|
+
let text;
|
|
4098
|
+
if (!this.listSessions) {
|
|
4099
|
+
text = "_(session listing not available)_";
|
|
4100
|
+
} else {
|
|
4101
|
+
const sessions = await this.listSessions();
|
|
4102
|
+
if (sessions.length === 0) {
|
|
4103
|
+
text = "_(no sessions)_";
|
|
4104
|
+
} else {
|
|
4105
|
+
const lines = sessions.map((s) => {
|
|
4106
|
+
const id = s.sessionId.replace(/^hydra_session_/, "").slice(0, 12);
|
|
4107
|
+
const model = s.currentModel ? ` \xB7 ${s.currentModel}` : "";
|
|
4108
|
+
const marker = s.sessionId === this.sessionId ? " \u25C0" : "";
|
|
4109
|
+
const title = s.title ? ` ${s.title}` : "";
|
|
4110
|
+
return `\`${id}\` ${s.cwd}${model}${marker}${title}`;
|
|
4111
|
+
});
|
|
4112
|
+
text = `Sessions (${sessions.length}):
|
|
4113
|
+
${lines.join("\n")}`;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
this.recordAndBroadcast("session/update", {
|
|
4117
|
+
sessionId: this.upstreamSessionId,
|
|
4118
|
+
update: {
|
|
4119
|
+
sessionUpdate: "agent_message_chunk",
|
|
4120
|
+
content: { type: "text", text: `
|
|
4121
|
+
${text}
|
|
4122
|
+
` },
|
|
4123
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4124
|
+
}
|
|
4125
|
+
});
|
|
4126
|
+
return { stopReason: "end_turn" };
|
|
4127
|
+
}
|
|
4128
|
+
handleHelpCommand() {
|
|
4129
|
+
const commands = this.mergedAvailableCommands();
|
|
4130
|
+
const lines = commands.map((c) => {
|
|
4131
|
+
const desc = c.description ? ` ${c.description}` : "";
|
|
4132
|
+
return `\`/${c.name}\`${desc}`;
|
|
4133
|
+
});
|
|
4134
|
+
const text = lines.length > 0 ? `Available commands:
|
|
4135
|
+
${lines.join("\n")}` : "_(no commands advertised)_";
|
|
4136
|
+
this.recordAndBroadcast("session/update", {
|
|
4137
|
+
sessionId: this.upstreamSessionId,
|
|
4138
|
+
update: {
|
|
4139
|
+
sessionUpdate: "agent_message_chunk",
|
|
4140
|
+
content: { type: "text", text: `
|
|
4141
|
+
${text}
|
|
4142
|
+
` },
|
|
4143
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
return Promise.resolve({ stopReason: "end_turn" });
|
|
4147
|
+
}
|
|
4148
|
+
async handleModelCommand(text) {
|
|
4149
|
+
const arg = text.slice("/model".length).trim();
|
|
4150
|
+
if (arg === "") {
|
|
4151
|
+
const models = this.agentAdvertisedModels;
|
|
4152
|
+
const current = this.currentModel;
|
|
4153
|
+
let body;
|
|
4154
|
+
if (models.length === 0) {
|
|
4155
|
+
body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
|
|
4156
|
+
} else {
|
|
4157
|
+
const lines = models.map((m) => {
|
|
4158
|
+
const marker = m.modelId === current ? " \u25C0" : "";
|
|
4159
|
+
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
4160
|
+
return `${m.modelId}${marker}${desc}`;
|
|
4161
|
+
});
|
|
4162
|
+
body = lines.join("\n");
|
|
4163
|
+
}
|
|
4164
|
+
this.recordAndBroadcast("session/update", {
|
|
4165
|
+
sessionId: this.upstreamSessionId,
|
|
4166
|
+
update: {
|
|
4167
|
+
sessionUpdate: "agent_message_chunk",
|
|
4168
|
+
content: { type: "text", text: `
|
|
4169
|
+
${body}
|
|
4170
|
+
` },
|
|
4171
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
return { stopReason: "end_turn" };
|
|
4175
|
+
}
|
|
4176
|
+
await this.forwardRequest("session/set_model", {
|
|
4177
|
+
sessionId: this.sessionId,
|
|
4178
|
+
modelId: arg
|
|
4179
|
+
});
|
|
4180
|
+
return { stopReason: "end_turn" };
|
|
4181
|
+
}
|
|
4023
4182
|
// Runs as a normal queued prompt (so it serializes after any in-flight
|
|
4024
4183
|
// turn). With an arg, sets the title directly. Without one, runs a
|
|
4025
4184
|
// suppressed sub-prompt to the agent and uses its reply as the title.
|
|
@@ -4141,6 +4300,57 @@ var Session = class {
|
|
|
4141
4300
|
await this.close({ deleteRecord: false });
|
|
4142
4301
|
return { stopReason: "end_turn" };
|
|
4143
4302
|
}
|
|
4303
|
+
// Restart the underlying agent with a fresh session/new, preserving the
|
|
4304
|
+
// conversation history. Unlike /hydra agent, this allows the same agentId
|
|
4305
|
+
// — useful when the proxy has changed available models (e.g. opus became
|
|
4306
|
+
// available after a quota reset) and the resumed session is locked to a
|
|
4307
|
+
// stale model list.
|
|
4308
|
+
runRestartCommand() {
|
|
4309
|
+
if (!this.spawnReplacementAgent) {
|
|
4310
|
+
throw withCode(
|
|
4311
|
+
new Error("agent restart not configured for this session"),
|
|
4312
|
+
JsonRpcErrorCodes.InternalError
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
const spawnAgent = this.spawnReplacementAgent;
|
|
4316
|
+
const agentId = this.agentId;
|
|
4317
|
+
return this.enqueuePrompt(async () => {
|
|
4318
|
+
const transcript = await this.buildSwitchTranscript(agentId);
|
|
4319
|
+
const fresh = await spawnAgent({
|
|
4320
|
+
agentId,
|
|
4321
|
+
cwd: this.cwd,
|
|
4322
|
+
agentArgs: this.agentArgs
|
|
4323
|
+
});
|
|
4324
|
+
this.accumulateAndResetCost();
|
|
4325
|
+
this.wireAgent(fresh.agent);
|
|
4326
|
+
const oldAgent = this.agent;
|
|
4327
|
+
this.agent = fresh.agent;
|
|
4328
|
+
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
4329
|
+
this.agentMeta = fresh.agentMeta;
|
|
4330
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
4331
|
+
this.agentAdvertisedCommands = [];
|
|
4332
|
+
this.broadcastMergedCommands();
|
|
4333
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
4334
|
+
this.setAgentAdvertisedModels([]);
|
|
4335
|
+
}
|
|
4336
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
4337
|
+
this.setAgentAdvertisedModes([]);
|
|
4338
|
+
}
|
|
4339
|
+
await oldAgent.kill().catch(() => void 0);
|
|
4340
|
+
if (transcript) {
|
|
4341
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
4342
|
+
}
|
|
4343
|
+
this.broadcastAgentSwitch(agentId, agentId);
|
|
4344
|
+
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
4345
|
+
for (const handler of this.agentChangeHandlers) {
|
|
4346
|
+
try {
|
|
4347
|
+
handler(info);
|
|
4348
|
+
} catch {
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
return { stopReason: "end_turn" };
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4144
4354
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
4145
4355
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
4146
4356
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
@@ -4757,6 +4967,22 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4757
4967
|
return entry.task();
|
|
4758
4968
|
}
|
|
4759
4969
|
this.broadcastPromptReceived(entry);
|
|
4970
|
+
const promptText = extractPromptText(entry.prompt).trim();
|
|
4971
|
+
if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/sessions" || promptText === "/help") {
|
|
4972
|
+
let result;
|
|
4973
|
+
if (promptText === "/sessions") {
|
|
4974
|
+
result = await this.handleSessionsCommand();
|
|
4975
|
+
} else if (promptText === "/help") {
|
|
4976
|
+
result = await this.handleHelpCommand();
|
|
4977
|
+
} else {
|
|
4978
|
+
result = await this.handleModelCommand(promptText);
|
|
4979
|
+
}
|
|
4980
|
+
if (!this.closed) {
|
|
4981
|
+
this.broadcastTurnComplete(entry.clientId, result, entry.messageId, entry.wasAmend);
|
|
4982
|
+
}
|
|
4983
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
4984
|
+
return result;
|
|
4985
|
+
}
|
|
4760
4986
|
let response;
|
|
4761
4987
|
try {
|
|
4762
4988
|
response = await this.agent.connection.request(
|
|
@@ -5548,6 +5774,7 @@ var SessionManager = class {
|
|
|
5548
5774
|
idleEventTimeoutMs: this.idleEventTimeoutMs,
|
|
5549
5775
|
logger: this.logger,
|
|
5550
5776
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
5777
|
+
listSessions: () => this.list(),
|
|
5551
5778
|
historyStore: this.histories,
|
|
5552
5779
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5553
5780
|
currentModel: fresh.initialModel,
|
|
@@ -5649,6 +5876,22 @@ var SessionManager = class {
|
|
|
5649
5876
|
} else {
|
|
5650
5877
|
agent.connection.drainBuffered("session/update");
|
|
5651
5878
|
}
|
|
5879
|
+
const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
|
|
5880
|
+
const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
|
|
5881
|
+
this.logger?.info(
|
|
5882
|
+
`resurrect: sessionId=${params.hydraSessionId} persistedMode=${JSON.stringify(params.currentMode)} agentReportedMode=${JSON.stringify(agentReportedMode)} advertisedModes=${JSON.stringify(advertisedModes?.map((m) => m.id))}`
|
|
5883
|
+
);
|
|
5884
|
+
const effectiveMode = await restoreCurrentMode({
|
|
5885
|
+
agent,
|
|
5886
|
+
upstreamSessionId: params.upstreamSessionId,
|
|
5887
|
+
persistedMode: params.currentMode,
|
|
5888
|
+
agentReportedMode,
|
|
5889
|
+
advertisedModes,
|
|
5890
|
+
logger: this.logger
|
|
5891
|
+
});
|
|
5892
|
+
this.logger?.info(
|
|
5893
|
+
`resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
|
|
5894
|
+
);
|
|
5652
5895
|
const session = new Session({
|
|
5653
5896
|
sessionId: params.hydraSessionId,
|
|
5654
5897
|
cwd: params.cwd,
|
|
@@ -5662,6 +5905,7 @@ var SessionManager = class {
|
|
|
5662
5905
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5663
5906
|
logger: this.logger,
|
|
5664
5907
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
5908
|
+
listSessions: () => this.list(),
|
|
5665
5909
|
historyStore: this.histories,
|
|
5666
5910
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5667
5911
|
// Prefer what we previously stored from a current_model_update; if
|
|
@@ -5669,11 +5913,15 @@ var SessionManager = class {
|
|
|
5669
5913
|
// this fix), fall back to the model the agent ships in its
|
|
5670
5914
|
// session/load response body.
|
|
5671
5915
|
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
5672
|
-
currentMode:
|
|
5916
|
+
currentMode: effectiveMode,
|
|
5673
5917
|
currentUsage: params.currentUsage,
|
|
5674
5918
|
agentCommands: params.agentCommands,
|
|
5675
|
-
agentModes:
|
|
5676
|
-
|
|
5919
|
+
agentModes: advertisedModes,
|
|
5920
|
+
// Always prefer the fresh list from session/load over the persisted
|
|
5921
|
+
// snapshot — the proxy's available models can change between daemon
|
|
5922
|
+
// restarts (quota resets, rollouts), so meta.json is intentionally
|
|
5923
|
+
// treated as a cold fallback here, not the authoritative source.
|
|
5924
|
+
agentModels: nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels,
|
|
5677
5925
|
// Only gate the first-prompt title heuristic when we actually have
|
|
5678
5926
|
// a title to preserve. A title-less session (lost to a write race
|
|
5679
5927
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -5700,6 +5948,15 @@ var SessionManager = class {
|
|
|
5700
5948
|
mcpServers: [],
|
|
5701
5949
|
onInstallProgress: params.onInstallProgress
|
|
5702
5950
|
});
|
|
5951
|
+
const advertisedModes = params.agentModes ?? fresh.initialModes;
|
|
5952
|
+
const effectiveMode = await restoreCurrentMode({
|
|
5953
|
+
agent: fresh.agent,
|
|
5954
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
5955
|
+
persistedMode: params.currentMode,
|
|
5956
|
+
agentReportedMode: fresh.initialMode,
|
|
5957
|
+
advertisedModes,
|
|
5958
|
+
logger: this.logger
|
|
5959
|
+
});
|
|
5703
5960
|
const session = new Session({
|
|
5704
5961
|
sessionId: params.hydraSessionId,
|
|
5705
5962
|
cwd,
|
|
@@ -5713,15 +5970,16 @@ var SessionManager = class {
|
|
|
5713
5970
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5714
5971
|
logger: this.logger,
|
|
5715
5972
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
5973
|
+
listSessions: () => this.list(),
|
|
5716
5974
|
historyStore: this.histories,
|
|
5717
5975
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5718
5976
|
// Prefer the stored value (set by a previous current_model_update);
|
|
5719
5977
|
// fall back to whatever the agent ships in its session/new response.
|
|
5720
5978
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
5721
|
-
currentMode:
|
|
5979
|
+
currentMode: effectiveMode,
|
|
5722
5980
|
currentUsage: params.currentUsage,
|
|
5723
5981
|
agentCommands: params.agentCommands,
|
|
5724
|
-
agentModes:
|
|
5982
|
+
agentModes: advertisedModes,
|
|
5725
5983
|
agentModels: params.agentModels ?? fresh.initialModels,
|
|
5726
5984
|
firstPromptSeeded: !!params.title,
|
|
5727
5985
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -5923,13 +6181,15 @@ var SessionManager = class {
|
|
|
5923
6181
|
modelId: desired
|
|
5924
6182
|
});
|
|
5925
6183
|
initialModel = desired;
|
|
5926
|
-
} catch {
|
|
6184
|
+
} catch (err) {
|
|
6185
|
+
this.logger?.warn(
|
|
6186
|
+
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} rejected by agent (${err.message}); session will use ${JSON.stringify(initialModel)}`
|
|
6187
|
+
);
|
|
5927
6188
|
}
|
|
5928
6189
|
} else {
|
|
5929
6190
|
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
5930
|
-
|
|
5931
|
-
`
|
|
5932
|
-
`
|
|
6191
|
+
this.logger?.warn(
|
|
6192
|
+
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} not in agent's availableModels ([${known}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
|
|
5933
6193
|
);
|
|
5934
6194
|
}
|
|
5935
6195
|
}
|
|
@@ -6106,6 +6366,9 @@ var SessionManager = class {
|
|
|
6106
6366
|
get(sessionId) {
|
|
6107
6367
|
return this.sessions.get(sessionId);
|
|
6108
6368
|
}
|
|
6369
|
+
liveSessions() {
|
|
6370
|
+
return this.sessions.values();
|
|
6371
|
+
}
|
|
6109
6372
|
// Snapshot of which agent versions are currently in use by live
|
|
6110
6373
|
// sessions, keyed by agentId. Read by the registry-fetch prune sweep
|
|
6111
6374
|
// so it can skip install dirs that still back a running process.
|
|
@@ -6167,7 +6430,7 @@ var SessionManager = class {
|
|
|
6167
6430
|
title: session.title,
|
|
6168
6431
|
agentId: session.agentId,
|
|
6169
6432
|
currentModel: session.currentModel,
|
|
6170
|
-
currentUsage: session.
|
|
6433
|
+
currentUsage: session.currentUsage,
|
|
6171
6434
|
parentSessionId: session.parentSessionId,
|
|
6172
6435
|
updatedAt: used,
|
|
6173
6436
|
attachedClients: session.attachedCount,
|
|
@@ -6319,6 +6582,7 @@ var SessionManager = class {
|
|
|
6319
6582
|
currentMode: args.bundle.session.currentMode,
|
|
6320
6583
|
currentUsage: args.bundle.session.currentUsage,
|
|
6321
6584
|
agentCommands: args.bundle.session.agentCommands,
|
|
6585
|
+
agentModes: args.bundle.session.agentModes,
|
|
6322
6586
|
createdAt: args.preservedCreatedAt ?? now,
|
|
6323
6587
|
// Fallback path for historyMtimeIso (used when the history file
|
|
6324
6588
|
// is missing). Keep this consistent with the utimes stamp above.
|
|
@@ -6708,6 +6972,47 @@ function extractInitialCurrentMode(result) {
|
|
|
6708
6972
|
}
|
|
6709
6973
|
return void 0;
|
|
6710
6974
|
}
|
|
6975
|
+
async function restoreCurrentMode(opts) {
|
|
6976
|
+
const {
|
|
6977
|
+
agent,
|
|
6978
|
+
upstreamSessionId,
|
|
6979
|
+
persistedMode,
|
|
6980
|
+
agentReportedMode,
|
|
6981
|
+
advertisedModes,
|
|
6982
|
+
logger
|
|
6983
|
+
} = opts;
|
|
6984
|
+
if (!persistedMode) {
|
|
6985
|
+
return agentReportedMode;
|
|
6986
|
+
}
|
|
6987
|
+
if (persistedMode === agentReportedMode) {
|
|
6988
|
+
return persistedMode;
|
|
6989
|
+
}
|
|
6990
|
+
if (advertisedModes && advertisedModes.length > 0 && !advertisedModes.some((m) => m.id === persistedMode)) {
|
|
6991
|
+
const known = advertisedModes.map((m) => m.id).join(", ");
|
|
6992
|
+
logger?.warn(
|
|
6993
|
+
`resurrect: persisted currentMode=${JSON.stringify(persistedMode)} not in agent's availableModes ([${known}]); skipping session/set_mode, session will use ${JSON.stringify(agentReportedMode)}`
|
|
6994
|
+
);
|
|
6995
|
+
return agentReportedMode;
|
|
6996
|
+
}
|
|
6997
|
+
try {
|
|
6998
|
+
logger?.info(
|
|
6999
|
+
`resurrect: pushing persisted modeId=${JSON.stringify(persistedMode)} to agent (agentReported=${JSON.stringify(agentReportedMode)})`
|
|
7000
|
+
);
|
|
7001
|
+
await agent.connection.request("session/set_mode", {
|
|
7002
|
+
sessionId: upstreamSessionId,
|
|
7003
|
+
modeId: persistedMode
|
|
7004
|
+
});
|
|
7005
|
+
logger?.info(
|
|
7006
|
+
`resurrect: session/set_mode accepted, effectiveMode=${JSON.stringify(persistedMode)}`
|
|
7007
|
+
);
|
|
7008
|
+
return persistedMode;
|
|
7009
|
+
} catch (err) {
|
|
7010
|
+
logger?.warn(
|
|
7011
|
+
`resurrect: session/set_mode rejected by agent for modeId=${JSON.stringify(persistedMode)} (${err.message}); session will use ${JSON.stringify(agentReportedMode)}`
|
|
7012
|
+
);
|
|
7013
|
+
return agentReportedMode;
|
|
7014
|
+
}
|
|
7015
|
+
}
|
|
6711
7016
|
function parseModesList(list) {
|
|
6712
7017
|
if (!Array.isArray(list)) {
|
|
6713
7018
|
return [];
|
|
@@ -8513,7 +8818,15 @@ function mapModel(u) {
|
|
|
8513
8818
|
if (!model) {
|
|
8514
8819
|
return null;
|
|
8515
8820
|
}
|
|
8516
|
-
|
|
8821
|
+
const raw = u.availableModels;
|
|
8822
|
+
const availableModels = Array.isArray(raw) ? raw.map(
|
|
8823
|
+
(m) => typeof m === "object" && m !== null ? m.modelId : typeof m === "string" ? m : void 0
|
|
8824
|
+
).filter((id) => typeof id === "string" && id.length > 0) : void 0;
|
|
8825
|
+
return {
|
|
8826
|
+
kind: "model-changed",
|
|
8827
|
+
model: sanitizeSingleLine(model),
|
|
8828
|
+
...availableModels && availableModels.length > 0 ? { availableModels } : {}
|
|
8829
|
+
};
|
|
8517
8830
|
}
|
|
8518
8831
|
function mapTurnComplete(u) {
|
|
8519
8832
|
const stopReason = readString(u, "stopReason");
|
|
@@ -9677,6 +9990,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9677
9990
|
connection,
|
|
9678
9991
|
intercepts
|
|
9679
9992
|
);
|
|
9993
|
+
if (deps.manager?.defaultTransformers.includes(processIdentity.name)) {
|
|
9994
|
+
const ref = deps.transformers.resolveChain([processIdentity.name])[0];
|
|
9995
|
+
if (ref) {
|
|
9996
|
+
for (const session of deps.manager.liveSessions()) {
|
|
9997
|
+
session.addTransformer(ref);
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
9680
10001
|
}
|
|
9681
10002
|
return { ack: true };
|
|
9682
10003
|
});
|
|
@@ -9892,16 +10213,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9892
10213
|
let resurrectParams = fromDisk;
|
|
9893
10214
|
if (hydraHints) {
|
|
9894
10215
|
resurrectParams = {
|
|
10216
|
+
...fromDisk,
|
|
9895
10217
|
hydraSessionId: params.sessionId,
|
|
9896
10218
|
upstreamSessionId: hydraHints.upstreamSessionId,
|
|
9897
10219
|
agentId: hydraHints.agentId,
|
|
9898
10220
|
cwd: hydraHints.cwd,
|
|
9899
|
-
title: hydraHints.title
|
|
9900
|
-
agentArgs: hydraHints.agentArgs
|
|
9901
|
-
currentModel: fromDisk?.currentModel,
|
|
9902
|
-
currentMode: fromDisk?.currentMode,
|
|
9903
|
-
agentCommands: fromDisk?.agentCommands,
|
|
9904
|
-
createdAt: fromDisk?.createdAt
|
|
10221
|
+
...hydraHints.title !== void 0 ? { title: hydraHints.title } : {},
|
|
10222
|
+
...hydraHints.agentArgs !== void 0 ? { agentArgs: hydraHints.agentArgs } : {}
|
|
9905
10223
|
};
|
|
9906
10224
|
}
|
|
9907
10225
|
if (!resurrectParams) {
|
|
@@ -9915,6 +10233,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9915
10233
|
...resurrectParams,
|
|
9916
10234
|
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
9917
10235
|
});
|
|
10236
|
+
wireDefaultTransformers(session, deps);
|
|
9918
10237
|
}
|
|
9919
10238
|
const client = bindClientToSession(
|
|
9920
10239
|
connection,
|
|
@@ -10004,6 +10323,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10004
10323
|
`session/prompt auto-resurrecting cold sessionId=${params.sessionId}`
|
|
10005
10324
|
);
|
|
10006
10325
|
session = await deps.manager.resurrect(fromDisk);
|
|
10326
|
+
wireDefaultTransformers(session, deps);
|
|
10007
10327
|
const client = bindClientToSession(
|
|
10008
10328
|
connection,
|
|
10009
10329
|
session,
|
|
@@ -10156,6 +10476,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10156
10476
|
throw err;
|
|
10157
10477
|
}
|
|
10158
10478
|
session = await deps.manager.resurrect(fromDisk);
|
|
10479
|
+
wireDefaultTransformers(session, deps);
|
|
10159
10480
|
}
|
|
10160
10481
|
const client = bindClientToSession(connection, session, state);
|
|
10161
10482
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
@@ -10206,6 +10527,32 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10206
10527
|
app.log.info(decision.logMessage);
|
|
10207
10528
|
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
10208
10529
|
});
|
|
10530
|
+
connection.onRequest("session/set_mode", async (rawParams) => {
|
|
10531
|
+
const params = rawParams;
|
|
10532
|
+
const sessionIdField = params?.sessionId;
|
|
10533
|
+
if (typeof sessionIdField === "string") {
|
|
10534
|
+
denyIfReadonly(sessionIdField, "session/set_mode");
|
|
10535
|
+
}
|
|
10536
|
+
if (!params || typeof params.sessionId !== "string") {
|
|
10537
|
+
const err = new Error("session/set_mode requires string sessionId");
|
|
10538
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
10539
|
+
throw err;
|
|
10540
|
+
}
|
|
10541
|
+
if (typeof params.modeId !== "string") {
|
|
10542
|
+
const err = new Error("session/set_mode requires string modeId");
|
|
10543
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
10544
|
+
throw err;
|
|
10545
|
+
}
|
|
10546
|
+
const session = deps.manager.get(params.sessionId);
|
|
10547
|
+
if (!session) {
|
|
10548
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10549
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10550
|
+
throw err;
|
|
10551
|
+
}
|
|
10552
|
+
const result = await session.forwardRequest("session/set_mode", rawParams);
|
|
10553
|
+
session.applyModeChange(params.modeId);
|
|
10554
|
+
return result;
|
|
10555
|
+
});
|
|
10209
10556
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
10210
10557
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
10211
10558
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -10478,6 +10825,17 @@ function buildInitializeResult() {
|
|
|
10478
10825
|
})
|
|
10479
10826
|
};
|
|
10480
10827
|
}
|
|
10828
|
+
function wireDefaultTransformers(session, deps) {
|
|
10829
|
+
if (!deps.transformers || !deps.manager) {
|
|
10830
|
+
return;
|
|
10831
|
+
}
|
|
10832
|
+
for (const name of deps.manager.defaultTransformers) {
|
|
10833
|
+
const ref = deps.transformers.resolveChain([name])[0];
|
|
10834
|
+
if (ref) {
|
|
10835
|
+
session.addTransformer(ref);
|
|
10836
|
+
}
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
10481
10839
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
10482
10840
|
void state;
|
|
10483
10841
|
void session;
|