@chipschat/hermes 1.0.7 → 1.0.10
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 +43 -11
- package/dist/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/main.js +1821 -1334
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -8,11 +8,11 @@ var __export = (target, all) => {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
// src/main.ts
|
|
11
|
-
import { randomBytes as
|
|
12
|
-
import { existsSync as
|
|
11
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
12
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
|
|
13
13
|
import { createServer as createServer2 } from "node:net";
|
|
14
14
|
import { homedir as homedir8 } from "node:os";
|
|
15
|
-
import { dirname as dirname6, join as
|
|
15
|
+
import { dirname as dirname6, join as join13, resolve as resolve6 } from "node:path";
|
|
16
16
|
|
|
17
17
|
// node_modules/zod/v4/classic/external.js
|
|
18
18
|
var external_exports = {};
|
|
@@ -19945,8 +19945,9 @@ var policyRule = external_exports.object({
|
|
|
19945
19945
|
external: external_exports.boolean().optional(),
|
|
19946
19946
|
irreversible: external_exports.boolean().optional(),
|
|
19947
19947
|
money_usd_over: external_exports.number().nonnegative().optional(),
|
|
19948
|
-
risk: external_exports.array(riskClass).max(8).optional()
|
|
19949
|
-
|
|
19948
|
+
risk: external_exports.array(riskClass).max(8).optional(),
|
|
19949
|
+
starts_with: external_exports.array(external_exports.string().min(1).max(400)).min(1).max(8).optional().describe("For an action that carries a command (ADR 0239): the words a part of it starts with. A command goes ahead on such rules only when every part starts with one that allows it.")
|
|
19950
|
+
}).optional().describe("Extra conditions on the action's flags, and for a command, how its parts start."),
|
|
19950
19951
|
decision: external_exports.enum(["allow", "ask", "deny"]).describe("What to do when the rule matches."),
|
|
19951
19952
|
approvers: external_exports.array(approverRef).max(16).optional().describe("Who may answer, when the decision is `ask`."),
|
|
19952
19953
|
reason: external_exports.string().max(400).optional().describe("Shown to the harness, and recorded in the audit log."),
|
|
@@ -20465,9 +20466,17 @@ var roomIdle = external_exports.object({
|
|
|
20465
20466
|
var roomNote = external_exports.object({
|
|
20466
20467
|
members: external_exports.array(roomMember).max(500).describe("The room's people, then its agents, each by name, anybody removed left out."),
|
|
20467
20468
|
notes: external_exports.record(external_exports.string().max(120), external_exports.string().max(64e3)).describe("The note for each who takes the turn (`for`), by their id: who is here, with handles, and that only those @-mentioned act. A harness puts it at the top of the turn."),
|
|
20469
|
+
rules: external_exports.record(external_exports.string().max(120), external_exports.string().max(4e3)).optional().describe("How the space works, for each who takes the turn, by their id: the words its note ends with (ADR 0250), that everyone here reads what it writes and, beside other agents, what an @ does and when to answer NO_REPLY. They stay the same while the space's agents do, so a harness whose session keeps its earlier turns may say them once, and the note without them after."),
|
|
20468
20470
|
working: external_exports.array(roomWork).max(500).optional().describe("The room's agents with a turn going as Chips sends this one (ADR 0104), the takers' own included. A turn nothing has been heard from for ten minutes is not counted."),
|
|
20469
20471
|
idle: external_exports.array(roomIdle).max(500).optional().describe("The room's other agents that have had a turn here: how long since the last one ended (ADR 0104).")
|
|
20470
20472
|
});
|
|
20473
|
+
function noteWithoutRules(room, taker) {
|
|
20474
|
+
const note = room?.notes[taker];
|
|
20475
|
+
const rules = room?.rules?.[taker];
|
|
20476
|
+
if (note === void 0 || rules === void 0 || !note.endsWith(rules))
|
|
20477
|
+
return note;
|
|
20478
|
+
return note.slice(0, note.length - rules.length).trimEnd();
|
|
20479
|
+
}
|
|
20471
20480
|
var MONTHS = [
|
|
20472
20481
|
"January",
|
|
20473
20482
|
"February",
|
|
@@ -20487,8 +20496,8 @@ var MONTHS = [
|
|
|
20487
20496
|
var CONTEXT_CEILING = { lines: 200, chars: 32e3 };
|
|
20488
20497
|
var CONTEXT_HEADING = "Said in this space since you last spoke:";
|
|
20489
20498
|
var contextBudget = external_exports.object({
|
|
20490
|
-
lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed,
|
|
20491
|
-
chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line,
|
|
20499
|
+
lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 40 when absent. Chips hands over 200 at most, and none when this is 0."),
|
|
20500
|
+
chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 16,000 when absent. Chips hands over 32,000 at most.")
|
|
20492
20501
|
}).describe("How much of the room a harness is handed with a message for it (ADR 0088). Its own to say: a budget promises nothing to anybody else.");
|
|
20493
20502
|
var contextLine = external_exports.object({
|
|
20494
20503
|
seq: external_exports.int().positive().describe("Where in the log it happened."),
|
|
@@ -20868,7 +20877,7 @@ var messageNew = defineEvent({
|
|
|
20868
20877
|
summary: "Someone said something, with any attachments as signed URLs the harness can fetch.",
|
|
20869
20878
|
requiresRun: false,
|
|
20870
20879
|
payload: deliveredMessage.extend({
|
|
20871
|
-
context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`:
|
|
20880
|
+
context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 40 lines and 16,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
|
|
20872
20881
|
room: roomNote.optional().describe("Who is in the room, with the handle that reaches each, and a note for each who takes the turn saying so and what an @ does (ADR 0092). A harness puts `notes[<taker>]` at the top of that turn. Absent on a message in no room."),
|
|
20873
20882
|
exchange: deliveredExchange.optional().describe("How the agents' talk in the room stands, once it is worth a word (ADR 0097): from eight messages between agents since a person last acted or an agent last made something new. A harness puts `note` at the top of the turn, as Chips's, ahead of the room's note. Absent below that, and worked out from the log, so a replay hands over the same facts."),
|
|
20874
20883
|
handed: external_exports.literal(true).optional().describe("Chips held this message when it was said and hands it over now a person has acted (ADR 0097): the newest of an agent's held messages, once; or, with `lead`, hands it to the room's lead. Its seq and id are the hand-over's, its `message_id` the message's own."),
|
|
@@ -20918,6 +20927,7 @@ var approvalResolved = defineEvent({
|
|
|
20918
20927
|
approval_id: id("approval"),
|
|
20919
20928
|
decision: external_exports.enum(["approved", "edited", "denied", "expired"]).describe("`expired` means nobody answered and the fallback applied."),
|
|
20920
20929
|
scope: external_exports.enum(["once", "session", "always"]).default("once").describe("How far the answer reaches. `always` becomes a policy rule."),
|
|
20930
|
+
everywhere: external_exports.boolean().optional().describe("With `always`: the rule is the harness's in every space, not only this one (ADR 0239). An owner or an admin only."),
|
|
20921
20931
|
updated_input: external_exports.unknown().optional().describe("The edited action input, when the decision is `edited`."),
|
|
20922
20932
|
reason: external_exports.string().max(4e3).optional().describe("Why. The harness gets to read this."),
|
|
20923
20933
|
resolved_by: external_exports.string().min(1).max(120).describe("The participant who answered, or `system` on expiry \u2014 a harness when it settles its own ask, answered in its own tools. Filled in by Chips from the credential that sent it; a value the sender writes is replaced."),
|
|
@@ -21182,6 +21192,10 @@ function namesInWords(names) {
|
|
|
21182
21192
|
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
21183
21193
|
}
|
|
21184
21194
|
function renderProjectNote(facts, options) {
|
|
21195
|
+
return `${renderProjectFacts(facts)}
|
|
21196
|
+
${renderProjectAsks(facts, options)}`;
|
|
21197
|
+
}
|
|
21198
|
+
function renderProjectFacts(facts) {
|
|
21185
21199
|
const files = `${facts.files} file${facts.files === 1 ? "" : "s"}`;
|
|
21186
21200
|
const lines2 = [
|
|
21187
21201
|
`The space's project is a git repository. Your copy is ${facts.folder} (${files}), ${facts.trouble === void 0 ? "brought up to date just now" : "not brought up to date"}.`
|
|
@@ -21199,9 +21213,9 @@ function renderProjectNote(facts, options) {
|
|
|
21199
21213
|
for (const key of facts.keys) {
|
|
21200
21214
|
lines2.push(`${key.path} holds what looks like ${key.what}, so your last change wasn't sent: take it out and read it from outside the project.`);
|
|
21201
21215
|
}
|
|
21202
|
-
return
|
|
21216
|
+
return lines2.join("\n");
|
|
21203
21217
|
}
|
|
21204
|
-
function
|
|
21218
|
+
function renderProjectAsks(facts, options) {
|
|
21205
21219
|
const lines2 = [
|
|
21206
21220
|
"Work on the project in that folder and leave git to Chips: when your turn ends, what you changed is committed under your name and sent to everyone here. Keep scratch work outside it.",
|
|
21207
21221
|
// Sent with the answer too, a project's file became a room file of its own, with versions of
|
|
@@ -21222,7 +21236,7 @@ function asks(facts, options) {
|
|
|
21222
21236
|
if (facts.pages === true) {
|
|
21223
21237
|
lines2.push("A page opens in Chips as one document: name every file it needs in its tags and imports (<link>, <script src>, <img src>, import), since it can't fetch files while it runs.");
|
|
21224
21238
|
}
|
|
21225
|
-
return lines2;
|
|
21239
|
+
return lines2.join("\n");
|
|
21226
21240
|
}
|
|
21227
21241
|
|
|
21228
21242
|
// node_modules/@chipschat/protocol/dist/src/events/harness-to-chips.js
|
|
@@ -21874,7 +21888,7 @@ var membershipRemoved = defineEvent({
|
|
|
21874
21888
|
payload: external_exports.object({
|
|
21875
21889
|
participant_id: external_exports.string().min(1).max(120).describe("Who left."),
|
|
21876
21890
|
removed_by: external_exports.string().min(1).max(120).optional().describe("Who removed them, if not themselves."),
|
|
21877
|
-
reason: external_exports.enum([...RELEASE_REASONS, "person_removed"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160). Written by Chips; absent, a person took them out or they left."),
|
|
21891
|
+
reason: external_exports.enum([...RELEASE_REASONS, "person_removed", "person_left"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160); `person_left`: they deleted their account and left it (ADR 0243). Written by Chips; absent, a person took them out or they left the room."),
|
|
21878
21892
|
moved_to: id("space").optional().describe("For an agent moved away: the room it went to, so this room can say so.")
|
|
21879
21893
|
})
|
|
21880
21894
|
});
|
|
@@ -22183,14 +22197,14 @@ var personRemoved = defineEvent({
|
|
|
22183
22197
|
type: "person.removed",
|
|
22184
22198
|
direction: "workspace",
|
|
22185
22199
|
sender: "chips",
|
|
22186
|
-
summary: "A person was removed from the workspace: signed out everywhere, out of every room, and the harnesses they owned handed
|
|
22200
|
+
summary: "A person was removed from the workspace, or deleted their account: signed out everywhere, out of every room, and the harnesses they owned handed on. What they said stays, under their name.",
|
|
22187
22201
|
requiresRun: false,
|
|
22188
22202
|
payload: external_exports.object({
|
|
22189
22203
|
participant_id: id("person").describe("Who was removed."),
|
|
22190
22204
|
display_name: external_exports.string().min(1).max(120).describe("What they were called when they were removed, so the audit needs no lookup."),
|
|
22191
|
-
removed_by: id("person").describe("The owner or admin who removed them."),
|
|
22205
|
+
removed_by: id("person").describe("The owner or admin who removed them, or they themselves when they deleted their account (ADR 0243)."),
|
|
22192
22206
|
spaces: external_exports.array(id("space")).max(1e3).optional().describe("The rooms they were taken out of, each with its own `membership.removed`."),
|
|
22193
|
-
harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, handed to `removed_by
|
|
22207
|
+
harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, each handed on by `harness.transferred`: to `removed_by`, or, for an account deleted, to an owner.")
|
|
22194
22208
|
})
|
|
22195
22209
|
});
|
|
22196
22210
|
var joinWho = {
|
|
@@ -22536,7 +22550,7 @@ var clientHelloFrame = external_exports.object({
|
|
|
22536
22550
|
})).max(64).optional().describe("Not read by Chips: a connection is its token's, and the server's `hello` names the token's harness and its agents (ADR 0087). Absent for a person's client."),
|
|
22537
22551
|
since: external_exports.int().nonnegative().optional().describe("Not read by Chips: a stream resumes from `subscribe`'s `since`."),
|
|
22538
22552
|
accepts: external_exports.array(external_exports.string().min(1).max(40)).max(32).optional().describe("Server frames beyond the base set this client understands, by the names in `SERVER_EXTENSIONS`. A name the server does not know is ignored rather than refused, so a client may name a frame a newer server sends and still be answered by an older one (ADR 0211, Phase 3). A client that says `events` is sent its replay in pages rather than one frame per event; one that says `presence` is sent an agent's state as it changes (ADR 0083); a harness's socket that says `restart` is sent `restart` when a person asks it to restart (ADR 0094); an owner's or admin's that says `join` is sent each join request as it changes (ADR 0122); a harness's that says `context` is sent `context.edit` when a person changes one of its context files (ADR 0134); a harness's that says `models` is sent `model.change` when a person changes an agent's model or effort (ADR 0140); a harness's that says `heartbeats` is sent `heartbeat.change` when a person switches an agent's heartbeat or changes how often it beats (ADR 0143); a harness's that says `context_windows` is sent `context_window.compact` and `context_window.change` when a person asks an agent to compact now or changes the limit it compacts itself at (ADR 0146); a person's that says `folders` is sent `folder.open` when they ask, from another of their devices, for a room's folder to be opened in Finder (ADR 0225), and `folder.run` when they ask for its project to be run or stopped there (ADR 0226); a person's that says `folder_runs` is sent `folder.run_state` when their Mac says how a run of theirs stands (ADR 0226); one that says nothing is sent what it was always sent, since an unknown frame is dropped in silence."),
|
|
22539
|
-
context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088):
|
|
22553
|
+
context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 40 lines and 16,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
|
|
22540
22554
|
}).describe("The first frame a client sends.");
|
|
22541
22555
|
var subscribeFrame = external_exports.object({
|
|
22542
22556
|
frame: external_exports.literal("subscribe"),
|
|
@@ -25098,8 +25112,19 @@ async function redeem(url2, secret, options, send, machine) {
|
|
|
25098
25112
|
throw await refused(answered);
|
|
25099
25113
|
return await answered.json();
|
|
25100
25114
|
}
|
|
25115
|
+
function reaching(send, url2) {
|
|
25116
|
+
return async (input2, init) => {
|
|
25117
|
+
try {
|
|
25118
|
+
return await send(input2, init);
|
|
25119
|
+
} catch (error62) {
|
|
25120
|
+
if (error62 instanceof Error && error62.name === "AbortError")
|
|
25121
|
+
throw error62;
|
|
25122
|
+
throw new JoinError("unreachable", `Can't reach Chips at ${url2} from this computer. It has to be on the same network as Chips, on its tailnet, or Chips has to be at an address this computer can reach.`);
|
|
25123
|
+
}
|
|
25124
|
+
};
|
|
25125
|
+
}
|
|
25101
25126
|
function askOrRedeem(server, options) {
|
|
25102
|
-
const send = options.fetch ?? fetch;
|
|
25127
|
+
const send = reaching(options.fetch ?? fetch, server.url);
|
|
25103
25128
|
const machine = options.machine ?? hostname3();
|
|
25104
25129
|
return server.secret === void 0 ? ask(server.url, options, send, machine) : redeem(server.url, server.secret, options, send, machine);
|
|
25105
25130
|
}
|
|
@@ -25265,14 +25290,31 @@ async function sendAnswer(answer, options) {
|
|
|
25265
25290
|
}
|
|
25266
25291
|
|
|
25267
25292
|
// node_modules/@chipschat/harness/dist/prompt.js
|
|
25293
|
+
function takerOf(message, options) {
|
|
25294
|
+
return options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
|
|
25295
|
+
}
|
|
25296
|
+
function othersBeside(message, taker) {
|
|
25297
|
+
return (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
|
|
25298
|
+
}
|
|
25299
|
+
function rulesFor(message, options = {}) {
|
|
25300
|
+
const taker = takerOf(message, options);
|
|
25301
|
+
const { project } = options;
|
|
25302
|
+
const rules = message.room?.rules ?? {};
|
|
25303
|
+
return [
|
|
25304
|
+
taker === void 0 ? void 0 : rules[taker],
|
|
25305
|
+
project === void 0 ? void 0 : renderProjectAsks(project, { othersInRoom: othersBeside(message, taker) }),
|
|
25306
|
+
options.sendsFiles === true ? sendingFiles({ project: project !== void 0 }) : void 0
|
|
25307
|
+
].filter((part) => part !== void 0 && part !== "").join("\n\n");
|
|
25308
|
+
}
|
|
25268
25309
|
function promptFor(message, options = {}) {
|
|
25269
|
-
const taker =
|
|
25310
|
+
const taker = takerOf(message, options);
|
|
25311
|
+
const held = options.rulesHeld === true;
|
|
25270
25312
|
const notes = message.room?.notes ?? {};
|
|
25271
|
-
const note = taker === void 0 ? void 0 : notes[taker];
|
|
25313
|
+
const note = taker === void 0 ? void 0 : held ? noteWithoutRules(message.room, taker) : notes[taker];
|
|
25272
25314
|
const who = options.sayWho === false ? "" : `${withRole(message.from.display_name, message.from)}: `;
|
|
25273
25315
|
const files = (message.attachments ?? []).map(renderAttachment);
|
|
25274
25316
|
const words4 = [`${who}${message.text}`, ...files].join("\n");
|
|
25275
|
-
const sending = options.sendsFiles === true ? sendingFiles({ project: options.project !== void 0 }) : void 0;
|
|
25317
|
+
const sending = options.sendsFiles === true && !held ? sendingFiles({ project: options.project !== void 0 }) : void 0;
|
|
25276
25318
|
const replying = renderReplyingTo(message.replying_to, {
|
|
25277
25319
|
sender: {
|
|
25278
25320
|
id: message.from.participant_id,
|
|
@@ -25284,8 +25326,7 @@ function promptFor(message, options = {}) {
|
|
|
25284
25326
|
at: message.ts,
|
|
25285
25327
|
zone: options.zone
|
|
25286
25328
|
});
|
|
25287
|
-
const
|
|
25288
|
-
const project = options.project === void 0 ? void 0 : renderProjectNote(options.project, { othersInRoom: others });
|
|
25329
|
+
const project = options.project === void 0 ? void 0 : held ? renderProjectFacts(options.project) : renderProjectNote(options.project, { othersInRoom: othersBeside(message, taker) });
|
|
25289
25330
|
return [
|
|
25290
25331
|
message.exchange?.note,
|
|
25291
25332
|
note,
|
|
@@ -25472,7 +25513,10 @@ function nodeOnPath(env = process.env) {
|
|
|
25472
25513
|
}
|
|
25473
25514
|
var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
25474
25515
|
var LOAD_TRIES = 40;
|
|
25475
|
-
var runOf = (options) => options.run ?? ((command) =>
|
|
25516
|
+
var runOf = (options) => options.run ?? ((command) => execFileSync2(command[0], command.slice(1), {
|
|
25517
|
+
encoding: "utf8",
|
|
25518
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
25519
|
+
}));
|
|
25476
25520
|
var ours = (path) => !existsSync3(path) || readFileSync3(path, "utf8").includes(MARK);
|
|
25477
25521
|
function runOne(command, run2, retryMs) {
|
|
25478
25522
|
const tries = command[0] === "launchctl" && command[1] === "bootstrap" ? LOAD_TRIES : 1;
|
|
@@ -25495,6 +25539,24 @@ function runAll(commands, options) {
|
|
|
25495
25539
|
runOne(command, run2, options.retryMs ?? 250);
|
|
25496
25540
|
}
|
|
25497
25541
|
var uidOf = (options) => options.uid ?? process.getuid?.() ?? 0;
|
|
25542
|
+
function lingers(options) {
|
|
25543
|
+
const run2 = runOf(options);
|
|
25544
|
+
const uid = String(uidOf(options));
|
|
25545
|
+
try {
|
|
25546
|
+
const on = run2(["loginctl", "show-user", uid, "--property=Linger", "--value"]);
|
|
25547
|
+
if (typeof on === "string" && on.trim() === "yes")
|
|
25548
|
+
return true;
|
|
25549
|
+
} catch {
|
|
25550
|
+
}
|
|
25551
|
+
try {
|
|
25552
|
+
run2(["loginctl", "enable-linger", uid]);
|
|
25553
|
+
return true;
|
|
25554
|
+
} catch {
|
|
25555
|
+
return false;
|
|
25556
|
+
}
|
|
25557
|
+
}
|
|
25558
|
+
var sayOf = (options) => options.say ?? ((line2) => void process.stderr.write(`${line2}
|
|
25559
|
+
`));
|
|
25498
25560
|
function keepRunning(spec, options = {}) {
|
|
25499
25561
|
if (machineOf(options.machine).env["CHIPS_KEEP"] === "off")
|
|
25500
25562
|
return { state: "off" };
|
|
@@ -25509,6 +25571,9 @@ function keepRunning(spec, options = {}) {
|
|
|
25509
25571
|
writeFileSync2(file3.path, file3.contents, { mode: 384 });
|
|
25510
25572
|
chmodSync2(file3.path, 384);
|
|
25511
25573
|
runAll(startCommands(file3.kind, spec.label, file3.path, uidOf(options)), options);
|
|
25574
|
+
if (file3.kind === "systemd" && !lingers(options)) {
|
|
25575
|
+
sayOf(options)("It stops when you log out. To keep it running on a machine nobody logs in to, run this once: sudo loginctl enable-linger $USER");
|
|
25576
|
+
}
|
|
25512
25577
|
return { state: "kept", path: file3.path, logFile: file3.logFile };
|
|
25513
25578
|
}
|
|
25514
25579
|
function stopKeeping(label2, options = {}) {
|
|
@@ -25555,7 +25620,7 @@ function fileStore(path) {
|
|
|
25555
25620
|
|
|
25556
25621
|
// src/bridge.ts
|
|
25557
25622
|
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
25558
|
-
import { join as
|
|
25623
|
+
import { join as join10 } from "node:path";
|
|
25559
25624
|
|
|
25560
25625
|
// src/context.ts
|
|
25561
25626
|
import { spawn } from "node:child_process";
|
|
@@ -25578,7 +25643,7 @@ import { basename as basename3, dirname as dirname4, join as join6 } from "node:
|
|
|
25578
25643
|
|
|
25579
25644
|
// src/home.ts
|
|
25580
25645
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
25581
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
|
|
25646
|
+
import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
|
|
25582
25647
|
import { homedir as homedir4 } from "node:os";
|
|
25583
25648
|
import { join as join5, resolve as resolve2 } from "node:path";
|
|
25584
25649
|
function hermesRoot(env = process.env) {
|
|
@@ -25605,6 +25670,14 @@ function envValue(home2, name2) {
|
|
|
25605
25670
|
}
|
|
25606
25671
|
return found;
|
|
25607
25672
|
}
|
|
25673
|
+
function appendEnv(home2, name2, value) {
|
|
25674
|
+
const path = join5(home2, ".env");
|
|
25675
|
+
const text = readFileSync5(path, "utf8");
|
|
25676
|
+
const mode = statSync(path).mode & 511;
|
|
25677
|
+
appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${name2}=${value}
|
|
25678
|
+
`);
|
|
25679
|
+
chmodSync3(path, mode);
|
|
25680
|
+
}
|
|
25608
25681
|
function configText(home2) {
|
|
25609
25682
|
const path = join5(home2, "config.yaml");
|
|
25610
25683
|
try {
|
|
@@ -25839,13 +25912,9 @@ function fileOf(source2, name2) {
|
|
|
25839
25912
|
}
|
|
25840
25913
|
var byOrder = (a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) || a.profile.localeCompare(b.profile);
|
|
25841
25914
|
var Picture = class {
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
25845
|
-
shared = /* @__PURE__ */ new Map();
|
|
25846
|
-
/** The agents and the files each reads, in page order. */
|
|
25847
|
-
agents = /* @__PURE__ */ new Map();
|
|
25848
|
-
constructor(readers, first, env = process.env) {
|
|
25915
|
+
constructor(readers, first, env = process.env, ownProfiles = false) {
|
|
25916
|
+
this.first = first;
|
|
25917
|
+
this.ownProfiles = ownProfiles;
|
|
25849
25918
|
const byProfile = /* @__PURE__ */ new Map();
|
|
25850
25919
|
const sourcesFor = (profile) => {
|
|
25851
25920
|
let sources = byProfile.get(profile.home);
|
|
@@ -25867,7 +25936,7 @@ var Picture = class {
|
|
|
25867
25936
|
}
|
|
25868
25937
|
const taken = /* @__PURE__ */ new Set();
|
|
25869
25938
|
const ids2 = /* @__PURE__ */ new Set();
|
|
25870
|
-
const sharing = [...this.readers.values()].filter(({ source: source2, agents }) => source2.kind === "project" || source2
|
|
25939
|
+
const sharing = [...this.readers.values()].filter(({ source: source2, agents }) => source2.kind === "project" || this.forNewAgents(source2) || agents.length > 1).sort((a, b) => (a.source.profile === first?.name ? 0 : 1) - (b.source.profile === first?.name ? 0 : 1) || byOrder(a.source, b.source));
|
|
25871
25940
|
for (const { source: source2 } of sharing) {
|
|
25872
25941
|
const name2 = taken.has(source2.file) ? `${source2.file} (${source2.profile})` : source2.file;
|
|
25873
25942
|
taken.add(name2);
|
|
@@ -25877,8 +25946,24 @@ var Picture = class {
|
|
|
25877
25946
|
this.shared.set(source2.path, { id: id2, name: name2 });
|
|
25878
25947
|
}
|
|
25879
25948
|
}
|
|
25949
|
+
first;
|
|
25950
|
+
ownProfiles;
|
|
25951
|
+
/** Each file by its path, and the agents that read it. */
|
|
25952
|
+
readers = /* @__PURE__ */ new Map();
|
|
25953
|
+
/** The shared files, each with its id and the name the set shows. */
|
|
25954
|
+
shared = /* @__PURE__ */ new Map();
|
|
25955
|
+
/** The agents and the files each reads, in page order. */
|
|
25956
|
+
agents = /* @__PURE__ */ new Map();
|
|
25957
|
+
/**
|
|
25958
|
+
* Whether each new agent starts with this file or a copy of it: the first profile's files, or
|
|
25959
|
+
* where each new agent gets a profile of its own, the first profile's SOUL.md and the project file.
|
|
25960
|
+
*/
|
|
25961
|
+
forNewAgents(source2) {
|
|
25962
|
+
if (source2.profile !== this.first?.name) return false;
|
|
25963
|
+
return !this.ownProfiles || source2.kind === "soul" || source2.kind === "project";
|
|
25964
|
+
}
|
|
25880
25965
|
/** The harness's own set, as `PUT /v1/harnesses/:id/context` takes it. */
|
|
25881
|
-
harnessCopy(
|
|
25966
|
+
harnessCopy() {
|
|
25882
25967
|
const shown2 = [];
|
|
25883
25968
|
const notShown = [];
|
|
25884
25969
|
for (const [path, { id: id2, name: name2 }] of this.shared) {
|
|
@@ -25894,8 +25979,7 @@ var Picture = class {
|
|
|
25894
25979
|
...file3,
|
|
25895
25980
|
id: id2,
|
|
25896
25981
|
used_by: agents.length,
|
|
25897
|
-
|
|
25898
|
-
...source2.profile === first?.name ? { for_new_agents: true } : {}
|
|
25982
|
+
...this.forNewAgents(source2) ? { for_new_agents: true } : {}
|
|
25899
25983
|
});
|
|
25900
25984
|
}
|
|
25901
25985
|
return {
|
|
@@ -26056,9 +26140,9 @@ var Contexts = class {
|
|
|
26056
26140
|
/** Every copy read again, and sent where it moved. */
|
|
26057
26141
|
refresh() {
|
|
26058
26142
|
return this.inLine(async () => {
|
|
26059
|
-
const { readers, first } = this.roster();
|
|
26060
|
-
const picture = new Picture(readers, first, this.env);
|
|
26061
|
-
await this.port.harness(picture.harnessCopy(
|
|
26143
|
+
const { readers, first, ownProfiles } = this.roster();
|
|
26144
|
+
const picture = new Picture(readers, first, this.env, ownProfiles);
|
|
26145
|
+
await this.port.harness(picture.harnessCopy()).catch((error62) => {
|
|
26062
26146
|
this.log(`the harness's context not sent: ${messageOf(error62)}`);
|
|
26063
26147
|
});
|
|
26064
26148
|
for (const { agentId: agentId2 } of readers) {
|
|
@@ -26076,12 +26160,12 @@ var Contexts = class {
|
|
|
26076
26160
|
*/
|
|
26077
26161
|
async edit(frame) {
|
|
26078
26162
|
const answer = await this.inLine(async () => {
|
|
26079
|
-
const { readers, first } = this.roster();
|
|
26163
|
+
const { readers, first, ownProfiles } = this.roster();
|
|
26080
26164
|
const copyNow = () => {
|
|
26081
|
-
const picture = new Picture(readers, first, this.env);
|
|
26082
|
-
return frame.agent_id === void 0 ? picture.harnessCopy(
|
|
26165
|
+
const picture = new Picture(readers, first, this.env, ownProfiles);
|
|
26166
|
+
return frame.agent_id === void 0 ? picture.harnessCopy() : picture.agentCopy(frame.agent_id);
|
|
26083
26167
|
};
|
|
26084
|
-
const found = new Picture(readers, first, this.env).find(frame.agent_id, frame.name);
|
|
26168
|
+
const found = new Picture(readers, first, this.env, ownProfiles).find(frame.agent_id, frame.name);
|
|
26085
26169
|
if (found === void 0) {
|
|
26086
26170
|
throw new Refuse(`Hermes gives this agent no file called ${frame.name}.`, copyNow(), "failed");
|
|
26087
26171
|
}
|
|
@@ -26148,6 +26232,8 @@ function numbersOf(profile, body) {
|
|
|
26148
26232
|
if (typeof body !== "object" || body === null) return void 0;
|
|
26149
26233
|
const top = body;
|
|
26150
26234
|
if (top["hook_event_name"] !== "post_api_request") return void 0;
|
|
26235
|
+
const sender = top["profile"];
|
|
26236
|
+
if (typeof sender === "string" && sender !== profile && sender !== "custom") return void 0;
|
|
26151
26237
|
const extra = typeof top["extra"] === "object" && top["extra"] !== null ? top["extra"] : {};
|
|
26152
26238
|
const named = typeof top["session_id"] === "string" ? top["session_id"] : "";
|
|
26153
26239
|
const task2 = typeof extra["task_id"] === "string" && extra["task_id"] !== "" ? extra["task_id"] : void 0;
|
|
@@ -26453,6 +26539,13 @@ var HermesClient = class {
|
|
|
26453
26539
|
const latest = read["session_id"];
|
|
26454
26540
|
return typeof latest === "string" && latest !== "" ? latest : sessionId;
|
|
26455
26541
|
}
|
|
26542
|
+
/**
|
|
26543
|
+
* The models Hermes has for this profile, as its own picker lists them: `GET /api/model/options`,
|
|
26544
|
+
* only the providers the profile is set up for (`models.ts`).
|
|
26545
|
+
*/
|
|
26546
|
+
async modelOptions() {
|
|
26547
|
+
return await this.call("GET", "/api/model/options?include_unconfigured=false");
|
|
26548
|
+
}
|
|
26456
26549
|
/** A run in a session: what it is told, and under a key a retry of the same turn repeats. */
|
|
26457
26550
|
async startRun(input2) {
|
|
26458
26551
|
const started = await this.call(
|
|
@@ -26462,6 +26555,7 @@ var HermesClient = class {
|
|
|
26462
26555
|
input: input2.text,
|
|
26463
26556
|
session_id: input2.sessionId,
|
|
26464
26557
|
...input2.model === void 0 ? {} : { model: input2.model },
|
|
26558
|
+
...input2.provider === void 0 ? {} : { provider: input2.provider },
|
|
26465
26559
|
...input2.instructions === void 0 ? {} : { instructions: input2.instructions }
|
|
26466
26560
|
},
|
|
26467
26561
|
{ "Idempotency-Key": input2.key.slice(0, 255) }
|
|
@@ -26558,7 +26652,7 @@ async function failureOf(answer) {
|
|
|
26558
26652
|
}
|
|
26559
26653
|
|
|
26560
26654
|
// src/hooks.ts
|
|
26561
|
-
import {
|
|
26655
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
26562
26656
|
import { join as join7 } from "node:path";
|
|
26563
26657
|
var HOOK_NAME = "chips";
|
|
26564
26658
|
var SECRET_ENV = "CHIPS_HOOK_SECRET";
|
|
@@ -26595,12 +26689,7 @@ function entryLines(indent, url2, secret) {
|
|
|
26595
26689
|
function secretInEnv(home2, secret) {
|
|
26596
26690
|
const path = join7(home2, ".env");
|
|
26597
26691
|
if (!existsSync7(path)) return false;
|
|
26598
|
-
if (envValue(home2, SECRET_ENV)
|
|
26599
|
-
const text = readFileSync7(path, "utf8");
|
|
26600
|
-
const mode = statSync3(path).mode & 511;
|
|
26601
|
-
appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${SECRET_ENV}=${secret}
|
|
26602
|
-
`);
|
|
26603
|
-
chmodSync3(path, mode);
|
|
26692
|
+
if (envValue(home2, SECRET_ENV) === void 0) appendEnv(home2, SECRET_ENV, secret);
|
|
26604
26693
|
return true;
|
|
26605
26694
|
}
|
|
26606
26695
|
function addHook(home2, port, profile, secret) {
|
|
@@ -26661,1417 +26750,1807 @@ function addHook(home2, port, profile, secret) {
|
|
|
26661
26750
|
return { state: "added", path };
|
|
26662
26751
|
}
|
|
26663
26752
|
|
|
26664
|
-
// src/
|
|
26665
|
-
var
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26753
|
+
// src/models.ts
|
|
26754
|
+
var MODEL_CHOICES_MAX2 = 40;
|
|
26755
|
+
var LABEL_MAX = 80;
|
|
26756
|
+
var DEFAULT_LABEL = "Hermes's default";
|
|
26757
|
+
function choiceId(provider, model) {
|
|
26758
|
+
return `${provider}/${model}`;
|
|
26759
|
+
}
|
|
26760
|
+
function runModelOf(id2) {
|
|
26761
|
+
const at = id2.indexOf("/");
|
|
26762
|
+
return at <= 0 ? { model: id2 } : { provider: id2.slice(0, at), model: id2.slice(at + 1) };
|
|
26763
|
+
}
|
|
26764
|
+
function providersOf(options) {
|
|
26765
|
+
const ready = (options.providers ?? []).filter(
|
|
26766
|
+
(provider) => provider.authenticated !== false && (provider.models ?? []).length > 0
|
|
26767
|
+
);
|
|
26768
|
+
const current = (provider) => provider.is_current === true || provider.slug === options.provider;
|
|
26769
|
+
return [...ready.filter(current), ...ready.filter((provider) => !current(provider))];
|
|
26770
|
+
}
|
|
26771
|
+
function choicesOf(options) {
|
|
26772
|
+
const choices = [];
|
|
26773
|
+
for (const provider of providersOf(options)) {
|
|
26774
|
+
for (const model of provider.models ?? []) {
|
|
26775
|
+
const id2 = choiceId(provider.slug, model);
|
|
26776
|
+
if (choices.some((choice) => choice.id === id2)) continue;
|
|
26777
|
+
choices.push({ id: id2, label: `${model} \xB7 ${provider.name ?? provider.slug}`.slice(0, LABEL_MAX), efforts: [] });
|
|
26778
|
+
}
|
|
26779
|
+
}
|
|
26780
|
+
return choices;
|
|
26781
|
+
}
|
|
26782
|
+
function defaultOf(options) {
|
|
26783
|
+
const model = options.model?.trim();
|
|
26784
|
+
if (model === void 0 || model === "") return void 0;
|
|
26785
|
+
return options.provider === void 0 || options.provider === "" ? model : choiceId(options.provider, model);
|
|
26786
|
+
}
|
|
26787
|
+
function matched(choices, model) {
|
|
26788
|
+
if (choices.some((choice) => choice.id === model)) return model;
|
|
26789
|
+
return choices.find((choice) => runModelOf(choice.id).model === model)?.id ?? model;
|
|
26790
|
+
}
|
|
26791
|
+
function copyOf(options, own2) {
|
|
26792
|
+
let choices = choicesOf(options);
|
|
26793
|
+
const fallback = defaultOf(options);
|
|
26794
|
+
const byDefault = fallback === void 0 ? void 0 : matched(choices, fallback);
|
|
26795
|
+
const mine = own2 === void 0 ? void 0 : matched(choices, own2);
|
|
26796
|
+
const model = mine ?? byDefault ?? choices[0]?.id;
|
|
26797
|
+
if (model === void 0) return void 0;
|
|
26798
|
+
const keep = [model, ...byDefault === void 0 ? [] : [byDefault]];
|
|
26799
|
+
for (const id2 of keep) {
|
|
26800
|
+
if (!choices.some((choice) => choice.id === id2)) choices = [{ id: id2, label: id2.slice(0, LABEL_MAX), efforts: [] }, ...choices];
|
|
26801
|
+
}
|
|
26802
|
+
let others = MODEL_CHOICES_MAX2 - keep.filter((id2, at) => keep.indexOf(id2) === at).length;
|
|
26803
|
+
choices = choices.filter((choice) => keep.includes(choice.id) || others-- > 0);
|
|
26672
26804
|
return {
|
|
26673
|
-
|
|
26674
|
-
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26805
|
+
model,
|
|
26806
|
+
own: mine !== void 0,
|
|
26807
|
+
...byDefault === void 0 ? {} : { default: { label: DEFAULT_LABEL, model: byDefault } },
|
|
26808
|
+
choices,
|
|
26809
|
+
editable: true
|
|
26678
26810
|
};
|
|
26679
26811
|
}
|
|
26680
|
-
|
|
26681
|
-
|
|
26812
|
+
|
|
26813
|
+
// src/profiles.ts
|
|
26814
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
26815
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, rmSync as rmSync4 } from "node:fs";
|
|
26816
|
+
import { createConnection } from "node:net";
|
|
26817
|
+
import { join as join9 } from "node:path";
|
|
26818
|
+
|
|
26819
|
+
// src/gateway.ts
|
|
26820
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
26821
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
26822
|
+
import { accessSync as accessSync2, appendFileSync as appendFileSync2, closeSync as closeSync2, constants as constants2, existsSync as existsSync8, openSync as openSync2, readFileSync as readFileSync8, readSync, statSync as statSync3 } from "node:fs";
|
|
26823
|
+
import { homedir as homedir5 } from "node:os";
|
|
26824
|
+
import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join8, resolve as resolve3 } from "node:path";
|
|
26825
|
+
var TIMING = {
|
|
26826
|
+
stopMs: 15e3,
|
|
26827
|
+
killMs: 5e3,
|
|
26828
|
+
earlyMs: 1e4,
|
|
26829
|
+
upMs: 9e4,
|
|
26830
|
+
serviceMs: 15e4,
|
|
26831
|
+
pollMs: 500
|
|
26832
|
+
};
|
|
26833
|
+
var EXPAT = "/opt/homebrew/opt/expat/lib/libexpat.1.dylib";
|
|
26834
|
+
var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
|
|
26835
|
+
function gatewaysOf(agents) {
|
|
26836
|
+
return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
|
|
26682
26837
|
}
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
|
|
26686
|
-
|
|
26838
|
+
function isLocal(url2) {
|
|
26839
|
+
try {
|
|
26840
|
+
return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
|
|
26841
|
+
} catch {
|
|
26842
|
+
return false;
|
|
26843
|
+
}
|
|
26687
26844
|
}
|
|
26688
|
-
function
|
|
26689
|
-
const
|
|
26690
|
-
|
|
26691
|
-
|
|
26692
|
-
|
|
26845
|
+
function portOf(url2) {
|
|
26846
|
+
const parsed = new URL(url2);
|
|
26847
|
+
return Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
|
|
26848
|
+
}
|
|
26849
|
+
function line(words4) {
|
|
26850
|
+
const one = words4.replace(/\s+/g, " ").trim();
|
|
26851
|
+
return one.length <= 200 ? one : `${one.slice(0, 199)}\u2026`;
|
|
26852
|
+
}
|
|
26853
|
+
function whyFrom(output2, code) {
|
|
26854
|
+
const lines2 = output2.split("\n").map((each) => each.trim()).filter((each) => each !== "");
|
|
26855
|
+
const why = lines2.findLast((each) => /^[A-Za-z_][\w.]*(Error|Exception)\b/.test(each)) ?? lines2.at(-1);
|
|
26856
|
+
if (why === void 0) return code === null || code === void 0 ? "it was stopped" : `it exited ${code}`;
|
|
26857
|
+
return why.length <= 200 ? why : why.replace(/\/(?:[^\s/(),:'"]+\/)+([^\s/(),:'"]+)/g, "$1");
|
|
26858
|
+
}
|
|
26859
|
+
function gatewayRunArgs(command) {
|
|
26860
|
+
const words4 = command.trim().split(/\s+/);
|
|
26861
|
+
const at = words4.findIndex((word) => basename4(word) === "hermes");
|
|
26862
|
+
if (at === -1) return void 0;
|
|
26863
|
+
const args = words4.slice(at + 1);
|
|
26864
|
+
const plain = [];
|
|
26865
|
+
for (let next = 0; next < args.length; next += 1) {
|
|
26866
|
+
const word = args[next] ?? "";
|
|
26867
|
+
if (word === "-p" || word === "--profile") next += 1;
|
|
26868
|
+
else if (!word.startsWith("-")) plain.push(word);
|
|
26693
26869
|
}
|
|
26870
|
+
return plain[0] === "gateway" && plain[1] === "run" ? args : void 0;
|
|
26694
26871
|
}
|
|
26695
|
-
|
|
26696
|
-
|
|
26697
|
-
|
|
26698
|
-
|
|
26699
|
-
|
|
26700
|
-
|
|
26701
|
-
|
|
26702
|
-
|
|
26703
|
-
|
|
26704
|
-
|
|
26705
|
-
|
|
26706
|
-
|
|
26707
|
-
|
|
26708
|
-
|
|
26709
|
-
|
|
26710
|
-
|
|
26711
|
-
image_generate: "image",
|
|
26712
|
-
memory: "memory",
|
|
26713
|
-
delegate_task: "handoff"
|
|
26714
|
-
};
|
|
26715
|
-
function keyOf(agent) {
|
|
26716
|
-
return `hermes:${(agent.address.profile ?? "default").toLowerCase()}:${agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
26872
|
+
function serviceOf(system, root2, env = {}) {
|
|
26873
|
+
const home2 = resolve3(root2);
|
|
26874
|
+
const native = resolve3(system.home, ".hermes");
|
|
26875
|
+
const suffix = home2 === native ? "" : dirname5(home2) === join8(native, "profiles") ? basename4(home2) : createHash3("sha256").update(home2).digest("hex").slice(0, 8);
|
|
26876
|
+
if (system.platform === "darwin") {
|
|
26877
|
+
const path = join8(system.home, "Library", "LaunchAgents", `ai.hermes.gateway${suffix ? `-${suffix}` : ""}.plist`);
|
|
26878
|
+
return system.exists(path) ? { kind: "launchd", path } : void 0;
|
|
26879
|
+
}
|
|
26880
|
+
if (system.platform === "linux") {
|
|
26881
|
+
const unit = `hermes-gateway${suffix ? `-${suffix}` : ""}.service`;
|
|
26882
|
+
const user = join8(env["XDG_CONFIG_HOME"] || join8(system.home, ".config"), "systemd", "user", unit);
|
|
26883
|
+
if (system.exists(user)) return { kind: "systemd", path: user };
|
|
26884
|
+
const machine = join8("/etc/systemd/system", unit);
|
|
26885
|
+
if (system.exists(machine)) return { kind: "systemd", path: machine, system: true };
|
|
26886
|
+
}
|
|
26887
|
+
return void 0;
|
|
26717
26888
|
}
|
|
26718
|
-
|
|
26889
|
+
function expand(text, env) {
|
|
26890
|
+
let out = "";
|
|
26891
|
+
let at = 0;
|
|
26892
|
+
while (at < text.length) {
|
|
26893
|
+
const char = text[at] ?? "";
|
|
26894
|
+
if (char === "\\" && at + 1 < text.length) {
|
|
26895
|
+
out += text[at + 1];
|
|
26896
|
+
at += 2;
|
|
26897
|
+
} else if (char !== "$") {
|
|
26898
|
+
out += char;
|
|
26899
|
+
at += 1;
|
|
26900
|
+
} else if (text[at + 1] === "{") {
|
|
26901
|
+
let depth = 1;
|
|
26902
|
+
let end2 = at + 2;
|
|
26903
|
+
for (; end2 < text.length; end2 += 1) {
|
|
26904
|
+
if (text[end2] === "{") depth += 1;
|
|
26905
|
+
else if (text[end2] === "}" && --depth === 0) break;
|
|
26906
|
+
}
|
|
26907
|
+
const inner = /^([A-Za-z_][A-Za-z0-9_]*)(?:(:?)-([\s\S]*))?$/.exec(text.slice(at + 2, end2));
|
|
26908
|
+
if (inner !== null) {
|
|
26909
|
+
const value = env[inner[1] ?? ""];
|
|
26910
|
+
const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
|
|
26911
|
+
out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
|
|
26912
|
+
}
|
|
26913
|
+
at = end2 + 1;
|
|
26914
|
+
} else {
|
|
26915
|
+
const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
|
|
26916
|
+
out += name2 === void 0 ? "$" : env[name2] ?? "";
|
|
26917
|
+
at += 1 + (name2?.length ?? 0);
|
|
26918
|
+
}
|
|
26919
|
+
}
|
|
26920
|
+
return out;
|
|
26921
|
+
}
|
|
26922
|
+
function wrapperOf(text, env) {
|
|
26923
|
+
for (const each of text.split("\n")) {
|
|
26924
|
+
const found = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*="(?:[^"\\]|\\.)*"\s+)*)exec\s+"([^"]+)"/.exec(each);
|
|
26925
|
+
if (found === null) continue;
|
|
26926
|
+
const vars = {};
|
|
26927
|
+
for (const set2 of (found[1] ?? "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:[^"\\]|\\.)*)"/g)) {
|
|
26928
|
+
vars[set2[1] ?? ""] = expand(set2[2] ?? "", env);
|
|
26929
|
+
}
|
|
26930
|
+
return { entry: expand(found[2] ?? "", env), vars };
|
|
26931
|
+
}
|
|
26932
|
+
return void 0;
|
|
26933
|
+
}
|
|
26934
|
+
var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
|
|
26935
|
+
var messageOf2 = (error62) => error62 instanceof Error ? error62.message : String(error62);
|
|
26936
|
+
function gatewayEnv(env) {
|
|
26937
|
+
return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
|
|
26938
|
+
}
|
|
26939
|
+
async function runHermes(system, env, args, timeoutMs, log2) {
|
|
26940
|
+
const hermes = system.which("hermes", env);
|
|
26941
|
+
if (hermes === void 0) return { code: null, output: "The hermes command isn't on this machine's PATH." };
|
|
26942
|
+
const plain = gatewayEnv(env);
|
|
26943
|
+
const ran = await system.run(hermes, args, plain, timeoutMs);
|
|
26944
|
+
if (ran.code === 0 || !ran.output.includes(EXPAT_SYMBOL) || !system.exists(EXPAT)) return ran;
|
|
26945
|
+
const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
|
|
26946
|
+
if (wrapper === void 0) return ran;
|
|
26947
|
+
const expat = dirname5(EXPAT);
|
|
26948
|
+
log2(
|
|
26949
|
+
`hermes ${args.slice(0, 2).join(" ")} stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Running ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
|
|
26950
|
+
);
|
|
26951
|
+
const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
|
|
26952
|
+
return system.run(wrapper.entry, args, { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries }, timeoutMs);
|
|
26953
|
+
}
|
|
26954
|
+
var Gateways = class {
|
|
26719
26955
|
constructor(options) {
|
|
26720
26956
|
this.options = options;
|
|
26721
|
-
this.log = options.log ?? (() => void 0);
|
|
26722
|
-
const { chips: chips2, hermesRoot: hermesRoot2 } = options;
|
|
26723
|
-
if (hermesRoot2 === void 0) return;
|
|
26724
|
-
this.contexts = new Contexts(
|
|
26725
|
-
() => this.roster(),
|
|
26726
|
-
{
|
|
26727
|
-
agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
|
|
26728
|
-
harness: (copy) => chips2.settings.harnessContext(copy)
|
|
26729
|
-
},
|
|
26730
|
-
(line2) => this.log(`context: ${line2}`),
|
|
26731
|
-
options.env ?? process.env
|
|
26732
|
-
);
|
|
26733
|
-
if (options.fills === void 0) return;
|
|
26734
|
-
const kept = options.state.fills ??= {};
|
|
26735
|
-
this.fills = new Fills({
|
|
26736
|
-
whose: (sessionId) => this.whoseSession(sessionId),
|
|
26737
|
-
profileOf: (agentId2) => {
|
|
26738
|
-
const profile = this.profileFor(agentId2);
|
|
26739
|
-
return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
|
|
26740
|
-
},
|
|
26741
|
-
secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
|
|
26742
|
-
send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
|
|
26743
|
-
kept,
|
|
26744
|
-
save: () => options.saveState(options.state),
|
|
26745
|
-
log: (line2) => this.log(`context window: ${line2}`),
|
|
26746
|
-
sendEveryMs: options.fillsEveryMs
|
|
26747
|
-
});
|
|
26748
26957
|
}
|
|
26749
26958
|
options;
|
|
26750
|
-
|
|
26751
|
-
|
|
26752
|
-
|
|
26753
|
-
|
|
26754
|
-
|
|
26755
|
-
|
|
26756
|
-
|
|
26757
|
-
|
|
26758
|
-
|
|
26759
|
-
|
|
26760
|
-
|
|
26761
|
-
|
|
26762
|
-
/** The harness's agents in Chips as last listed: whose context and window copies are sent. */
|
|
26763
|
-
listed = [];
|
|
26764
|
-
/** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
|
|
26765
|
-
contexts;
|
|
26766
|
-
contextTimer;
|
|
26767
|
-
/** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
|
|
26768
|
-
fills;
|
|
26769
|
-
/** The port Hermes's webhook reaches the bridge on, once it listens. */
|
|
26770
|
-
fillsPort;
|
|
26771
|
-
toldNoReadings = false;
|
|
26772
|
-
/** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
|
|
26773
|
-
held = false;
|
|
26774
|
-
/** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
|
|
26775
|
-
why;
|
|
26776
|
-
/** Register the agents, start hearing what is said to them, and say how their gateways are. */
|
|
26777
|
-
async start() {
|
|
26778
|
-
const { chips: chips2 } = this.options;
|
|
26779
|
-
const known = await chips2.agents.list();
|
|
26780
|
-
for (const [index, config3] of this.options.agents.entries()) {
|
|
26781
|
-
const key = keyOf(config3);
|
|
26782
|
-
const registered = index === 0 ? await this.seatMain(config3, known) : await chips2.agents.register({ key, name: config3.name });
|
|
26783
|
-
const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
|
|
26784
|
-
this.agents.push({ config: config3, key, client, agentId: registered.agentId, up: true });
|
|
26959
|
+
timing = TIMING;
|
|
26960
|
+
/** Each gateway restarted, one after another: what went wrong, in a line, or nothing. */
|
|
26961
|
+
async restart() {
|
|
26962
|
+
const troubles = [];
|
|
26963
|
+
for (const gateway of this.options.gateways) {
|
|
26964
|
+
let trouble;
|
|
26965
|
+
try {
|
|
26966
|
+
trouble = await this.restartOne(gateway);
|
|
26967
|
+
} catch (error62) {
|
|
26968
|
+
trouble = `Restart failed: ${messageOf2(error62)}`;
|
|
26969
|
+
}
|
|
26970
|
+
if (trouble !== void 0) troubles.push(trouble);
|
|
26785
26971
|
}
|
|
26786
|
-
|
|
26787
|
-
chips2.on("message.new", (message) => void this.hear(message));
|
|
26788
|
-
chips2.on("agent.asked", (asked) => {
|
|
26789
|
-
this.make(asked).catch((error62) => this.log(`a new agent went wrong: ${messageOf2(error62)}`));
|
|
26790
|
-
});
|
|
26791
|
-
chips2.on("agent.fork", (fork) => {
|
|
26792
|
-
this.copying = this.copying.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf2(error62)}`));
|
|
26793
|
-
});
|
|
26794
|
-
chips2.on("feedback", (feedback2) => {
|
|
26795
|
-
this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf2(error62)}`));
|
|
26796
|
-
});
|
|
26797
|
-
chips2.on("agent.attached", () => void this.agentsMoved());
|
|
26798
|
-
chips2.on("agent.released", () => void this.agentsMoved());
|
|
26799
|
-
await this.sayHowItIs();
|
|
26800
|
-
this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
|
|
26801
|
-
this.health.unref();
|
|
26802
|
-
await this.startSettings();
|
|
26972
|
+
return troubles.length === 0 ? void 0 : line(troubles.join(" "));
|
|
26803
26973
|
}
|
|
26804
|
-
/**
|
|
26805
|
-
|
|
26806
|
-
|
|
26974
|
+
/** Each local gateway that doesn't answer, started: the bridge is starting. */
|
|
26975
|
+
async startIfDown() {
|
|
26976
|
+
const { system, log: log2 } = this.options;
|
|
26977
|
+
for (const gateway of this.options.gateways) {
|
|
26978
|
+
if (!isLocal(gateway.url)) continue;
|
|
26979
|
+
const port = portOf(gateway.url);
|
|
26980
|
+
try {
|
|
26981
|
+
if (await system.healthy(gateway.url)) continue;
|
|
26982
|
+
let trouble;
|
|
26983
|
+
if (this.serviceFor(gateway) !== void 0) {
|
|
26984
|
+
log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
|
|
26985
|
+
trouble = await this.byService(gateway, "start", void 0);
|
|
26986
|
+
} else {
|
|
26987
|
+
const pid = await system.listener(port);
|
|
26988
|
+
if (pid !== void 0) {
|
|
26989
|
+
log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
|
|
26990
|
+
continue;
|
|
26991
|
+
}
|
|
26992
|
+
log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
|
|
26993
|
+
trouble = await this.start(gateway, ["gateway", "run"], void 0);
|
|
26994
|
+
}
|
|
26995
|
+
if (trouble !== void 0) log2(trouble);
|
|
26996
|
+
} catch (error62) {
|
|
26997
|
+
log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf2(error62)}`);
|
|
26998
|
+
}
|
|
26999
|
+
}
|
|
26807
27000
|
}
|
|
26808
27001
|
/**
|
|
26809
|
-
*
|
|
26810
|
-
*
|
|
27002
|
+
* The service of Hermes's root, for the Main agent's gateway: a gateway at another address is
|
|
27003
|
+
* another profile's own, which `hermes gateway status` of the root doesn't speak for.
|
|
26811
27004
|
*/
|
|
26812
|
-
|
|
26813
|
-
this.
|
|
26814
|
-
this.
|
|
26815
|
-
if (trouble === void 0) return this.sayHowItIs();
|
|
26816
|
-
await this.options.chips.reportTrouble(trouble).catch(() => void 0);
|
|
26817
|
-
}
|
|
26818
|
-
stop() {
|
|
26819
|
-
if (this.health !== void 0) clearInterval(this.health);
|
|
26820
|
-
if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
|
|
26821
|
-
void this.fills?.close();
|
|
27005
|
+
serviceFor(gateway) {
|
|
27006
|
+
if (gateway !== this.options.gateways[0]) return void 0;
|
|
27007
|
+
return serviceOf(this.options.system, this.options.root, this.options.env);
|
|
26822
27008
|
}
|
|
26823
|
-
|
|
26824
|
-
|
|
26825
|
-
|
|
26826
|
-
|
|
26827
|
-
*/
|
|
26828
|
-
async startSettings() {
|
|
26829
|
-
if (this.contexts === void 0) return;
|
|
26830
|
-
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
26831
|
-
await this.contexts.refresh();
|
|
26832
|
-
this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
|
|
26833
|
-
this.contextTimer.unref();
|
|
26834
|
-
const fills2 = this.fills;
|
|
26835
|
-
const asked = this.options.fills;
|
|
26836
|
-
if (fills2 === void 0 || asked === void 0) return;
|
|
26837
|
-
const kept = this.options.state.fills ??= {};
|
|
26838
|
-
try {
|
|
26839
|
-
this.fillsPort = await fills2.listen(asked.port);
|
|
26840
|
-
if (kept.port !== this.fillsPort) {
|
|
26841
|
-
kept.port = this.fillsPort;
|
|
26842
|
-
this.options.saveState(this.options.state);
|
|
26843
|
-
}
|
|
26844
|
-
} catch (error62) {
|
|
26845
|
-
this.log(`context window: nothing can listen on port ${asked.port} (${messageOf2(error62)}), so Chips won't show how full each agent's context is`);
|
|
27009
|
+
async restartOne(gateway) {
|
|
27010
|
+
const { system, log: log2 } = this.options;
|
|
27011
|
+
if (!isLocal(gateway.url)) {
|
|
27012
|
+
return `Hermes's gateway at ${new URL(gateway.url).host} isn't on this machine, so it can't be restarted from here.`;
|
|
26846
27013
|
}
|
|
26847
|
-
|
|
26848
|
-
|
|
26849
|
-
|
|
26850
|
-
|
|
26851
|
-
|
|
26852
|
-
}
|
|
26853
|
-
/** The harness's agents listed again, the copies of any gone forgotten, and the context sent. */
|
|
26854
|
-
async agentsMoved() {
|
|
26855
|
-
if (this.contexts === void 0) return;
|
|
26856
|
-
const before = this.listed;
|
|
26857
|
-
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
26858
|
-
const now = new Set(this.active().map((agent) => agent.agentId));
|
|
26859
|
-
for (const gone of before) {
|
|
26860
|
-
if (now.has(gone.agentId)) continue;
|
|
26861
|
-
this.options.chips.settings.forget(gone.agentId);
|
|
26862
|
-
this.fills?.forget(gone.agentId);
|
|
27014
|
+
const port = portOf(gateway.url);
|
|
27015
|
+
const pid = await system.listener(port);
|
|
27016
|
+
if (this.serviceFor(gateway) !== void 0) {
|
|
27017
|
+
log2("restarting Hermes's service: hermes gateway restart");
|
|
27018
|
+
return this.byService(gateway, "restart", pid);
|
|
26863
27019
|
}
|
|
26864
|
-
|
|
26865
|
-
|
|
26866
|
-
|
|
26867
|
-
|
|
26868
|
-
|
|
27020
|
+
if (pid === void 0) {
|
|
27021
|
+
if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
|
|
27022
|
+
log2(`nothing listens on port ${port}: starting hermes gateway run`);
|
|
27023
|
+
return this.start(gateway, ["gateway", "run"], void 0);
|
|
27024
|
+
}
|
|
27025
|
+
const info = await system.describe(pid);
|
|
27026
|
+
if (info === void 0) return this.start(gateway, ["gateway", "run"], void 0);
|
|
27027
|
+
const args = this.ours(info);
|
|
27028
|
+
if (args === void 0) {
|
|
27029
|
+
log2(`pid ${pid} on port ${port} isn't this user's hermes gateway run, so it was left alone: ${info.command.slice(0, 160)}`);
|
|
27030
|
+
return `Port ${port} is held by something other than hermes gateway run (pid ${pid}), so nothing was restarted.`;
|
|
27031
|
+
}
|
|
27032
|
+
if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
|
|
27033
|
+
log2(`stopping Hermes's gateway, pid ${pid}`);
|
|
27034
|
+
if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
|
|
27035
|
+
return this.start(gateway, args, info.cwd);
|
|
26869
27036
|
}
|
|
26870
|
-
/** A
|
|
26871
|
-
|
|
26872
|
-
|
|
26873
|
-
if (root2 === void 0) return void 0;
|
|
26874
|
-
return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
|
|
27037
|
+
/** A `hermes gateway run` of this user's: the words to start it again with. */
|
|
27038
|
+
ours(info) {
|
|
27039
|
+
return info.uid === this.options.system.uid ? gatewayRunArgs(info.command) : void 0;
|
|
26875
27040
|
}
|
|
26876
|
-
/**
|
|
26877
|
-
|
|
26878
|
-
const
|
|
26879
|
-
|
|
26880
|
-
|
|
26881
|
-
const
|
|
26882
|
-
|
|
27041
|
+
/** SIGTERM, then SIGKILL once it has had a while, and only while it is still that gateway. */
|
|
27042
|
+
async stop(pid) {
|
|
27043
|
+
const { system, log: log2 } = this.options;
|
|
27044
|
+
system.signal(pid, "SIGTERM");
|
|
27045
|
+
if (await this.gone(pid, this.timing.stopMs)) return true;
|
|
27046
|
+
const still = await system.describe(pid);
|
|
27047
|
+
if (still === void 0) return true;
|
|
27048
|
+
if (this.ours(still) === void 0) return false;
|
|
27049
|
+
log2(`Hermes's gateway, pid ${pid}, is still running ${this.timing.stopMs / 1e3} s after SIGTERM: SIGKILL`);
|
|
27050
|
+
system.signal(pid, "SIGKILL");
|
|
27051
|
+
return this.gone(pid, this.timing.killMs);
|
|
26883
27052
|
}
|
|
26884
|
-
|
|
26885
|
-
|
|
26886
|
-
const
|
|
26887
|
-
|
|
26888
|
-
|
|
26889
|
-
|
|
26890
|
-
const profile = this.profileFor(agentId2);
|
|
26891
|
-
if (profile !== void 0) readers.push({ agentId: agentId2, profile });
|
|
27053
|
+
async gone(pid, withinMs) {
|
|
27054
|
+
const { system } = this.options;
|
|
27055
|
+
const by = system.now() + withinMs;
|
|
27056
|
+
while (system.alive(pid)) {
|
|
27057
|
+
if (system.now() >= by) return false;
|
|
27058
|
+
await system.sleep(this.timing.pollMs);
|
|
26892
27059
|
}
|
|
26893
|
-
|
|
26894
|
-
return { readers, first: first === void 0 ? void 0 : this.profileOfRunning(first) };
|
|
26895
|
-
}
|
|
26896
|
-
/** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
|
|
26897
|
-
whoseSession(sessionId) {
|
|
26898
|
-
const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
|
|
26899
|
-
if (entry === void 0) return void 0;
|
|
26900
|
-
const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
|
|
26901
|
-
const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
|
|
26902
|
-
const profile = this.profileFor(agentId2);
|
|
26903
|
-
return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
|
|
27060
|
+
return true;
|
|
26904
27061
|
}
|
|
26905
|
-
/**
|
|
26906
|
-
async
|
|
26907
|
-
|
|
26908
|
-
|
|
26909
|
-
|
|
27062
|
+
/** Hermes's own `hermes gateway restart|start`, then the gateway answering, from a new process. */
|
|
27063
|
+
async byService(gateway, verb, before) {
|
|
27064
|
+
const { system, env } = this.options;
|
|
27065
|
+
const hermes = system.which("hermes", env);
|
|
27066
|
+
if (hermes === void 0) return NO_HERMES;
|
|
27067
|
+
const scope = this.serviceFor(gateway)?.system === true ? ["--system"] : [];
|
|
27068
|
+
const ran = await system.run(hermes, ["gateway", verb, ...scope], gatewayEnv(env), this.timing.serviceMs);
|
|
27069
|
+
if (ran.code !== 0) return line(`hermes gateway ${verb} failed: ${whyFrom(ran.output, ran.code)}`);
|
|
27070
|
+
const by = system.now() + this.timing.serviceMs;
|
|
27071
|
+
for (; ; ) {
|
|
27072
|
+
const old = before !== void 0 && system.alive(before);
|
|
27073
|
+
if (!old && await system.healthy(gateway.url)) return void 0;
|
|
27074
|
+
if (system.now() >= by) {
|
|
27075
|
+
if (await system.healthy(gateway.url)) return void 0;
|
|
27076
|
+
return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
|
|
27077
|
+
}
|
|
27078
|
+
await system.sleep(this.timing.pollMs);
|
|
26910
27079
|
}
|
|
26911
|
-
return this.contexts.edit(frame);
|
|
26912
|
-
}
|
|
26913
|
-
/** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
|
|
26914
|
-
onWindowCompact(frame) {
|
|
26915
|
-
throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
|
|
26916
|
-
}
|
|
26917
|
-
/** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
|
|
26918
|
-
onWindowChange(frame) {
|
|
26919
|
-
const profile = this.profileFor(frame.agent_id);
|
|
26920
|
-
throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
|
|
26921
|
-
}
|
|
26922
|
-
/** The Main agent, the first registered, once started: who says the harness's first words. */
|
|
26923
|
-
get mainAgentId() {
|
|
26924
|
-
return this.agents[0]?.agentId;
|
|
26925
27080
|
}
|
|
26926
27081
|
/**
|
|
26927
|
-
*
|
|
26928
|
-
*
|
|
26929
|
-
* second beside it. It goes home, and a Main of ours from before that, out of its room now, is
|
|
26930
|
-
* archived, which gives its name up. With no such agent (a harness from before them), the Main
|
|
26931
|
-
* Chips has is kept: a harness has one at most.
|
|
27082
|
+
* `hermes <args>` (`gateway run`), on its own, in `cwd` (else the home), then through Homebrew's
|
|
27083
|
+
* expat when it died of macOS's.
|
|
26932
27084
|
*/
|
|
26933
|
-
async
|
|
26934
|
-
const {
|
|
26935
|
-
const
|
|
26936
|
-
const
|
|
26937
|
-
if (
|
|
26938
|
-
|
|
26939
|
-
|
|
26940
|
-
}
|
|
26941
|
-
|
|
26942
|
-
|
|
26943
|
-
|
|
26944
|
-
|
|
27085
|
+
async start(gateway, args, cwd) {
|
|
27086
|
+
const { system, env, log: log2 } = this.options;
|
|
27087
|
+
const port = portOf(gateway.url);
|
|
27088
|
+
const hermes = system.which("hermes", env);
|
|
27089
|
+
if (hermes === void 0) return NO_HERMES;
|
|
27090
|
+
const folder2 = cwd ?? system.home;
|
|
27091
|
+
const plain = gatewayEnv(env);
|
|
27092
|
+
let outcome = await this.launch(gateway, hermes, args, plain, folder2, `hermes ${args.join(" ")}`);
|
|
27093
|
+
if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
|
|
27094
|
+
const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
|
|
27095
|
+
if (wrapper !== void 0) {
|
|
27096
|
+
const expat = dirname5(EXPAT);
|
|
27097
|
+
log2(
|
|
27098
|
+
`hermes gateway run stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Starting ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
|
|
27099
|
+
);
|
|
27100
|
+
const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
|
|
27101
|
+
outcome = await this.launch(
|
|
27102
|
+
gateway,
|
|
27103
|
+
wrapper.entry,
|
|
27104
|
+
args,
|
|
27105
|
+
{ ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
|
|
27106
|
+
folder2,
|
|
27107
|
+
`${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
|
|
27108
|
+
);
|
|
27109
|
+
}
|
|
26945
27110
|
}
|
|
26946
|
-
|
|
27111
|
+
if (outcome.state === "up") {
|
|
27112
|
+
log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
|
|
27113
|
+
return void 0;
|
|
27114
|
+
}
|
|
27115
|
+
if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
|
|
27116
|
+
return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
|
|
26947
27117
|
}
|
|
26948
|
-
/**
|
|
26949
|
-
async
|
|
26950
|
-
const {
|
|
26951
|
-
|
|
26952
|
-
const
|
|
26953
|
-
|
|
27118
|
+
/** A program started, and watched until it answers, ends, or has had its time. */
|
|
27119
|
+
async launch(gateway, command, args, env, cwd, what) {
|
|
27120
|
+
const { system, logFile } = this.options;
|
|
27121
|
+
system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
|
|
27122
|
+
const from = system.size(logFile);
|
|
27123
|
+
const startedAt = system.now();
|
|
27124
|
+
const child = system.launch({ command, args, env, cwd, log: logFile });
|
|
27125
|
+
let ended;
|
|
27126
|
+
void child.exited.then(({ code }) => {
|
|
27127
|
+
ended = { code, at: system.now() };
|
|
27128
|
+
});
|
|
27129
|
+
for (; ; ) {
|
|
27130
|
+
await Promise.resolve();
|
|
27131
|
+
if (ended !== void 0) {
|
|
27132
|
+
return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
|
|
27133
|
+
}
|
|
27134
|
+
if (await system.healthy(gateway.url)) return { state: "up", pid: child.pid };
|
|
27135
|
+
if (system.now() - startedAt >= this.timing.upMs) return { state: "silent" };
|
|
27136
|
+
await system.sleep(this.timing.pollMs);
|
|
27137
|
+
}
|
|
26954
27138
|
}
|
|
26955
|
-
|
|
26956
|
-
|
|
26957
|
-
|
|
26958
|
-
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
26962
|
-
|
|
26963
|
-
|
|
26964
|
-
|
|
26965
|
-
|
|
26966
|
-
|
|
26967
|
-
|
|
27139
|
+
};
|
|
27140
|
+
function restarter(deps) {
|
|
27141
|
+
let running;
|
|
27142
|
+
return (ask2) => {
|
|
27143
|
+
if (running !== void 0) {
|
|
27144
|
+
deps.log(`restart asked by ${ask2.askedBy} while one runs: it is the same restart`);
|
|
27145
|
+
return running;
|
|
27146
|
+
}
|
|
27147
|
+
running = (async () => {
|
|
27148
|
+
deps.log(`restart asked by ${ask2.askedBy}: restarting Hermes's gateway`);
|
|
27149
|
+
deps.hold(true);
|
|
27150
|
+
let trouble;
|
|
27151
|
+
try {
|
|
27152
|
+
trouble = await deps.restart();
|
|
27153
|
+
} catch (error62) {
|
|
27154
|
+
trouble = line(`Restart failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
|
|
27155
|
+
}
|
|
27156
|
+
deps.log(trouble === void 0 ? "restarted, and Hermes answers" : `restart: ${trouble}`);
|
|
27157
|
+
try {
|
|
27158
|
+
await deps.say(trouble);
|
|
27159
|
+
} catch (error62) {
|
|
27160
|
+
deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
|
|
27161
|
+
} finally {
|
|
27162
|
+
deps.hold(false);
|
|
27163
|
+
}
|
|
27164
|
+
})().finally(() => {
|
|
27165
|
+
running = void 0;
|
|
27166
|
+
});
|
|
27167
|
+
return running;
|
|
27168
|
+
};
|
|
27169
|
+
}
|
|
27170
|
+
function gatewayLog(configFolder, platform = process.platform) {
|
|
27171
|
+
return platform === "darwin" ? join8(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join8(configFolder, "gateway.log");
|
|
27172
|
+
}
|
|
27173
|
+
function runToEnd(command, args, options) {
|
|
27174
|
+
return new Promise((done) => {
|
|
27175
|
+
const child = spawn2(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
27176
|
+
let output2 = "";
|
|
27177
|
+
const keep = (data) => {
|
|
27178
|
+
output2 = (output2 + String(data)).slice(-8e3);
|
|
27179
|
+
};
|
|
27180
|
+
child.stdout.on("data", keep);
|
|
27181
|
+
child.stderr.on("data", keep);
|
|
27182
|
+
const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
|
|
27183
|
+
child.on("error", (error62) => {
|
|
27184
|
+
clearTimeout(limit);
|
|
27185
|
+
done({ code: null, output: error62.message });
|
|
27186
|
+
});
|
|
27187
|
+
child.on("close", (code) => {
|
|
27188
|
+
clearTimeout(limit);
|
|
27189
|
+
done({ code, output: output2 });
|
|
27190
|
+
});
|
|
27191
|
+
});
|
|
27192
|
+
}
|
|
27193
|
+
function thisMachine() {
|
|
27194
|
+
return {
|
|
27195
|
+
platform: process.platform,
|
|
27196
|
+
uid: process.getuid?.() ?? -1,
|
|
27197
|
+
home: homedir5(),
|
|
27198
|
+
exists: existsSync8,
|
|
27199
|
+
read(path) {
|
|
27200
|
+
try {
|
|
27201
|
+
return readFileSync8(path, "utf8");
|
|
27202
|
+
} catch {
|
|
27203
|
+
return void 0;
|
|
27204
|
+
}
|
|
27205
|
+
},
|
|
27206
|
+
which(command, env) {
|
|
27207
|
+
for (const folder2 of (env["PATH"] ?? "").split(delimiter2)) {
|
|
27208
|
+
if (folder2 === "") continue;
|
|
27209
|
+
const path = join8(folder2, command);
|
|
27210
|
+
try {
|
|
27211
|
+
accessSync2(path, constants2.X_OK);
|
|
27212
|
+
return path;
|
|
27213
|
+
} catch {
|
|
27214
|
+
}
|
|
27215
|
+
}
|
|
27216
|
+
return void 0;
|
|
27217
|
+
},
|
|
27218
|
+
async listener(port) {
|
|
27219
|
+
const { output: output2 } = await runToEnd("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], { timeoutMs: 5e3 });
|
|
27220
|
+
const pid = Number(/^p(\d+)$/m.exec(output2)?.[1]);
|
|
27221
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
27222
|
+
},
|
|
27223
|
+
async describe(pid) {
|
|
27224
|
+
const { code, output: output2 } = await runToEnd("ps", ["-ww", "-o", "uid=", "-o", "command=", "-p", String(pid)], { timeoutMs: 5e3 });
|
|
27225
|
+
const found = /^\s*(\d+)\s+(.*)$/m.exec(output2);
|
|
27226
|
+
if (code !== 0 || found === null) return void 0;
|
|
27227
|
+
return { uid: Number(found[1]), command: found[2] ?? "", cwd: processFolder(pid) };
|
|
27228
|
+
},
|
|
27229
|
+
alive(pid) {
|
|
27230
|
+
try {
|
|
27231
|
+
process.kill(pid, 0);
|
|
27232
|
+
return true;
|
|
27233
|
+
} catch (error62) {
|
|
27234
|
+
return error62.code === "EPERM";
|
|
27235
|
+
}
|
|
27236
|
+
},
|
|
27237
|
+
signal(pid, signal) {
|
|
27238
|
+
try {
|
|
27239
|
+
process.kill(pid, signal);
|
|
27240
|
+
} catch {
|
|
27241
|
+
}
|
|
27242
|
+
},
|
|
27243
|
+
run: (command, args, env, timeoutMs) => runToEnd(command, args, { env, timeoutMs }),
|
|
27244
|
+
launch({ command, args, env, cwd, log: log2 }) {
|
|
27245
|
+
const out = openSync2(log2, "a");
|
|
27246
|
+
try {
|
|
27247
|
+
const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
|
|
27248
|
+
const exited = new Promise((done) => {
|
|
27249
|
+
child.once("exit", (code, signal) => done({ code, signal }));
|
|
27250
|
+
child.once("error", (error62) => {
|
|
27251
|
+
appendFileSync2(log2, `${error62.message}
|
|
27252
|
+
`);
|
|
27253
|
+
done({ code: null, signal: null });
|
|
27254
|
+
});
|
|
27255
|
+
});
|
|
27256
|
+
child.unref();
|
|
27257
|
+
return { pid: child.pid, exited };
|
|
27258
|
+
} finally {
|
|
27259
|
+
closeSync2(out);
|
|
27260
|
+
}
|
|
27261
|
+
},
|
|
27262
|
+
note(log2, words4) {
|
|
27263
|
+
appendFileSync2(log2, `${words4}
|
|
27264
|
+
`);
|
|
27265
|
+
},
|
|
27266
|
+
size(log2) {
|
|
27267
|
+
try {
|
|
27268
|
+
return statSync3(log2).size;
|
|
27269
|
+
} catch {
|
|
27270
|
+
return 0;
|
|
27271
|
+
}
|
|
27272
|
+
},
|
|
27273
|
+
since(log2, from) {
|
|
27274
|
+
try {
|
|
27275
|
+
const to = statSync3(log2).size;
|
|
27276
|
+
const start = Math.max(from, to - 64e3);
|
|
27277
|
+
const buffer = Buffer.alloc(Math.max(0, to - start));
|
|
27278
|
+
const fd = openSync2(log2, "r");
|
|
27279
|
+
try {
|
|
27280
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
27281
|
+
} finally {
|
|
27282
|
+
closeSync2(fd);
|
|
27283
|
+
}
|
|
27284
|
+
return buffer.toString("utf8");
|
|
27285
|
+
} catch {
|
|
27286
|
+
return "";
|
|
27287
|
+
}
|
|
27288
|
+
},
|
|
27289
|
+
healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
|
|
27290
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
27291
|
+
now: () => Date.now()
|
|
27292
|
+
};
|
|
27293
|
+
}
|
|
27294
|
+
|
|
27295
|
+
// src/profiles.ts
|
|
27296
|
+
var KEY_ENV = "API_SERVER_KEY";
|
|
27297
|
+
var RESERVED = /* @__PURE__ */ new Set(["hermes", "default", "test", "tmp", "root", "sudo"]);
|
|
27298
|
+
var CREATE_MS = 12e4;
|
|
27299
|
+
function freeProfile(root2, name2, taken) {
|
|
27300
|
+
const slug = name2.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 56).replace(/^-+|-+$/g, "") || "agent";
|
|
27301
|
+
for (let count = 1; ; count += 1) {
|
|
27302
|
+
if (count === 1 && RESERVED.has(slug)) continue;
|
|
27303
|
+
const profile = count === 1 ? slug : `${slug}-${count}`;
|
|
27304
|
+
if (!taken.includes(profile) && !existsSync9(join9(root2, "profiles", profile))) return profile;
|
|
27305
|
+
}
|
|
27306
|
+
}
|
|
27307
|
+
function readyProfile(home2, names, hook) {
|
|
27308
|
+
for (const file3 of ["MEMORY.md", "USER.md"]) rmSync4(join9(home2, "memories", file3), { force: true });
|
|
27309
|
+
if (envValue(home2, KEY_ENV) === void 0) appendEnv(home2, KEY_ENV, randomBytes2(32).toString("hex"));
|
|
27310
|
+
if (hook === void 0) return void 0;
|
|
27311
|
+
const path = join9(home2, "config.yaml");
|
|
27312
|
+
if (existsSync9(path)) {
|
|
27313
|
+
const theirs = hookUrl(hook.port, names.from).replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
27314
|
+
const entry = new RegExp(`^(\\s*(?:-\\s*)?url:\\s*["']?)${theirs}(["']?\\s*(?:#.*)?)$`, "m");
|
|
27315
|
+
const text = readFileSync9(path, "utf8");
|
|
27316
|
+
if (entry.test(text)) writeWhole(path, text.replace(entry, `$1${hookUrl(hook.port, names.profile)}$2`));
|
|
27317
|
+
}
|
|
27318
|
+
return addHook(home2, hook.port, names.profile, hook.secret);
|
|
27319
|
+
}
|
|
27320
|
+
function hermesProfiles(options) {
|
|
27321
|
+
return {
|
|
27322
|
+
async create(name2, from) {
|
|
27323
|
+
const args = ["profile", "create", name2, "--clone-from", from, "--no-alias"];
|
|
27324
|
+
const ran = await runHermes(options.system, options.env, args, CREATE_MS, options.log);
|
|
27325
|
+
if (ran.code !== 0) throw new Error(whyFrom(ran.output, ran.code).replace(/^Error:\s*/, ""));
|
|
27326
|
+
},
|
|
27327
|
+
async serve() {
|
|
27328
|
+
const result = await askGateway(options.root, "rescan-profiles");
|
|
27329
|
+
return typeof result?.["multiplex"] === "boolean" ? result["multiplex"] : void 0;
|
|
27330
|
+
}
|
|
27331
|
+
};
|
|
27332
|
+
}
|
|
27333
|
+
function controlSocketOf(root2) {
|
|
27334
|
+
const direct = join9(root2, "gateway.sock");
|
|
27335
|
+
if (existsSync9(direct)) return direct;
|
|
27336
|
+
try {
|
|
27337
|
+
const pointed = readFileSync9(join9(root2, "gateway.sock.path"), "utf8").trim();
|
|
27338
|
+
return pointed !== "" && existsSync9(pointed) ? pointed : void 0;
|
|
27339
|
+
} catch {
|
|
27340
|
+
return void 0;
|
|
27341
|
+
}
|
|
27342
|
+
}
|
|
27343
|
+
function askGateway(root2, verb, timeoutMs = 8e3) {
|
|
27344
|
+
const path = controlSocketOf(root2);
|
|
27345
|
+
if (path === void 0) return Promise.resolve(void 0);
|
|
27346
|
+
return new Promise((done) => {
|
|
27347
|
+
let heard = "";
|
|
27348
|
+
let over = false;
|
|
27349
|
+
const socket = createConnection(path);
|
|
27350
|
+
const finish = (line2) => {
|
|
27351
|
+
if (over) return;
|
|
27352
|
+
over = true;
|
|
27353
|
+
clearTimeout(timer);
|
|
27354
|
+
socket.destroy();
|
|
27355
|
+
done(line2 === void 0 ? void 0 : resultOf(line2));
|
|
27356
|
+
};
|
|
27357
|
+
const timer = setTimeout(() => finish(void 0), timeoutMs);
|
|
27358
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ verb, id: 1, protocol: 1 })}
|
|
27359
|
+
`));
|
|
27360
|
+
socket.on("data", (chunk) => {
|
|
27361
|
+
heard += String(chunk);
|
|
27362
|
+
const end2 = heard.indexOf("\n");
|
|
27363
|
+
if (end2 >= 0) finish(heard.slice(0, end2));
|
|
27364
|
+
});
|
|
27365
|
+
socket.on("end", () => finish(heard));
|
|
27366
|
+
socket.on("error", () => finish(void 0));
|
|
27367
|
+
});
|
|
27368
|
+
}
|
|
27369
|
+
function resultOf(line2) {
|
|
27370
|
+
try {
|
|
27371
|
+
const answer = JSON.parse(line2);
|
|
27372
|
+
return answer.ok === true && typeof answer.result === "object" && answer.result !== null ? answer.result : void 0;
|
|
27373
|
+
} catch {
|
|
27374
|
+
return void 0;
|
|
27375
|
+
}
|
|
27376
|
+
}
|
|
27377
|
+
|
|
27378
|
+
// src/bridge.ts
|
|
27379
|
+
var WANTS = [
|
|
27380
|
+
"agents",
|
|
27381
|
+
"new_spaces",
|
|
27382
|
+
"new_agents",
|
|
27383
|
+
"forks",
|
|
27384
|
+
"context_files",
|
|
27385
|
+
"models",
|
|
27386
|
+
"context_windows",
|
|
27387
|
+
"restarts"
|
|
27388
|
+
];
|
|
27389
|
+
function settingsFor(active) {
|
|
27390
|
+
const bridge2 = () => {
|
|
27391
|
+
const there = active();
|
|
27392
|
+
if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
|
|
27393
|
+
return there;
|
|
27394
|
+
};
|
|
27395
|
+
return {
|
|
27396
|
+
context: (edit) => bridge2().onContextEdit(edit),
|
|
27397
|
+
models: (change) => bridge2().onModelChange(change),
|
|
27398
|
+
contextWindows: {
|
|
27399
|
+
compact: async (ask2) => bridge2().onWindowCompact(ask2),
|
|
27400
|
+
change: async (ask2) => bridge2().onWindowChange(ask2)
|
|
27401
|
+
}
|
|
27402
|
+
};
|
|
27403
|
+
}
|
|
27404
|
+
function filesHere(project = false) {
|
|
27405
|
+
return `${sendingFiles({ project })} Hermes's note that this API server sends only pictures, and shows a MEDIA: tag on the runs endpoint as text, does not hold for these runs: they come from Chips, whose bridge takes each MEDIA: line out of your answer and attaches that file to your message, whatever its type, up to 100 MB.`;
|
|
27406
|
+
}
|
|
27407
|
+
var FILES_HERE = filesHere();
|
|
27408
|
+
var MARKDOWN_HERE = "Hermes's note that this API server's replies are plain text does not hold for these runs either: Chips shows your answer as Markdown, so use headings, lists, bold, tables and code blocks where they help a reader.";
|
|
27409
|
+
function instructionsFor(folder2, project = false, rules = "") {
|
|
27410
|
+
const files = filesHere(project);
|
|
27411
|
+
const here2 = folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
|
|
27412
|
+
return [here2, MARKDOWN_HERE, rules].filter((part) => part !== "").join("\n\n");
|
|
27413
|
+
}
|
|
27414
|
+
function freeFolder(root2, name2, taken) {
|
|
27415
|
+
const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
27416
|
+
for (let count = 1; ; count += 1) {
|
|
27417
|
+
const folder2 = join10(root2, count === 1 ? slug : `${slug}-${count}`);
|
|
27418
|
+
if (!taken.includes(folder2)) return folder2;
|
|
27419
|
+
}
|
|
27420
|
+
}
|
|
27421
|
+
var ids = createIdGenerator();
|
|
27422
|
+
var QUIET_MS = 1500;
|
|
27423
|
+
var LOST_STREAM_MS = 12e4;
|
|
27424
|
+
var KEPT_FEEDBACK = 500;
|
|
27425
|
+
var KINDS = {
|
|
27426
|
+
terminal: "shell",
|
|
27427
|
+
process: "shell",
|
|
27428
|
+
execute_code: "shell",
|
|
27429
|
+
web_search: "search",
|
|
27430
|
+
search_files: "search",
|
|
27431
|
+
session_search: "search",
|
|
27432
|
+
web_extract: "web",
|
|
27433
|
+
read_file: "file",
|
|
27434
|
+
write_file: "edit",
|
|
27435
|
+
patch: "edit",
|
|
27436
|
+
vision_analyze: "image",
|
|
27437
|
+
image_generate: "image",
|
|
27438
|
+
memory: "memory",
|
|
27439
|
+
delegate_task: "handoff"
|
|
27440
|
+
};
|
|
27441
|
+
function keyOf(agent) {
|
|
27442
|
+
return `hermes:${(agent.address.profile ?? "default").toLowerCase()}:${agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
27443
|
+
}
|
|
27444
|
+
var Bridge = class {
|
|
27445
|
+
constructor(options) {
|
|
27446
|
+
this.options = options;
|
|
27447
|
+
this.log = options.log ?? (() => void 0);
|
|
27448
|
+
const { chips: chips2, hermesRoot: hermesRoot2 } = options;
|
|
27449
|
+
if (hermesRoot2 === void 0) return;
|
|
27450
|
+
this.contexts = new Contexts(
|
|
27451
|
+
() => this.roster(),
|
|
27452
|
+
{
|
|
27453
|
+
agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
|
|
27454
|
+
harness: (copy) => chips2.settings.harnessContext(copy)
|
|
27455
|
+
},
|
|
27456
|
+
(line2) => this.log(`context: ${line2}`),
|
|
27457
|
+
options.env ?? process.env
|
|
27458
|
+
);
|
|
27459
|
+
if (options.fills === void 0) return;
|
|
27460
|
+
const kept = options.state.fills ??= {};
|
|
27461
|
+
this.fills = new Fills({
|
|
27462
|
+
whose: (sessionId) => this.whoseSession(sessionId),
|
|
27463
|
+
profileOf: (agentId2) => {
|
|
27464
|
+
const profile = this.profileFor(agentId2);
|
|
27465
|
+
return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
|
|
27466
|
+
},
|
|
27467
|
+
secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
|
|
27468
|
+
send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
|
|
27469
|
+
kept,
|
|
27470
|
+
save: () => options.saveState(options.state),
|
|
27471
|
+
log: (line2) => this.log(`context window: ${line2}`),
|
|
27472
|
+
sendEveryMs: options.fillsEveryMs
|
|
26968
27473
|
});
|
|
26969
|
-
this.ours.add(agent.agentId);
|
|
26970
|
-
this.names.set(agent.agentId, agent.name);
|
|
26971
|
-
this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
|
|
26972
|
-
void this.agentsMoved();
|
|
26973
27474
|
}
|
|
27475
|
+
options;
|
|
27476
|
+
agents = [];
|
|
27477
|
+
rooms = /* @__PURE__ */ new Map();
|
|
27478
|
+
/** Every Chips agent of the harness's, configured here or not. */
|
|
27479
|
+
ours = /* @__PURE__ */ new Set();
|
|
27480
|
+
/** The harness's agents' names, by Chips id, for the folders named after them. */
|
|
27481
|
+
names = /* @__PURE__ */ new Map();
|
|
27482
|
+
/** The agents made in Chips with a profile of their own, by Chips id (Chips ADR 0241). */
|
|
27483
|
+
own = /* @__PURE__ */ new Map();
|
|
27484
|
+
/** The feedback events answered with a turn, the newest `KEPT_FEEDBACK`. */
|
|
27485
|
+
lookedAt = /* @__PURE__ */ new Set();
|
|
26974
27486
|
/**
|
|
26975
|
-
*
|
|
26976
|
-
* agent
|
|
26977
|
-
* session in the room asked. The copy answers through its original's profile, whose Hermes has
|
|
26978
|
-
* that session, and works in its original's folder. It is registered with `forkedFrom` for the
|
|
26979
|
-
* room, which Chips puts it in while the room waits. The fork and the copy are keyed by the ask,
|
|
26980
|
-
* so an ask heard twice forks once (Hermes refuses the id again) and registers the same copy. An
|
|
26981
|
-
* agent that never spoke is copied with nothing to remember. A turn still going is not in the
|
|
26982
|
-
* copy: Hermes copies what the session holds. Nothing is made for an ask the room has stopped
|
|
26983
|
-
* waiting for (`FORK_WAIT_MS`), nor for an agent that isn't the harness's.
|
|
27487
|
+
* The agents made, the spaces claimed and the copies made, one after another: a message for an
|
|
27488
|
+
* agent not settled yet waits for them (see `runnerOf`).
|
|
26984
27489
|
*/
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
26990
|
-
|
|
26991
|
-
|
|
26992
|
-
|
|
27490
|
+
making = Promise.resolve();
|
|
27491
|
+
log;
|
|
27492
|
+
health;
|
|
27493
|
+
/** The harness's agents in Chips as last listed: whose context and window copies are sent. */
|
|
27494
|
+
listed = [];
|
|
27495
|
+
/** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
|
|
27496
|
+
contexts;
|
|
27497
|
+
contextTimer;
|
|
27498
|
+
/** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
|
|
27499
|
+
fills;
|
|
27500
|
+
/** The port Hermes's webhook reaches the bridge on, once it listens. */
|
|
27501
|
+
fillsPort;
|
|
27502
|
+
toldNoReadings = false;
|
|
27503
|
+
/** Whether the log has said once that a model copy wasn't sent. */
|
|
27504
|
+
toldModels = false;
|
|
27505
|
+
/** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
|
|
27506
|
+
held = false;
|
|
27507
|
+
/** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
|
|
27508
|
+
why;
|
|
27509
|
+
/** Register the agents, start hearing what is said to them, and say how their gateways are. */
|
|
27510
|
+
async start() {
|
|
27511
|
+
const { chips: chips2 } = this.options;
|
|
27512
|
+
const known = await chips2.agents.list();
|
|
27513
|
+
for (const [index, config3] of this.options.agents.entries()) {
|
|
27514
|
+
const key = keyOf(config3);
|
|
27515
|
+
const registered = index === 0 ? await this.seatMain(config3, known) : await chips2.agents.register({ key, name: config3.name });
|
|
27516
|
+
const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
|
|
27517
|
+
this.agents.push({ config: config3, key, client, agentId: registered.agentId, up: true });
|
|
26993
27518
|
}
|
|
26994
|
-
const
|
|
26995
|
-
|
|
26996
|
-
|
|
26997
|
-
return;
|
|
27519
|
+
for (const [agentId2, profile] of Object.entries(this.options.state.made ?? {})) {
|
|
27520
|
+
const running = this.runningOn(agentId2, profile, known.find((agent) => agent.agentId === agentId2)?.name ?? profile);
|
|
27521
|
+
if (running !== void 0) this.own.set(agentId2, running);
|
|
26998
27522
|
}
|
|
26999
|
-
|
|
27000
|
-
|
|
27001
|
-
|
|
27002
|
-
|
|
27003
|
-
|
|
27004
|
-
|
|
27005
|
-
|
|
27006
|
-
|
|
27007
|
-
|
|
27008
|
-
|
|
27009
|
-
|
|
27010
|
-
|
|
27011
|
-
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
|
|
27016
|
-
this.
|
|
27017
|
-
|
|
27523
|
+
this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
27524
|
+
chips2.on("message.new", (message) => void this.hear(message));
|
|
27525
|
+
chips2.on("agent.asked", (asked) => {
|
|
27526
|
+
this.making = this.making.then(() => this.make(asked)).catch((error62) => this.log(`a new agent went wrong: ${messageOf3(error62)}`));
|
|
27527
|
+
});
|
|
27528
|
+
chips2.on("agent.fork", (fork) => {
|
|
27529
|
+
this.making = this.making.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf3(error62)}`));
|
|
27530
|
+
});
|
|
27531
|
+
chips2.onSpaceAsked((asked) => {
|
|
27532
|
+
this.making = this.making.then(() => this.claim(asked)).catch((error62) => this.log(`a space from the + went wrong: ${messageOf3(error62)}`));
|
|
27533
|
+
});
|
|
27534
|
+
chips2.on("feedback", (feedback2) => {
|
|
27535
|
+
this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf3(error62)}`));
|
|
27536
|
+
});
|
|
27537
|
+
chips2.on("agent.attached", () => void this.agentsMoved());
|
|
27538
|
+
chips2.on("agent.released", () => void this.agentsMoved());
|
|
27539
|
+
await this.sayHowItIs();
|
|
27540
|
+
this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
|
|
27541
|
+
this.health.unref();
|
|
27542
|
+
await this.startSettings();
|
|
27018
27543
|
}
|
|
27019
|
-
/**
|
|
27020
|
-
|
|
27021
|
-
|
|
27022
|
-
const listed = await this.options.chips.agents.list().catch(() => []);
|
|
27023
|
-
for (const agent of listed) this.names.set(agent.agentId, agent.name);
|
|
27024
|
-
}
|
|
27025
|
-
return this.names.get(agentId2) ?? otherwise;
|
|
27544
|
+
/** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
|
|
27545
|
+
hold(held) {
|
|
27546
|
+
this.held = held;
|
|
27026
27547
|
}
|
|
27027
27548
|
/**
|
|
27028
|
-
*
|
|
27029
|
-
*
|
|
27030
|
-
* takes a turn and kept, so a rename keeps it. Every agent once shared one folder, and a team's
|
|
27031
|
-
* files overwrote a one-agent room's game (Chips TT-16); and the one folder had to be Hermes's
|
|
27032
|
-
* own working folder, which a plain Hermes doesn't set (the setup audit, 17 October 2026).
|
|
27549
|
+
* How a restart went (Chips ADR 0094): fine as the gateways now are, or its words, which stand
|
|
27550
|
+
* while Hermes doesn't answer. The health watch speaks for itself again after.
|
|
27033
27551
|
*/
|
|
27034
|
-
|
|
27035
|
-
|
|
27036
|
-
|
|
27037
|
-
if (
|
|
27038
|
-
|
|
27039
|
-
|
|
27040
|
-
|
|
27041
|
-
if (
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
}
|
|
27045
|
-
return folder2;
|
|
27552
|
+
async restarted(trouble) {
|
|
27553
|
+
this.held = false;
|
|
27554
|
+
this.why = trouble;
|
|
27555
|
+
if (trouble === void 0) return this.sayHowItIs();
|
|
27556
|
+
await this.options.chips.reportTrouble(trouble).catch(() => void 0);
|
|
27557
|
+
}
|
|
27558
|
+
stop() {
|
|
27559
|
+
if (this.health !== void 0) clearInterval(this.health);
|
|
27560
|
+
if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
|
|
27561
|
+
void this.fills?.close();
|
|
27046
27562
|
}
|
|
27563
|
+
// ------------------------------------------------------------------ an agent's page
|
|
27047
27564
|
/**
|
|
27048
|
-
* The
|
|
27049
|
-
*
|
|
27050
|
-
*
|
|
27565
|
+
* The agents' models, context and context windows, sent at start and kept current: the models
|
|
27566
|
+
* again when an agent comes or goes, the files read again every so often and after each turn, a
|
|
27567
|
+
* window copy on each reading Hermes sends.
|
|
27051
27568
|
*/
|
|
27052
|
-
async
|
|
27053
|
-
|
|
27054
|
-
|
|
27055
|
-
if (
|
|
27569
|
+
async startSettings() {
|
|
27570
|
+
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
27571
|
+
await this.sendModels();
|
|
27572
|
+
if (this.contexts === void 0) return;
|
|
27573
|
+
await this.contexts.refresh();
|
|
27574
|
+
this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
|
|
27575
|
+
this.contextTimer.unref();
|
|
27576
|
+
const fills2 = this.fills;
|
|
27577
|
+
const asked = this.options.fills;
|
|
27578
|
+
if (fills2 === void 0 || asked === void 0) return;
|
|
27579
|
+
const kept = this.options.state.fills ??= {};
|
|
27056
27580
|
try {
|
|
27057
|
-
|
|
27058
|
-
|
|
27059
|
-
|
|
27060
|
-
|
|
27581
|
+
this.fillsPort = await fills2.listen(asked.port);
|
|
27582
|
+
if (kept.port !== this.fillsPort) {
|
|
27583
|
+
kept.port = this.fillsPort;
|
|
27584
|
+
this.options.saveState(this.options.state);
|
|
27585
|
+
}
|
|
27061
27586
|
} catch (error62) {
|
|
27062
|
-
this.log(
|
|
27063
|
-
return void 0;
|
|
27587
|
+
this.log(`context window: nothing can listen on port ${asked.port} (${messageOf3(error62)}), so Chips won't show how full each agent's context is`);
|
|
27064
27588
|
}
|
|
27589
|
+
await fills2.sendAll();
|
|
27065
27590
|
}
|
|
27066
|
-
/**
|
|
27067
|
-
|
|
27068
|
-
|
|
27069
|
-
|
|
27070
|
-
|
|
27071
|
-
async
|
|
27072
|
-
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27076
|
-
|
|
27077
|
-
|
|
27078
|
-
|
|
27079
|
-
return note === void 0 ? {} : { note };
|
|
27080
|
-
} catch (error62) {
|
|
27081
|
-
this.log(`${name2}'s project couldn't be sent: ${messageOf2(error62)}`);
|
|
27082
|
-
const note = heldNote({ state: "failed", why: messageOf2(error62) }, name2);
|
|
27083
|
-
return note === void 0 ? {} : { note };
|
|
27591
|
+
/** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
|
|
27592
|
+
hookUrl(profile = "default") {
|
|
27593
|
+
return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
|
|
27594
|
+
}
|
|
27595
|
+
/** The harness's agents listed again, the copies of any gone forgotten, and the context and models sent. */
|
|
27596
|
+
async agentsMoved() {
|
|
27597
|
+
const before = this.listed;
|
|
27598
|
+
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
27599
|
+
const now = new Set(this.active().map((agent) => agent.agentId));
|
|
27600
|
+
for (const gone of before) {
|
|
27601
|
+
if (now.has(gone.agentId)) continue;
|
|
27602
|
+
this.options.chips.settings.forget(gone.agentId);
|
|
27603
|
+
this.fills?.forget(gone.agentId);
|
|
27084
27604
|
}
|
|
27605
|
+
await this.contexts?.refresh();
|
|
27606
|
+
await this.sendModels();
|
|
27607
|
+
}
|
|
27608
|
+
/** The harness's agents that read anything now: the active ones listed. */
|
|
27609
|
+
active() {
|
|
27610
|
+
return this.listed.filter((agent) => agent.state === "active");
|
|
27611
|
+
}
|
|
27612
|
+
/** A profile as the context needs it: its name and its home under Hermes's root. */
|
|
27613
|
+
profileOfRunning(agent) {
|
|
27614
|
+
const root2 = this.options.hermesRoot;
|
|
27615
|
+
if (root2 === void 0) return void 0;
|
|
27616
|
+
return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
|
|
27617
|
+
}
|
|
27618
|
+
/** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
|
|
27619
|
+
profileFor(agentId2) {
|
|
27620
|
+
const running = this.runningFor(agentId2);
|
|
27621
|
+
return running === void 0 ? void 0 : this.profileOfRunning(running);
|
|
27085
27622
|
}
|
|
27086
|
-
/**
|
|
27087
|
-
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
const down = this.agents.filter((agent) => !agent.up);
|
|
27092
|
-
if (down.length === 0) this.why = void 0;
|
|
27093
|
-
const chips2 = this.options.chips;
|
|
27094
|
-
const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
|
|
27095
|
-
await said2.catch(() => void 0);
|
|
27623
|
+
/** What answers for an agent: its own profile, a copy's original's, else the first. */
|
|
27624
|
+
runningFor(agentId2) {
|
|
27625
|
+
const own2 = this.agents.find((agent) => agent.agentId === agentId2) ?? this.own.get(agentId2);
|
|
27626
|
+
const copied = this.options.state.profiles?.[agentId2];
|
|
27627
|
+
return own2 ?? (copied === void 0 ? void 0 : this.byKey(copied)) ?? this.agents[0];
|
|
27096
27628
|
}
|
|
27097
|
-
|
|
27098
|
-
|
|
27099
|
-
|
|
27100
|
-
const spaceId = message.space_id;
|
|
27101
|
-
if (spaceId === void 0) return;
|
|
27102
|
-
const harnessId = this.options.chips.harnessId;
|
|
27103
|
-
for (const taker of message.for ?? [harnessId]) {
|
|
27104
|
-
const agent = await this.runnerOf(taker);
|
|
27105
|
-
if (agent === void 0) continue;
|
|
27106
|
-
const room = this.roomOf(agent, taker, spaceId);
|
|
27107
|
-
room.queue.push({ message, speakerId: taker === harnessId ? void 0 : taker, ...how });
|
|
27108
|
-
void this.pump(room);
|
|
27109
|
-
}
|
|
27629
|
+
/** A profile the bridge speaks to, by its key: a configured agent's, or a made agent's Chips id. */
|
|
27630
|
+
byKey(key) {
|
|
27631
|
+
return this.agents.find((agent) => agent.key === key) ?? this.own.get(key);
|
|
27110
27632
|
}
|
|
27111
27633
|
/**
|
|
27112
|
-
*
|
|
27113
|
-
*
|
|
27114
|
-
*
|
|
27115
|
-
* reaction is the person's own. Once per event, however often it is heard.
|
|
27634
|
+
* Who reads what: each active agent with its profile, and the first profile, whose SOUL.md each
|
|
27635
|
+
* new agent starts from (and whose every file each new agent reads, where none gets a profile of
|
|
27636
|
+
* its own).
|
|
27116
27637
|
*/
|
|
27117
|
-
|
|
27118
|
-
const
|
|
27119
|
-
const
|
|
27120
|
-
const
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
if (this.lookedAt.size <= KEPT_FEEDBACK) break;
|
|
27125
|
-
this.lookedAt.delete(old);
|
|
27638
|
+
roster() {
|
|
27639
|
+
const readers = [];
|
|
27640
|
+
const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
27641
|
+
for (const agentId2 of ids2) {
|
|
27642
|
+
if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
|
|
27643
|
+
const profile = this.profileFor(agentId2);
|
|
27644
|
+
if (profile !== void 0) readers.push({ agentId: agentId2, profile });
|
|
27126
27645
|
}
|
|
27127
|
-
const
|
|
27128
|
-
|
|
27129
|
-
|
|
27130
|
-
|
|
27131
|
-
|
|
27132
|
-
// An id of its own, so nothing of the turn is taken for the reply's.
|
|
27133
|
-
message_id: messageIdOf(feedback2.id),
|
|
27134
|
-
text: lookAgain(feedback2.from.display_name, line2?.text, note),
|
|
27135
|
-
from: feedback2.from,
|
|
27136
|
-
...line2 === void 0 ? {} : { for: [line2.from.id] }
|
|
27646
|
+
const first = this.agents[0];
|
|
27647
|
+
return {
|
|
27648
|
+
readers,
|
|
27649
|
+
first: first === void 0 ? void 0 : this.profileOfRunning(first),
|
|
27650
|
+
ownProfiles: this.options.profiles !== void 0
|
|
27137
27651
|
};
|
|
27138
|
-
await this.hear(message, { goal: lookingAgain(note), sayWho: false });
|
|
27139
27652
|
}
|
|
27653
|
+
/** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
|
|
27654
|
+
whoseSession(sessionId) {
|
|
27655
|
+
const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
|
|
27656
|
+
if (entry === void 0) return void 0;
|
|
27657
|
+
const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
|
|
27658
|
+
const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
|
|
27659
|
+
const profile = this.profileFor(agentId2);
|
|
27660
|
+
return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
|
|
27661
|
+
}
|
|
27662
|
+
/** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
|
|
27663
|
+
async onContextEdit(frame) {
|
|
27664
|
+
if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
|
|
27665
|
+
if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
|
|
27666
|
+
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
27667
|
+
}
|
|
27668
|
+
return this.contexts.edit(frame);
|
|
27669
|
+
}
|
|
27670
|
+
// ------------------------------------------------------------------ an agent's model
|
|
27140
27671
|
/**
|
|
27141
|
-
*
|
|
27142
|
-
*
|
|
27672
|
+
* The model a person picked for an agent, as its copy names it: a choice's id, or the model its
|
|
27673
|
+
* config says each of its profile's runs asks for; nothing for Hermes's default.
|
|
27143
27674
|
*/
|
|
27144
|
-
|
|
27145
|
-
|
|
27146
|
-
return
|
|
27675
|
+
ownModel(agentId2, running) {
|
|
27676
|
+
const picked = this.options.state.models?.[agentId2];
|
|
27677
|
+
return picked === null ? void 0 : picked ?? running.config.model;
|
|
27678
|
+
}
|
|
27679
|
+
/** What a turn's runs ask Hermes for: the agent's own model, else nothing, and the profile's runs. */
|
|
27680
|
+
runModel(turn) {
|
|
27681
|
+
const picked = this.options.state.models?.[turn.who];
|
|
27682
|
+
if (picked === null) return {};
|
|
27683
|
+
if (picked !== void 0) return runModelOf(picked);
|
|
27684
|
+
return turn.agent.config.model === void 0 ? {} : { model: turn.agent.config.model };
|
|
27147
27685
|
}
|
|
27148
27686
|
/**
|
|
27149
|
-
*
|
|
27150
|
-
*
|
|
27151
|
-
*
|
|
27687
|
+
* Each agent's model, sent to Chips (Chips ADR 0141): the models Hermes has for its profile, read
|
|
27688
|
+
* once a profile, with the one it runs on. One whose Hermes doesn't answer is sent none, and the
|
|
27689
|
+
* log says so once.
|
|
27152
27690
|
*/
|
|
27153
|
-
async
|
|
27154
|
-
const
|
|
27155
|
-
|
|
27156
|
-
const
|
|
27157
|
-
const
|
|
27158
|
-
|
|
27159
|
-
|
|
27160
|
-
|
|
27161
|
-
|
|
27162
|
-
|
|
27163
|
-
|
|
27691
|
+
async sendModels() {
|
|
27692
|
+
const { chips: chips2 } = this.options;
|
|
27693
|
+
const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
27694
|
+
const read = /* @__PURE__ */ new Map();
|
|
27695
|
+
for (const agentId2 of ids2) {
|
|
27696
|
+
const running = this.runningFor(agentId2);
|
|
27697
|
+
if (running === void 0) continue;
|
|
27698
|
+
try {
|
|
27699
|
+
let options = read.get(running);
|
|
27700
|
+
if (options === void 0) {
|
|
27701
|
+
options = running.client.modelOptions();
|
|
27702
|
+
read.set(running, options);
|
|
27703
|
+
}
|
|
27704
|
+
const copy = copyOf(await options, this.ownModel(agentId2, running));
|
|
27705
|
+
if (copy !== void 0) await chips2.settings.model(agentId2, copy);
|
|
27706
|
+
} catch (error62) {
|
|
27707
|
+
if (!this.toldModels) this.log(`models: none sent for ${running.config.name}: ${messageOf3(error62)}`);
|
|
27708
|
+
this.toldModels = true;
|
|
27164
27709
|
}
|
|
27165
27710
|
}
|
|
27166
|
-
return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
|
|
27167
27711
|
}
|
|
27168
27712
|
/**
|
|
27169
|
-
*
|
|
27170
|
-
*
|
|
27713
|
+
* A person picked an agent's model (Chips ADR 0141): one Hermes has for its profile, kept as the
|
|
27714
|
+
* model each of its runs asks for from now on, or Hermes's default (`null`), the profile's own.
|
|
27715
|
+
* The model it runs on already changes nothing. Hermes says no levels of effort, so none is taken.
|
|
27171
27716
|
*/
|
|
27172
|
-
|
|
27173
|
-
const
|
|
27174
|
-
const
|
|
27175
|
-
|
|
27176
|
-
|
|
27177
|
-
|
|
27178
|
-
|
|
27179
|
-
}
|
|
27180
|
-
return room;
|
|
27181
|
-
}
|
|
27182
|
-
async pump(room) {
|
|
27183
|
-
if (room.busy) return;
|
|
27184
|
-
const asked = room.queue.shift();
|
|
27185
|
-
if (asked === void 0) return;
|
|
27186
|
-
room.busy = true;
|
|
27187
|
-
try {
|
|
27188
|
-
await this.turn(room, asked);
|
|
27189
|
-
} catch (error62) {
|
|
27190
|
-
this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf2(error62)}`);
|
|
27191
|
-
} finally {
|
|
27192
|
-
room.busy = false;
|
|
27193
|
-
void this.contexts?.refresh();
|
|
27194
|
-
void this.pump(room);
|
|
27195
|
-
}
|
|
27196
|
-
}
|
|
27197
|
-
async turn(room, { message, speakerId, goal, sayWho }) {
|
|
27198
|
-
const { agent, spaceId } = room;
|
|
27199
|
-
const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
|
|
27200
|
-
const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
|
|
27201
|
-
const turn = {
|
|
27202
|
-
run: run2,
|
|
27203
|
-
speaker,
|
|
27204
|
-
agent,
|
|
27205
|
-
hermesRun: "",
|
|
27206
|
-
reply: "",
|
|
27207
|
-
saidAt: 0,
|
|
27208
|
-
cancelled: false,
|
|
27209
|
-
next: [],
|
|
27210
|
-
open: /* @__PURE__ */ new Map(),
|
|
27211
|
-
counted: 0,
|
|
27212
|
-
approvals: /* @__PURE__ */ new Map(),
|
|
27213
|
-
allowed: [],
|
|
27214
|
-
interrupted: false
|
|
27215
|
-
};
|
|
27216
|
-
run2.on("cancel", () => {
|
|
27217
|
-
turn.cancelled = true;
|
|
27218
|
-
if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
27219
|
-
});
|
|
27220
|
-
run2.on("steer", (steer) => {
|
|
27221
|
-
const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
|
|
27222
|
-
void this.steered(turn, words4, steer.mode);
|
|
27223
|
-
});
|
|
27224
|
-
let session;
|
|
27717
|
+
async onModelChange(frame) {
|
|
27718
|
+
const { state: state2 } = this.options;
|
|
27719
|
+
const agentId2 = frame.agent_id;
|
|
27720
|
+
const ours2 = this.ours.has(agentId2) || this.agents.some((agent) => agent.agentId === agentId2) || this.own.has(agentId2);
|
|
27721
|
+
const running = ours2 ? this.runningFor(agentId2) : void 0;
|
|
27722
|
+
if (running === void 0) throw new Refuse(`${agentId2} isn't one of this harness's agents.`);
|
|
27723
|
+
let options;
|
|
27225
27724
|
try {
|
|
27226
|
-
|
|
27725
|
+
options = await running.client.modelOptions();
|
|
27227
27726
|
} catch (error62) {
|
|
27228
|
-
|
|
27229
|
-
return;
|
|
27727
|
+
throw new Refuse(`Hermes didn't say which models it has: ${messageOf3(error62)}`);
|
|
27230
27728
|
}
|
|
27231
|
-
const
|
|
27232
|
-
|
|
27233
|
-
|
|
27234
|
-
|
|
27235
|
-
|
|
27236
|
-
const last = await this.once(turn, session, text, `${run2.id}:${round}`);
|
|
27237
|
-
if (last === void 0) return;
|
|
27238
|
-
if (last.event === "run.cancelled" && turn.interrupted && turn.next.length > 0) {
|
|
27239
|
-
turn.interrupted = false;
|
|
27240
|
-
text = turn.next.shift();
|
|
27241
|
-
continue;
|
|
27242
|
-
}
|
|
27243
|
-
if (last.event !== "run.completed") {
|
|
27244
|
-
await this.ended(turn, last);
|
|
27245
|
-
return;
|
|
27246
|
-
}
|
|
27247
|
-
text = turn.next.shift();
|
|
27248
|
-
if (text === void 0) await this.finish(turn, last);
|
|
27729
|
+
const now = copyOf(options, this.ownModel(agentId2, running));
|
|
27730
|
+
if (now === void 0) throw new Refuse("Hermes has no model for this agent's profile.");
|
|
27731
|
+
if (frame.model === now.model) return now;
|
|
27732
|
+
if (frame.model !== null && !now.choices.some((choice) => choice.id === frame.model)) {
|
|
27733
|
+
throw new Refuse(`Hermes has no ${frame.model} for this agent's profile.`, now);
|
|
27249
27734
|
}
|
|
27735
|
+
state2.models = { ...state2.models, [agentId2]: frame.model };
|
|
27736
|
+
this.options.saveState(state2);
|
|
27737
|
+
const name2 = await this.nameOf(agentId2, running.config.name);
|
|
27738
|
+
this.log(`${name2} runs on ${frame.model ?? `Hermes's default (${now.default?.model ?? "its profile's"})`}, from Chips`);
|
|
27739
|
+
return copyOf(options, this.ownModel(agentId2, running)) ?? now;
|
|
27250
27740
|
}
|
|
27251
|
-
/**
|
|
27252
|
-
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27741
|
+
/** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
|
|
27742
|
+
onWindowCompact(frame) {
|
|
27743
|
+
throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
|
|
27744
|
+
}
|
|
27745
|
+
/** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
|
|
27746
|
+
onWindowChange(frame) {
|
|
27747
|
+
const profile = this.profileFor(frame.agent_id);
|
|
27748
|
+
throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
|
|
27749
|
+
}
|
|
27750
|
+
/** The Main agent, the first registered, once started: who says the harness's first words. */
|
|
27751
|
+
get mainAgentId() {
|
|
27752
|
+
return this.agents[0]?.agentId;
|
|
27753
|
+
}
|
|
27754
|
+
/**
|
|
27755
|
+
* The Main agent: the one Chips made for the harness when it joined, keyed by the harness's own
|
|
27756
|
+
* id, registered as Main (Chips ADR 0183), so it is called what the config says and has no
|
|
27757
|
+
* second beside it. It goes home, and a Main of ours from before that, out of its room now, is
|
|
27758
|
+
* archived, which gives its name up. With no such agent (a harness from before them), the Main
|
|
27759
|
+
* Chips has is kept: a harness has one at most.
|
|
27760
|
+
*/
|
|
27761
|
+
async seatMain(config3, known) {
|
|
27762
|
+
const { chips: chips2 } = this.options;
|
|
27763
|
+
const before = known.find((agent) => agent.lead);
|
|
27764
|
+
const made = known.some((agent) => agent.key === chips2.harnessId);
|
|
27765
|
+
if (!made && before !== void 0) {
|
|
27766
|
+
await this.goHome(before, known);
|
|
27767
|
+
return before;
|
|
27259
27768
|
}
|
|
27260
|
-
|
|
27261
|
-
|
|
27262
|
-
|
|
27263
|
-
|
|
27264
|
-
} catch (error62) {
|
|
27265
|
-
this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf2(error62)}`);
|
|
27266
|
-
return await this.endOf(turn);
|
|
27769
|
+
const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
|
|
27770
|
+
await this.goHome(main2, known);
|
|
27771
|
+
if (before !== void 0 && before.agentId !== main2.agentId) {
|
|
27772
|
+
await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
|
|
27267
27773
|
}
|
|
27774
|
+
return main2;
|
|
27268
27775
|
}
|
|
27269
|
-
/** The
|
|
27270
|
-
async
|
|
27271
|
-
const
|
|
27272
|
-
|
|
27273
|
-
|
|
27274
|
-
|
|
27275
|
-
if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
|
|
27276
|
-
return { ...state2, event: `run.${status}` };
|
|
27277
|
-
}
|
|
27278
|
-
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
27279
|
-
}
|
|
27280
|
-
await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
|
|
27281
|
-
return void 0;
|
|
27776
|
+
/** The Main agent into the harness's own room, in the place of whichever of ours is there. */
|
|
27777
|
+
async goHome(main2, known) {
|
|
27778
|
+
const { chips: chips2, home: home2 } = this.options;
|
|
27779
|
+
if (home2 === void 0 || main2.spaces.includes(home2)) return;
|
|
27780
|
+
const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home2));
|
|
27781
|
+
await chips2.agents.attach(main2.agentId, { spaceId: home2, replacing: there?.agentId ?? chips2.harnessId }).catch((error62) => this.log(`${main2.name} stays out of its home: ${String(error62)}`));
|
|
27282
27782
|
}
|
|
27283
|
-
/**
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
this.options
|
|
27290
|
-
|
|
27291
|
-
|
|
27783
|
+
/**
|
|
27784
|
+
* A person asked for a new agent in a room (Chips ADR 0108): one of the harness's, keyed by the
|
|
27785
|
+
* ask so an ask heard twice makes one, registered for the room, which Chips puts it in while the
|
|
27786
|
+
* room waits. It answers through a Hermes profile of its own (`ownProfile`).
|
|
27787
|
+
*/
|
|
27788
|
+
async make(asked) {
|
|
27789
|
+
const { chips: chips2 } = this.options;
|
|
27790
|
+
if (asked.harness_id !== chips2.harnessId || asked.space_id === void 0) return;
|
|
27791
|
+
const agent = await chips2.agents.register({
|
|
27792
|
+
key: `hermes:asked:${asked.id}`,
|
|
27793
|
+
name: asked.name,
|
|
27794
|
+
madeFor: asked.id,
|
|
27795
|
+
spaceId: asked.space_id
|
|
27796
|
+
});
|
|
27797
|
+
this.names.set(agent.agentId, agent.name);
|
|
27798
|
+
this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
|
|
27799
|
+
await this.ownProfile(agent.agentId, agent.name);
|
|
27800
|
+
this.ours.add(agent.agentId);
|
|
27801
|
+
void this.agentsMoved();
|
|
27292
27802
|
}
|
|
27293
27803
|
/**
|
|
27294
|
-
* A
|
|
27295
|
-
*
|
|
27296
|
-
*
|
|
27804
|
+
* A Hermes profile of its own for an agent made in Chips (Chips ADR 0241), so it remembers only
|
|
27805
|
+
* its own conversations: named after the agent, cloned from the Main agent's profile so it starts
|
|
27806
|
+
* with its model, keys, tools, skills and SOUL.md, its memory empty (`profiles.ts`). The gateway
|
|
27807
|
+
* is asked to serve it now; one that serves only the profile it started with, as Hermes's does
|
|
27808
|
+
* when it started with one, is restarted once. Where Hermes doesn't make one, the agent runs on
|
|
27809
|
+
* the first profile, in a session of its own, and the log says why.
|
|
27297
27810
|
*/
|
|
27298
|
-
async
|
|
27299
|
-
|
|
27300
|
-
|
|
27301
|
-
|
|
27302
|
-
|
|
27811
|
+
async ownProfile(agentId2, name2) {
|
|
27812
|
+
const { profiles, hermesRoot: root2, state: state2 } = this.options;
|
|
27813
|
+
const main2 = this.agents[0];
|
|
27814
|
+
if (profiles === void 0 || root2 === void 0 || main2 === void 0 || this.own.has(agentId2)) return;
|
|
27815
|
+
const from = profileName(main2.config.address.profile);
|
|
27816
|
+
const profile = freeProfile(root2, name2, Object.values(state2.made ?? {}));
|
|
27817
|
+
const home2 = profileHome(root2, profile);
|
|
27818
|
+
const secret = state2.fills?.secret ?? this.options.fills?.secret;
|
|
27819
|
+
try {
|
|
27820
|
+
await profiles.create(profile, from);
|
|
27821
|
+
const port = this.fillsPort;
|
|
27822
|
+
const hook = readyProfile(home2, { from, profile }, port === void 0 || secret === void 0 ? void 0 : { port, secret });
|
|
27823
|
+
if (hook?.state === "by_hand") this.log(`context window: ${hook.path} was left as it is: ${hook.why}. Add these lines to it:
|
|
27824
|
+
${hook.lines}`);
|
|
27825
|
+
} catch (error62) {
|
|
27826
|
+
this.log(`${name2} runs on the ${from} profile, in a session of its own: Hermes didn't make it one (${messageOf3(error62)})`);
|
|
27303
27827
|
return;
|
|
27304
27828
|
}
|
|
27305
|
-
|
|
27306
|
-
|
|
27307
|
-
|
|
27308
|
-
|
|
27309
|
-
|
|
27310
|
-
|
|
27311
|
-
|
|
27829
|
+
state2.made = { ...state2.made, [agentId2]: profile };
|
|
27830
|
+
this.options.saveState(state2);
|
|
27831
|
+
const running = this.runningOn(agentId2, profile, name2);
|
|
27832
|
+
if (running !== void 0) this.own.set(agentId2, running);
|
|
27833
|
+
this.log(`${name2} has a Hermes profile of its own, ${profile}, cloned from ${from}`);
|
|
27834
|
+
const served = await profiles.serve().catch(() => void 0);
|
|
27835
|
+
if (served === false) {
|
|
27836
|
+
this.log(`Hermes's gateway serves only the profile it started with, as it does when there was one: restarting it once, so it serves ${profile} too`);
|
|
27837
|
+
await this.options.restartGateway?.();
|
|
27838
|
+
} else if (served === void 0) {
|
|
27839
|
+
this.log(`Hermes's gateway didn't answer on its control socket: it serves ${profile} once it looks again, or starts again`);
|
|
27312
27840
|
}
|
|
27313
|
-
turn.next.push(text);
|
|
27314
27841
|
}
|
|
27315
|
-
|
|
27316
|
-
|
|
27317
|
-
|
|
27318
|
-
|
|
27319
|
-
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
|
|
27332
|
-
await this.completed(turn, event);
|
|
27333
|
-
return;
|
|
27334
|
-
case "approval.request":
|
|
27335
|
-
void this.ask(turn, event);
|
|
27336
|
-
return;
|
|
27337
|
-
case "approval.responded":
|
|
27338
|
-
await this.settled(turn, event);
|
|
27339
|
-
return;
|
|
27340
|
-
default:
|
|
27341
|
-
return;
|
|
27342
|
-
}
|
|
27842
|
+
/**
|
|
27843
|
+
* A profile made for an agent, as the bridge reaches it: under `/p/<profile>` of the gateway the
|
|
27844
|
+
* Main agent is reached at, with the key in the profile's own `.env`.
|
|
27845
|
+
*/
|
|
27846
|
+
runningOn(agentId2, profile, name2) {
|
|
27847
|
+
const main2 = this.agents[0];
|
|
27848
|
+
const root2 = this.options.hermesRoot;
|
|
27849
|
+
if (main2 === void 0 || root2 === void 0) return void 0;
|
|
27850
|
+
const home2 = profileHome(root2, profile);
|
|
27851
|
+
const config3 = {
|
|
27852
|
+
name: name2,
|
|
27853
|
+
address: { url: main2.config.address.url, profile, key: envValue(home2, KEY_ENV) ?? "" },
|
|
27854
|
+
keyEnv: KEY_ENV,
|
|
27855
|
+
keyFrom: join10(home2, ".env")
|
|
27856
|
+
};
|
|
27857
|
+
const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
|
|
27858
|
+
return { config: config3, key: agentId2, client, agentId: agentId2, up: true };
|
|
27343
27859
|
}
|
|
27344
|
-
|
|
27345
|
-
|
|
27346
|
-
|
|
27347
|
-
|
|
27348
|
-
|
|
27349
|
-
|
|
27860
|
+
/**
|
|
27861
|
+
* A space a person asked the harness for from the + (Chips ADR 0241), as the ACP bridge takes
|
|
27862
|
+
* one: claimed under a key made from the space, so a space handed over twice is claimed once,
|
|
27863
|
+
* and named from its first words. Chips makes the agent of that key and puts it in the space; it
|
|
27864
|
+
* gets a Hermes profile of its own (`ownProfile`). It answers the first words, which are the
|
|
27865
|
+
* ask's and aren't heard again.
|
|
27866
|
+
*/
|
|
27867
|
+
async claim(asked) {
|
|
27868
|
+
const { chips: chips2 } = this.options;
|
|
27869
|
+
const { spaceId, firstMessage } = asked;
|
|
27870
|
+
const key = `hermes:space:${spaceId}`;
|
|
27871
|
+
await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
|
|
27872
|
+
const agent = (await chips2.agents.list()).find((each) => each.key === key);
|
|
27873
|
+
if (agent === void 0) {
|
|
27874
|
+
this.log(`claimed ${spaceId}, and Chips put no agent there`);
|
|
27875
|
+
return;
|
|
27876
|
+
}
|
|
27877
|
+
this.names.set(agent.agentId, agent.name);
|
|
27878
|
+
this.log(`${agent.name} claimed ${spaceId}`);
|
|
27879
|
+
await this.ownProfile(agent.agentId, agent.name);
|
|
27880
|
+
this.ours.add(agent.agentId);
|
|
27881
|
+
void this.agentsMoved();
|
|
27882
|
+
const message = {
|
|
27883
|
+
type: "message.new",
|
|
27884
|
+
space_id: spaceId,
|
|
27885
|
+
message_id: firstMessage.messageId,
|
|
27886
|
+
text: firstMessage.text,
|
|
27887
|
+
from: { participant_id: asked.askedBy, kind: "person", display_name: await this.whoSaid(asked), role: "member" },
|
|
27888
|
+
for: [agent.agentId]
|
|
27889
|
+
};
|
|
27890
|
+
void this.hear(message);
|
|
27350
27891
|
}
|
|
27351
|
-
|
|
27352
|
-
|
|
27353
|
-
const
|
|
27354
|
-
|
|
27355
|
-
const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
|
|
27356
|
-
const stepId = callId ?? `${tool}-${turn.counted}`;
|
|
27357
|
-
const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
|
|
27358
|
-
turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
|
|
27359
|
-
const said2 = sub ? event["goal"] : event["preview"];
|
|
27360
|
-
await turn.run.step({
|
|
27361
|
-
stepId,
|
|
27362
|
-
status: "started",
|
|
27363
|
-
title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
|
|
27364
|
-
tool,
|
|
27365
|
-
...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
|
|
27366
|
-
}).catch(() => void 0);
|
|
27892
|
+
/** Who said a space's first words, from its lines. */
|
|
27893
|
+
async whoSaid(asked) {
|
|
27894
|
+
const context = await this.options.chips.roomContext(asked.spaceId, { limit: 20 }).catch(() => void 0);
|
|
27895
|
+
return context?.lines.find((line2) => line2.message_id === asked.firstMessage.messageId)?.from.name ?? "Someone";
|
|
27367
27896
|
}
|
|
27368
|
-
|
|
27369
|
-
|
|
27370
|
-
|
|
27371
|
-
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
|
|
27379
|
-
|
|
27897
|
+
/**
|
|
27898
|
+
* A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
|
|
27899
|
+
* agent's session in the room it is in, else the last it had, forked in Hermes, is the copy's
|
|
27900
|
+
* session in the room asked. The copy answers through its original's profile, whose Hermes has
|
|
27901
|
+
* that session (Hermes forks a session only within its profile, so a copy gets no profile of its
|
|
27902
|
+
* own: Chips ADR 0241), and works in its original's folder. It is registered with `forkedFrom` for the
|
|
27903
|
+
* room, which Chips puts it in while the room waits. The fork and the copy are keyed by the ask,
|
|
27904
|
+
* so an ask heard twice forks once (Hermes refuses the id again) and registers the same copy. An
|
|
27905
|
+
* agent that never spoke is copied with nothing to remember. A turn still going is not in the
|
|
27906
|
+
* copy: Hermes copies what the session holds. Nothing is made for an ask the room has stopped
|
|
27907
|
+
* waiting for (`FORK_WAIT_MS`), nor for an agent that isn't the harness's.
|
|
27908
|
+
*/
|
|
27909
|
+
async copy(fork) {
|
|
27910
|
+
const { chips: chips2, state: state2 } = this.options;
|
|
27911
|
+
const spaceId = fork.space_id;
|
|
27912
|
+
if (spaceId === void 0) return;
|
|
27913
|
+
const late = Date.now() - Date.parse(fork.ts);
|
|
27914
|
+
if (late > FORK_WAIT_MS) {
|
|
27915
|
+
this.log(`no copy of ${fork.agent_id}: asked ${Math.round(late / 1e3)} s ago, and the room waits a minute`);
|
|
27916
|
+
return;
|
|
27380
27917
|
}
|
|
27381
|
-
const
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
status: failed ? "failed" : "done",
|
|
27386
|
-
tool,
|
|
27387
|
-
...typeof summary === "string" && summary !== "" ? { summary: summary.slice(0, 2e3) } : {},
|
|
27388
|
-
...seconds === void 0 ? {} : { durationMs: Math.round(seconds * 1e3) }
|
|
27389
|
-
}).catch(() => void 0);
|
|
27390
|
-
const cost3 = event["cost_usd"];
|
|
27391
|
-
if (sub && typeof cost3 === "number" && cost3 > 0) {
|
|
27392
|
-
const model = typeof event["model"] === "string" ? event["model"] : "subagent";
|
|
27393
|
-
await turn.run.usage({ scope: "step", model, costUsd: cost3 }).catch(() => void 0);
|
|
27918
|
+
const original = await this.profileOf(fork.agent_id);
|
|
27919
|
+
if (original === void 0) {
|
|
27920
|
+
this.log(`no copy of ${fork.agent_id}: it isn't one of this harness's agents`);
|
|
27921
|
+
return;
|
|
27394
27922
|
}
|
|
27923
|
+
const name2 = await this.nameOf(fork.agent_id, original.config.name);
|
|
27924
|
+
const key = `hermes:fork:${fork.id}`;
|
|
27925
|
+
const who = fork.agent_id === original.agentId ? original.key : fork.agent_id;
|
|
27926
|
+
const listed = await chips2.agents.list().catch(() => []);
|
|
27927
|
+
const room = listed.find((agent) => agent.agentId === fork.agent_id)?.room ?? void 0;
|
|
27928
|
+
const from = (room === void 0 ? void 0 : state2.sessions[`${who}|${room}`]) ?? Object.entries(state2.sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
|
|
27929
|
+
if (from === void 0) this.log(`${name2} never spoke: its copy remembers nothing`);
|
|
27930
|
+
const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips space ${spaceId} \xB7 ${key}` });
|
|
27931
|
+
const folder2 = this.folderOf(original, fork.agent_id, name2);
|
|
27932
|
+
const copy = await chips2.agents.register({ key, name: forkName(name2), forkedFrom: fork.agent_id, spaceId });
|
|
27933
|
+
if (session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] ??= session;
|
|
27934
|
+
state2.profiles = { ...state2.profiles, [copy.agentId]: original.key };
|
|
27935
|
+
if (folder2 !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: folder2 };
|
|
27936
|
+
this.options.saveState(state2);
|
|
27937
|
+
this.ours.add(copy.agentId);
|
|
27938
|
+
this.names.set(copy.agentId, copy.name);
|
|
27939
|
+
const away = copy.attached === false && !copy.spaces.includes(spaceId);
|
|
27940
|
+
this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
|
|
27941
|
+
void this.agentsMoved();
|
|
27395
27942
|
}
|
|
27396
|
-
|
|
27397
|
-
|
|
27398
|
-
|
|
27399
|
-
|
|
27400
|
-
|
|
27401
|
-
const description = typeof event["description"] === "string" ? event["description"] : void 0;
|
|
27402
|
-
const choices = Array.isArray(event["choices"]) ? event["choices"].map(String) : ["once", "deny"];
|
|
27403
|
-
const approvalId = ids.next("approval");
|
|
27404
|
-
turn.approvals.set(requestId ?? approvalId, { approvalId, answered: false });
|
|
27405
|
-
const asked = turn.run.approval({
|
|
27406
|
-
approvalId,
|
|
27407
|
-
action: {
|
|
27408
|
-
kind: command === void 0 ? "hermes.approval" : "shell.exec",
|
|
27409
|
-
input: { ...command === void 0 ? {} : { command }, ...description === void 0 ? {} : { description } }
|
|
27410
|
-
},
|
|
27411
|
-
question: (description === void 0 ? "Run this command?" : `Allow it to ${lowerFirst(description)}?`).slice(0, 300),
|
|
27412
|
-
lasts: "once",
|
|
27413
|
-
risk: [],
|
|
27414
|
-
...command === void 0 ? {} : { preview: { format: "markdown", title: "Run a command", body: `\`\`\`sh
|
|
27415
|
-
${command}
|
|
27416
|
-
\`\`\`` } }
|
|
27417
|
-
});
|
|
27418
|
-
const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
|
|
27419
|
-
const decision = await Promise.race([asked, stopped2]);
|
|
27420
|
-
const pending = turn.approvals.get(requestId ?? approvalId);
|
|
27421
|
-
if (decision === void 0 || pending === void 0 || pending.answered) return;
|
|
27422
|
-
pending.answered = true;
|
|
27423
|
-
if (decision.allowed) turn.allowed.push(approvalId);
|
|
27424
|
-
const generation = event["execution_generation"];
|
|
27425
|
-
await turn.agent.client.approve(turn.hermesRun, {
|
|
27426
|
-
choice: choiceOf(decision, choices, command !== void 0),
|
|
27427
|
-
...requestId === void 0 ? {} : { request_id: requestId },
|
|
27428
|
-
...typeof generation === "number" ? { execution_generation: generation } : {}
|
|
27429
|
-
}).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf2(error62)}`));
|
|
27430
|
-
}
|
|
27431
|
-
/** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
|
|
27432
|
-
async settled(turn, event) {
|
|
27433
|
-
const requestId = idOf(event);
|
|
27434
|
-
const pending = requestId === void 0 ? void 0 : turn.approvals.get(requestId);
|
|
27435
|
-
if (pending === void 0 || pending.answered) return;
|
|
27436
|
-
pending.answered = true;
|
|
27437
|
-
const denied = event["choice"] === "deny";
|
|
27438
|
-
await this.options.chips.send(
|
|
27439
|
-
"approval.resolved",
|
|
27440
|
-
{ run_id: turn.run.id },
|
|
27441
|
-
{
|
|
27442
|
-
approval_id: pending.approvalId,
|
|
27443
|
-
decision: denied ? "denied" : "approved",
|
|
27444
|
-
scope: "once",
|
|
27445
|
-
reason: "Answered in Hermes."
|
|
27446
|
-
},
|
|
27447
|
-
`hermes-approval:${turn.hermesRun}:${requestId}:settled`
|
|
27448
|
-
).catch(() => void 0);
|
|
27449
|
-
}
|
|
27450
|
-
// ------------------------------------------------------------------ the end
|
|
27451
|
-
/** The turn is over: its words in the room, its tokens, and the run ended. */
|
|
27452
|
-
async finish(turn, last) {
|
|
27453
|
-
const { run: run2, agent } = turn;
|
|
27454
|
-
if (run2.over) return;
|
|
27455
|
-
await this.closeSteps(turn, "done");
|
|
27456
|
-
for (const approvalId of turn.allowed.splice(0)) {
|
|
27457
|
-
await run2.executed(approvalId, "executed", "Hermes didn't say how it went.").catch(() => void 0);
|
|
27458
|
-
}
|
|
27459
|
-
await this.counted(turn, last);
|
|
27460
|
-
const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
|
|
27461
|
-
const answer = output2 !== "" ? output2 : turn.reply.trim();
|
|
27462
|
-
const { change, note } = await this.sendProject(turn, answer);
|
|
27463
|
-
const { folder: folder2 } = turn;
|
|
27464
|
-
const { chips: chips2 } = this.options;
|
|
27465
|
-
const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
|
|
27466
|
-
const post = await sendAnswer(words4, {
|
|
27467
|
-
folders: folder2 === void 0 ? [] : [folder2],
|
|
27468
|
-
upload: (input2) => chips2.upload(input2)
|
|
27469
|
-
});
|
|
27470
|
-
const reply = post.text;
|
|
27471
|
-
if (reply !== "" || post.uploadIds.length > 0) {
|
|
27472
|
-
await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
|
|
27943
|
+
/** An agent's name, from Chips's list when it isn't known yet. */
|
|
27944
|
+
async nameOf(agentId2, otherwise) {
|
|
27945
|
+
if (!this.names.has(agentId2)) {
|
|
27946
|
+
const listed = await this.options.chips.agents.list().catch(() => []);
|
|
27947
|
+
for (const agent of listed) this.names.set(agent.agentId, agent.name);
|
|
27473
27948
|
}
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
27478
|
-
|
|
27949
|
+
return this.names.get(agentId2) ?? otherwise;
|
|
27950
|
+
}
|
|
27951
|
+
/**
|
|
27952
|
+
* The folder an agent works in and sends its files from: the one the config names for them all
|
|
27953
|
+
* (`folder`, as it was), else one of its own under `folders`, named after it the first time it
|
|
27954
|
+
* takes a turn and kept, so a rename keeps it. Every agent once shared one folder, and a team's
|
|
27955
|
+
* files overwrote a one-agent room's game (Chips TT-16); and the one folder had to be Hermes's
|
|
27956
|
+
* own working folder, which a plain Hermes doesn't set (the setup audit, 17 October 2026).
|
|
27957
|
+
*/
|
|
27958
|
+
folderOf(agent, agentId2, name2) {
|
|
27959
|
+
if (agent.config.folder !== void 0) return agent.config.folder;
|
|
27960
|
+
const { folders: root2, state: state2 } = this.options;
|
|
27961
|
+
if (root2 === void 0) return void 0;
|
|
27962
|
+
const kept = state2.folders?.[agentId2];
|
|
27963
|
+
const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
|
|
27964
|
+
mkdirSync5(folder2, { recursive: true });
|
|
27965
|
+
if (kept === void 0) {
|
|
27966
|
+
state2.folders = { ...state2.folders, [agentId2]: folder2 };
|
|
27967
|
+
this.options.saveState(state2);
|
|
27968
|
+
}
|
|
27969
|
+
return folder2;
|
|
27479
27970
|
}
|
|
27480
27971
|
/**
|
|
27481
|
-
*
|
|
27482
|
-
*
|
|
27483
|
-
*
|
|
27972
|
+
* The room's project for a turn (Chips ADR 0221): the agent's copy, in its folder, brought up to
|
|
27973
|
+
* date, and what the turn is told of it. None for an agent a person pointed at a folder of their
|
|
27974
|
+
* own, nor where this machine has no git for it.
|
|
27484
27975
|
*/
|
|
27485
|
-
|
|
27486
|
-
if (this.
|
|
27487
|
-
|
|
27488
|
-
|
|
27489
|
-
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
|
|
27493
|
-
|
|
27494
|
-
}
|
|
27495
|
-
|
|
27496
|
-
|
|
27497
|
-
async ended(turn, last) {
|
|
27498
|
-
await this.closeSteps(turn, "failed");
|
|
27499
|
-
await this.counted(turn, last);
|
|
27500
|
-
if (turn.cancelled || turn.run.over) return;
|
|
27501
|
-
const name2 = turn.agent.config.name;
|
|
27502
|
-
if (last.event === "run.cancelled") await this.failed(turn, `${name2}'s run was stopped in Hermes.`, false);
|
|
27503
|
-
else if (last.event === "run.interrupted") await this.failed(turn, `Hermes restarted during ${name2}'s run.`, true);
|
|
27504
|
-
else {
|
|
27505
|
-
const why = typeof last["error"] === "string" && last["error"] !== "" ? last["error"] : "it failed in Hermes";
|
|
27506
|
-
await this.failed(turn, `${name2}: ${why}`, true);
|
|
27976
|
+
async projectFor(turn, spaceId, agentId2, name2) {
|
|
27977
|
+
if (this.options.projects !== true || turn.agent.config.folder !== void 0) return void 0;
|
|
27978
|
+
const folder2 = turn.folder;
|
|
27979
|
+
if (folder2 === void 0) return void 0;
|
|
27980
|
+
try {
|
|
27981
|
+
const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line2) => this.log(line2) });
|
|
27982
|
+
const facts = await project.pull();
|
|
27983
|
+
turn.project = { copy: project, name: name2 };
|
|
27984
|
+
return facts;
|
|
27985
|
+
} catch (error62) {
|
|
27986
|
+
this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf3(error62)}`);
|
|
27987
|
+
return void 0;
|
|
27507
27988
|
}
|
|
27508
27989
|
}
|
|
27509
27990
|
/**
|
|
27510
|
-
*
|
|
27511
|
-
* `
|
|
27512
|
-
*
|
|
27513
|
-
* counts go with it, and are left out where Hermes doesn't say them.
|
|
27991
|
+
* What the turn changed in its copy, sent to the room's project before its words are posted: the
|
|
27992
|
+
* commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
|
|
27993
|
+
* the change didn't go, the line the reply says so in (Chips AP-6).
|
|
27514
27994
|
*/
|
|
27515
|
-
async
|
|
27516
|
-
|
|
27517
|
-
const
|
|
27518
|
-
|
|
27519
|
-
|
|
27520
|
-
|
|
27521
|
-
|
|
27522
|
-
|
|
27523
|
-
|
|
27524
|
-
|
|
27525
|
-
|
|
27526
|
-
|
|
27527
|
-
|
|
27528
|
-
cacheWriteTokens: count("cache_write_tokens"),
|
|
27529
|
-
outputTokens: output2
|
|
27530
|
-
}).catch(() => void 0);
|
|
27531
|
-
}
|
|
27532
|
-
async closeSteps(turn, status) {
|
|
27533
|
-
for (const [, ids2] of turn.open) {
|
|
27534
|
-
for (const stepId of ids2) await turn.run.step({ stepId, status }).catch(() => void 0);
|
|
27995
|
+
async sendProject(turn, words4) {
|
|
27996
|
+
if (turn.project === void 0) return {};
|
|
27997
|
+
const { copy, name: name2 } = turn.project;
|
|
27998
|
+
try {
|
|
27999
|
+
const pushed = await copy.push({ words: words4 });
|
|
28000
|
+
this.log(`${name2}'s project: ${pushedInWords(pushed)}`);
|
|
28001
|
+
if (pushed.state === "sent") return { change: pushed.commit };
|
|
28002
|
+
const note = heldNote(pushed, name2);
|
|
28003
|
+
return note === void 0 ? {} : { note };
|
|
28004
|
+
} catch (error62) {
|
|
28005
|
+
this.log(`${name2}'s project couldn't be sent: ${messageOf3(error62)}`);
|
|
28006
|
+
const note = heldNote({ state: "failed", why: messageOf3(error62) }, name2);
|
|
28007
|
+
return note === void 0 ? {} : { note };
|
|
27535
28008
|
}
|
|
27536
|
-
turn.open.clear();
|
|
27537
28009
|
}
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
if (
|
|
28010
|
+
/** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083); nothing while a restart runs. */
|
|
28011
|
+
async sayHowItIs() {
|
|
28012
|
+
if (this.held) return;
|
|
28013
|
+
for (const agent of this.agents) agent.up = await agent.client.healthy();
|
|
28014
|
+
if (this.held) return;
|
|
28015
|
+
const down = this.agents.filter((agent) => !agent.up);
|
|
28016
|
+
if (down.length === 0) this.why = void 0;
|
|
28017
|
+
const chips2 = this.options.chips;
|
|
28018
|
+
const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
|
|
28019
|
+
await said2.catch(() => void 0);
|
|
27541
28020
|
}
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
|
|
27547
|
-
|
|
27548
|
-
|
|
27549
|
-
|
|
27550
|
-
|
|
27551
|
-
|
|
27552
|
-
|
|
27553
|
-
|
|
27554
|
-
|
|
27555
|
-
if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
|
|
27556
|
-
return error62 instanceof Error ? error62.message : String(error62);
|
|
27557
|
-
}
|
|
27558
|
-
function forkName(original) {
|
|
27559
|
-
return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
|
|
27560
|
-
}
|
|
27561
|
-
function goalOf(message, name2) {
|
|
27562
|
-
const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
|
|
27563
|
-
const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
|
|
27564
|
-
const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
|
|
27565
|
-
return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
|
|
27566
|
-
}
|
|
27567
|
-
var REPLY_SHOWN = 1500;
|
|
27568
|
-
var GOAL_SHOWN = 120;
|
|
27569
|
-
function lookAgain(who, reply, note) {
|
|
27570
|
-
const said2 = reply?.trim() ?? "";
|
|
27571
|
-
return [
|
|
27572
|
-
`${who} says your reply in this space wasn't right.`,
|
|
27573
|
-
...said2 === "" ? [] : [`Your reply:
|
|
27574
|
-
\xAB${cut(said2, REPLY_SHOWN)}\xBB`],
|
|
27575
|
-
note === void 0 || note === "" ? "They didn't say what. Read your reply again and find what was wrong." : `What wasn't right: \xAB${note}\xBB`,
|
|
27576
|
-
"Look at it again and answer properly in its place. If it is not clear what was wrong, ask them in one short line. Say plainly what you changed."
|
|
27577
|
-
].join("\n\n");
|
|
27578
|
-
}
|
|
27579
|
-
function lookingAgain(note) {
|
|
27580
|
-
const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
|
|
27581
|
-
return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
|
|
27582
|
-
}
|
|
27583
|
-
function cut(text, most) {
|
|
27584
|
-
return text.length <= most ? text : `${text.slice(0, most - 1)}\u2026`;
|
|
27585
|
-
}
|
|
27586
|
-
function resultWords(preview2) {
|
|
27587
|
-
if (typeof preview2 !== "string" || !preview2.trimStart().startsWith("{")) return preview2;
|
|
27588
|
-
let result;
|
|
27589
|
-
try {
|
|
27590
|
-
result = JSON.parse(preview2);
|
|
27591
|
-
} catch {
|
|
27592
|
-
return preview2;
|
|
28021
|
+
// ------------------------------------------------------------------ what is said to it
|
|
28022
|
+
/** A message for some of its agents: each takes a turn in its room, one after another there. */
|
|
28023
|
+
async hear(message, how = {}) {
|
|
28024
|
+
const spaceId = message.space_id;
|
|
28025
|
+
if (spaceId === void 0) return;
|
|
28026
|
+
const harnessId = this.options.chips.harnessId;
|
|
28027
|
+
for (const taker of message.for ?? [harnessId]) {
|
|
28028
|
+
const agent = await this.runnerOf(taker);
|
|
28029
|
+
if (agent === void 0) continue;
|
|
28030
|
+
const room = this.roomOf(agent, taker, spaceId);
|
|
28031
|
+
room.queue.push({ message, speakerId: taker === harnessId ? void 0 : taker, ...how });
|
|
28032
|
+
void this.pump(room);
|
|
28033
|
+
}
|
|
27593
28034
|
}
|
|
27594
|
-
|
|
27595
|
-
|
|
27596
|
-
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27600
|
-
|
|
27601
|
-
|
|
27602
|
-
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
|
|
27611
|
-
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
27620
|
-
};
|
|
27621
|
-
|
|
27622
|
-
var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
|
|
27623
|
-
function gatewaysOf(agents) {
|
|
27624
|
-
return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
|
|
27625
|
-
}
|
|
27626
|
-
function isLocal(url2) {
|
|
27627
|
-
try {
|
|
27628
|
-
return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
|
|
27629
|
-
} catch {
|
|
27630
|
-
return false;
|
|
28035
|
+
/**
|
|
28036
|
+
* "Not right?" on one of its agents' replies (Chips ADR 0200): a person's note on it, or a bare
|
|
28037
|
+
* 👎 from an older phone, is a turn of the agent that said it, in its session in that room and
|
|
28038
|
+
* in line with the room's other turns, to look again and answer in its place. Any other
|
|
28039
|
+
* reaction is the person's own. Once per event, however often it is heard.
|
|
28040
|
+
*/
|
|
28041
|
+
async notRight(feedback2) {
|
|
28042
|
+
const { space_id: spaceId, message_id: messageId } = feedback2;
|
|
28043
|
+
const note = feedback2.kind === "correction" ? (feedback2.text ?? "").trim() : void 0;
|
|
28044
|
+
const flagged = note === void 0 ? feedback2.kind === "reaction" && feedback2.reaction === "\u{1F44E}" : note !== "";
|
|
28045
|
+
if (spaceId === void 0 || !messageId || !flagged || this.lookedAt.has(feedback2.id)) return;
|
|
28046
|
+
this.lookedAt.add(feedback2.id);
|
|
28047
|
+
for (const old of this.lookedAt) {
|
|
28048
|
+
if (this.lookedAt.size <= KEPT_FEEDBACK) break;
|
|
28049
|
+
this.lookedAt.delete(old);
|
|
28050
|
+
}
|
|
28051
|
+
const context = await this.options.chips.roomContext(spaceId, { limit: 200 }).catch(() => void 0);
|
|
28052
|
+
const line2 = context?.lines.find((each) => each.message_id === messageId);
|
|
28053
|
+
const message = {
|
|
28054
|
+
type: "message.new",
|
|
28055
|
+
space_id: spaceId,
|
|
28056
|
+
// An id of its own, so nothing of the turn is taken for the reply's.
|
|
28057
|
+
message_id: messageIdOf(feedback2.id),
|
|
28058
|
+
text: lookAgain(feedback2.from.display_name, line2?.text, note),
|
|
28059
|
+
from: feedback2.from,
|
|
28060
|
+
...line2 === void 0 ? {} : { for: [line2.from.id] }
|
|
28061
|
+
};
|
|
28062
|
+
await this.hear(message, { goal: lookingAgain(note), sayWho: false });
|
|
27631
28063
|
}
|
|
27632
|
-
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
|
|
27636
|
-
|
|
27637
|
-
|
|
27638
|
-
|
|
27639
|
-
|
|
27640
|
-
|
|
27641
|
-
|
|
27642
|
-
|
|
27643
|
-
|
|
27644
|
-
|
|
27645
|
-
|
|
27646
|
-
|
|
27647
|
-
|
|
27648
|
-
|
|
27649
|
-
|
|
27650
|
-
|
|
27651
|
-
|
|
27652
|
-
|
|
27653
|
-
|
|
27654
|
-
|
|
27655
|
-
|
|
27656
|
-
|
|
28064
|
+
/**
|
|
28065
|
+
* Which profile answers for a Chips agent, as `profileOf` says. An agent not settled may be one
|
|
28066
|
+
* being made, or a copy, which is waited for, so that its first message is answered on its own
|
|
28067
|
+
* profile, or in the session it was forked.
|
|
28068
|
+
*/
|
|
28069
|
+
async runnerOf(taker) {
|
|
28070
|
+
const settled = taker === this.options.chips.harnessId || this.own.has(taker) || this.agents.some((agent) => agent.agentId === taker);
|
|
28071
|
+
if (!settled) await this.making;
|
|
28072
|
+
return this.profileOf(taker);
|
|
28073
|
+
}
|
|
28074
|
+
/**
|
|
28075
|
+
* Which profile answers for a Chips agent: its own, configured or made for it; a copy's
|
|
28076
|
+
* original's; or the first for the harness itself and for any other of the harness's — its
|
|
28077
|
+
* default agent, one made before profiles were — read afresh once when a name is new.
|
|
28078
|
+
*/
|
|
28079
|
+
async profileOf(taker) {
|
|
28080
|
+
const own2 = this.agents.find((agent) => agent.agentId === taker) ?? this.own.get(taker);
|
|
28081
|
+
if (own2 !== void 0) return own2;
|
|
28082
|
+
const copied = this.options.state.profiles?.[taker];
|
|
28083
|
+
const original = copied === void 0 ? void 0 : this.byKey(copied);
|
|
28084
|
+
if (original !== void 0) return original;
|
|
28085
|
+
if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) {
|
|
28086
|
+
const listed = await this.options.chips.agents.list().catch(() => []);
|
|
28087
|
+
for (const agent of listed) {
|
|
28088
|
+
this.ours.add(agent.agentId);
|
|
28089
|
+
this.names.set(agent.agentId, agent.name);
|
|
28090
|
+
}
|
|
28091
|
+
}
|
|
28092
|
+
return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
|
|
27657
28093
|
}
|
|
27658
|
-
|
|
27659
|
-
|
|
27660
|
-
|
|
27661
|
-
|
|
27662
|
-
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
|
|
28094
|
+
/**
|
|
28095
|
+
* The session a turn is taken in: the profile's own in the room, for its agent or the harness
|
|
28096
|
+
* itself, and one of its own for any other of the harness's, so two in a room are two.
|
|
28097
|
+
*/
|
|
28098
|
+
roomOf(agent, taker, spaceId) {
|
|
28099
|
+
const own2 = taker === agent.agentId || taker === this.options.chips.harnessId;
|
|
28100
|
+
const id2 = `${own2 ? agent.key : taker}|${spaceId}`;
|
|
28101
|
+
let room = this.rooms.get(id2);
|
|
28102
|
+
if (room === void 0) {
|
|
28103
|
+
room = { id: id2, agent, spaceId, queue: [], busy: false };
|
|
28104
|
+
this.rooms.set(id2, room);
|
|
28105
|
+
}
|
|
28106
|
+
return room;
|
|
27667
28107
|
}
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
const
|
|
27671
|
-
if (
|
|
27672
|
-
|
|
27673
|
-
|
|
28108
|
+
async pump(room) {
|
|
28109
|
+
if (room.busy) return;
|
|
28110
|
+
const asked = room.queue.shift();
|
|
28111
|
+
if (asked === void 0) return;
|
|
28112
|
+
room.busy = true;
|
|
28113
|
+
try {
|
|
28114
|
+
await this.turn(room, asked);
|
|
28115
|
+
} catch (error62) {
|
|
28116
|
+
this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf3(error62)}`);
|
|
28117
|
+
} finally {
|
|
28118
|
+
room.busy = false;
|
|
28119
|
+
void this.contexts?.refresh();
|
|
28120
|
+
void this.pump(room);
|
|
28121
|
+
}
|
|
27674
28122
|
}
|
|
27675
|
-
|
|
27676
|
-
}
|
|
27677
|
-
|
|
27678
|
-
|
|
27679
|
-
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
28123
|
+
async turn(room, { message, speakerId, goal, sayWho }) {
|
|
28124
|
+
const { agent, spaceId } = room;
|
|
28125
|
+
const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
|
|
28126
|
+
const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
|
|
28127
|
+
const turn = {
|
|
28128
|
+
run: run2,
|
|
28129
|
+
speaker,
|
|
28130
|
+
agent,
|
|
28131
|
+
who: speakerId ?? agent.agentId,
|
|
28132
|
+
hermesRun: "",
|
|
28133
|
+
reply: "",
|
|
28134
|
+
saidAt: 0,
|
|
28135
|
+
cancelled: false,
|
|
28136
|
+
next: [],
|
|
28137
|
+
open: /* @__PURE__ */ new Map(),
|
|
28138
|
+
counted: 0,
|
|
28139
|
+
approvals: /* @__PURE__ */ new Map(),
|
|
28140
|
+
allowed: [],
|
|
28141
|
+
interrupted: false
|
|
28142
|
+
};
|
|
28143
|
+
run2.on("cancel", () => {
|
|
28144
|
+
turn.cancelled = true;
|
|
28145
|
+
if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
28146
|
+
});
|
|
28147
|
+
run2.on("steer", (steer) => {
|
|
28148
|
+
const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
|
|
28149
|
+
void this.steered(turn, words4, steer.mode);
|
|
28150
|
+
});
|
|
28151
|
+
let session;
|
|
28152
|
+
try {
|
|
28153
|
+
session = await this.sessionFor(room);
|
|
28154
|
+
} catch (error62) {
|
|
28155
|
+
await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf3(error62)}`, true);
|
|
28156
|
+
return;
|
|
28157
|
+
}
|
|
28158
|
+
const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
|
|
28159
|
+
turn.folder = this.folderOf(agent, speakerId ?? agent.agentId, name2);
|
|
28160
|
+
const project = await this.projectFor(turn, room.spaceId, speakerId ?? agent.agentId, name2);
|
|
28161
|
+
turn.rules = rulesFor(message, { taker: speakerId, project });
|
|
28162
|
+
let text = promptFor(message, { taker: speakerId, sayWho, project, rulesHeld: true });
|
|
28163
|
+
for (let round = 0; text !== void 0 && !turn.cancelled && !run2.over; round += 1) {
|
|
28164
|
+
const last = await this.once(turn, session, text, `${run2.id}:${round}`);
|
|
28165
|
+
if (last === void 0) return;
|
|
28166
|
+
if (last.event === "run.cancelled" && turn.interrupted && turn.next.length > 0) {
|
|
28167
|
+
turn.interrupted = false;
|
|
28168
|
+
text = turn.next.shift();
|
|
28169
|
+
continue;
|
|
27694
28170
|
}
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
|
|
27699
|
-
out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
|
|
28171
|
+
if (last.event !== "run.completed") {
|
|
28172
|
+
await this.ended(turn, last);
|
|
28173
|
+
return;
|
|
27700
28174
|
}
|
|
27701
|
-
|
|
27702
|
-
|
|
27703
|
-
const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
|
|
27704
|
-
out += name2 === void 0 ? "$" : env[name2] ?? "";
|
|
27705
|
-
at += 1 + (name2?.length ?? 0);
|
|
28175
|
+
text = turn.next.shift();
|
|
28176
|
+
if (text === void 0) await this.finish(turn, last);
|
|
27706
28177
|
}
|
|
27707
28178
|
}
|
|
27708
|
-
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
28179
|
+
/** One Hermes run of the turn: started, its events heard to the end. The end, or nothing on failure. */
|
|
28180
|
+
async once(turn, session, text, key) {
|
|
28181
|
+
const { agent } = turn;
|
|
28182
|
+
try {
|
|
28183
|
+
turn.hermesRun = await agent.client.startRun({
|
|
28184
|
+
text,
|
|
28185
|
+
sessionId: session,
|
|
28186
|
+
...this.runModel(turn),
|
|
28187
|
+
key,
|
|
28188
|
+
instructions: instructionsFor(turn.folder, turn.project !== void 0, turn.rules)
|
|
28189
|
+
});
|
|
28190
|
+
} catch (error62) {
|
|
28191
|
+
await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf3(error62)}`, true);
|
|
28192
|
+
return void 0;
|
|
28193
|
+
}
|
|
28194
|
+
if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
28195
|
+
try {
|
|
28196
|
+
const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
|
|
28197
|
+
return last ?? await this.endOf(turn);
|
|
28198
|
+
} catch (error62) {
|
|
28199
|
+
this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf3(error62)}`);
|
|
28200
|
+
return await this.endOf(turn);
|
|
27717
28201
|
}
|
|
27718
|
-
return { entry: expand(found[2] ?? "", env), vars };
|
|
27719
|
-
}
|
|
27720
|
-
return void 0;
|
|
27721
|
-
}
|
|
27722
|
-
var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
|
|
27723
|
-
var messageOf3 = (error62) => error62 instanceof Error ? error62.message : String(error62);
|
|
27724
|
-
function gatewayEnv(env) {
|
|
27725
|
-
return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
|
|
27726
|
-
}
|
|
27727
|
-
var Gateways = class {
|
|
27728
|
-
constructor(options) {
|
|
27729
|
-
this.options = options;
|
|
27730
28202
|
}
|
|
27731
|
-
|
|
27732
|
-
|
|
27733
|
-
|
|
27734
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
trouble = await this.restartOne(gateway);
|
|
27740
|
-
} catch (error62) {
|
|
27741
|
-
trouble = `Restart failed: ${messageOf3(error62)}`;
|
|
28203
|
+
/** The end of a run whose stream was lost: its state read until it is over, as a terminal event. */
|
|
28204
|
+
async endOf(turn) {
|
|
28205
|
+
const by = Date.now() + LOST_STREAM_MS;
|
|
28206
|
+
while (Date.now() < by) {
|
|
28207
|
+
const state2 = await turn.agent.client.run(turn.hermesRun).catch(() => void 0);
|
|
28208
|
+
const status = typeof state2?.["status"] === "string" ? state2["status"] : void 0;
|
|
28209
|
+
if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
|
|
28210
|
+
return { ...state2, event: `run.${status}` };
|
|
27742
28211
|
}
|
|
27743
|
-
|
|
28212
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
27744
28213
|
}
|
|
27745
|
-
|
|
28214
|
+
await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
|
|
28215
|
+
return void 0;
|
|
27746
28216
|
}
|
|
27747
|
-
/**
|
|
27748
|
-
async
|
|
27749
|
-
const
|
|
27750
|
-
|
|
27751
|
-
|
|
27752
|
-
|
|
28217
|
+
/** The room's session: the one it had, or a new one Hermes makes for it. */
|
|
28218
|
+
async sessionFor({ id: id2, agent, spaceId }) {
|
|
28219
|
+
const kept = this.options.state.sessions[id2];
|
|
28220
|
+
if (kept !== void 0) return kept;
|
|
28221
|
+
const who = id2.slice(0, id2.lastIndexOf("|"));
|
|
28222
|
+
const made = await agent.client.newSession(`Chips space ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
|
|
28223
|
+
this.options.state.sessions[id2] = made;
|
|
28224
|
+
this.options.saveState(this.options.state);
|
|
28225
|
+
return made;
|
|
28226
|
+
}
|
|
28227
|
+
/**
|
|
28228
|
+
* A person's words for a run that is going. A steer is said to the Hermes run, or to the next one
|
|
28229
|
+
* of the turn when Hermes won't take it now. A follow-up waits for this one to end. An
|
|
28230
|
+
* interrupt stops it, and the turn goes on with the words.
|
|
28231
|
+
*/
|
|
28232
|
+
async steered(turn, text, mode) {
|
|
28233
|
+
if (mode === "interrupt" && turn.hermesRun !== "") {
|
|
28234
|
+
turn.next.unshift(text);
|
|
28235
|
+
turn.interrupted = true;
|
|
28236
|
+
await turn.agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
28237
|
+
return;
|
|
28238
|
+
}
|
|
28239
|
+
if (mode === "steer" && turn.hermesRun !== "") {
|
|
27753
28240
|
try {
|
|
27754
|
-
|
|
27755
|
-
|
|
27756
|
-
if (this.serviceFor(gateway) !== void 0) {
|
|
27757
|
-
log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
|
|
27758
|
-
trouble = await this.byService(gateway, "start", void 0);
|
|
27759
|
-
} else {
|
|
27760
|
-
const pid = await system.listener(port);
|
|
27761
|
-
if (pid !== void 0) {
|
|
27762
|
-
log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
|
|
27763
|
-
continue;
|
|
27764
|
-
}
|
|
27765
|
-
log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
|
|
27766
|
-
trouble = await this.start(gateway, ["gateway", "run"], void 0);
|
|
27767
|
-
}
|
|
27768
|
-
if (trouble !== void 0) log2(trouble);
|
|
28241
|
+
await turn.agent.client.steer(turn.hermesRun, text);
|
|
28242
|
+
return;
|
|
27769
28243
|
} catch (error62) {
|
|
27770
|
-
|
|
28244
|
+
this.log(`Hermes didn't take a steer mid-run (${messageOf3(error62)}); saying it next`);
|
|
27771
28245
|
}
|
|
27772
28246
|
}
|
|
28247
|
+
turn.next.push(text);
|
|
28248
|
+
}
|
|
28249
|
+
// ------------------------------------------------------------------ what Hermes streams
|
|
28250
|
+
async heard(turn, event) {
|
|
28251
|
+
if (turn.run.over) return;
|
|
28252
|
+
switch (event.event) {
|
|
28253
|
+
case "message.delta":
|
|
28254
|
+
turn.reply += typeof event["delta"] === "string" ? event["delta"] : "";
|
|
28255
|
+
return;
|
|
28256
|
+
case "message.interim":
|
|
28257
|
+
case "reasoning.available":
|
|
28258
|
+
this.sayThought(turn, String(event["text"] ?? ""), event.event === "message.interim" ? "writing" : "thinking");
|
|
28259
|
+
return;
|
|
28260
|
+
case "tool.started":
|
|
28261
|
+
case "subagent.start":
|
|
28262
|
+
await this.started(turn, event);
|
|
28263
|
+
return;
|
|
28264
|
+
case "tool.completed":
|
|
28265
|
+
case "subagent.complete":
|
|
28266
|
+
await this.completed(turn, event);
|
|
28267
|
+
return;
|
|
28268
|
+
case "approval.request":
|
|
28269
|
+
void this.ask(turn, event);
|
|
28270
|
+
return;
|
|
28271
|
+
case "approval.responded":
|
|
28272
|
+
await this.settled(turn, event);
|
|
28273
|
+
return;
|
|
28274
|
+
default:
|
|
28275
|
+
return;
|
|
28276
|
+
}
|
|
28277
|
+
}
|
|
28278
|
+
sayThought(turn, text, phase) {
|
|
28279
|
+
const now = Date.now();
|
|
28280
|
+
const line2 = lastLine(text);
|
|
28281
|
+
if (line2 === "" || now - turn.saidAt < QUIET_MS) return;
|
|
28282
|
+
turn.saidAt = now;
|
|
28283
|
+
void turn.run.status({ phase, verbPhrase: line2.slice(0, 200) }).catch(() => void 0);
|
|
27773
28284
|
}
|
|
27774
|
-
|
|
27775
|
-
|
|
27776
|
-
|
|
27777
|
-
|
|
27778
|
-
|
|
27779
|
-
|
|
27780
|
-
|
|
28285
|
+
async started(turn, event) {
|
|
28286
|
+
const sub = event.event === "subagent.start";
|
|
28287
|
+
const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
|
|
28288
|
+
turn.counted += 1;
|
|
28289
|
+
const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
|
|
28290
|
+
const stepId = callId ?? `${tool}-${turn.counted}`;
|
|
28291
|
+
const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
|
|
28292
|
+
turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
|
|
28293
|
+
const said2 = sub ? event["goal"] : event["preview"];
|
|
28294
|
+
await turn.run.step({
|
|
28295
|
+
stepId,
|
|
28296
|
+
status: "started",
|
|
28297
|
+
title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
|
|
28298
|
+
tool,
|
|
28299
|
+
...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
|
|
28300
|
+
}).catch(() => void 0);
|
|
27781
28301
|
}
|
|
27782
|
-
async
|
|
27783
|
-
const
|
|
27784
|
-
|
|
27785
|
-
|
|
27786
|
-
}
|
|
27787
|
-
const
|
|
27788
|
-
const
|
|
27789
|
-
|
|
27790
|
-
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
|
|
27795
|
-
log2(`nothing listens on port ${port}: starting hermes gateway run`);
|
|
27796
|
-
return this.start(gateway, ["gateway", "run"], void 0);
|
|
28302
|
+
async completed(turn, event) {
|
|
28303
|
+
const sub = event.event === "subagent.complete";
|
|
28304
|
+
const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
|
|
28305
|
+
const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
|
|
28306
|
+
const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
|
|
28307
|
+
const waiting = turn.open.get(lane) ?? [];
|
|
28308
|
+
const stepId = callId ?? waiting.shift() ?? `${tool}-${turn.counted += 1}`;
|
|
28309
|
+
turn.open.set(lane, waiting.filter((each) => each !== stepId));
|
|
28310
|
+
const failed = event["error"] === true || sub && event["status"] === "failed";
|
|
28311
|
+
const approvalId = sub ? void 0 : turn.allowed.shift();
|
|
28312
|
+
if (approvalId !== void 0) {
|
|
28313
|
+
await turn.run.executed(approvalId, failed ? "failed" : "executed").catch(() => void 0);
|
|
27797
28314
|
}
|
|
27798
|
-
const
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27803
|
-
|
|
28315
|
+
const summary = sub ? event["summary"] : resultWords(event["preview"]);
|
|
28316
|
+
const seconds = typeof event["duration"] === "number" ? event["duration"] : void 0;
|
|
28317
|
+
await turn.run.step({
|
|
28318
|
+
stepId,
|
|
28319
|
+
status: failed ? "failed" : "done",
|
|
28320
|
+
tool,
|
|
28321
|
+
...typeof summary === "string" && summary !== "" ? { summary: summary.slice(0, 2e3) } : {},
|
|
28322
|
+
...seconds === void 0 ? {} : { durationMs: Math.round(seconds * 1e3) }
|
|
28323
|
+
}).catch(() => void 0);
|
|
28324
|
+
const cost3 = event["cost_usd"];
|
|
28325
|
+
if (sub && typeof cost3 === "number" && cost3 > 0) {
|
|
28326
|
+
const model = typeof event["model"] === "string" ? event["model"] : "subagent";
|
|
28327
|
+
await turn.run.usage({ scope: "step", model, costUsd: cost3 }).catch(() => void 0);
|
|
27804
28328
|
}
|
|
27805
|
-
if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
|
|
27806
|
-
log2(`stopping Hermes's gateway, pid ${pid}`);
|
|
27807
|
-
if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
|
|
27808
|
-
return this.start(gateway, args, info.cwd);
|
|
27809
28329
|
}
|
|
27810
|
-
|
|
27811
|
-
|
|
27812
|
-
|
|
28330
|
+
// ------------------------------------------------------------------ what Hermes asks
|
|
28331
|
+
/** An approval Hermes wants before it acts, as a card; the answer is Hermes's choice. */
|
|
28332
|
+
async ask(turn, event) {
|
|
28333
|
+
const requestId = idOf(event);
|
|
28334
|
+
const command = typeof event["command"] === "string" ? event["command"] : void 0;
|
|
28335
|
+
const description = typeof event["description"] === "string" ? event["description"] : void 0;
|
|
28336
|
+
const choices = Array.isArray(event["choices"]) ? event["choices"].map(String) : ["once", "deny"];
|
|
28337
|
+
const approvalId = ids.next("approval");
|
|
28338
|
+
turn.approvals.set(requestId ?? approvalId, { approvalId, answered: false });
|
|
28339
|
+
const asked = turn.run.approval({
|
|
28340
|
+
approvalId,
|
|
28341
|
+
action: {
|
|
28342
|
+
kind: command === void 0 ? "hermes.approval" : "shell.exec",
|
|
28343
|
+
input: { ...command === void 0 ? {} : { command }, ...description === void 0 ? {} : { description } }
|
|
28344
|
+
},
|
|
28345
|
+
question: (description === void 0 ? "Run this command?" : `Allow it to ${lowerFirst(description)}?`).slice(0, 300),
|
|
28346
|
+
lasts: "once",
|
|
28347
|
+
risk: [],
|
|
28348
|
+
...command === void 0 ? {} : { preview: { format: "markdown", title: "Run a command", body: `\`\`\`sh
|
|
28349
|
+
${command}
|
|
28350
|
+
\`\`\`` } }
|
|
28351
|
+
});
|
|
28352
|
+
const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
|
|
28353
|
+
const decision = await Promise.race([asked, stopped2]);
|
|
28354
|
+
const pending = turn.approvals.get(requestId ?? approvalId);
|
|
28355
|
+
if (decision === void 0 || pending === void 0 || pending.answered) return;
|
|
28356
|
+
pending.answered = true;
|
|
28357
|
+
if (decision.allowed) turn.allowed.push(approvalId);
|
|
28358
|
+
const generation = event["execution_generation"];
|
|
28359
|
+
await turn.agent.client.approve(turn.hermesRun, {
|
|
28360
|
+
choice: choiceOf(decision, choices, command !== void 0),
|
|
28361
|
+
...requestId === void 0 ? {} : { request_id: requestId },
|
|
28362
|
+
...typeof generation === "number" ? { execution_generation: generation } : {}
|
|
28363
|
+
}).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf3(error62)}`));
|
|
27813
28364
|
}
|
|
27814
|
-
/**
|
|
27815
|
-
async
|
|
27816
|
-
const
|
|
27817
|
-
|
|
27818
|
-
if (
|
|
27819
|
-
|
|
27820
|
-
|
|
27821
|
-
|
|
27822
|
-
|
|
27823
|
-
|
|
27824
|
-
|
|
28365
|
+
/** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
|
|
28366
|
+
async settled(turn, event) {
|
|
28367
|
+
const requestId = idOf(event);
|
|
28368
|
+
const pending = requestId === void 0 ? void 0 : turn.approvals.get(requestId);
|
|
28369
|
+
if (pending === void 0 || pending.answered) return;
|
|
28370
|
+
pending.answered = true;
|
|
28371
|
+
const denied = event["choice"] === "deny";
|
|
28372
|
+
await this.options.chips.send(
|
|
28373
|
+
"approval.resolved",
|
|
28374
|
+
{ run_id: turn.run.id },
|
|
28375
|
+
{
|
|
28376
|
+
approval_id: pending.approvalId,
|
|
28377
|
+
decision: denied ? "denied" : "approved",
|
|
28378
|
+
scope: "once",
|
|
28379
|
+
reason: "Answered in Hermes."
|
|
28380
|
+
},
|
|
28381
|
+
`hermes-approval:${turn.hermesRun}:${requestId}:settled`
|
|
28382
|
+
).catch(() => void 0);
|
|
27825
28383
|
}
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
28384
|
+
// ------------------------------------------------------------------ the end
|
|
28385
|
+
/** The turn is over: its words in the room, its tokens, and the run ended. */
|
|
28386
|
+
async finish(turn, last) {
|
|
28387
|
+
const { run: run2, agent } = turn;
|
|
28388
|
+
if (run2.over) return;
|
|
28389
|
+
await this.closeSteps(turn, "done");
|
|
28390
|
+
for (const approvalId of turn.allowed.splice(0)) {
|
|
28391
|
+
await run2.executed(approvalId, "executed", "Hermes didn't say how it went.").catch(() => void 0);
|
|
27832
28392
|
}
|
|
27833
|
-
|
|
27834
|
-
|
|
27835
|
-
|
|
27836
|
-
|
|
27837
|
-
const {
|
|
27838
|
-
const
|
|
27839
|
-
|
|
27840
|
-
const
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
|
|
27845
|
-
|
|
27846
|
-
|
|
27847
|
-
if (system.now() >= by) {
|
|
27848
|
-
if (await system.healthy(gateway.url)) return void 0;
|
|
27849
|
-
return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
|
|
27850
|
-
}
|
|
27851
|
-
await system.sleep(this.timing.pollMs);
|
|
28393
|
+
await this.counted(turn, last);
|
|
28394
|
+
const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
|
|
28395
|
+
const answer = output2 !== "" ? output2 : turn.reply.trim();
|
|
28396
|
+
const { change, note } = await this.sendProject(turn, answer);
|
|
28397
|
+
const { folder: folder2 } = turn;
|
|
28398
|
+
const { chips: chips2 } = this.options;
|
|
28399
|
+
const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
|
|
28400
|
+
const post = await sendAnswer(words4, {
|
|
28401
|
+
folders: folder2 === void 0 ? [] : [folder2],
|
|
28402
|
+
upload: (input2) => chips2.upload(input2)
|
|
28403
|
+
});
|
|
28404
|
+
const reply = post.text;
|
|
28405
|
+
if (reply !== "" || post.uploadIds.length > 0) {
|
|
28406
|
+
await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
|
|
27852
28407
|
}
|
|
28408
|
+
const said2 = post.summary;
|
|
28409
|
+
const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
|
|
28410
|
+
await run2.complete({ summary });
|
|
28411
|
+
this.log(`${agent.config.name} answered in ${run2.spaceId}`);
|
|
28412
|
+
this.missingReadings();
|
|
27853
28413
|
}
|
|
27854
28414
|
/**
|
|
27855
|
-
*
|
|
27856
|
-
*
|
|
28415
|
+
* A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
|
|
28416
|
+
* likely started before Chips's webhook was in its config. Said once, a little after, since a
|
|
28417
|
+
* reading comes a moment after its model call.
|
|
27857
28418
|
*/
|
|
27858
|
-
|
|
27859
|
-
|
|
27860
|
-
|
|
27861
|
-
|
|
27862
|
-
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
27866
|
-
if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
|
|
27867
|
-
const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
|
|
27868
|
-
if (wrapper !== void 0) {
|
|
27869
|
-
const expat = dirname5(EXPAT);
|
|
27870
|
-
log2(
|
|
27871
|
-
`hermes gateway run stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Starting ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
|
|
27872
|
-
);
|
|
27873
|
-
const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
|
|
27874
|
-
outcome = await this.launch(
|
|
27875
|
-
gateway,
|
|
27876
|
-
wrapper.entry,
|
|
27877
|
-
args,
|
|
27878
|
-
{ ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
|
|
27879
|
-
folder2,
|
|
27880
|
-
`${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
|
|
27881
|
-
);
|
|
27882
|
-
}
|
|
27883
|
-
}
|
|
27884
|
-
if (outcome.state === "up") {
|
|
27885
|
-
log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
|
|
27886
|
-
return void 0;
|
|
27887
|
-
}
|
|
27888
|
-
if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
|
|
27889
|
-
return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
|
|
27890
|
-
}
|
|
27891
|
-
/** A program started, and watched until it answers, ends, or has had its time. */
|
|
27892
|
-
async launch(gateway, command, args, env, cwd, what) {
|
|
27893
|
-
const { system, logFile } = this.options;
|
|
27894
|
-
system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
|
|
27895
|
-
const from = system.size(logFile);
|
|
27896
|
-
const startedAt = system.now();
|
|
27897
|
-
const child = system.launch({ command, args, env, cwd, log: logFile });
|
|
27898
|
-
let ended;
|
|
27899
|
-
void child.exited.then(({ code }) => {
|
|
27900
|
-
ended = { code, at: system.now() };
|
|
27901
|
-
});
|
|
27902
|
-
for (; ; ) {
|
|
27903
|
-
await Promise.resolve();
|
|
27904
|
-
if (ended !== void 0) {
|
|
27905
|
-
return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
|
|
28419
|
+
missingReadings() {
|
|
28420
|
+
if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
|
|
28421
|
+
this.toldNoReadings = true;
|
|
28422
|
+
setTimeout(() => {
|
|
28423
|
+
if (this.fills?.anyHeard === false) {
|
|
28424
|
+
this.log(
|
|
28425
|
+
"context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
|
|
28426
|
+
);
|
|
27906
28427
|
}
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
|
|
28428
|
+
}, 1e4).unref();
|
|
28429
|
+
}
|
|
28430
|
+
/** A run that failed, was stopped or was cut short in Hermes. */
|
|
28431
|
+
async ended(turn, last) {
|
|
28432
|
+
await this.closeSteps(turn, "failed");
|
|
28433
|
+
await this.counted(turn, last);
|
|
28434
|
+
if (turn.cancelled || turn.run.over) return;
|
|
28435
|
+
const name2 = turn.agent.config.name;
|
|
28436
|
+
if (last.event === "run.cancelled") await this.failed(turn, `${name2}'s run was stopped in Hermes.`, false);
|
|
28437
|
+
else if (last.event === "run.interrupted") await this.failed(turn, `Hermes restarted during ${name2}'s run.`, true);
|
|
28438
|
+
else {
|
|
28439
|
+
const why = typeof last["error"] === "string" && last["error"] !== "" ? last["error"] : "it failed in Hermes";
|
|
28440
|
+
await this.failed(turn, `${name2}: ${why}`, true);
|
|
27910
28441
|
}
|
|
27911
28442
|
}
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
28443
|
+
/**
|
|
28444
|
+
* The tokens Hermes counted, as a step's share, so they add up over the turn's runs. Hermes's
|
|
28445
|
+
* `input_tokens` is already every token in, its cache's reads and writes among them (its
|
|
28446
|
+
* `CanonicalUsage.prompt_tokens`), which is what Chips counts (Chips ADR 0214); the cache's two
|
|
28447
|
+
* counts go with it, and are left out where Hermes doesn't say them.
|
|
28448
|
+
*/
|
|
28449
|
+
async counted(turn, last) {
|
|
28450
|
+
const usage2 = last["usage"] ?? {};
|
|
28451
|
+
const count = (key) => typeof usage2[key] === "number" ? usage2[key] : void 0;
|
|
28452
|
+
const input2 = count("input_tokens") ?? 0;
|
|
28453
|
+
const output2 = count("output_tokens") ?? 0;
|
|
28454
|
+
if (input2 + output2 === 0) return;
|
|
28455
|
+
const runtime = last["runtime"] ?? {};
|
|
28456
|
+
const model = typeof runtime["model"] === "string" ? runtime["model"] : this.runModel(turn).model ?? "hermes";
|
|
28457
|
+
await turn.run.usage({
|
|
28458
|
+
scope: "step",
|
|
28459
|
+
model,
|
|
28460
|
+
inputTokens: input2,
|
|
28461
|
+
cacheReadTokens: count("cache_read_tokens"),
|
|
28462
|
+
cacheWriteTokens: count("cache_write_tokens"),
|
|
28463
|
+
outputTokens: output2
|
|
28464
|
+
}).catch(() => void 0);
|
|
28465
|
+
}
|
|
28466
|
+
async closeSteps(turn, status) {
|
|
28467
|
+
for (const [, ids2] of turn.open) {
|
|
28468
|
+
for (const stepId of ids2) await turn.run.step({ stepId, status }).catch(() => void 0);
|
|
27919
28469
|
}
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
27923
|
-
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
|
|
27930
|
-
|
|
27931
|
-
|
|
27932
|
-
|
|
27933
|
-
deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
|
|
27934
|
-
} finally {
|
|
27935
|
-
deps.hold(false);
|
|
27936
|
-
}
|
|
27937
|
-
})().finally(() => {
|
|
27938
|
-
running = void 0;
|
|
27939
|
-
});
|
|
27940
|
-
return running;
|
|
27941
|
-
};
|
|
28470
|
+
turn.open.clear();
|
|
28471
|
+
}
|
|
28472
|
+
async failed(turn, error62, retryable) {
|
|
28473
|
+
if (!turn.run.over) await turn.run.fail({ error: error62, retryable }).catch(() => void 0);
|
|
28474
|
+
if (retryable) void this.sayHowItIs();
|
|
28475
|
+
}
|
|
28476
|
+
};
|
|
28477
|
+
function choiceOf(decision, choices, command = false) {
|
|
28478
|
+
if (!decision.allowed) return "deny";
|
|
28479
|
+
if (command) return "once";
|
|
28480
|
+
if (decision.scope === "always" && choices.includes("always")) return "always";
|
|
28481
|
+
if (decision.scope !== "once" && choices.includes("session")) return "session";
|
|
28482
|
+
return "once";
|
|
27942
28483
|
}
|
|
27943
|
-
function
|
|
27944
|
-
|
|
28484
|
+
function idOf(event) {
|
|
28485
|
+
const id2 = event["request_id"] ?? event["prompt_id"];
|
|
28486
|
+
return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
|
|
27945
28487
|
}
|
|
27946
|
-
function
|
|
27947
|
-
|
|
27948
|
-
|
|
27949
|
-
let output2 = "";
|
|
27950
|
-
const keep = (data) => {
|
|
27951
|
-
output2 = (output2 + String(data)).slice(-8e3);
|
|
27952
|
-
};
|
|
27953
|
-
child.stdout.on("data", keep);
|
|
27954
|
-
child.stderr.on("data", keep);
|
|
27955
|
-
const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
|
|
27956
|
-
child.on("error", (error62) => {
|
|
27957
|
-
clearTimeout(limit);
|
|
27958
|
-
done({ code: null, output: error62.message });
|
|
27959
|
-
});
|
|
27960
|
-
child.on("close", (code) => {
|
|
27961
|
-
clearTimeout(limit);
|
|
27962
|
-
done({ code, output: output2 });
|
|
27963
|
-
});
|
|
27964
|
-
});
|
|
28488
|
+
function messageOf3(error62) {
|
|
28489
|
+
if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
|
|
28490
|
+
return error62 instanceof Error ? error62.message : String(error62);
|
|
27965
28491
|
}
|
|
27966
|
-
function
|
|
27967
|
-
|
|
27968
|
-
|
|
27969
|
-
|
|
27970
|
-
|
|
27971
|
-
|
|
27972
|
-
|
|
27973
|
-
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
|
|
27978
|
-
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27986
|
-
|
|
27987
|
-
|
|
27988
|
-
|
|
27989
|
-
|
|
27990
|
-
|
|
27991
|
-
|
|
27992
|
-
|
|
27993
|
-
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
|
|
27998
|
-
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
|
|
28002
|
-
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28006
|
-
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
28012
|
-
|
|
28013
|
-
|
|
28014
|
-
|
|
28015
|
-
|
|
28016
|
-
|
|
28017
|
-
|
|
28018
|
-
|
|
28019
|
-
try {
|
|
28020
|
-
const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
|
|
28021
|
-
const exited = new Promise((done) => {
|
|
28022
|
-
child.once("exit", (code, signal) => done({ code, signal }));
|
|
28023
|
-
child.once("error", (error62) => {
|
|
28024
|
-
appendFileSync2(log2, `${error62.message}
|
|
28025
|
-
`);
|
|
28026
|
-
done({ code: null, signal: null });
|
|
28027
|
-
});
|
|
28028
|
-
});
|
|
28029
|
-
child.unref();
|
|
28030
|
-
return { pid: child.pid, exited };
|
|
28031
|
-
} finally {
|
|
28032
|
-
closeSync2(out);
|
|
28033
|
-
}
|
|
28034
|
-
},
|
|
28035
|
-
note(log2, words4) {
|
|
28036
|
-
appendFileSync2(log2, `${words4}
|
|
28037
|
-
`);
|
|
28038
|
-
},
|
|
28039
|
-
size(log2) {
|
|
28040
|
-
try {
|
|
28041
|
-
return statSync4(log2).size;
|
|
28042
|
-
} catch {
|
|
28043
|
-
return 0;
|
|
28044
|
-
}
|
|
28045
|
-
},
|
|
28046
|
-
since(log2, from) {
|
|
28047
|
-
try {
|
|
28048
|
-
const to = statSync4(log2).size;
|
|
28049
|
-
const start = Math.max(from, to - 64e3);
|
|
28050
|
-
const buffer = Buffer.alloc(Math.max(0, to - start));
|
|
28051
|
-
const fd = openSync2(log2, "r");
|
|
28052
|
-
try {
|
|
28053
|
-
readSync(fd, buffer, 0, buffer.length, start);
|
|
28054
|
-
} finally {
|
|
28055
|
-
closeSync2(fd);
|
|
28056
|
-
}
|
|
28057
|
-
return buffer.toString("utf8");
|
|
28058
|
-
} catch {
|
|
28059
|
-
return "";
|
|
28060
|
-
}
|
|
28061
|
-
},
|
|
28062
|
-
healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
|
|
28063
|
-
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
28064
|
-
now: () => Date.now()
|
|
28065
|
-
};
|
|
28492
|
+
function titleFrom(text) {
|
|
28493
|
+
const line2 = text.trim().split("\n")[0] ?? "";
|
|
28494
|
+
if (line2.length <= 48) return line2;
|
|
28495
|
+
const cut2 = line2.slice(0, 48);
|
|
28496
|
+
return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
|
|
28497
|
+
}
|
|
28498
|
+
function forkName(original) {
|
|
28499
|
+
return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
|
|
28500
|
+
}
|
|
28501
|
+
function goalOf(message, name2) {
|
|
28502
|
+
const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
|
|
28503
|
+
const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
|
|
28504
|
+
const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
|
|
28505
|
+
return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
|
|
28506
|
+
}
|
|
28507
|
+
var REPLY_SHOWN = 1500;
|
|
28508
|
+
var GOAL_SHOWN = 120;
|
|
28509
|
+
function lookAgain(who, reply, note) {
|
|
28510
|
+
const said2 = reply?.trim() ?? "";
|
|
28511
|
+
return [
|
|
28512
|
+
`${who} says your reply in this space wasn't right.`,
|
|
28513
|
+
...said2 === "" ? [] : [`Your reply:
|
|
28514
|
+
\xAB${cut(said2, REPLY_SHOWN)}\xBB`],
|
|
28515
|
+
note === void 0 || note === "" ? "They didn't say what. Read your reply again and find what was wrong." : `What wasn't right: \xAB${note}\xBB`,
|
|
28516
|
+
"Look at it again and answer properly in its place. If it is not clear what was wrong, ask them in one short line. Say plainly what you changed."
|
|
28517
|
+
].join("\n\n");
|
|
28518
|
+
}
|
|
28519
|
+
function lookingAgain(note) {
|
|
28520
|
+
const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
|
|
28521
|
+
return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
|
|
28522
|
+
}
|
|
28523
|
+
function cut(text, most) {
|
|
28524
|
+
return text.length <= most ? text : `${text.slice(0, most - 1)}\u2026`;
|
|
28525
|
+
}
|
|
28526
|
+
function resultWords(preview2) {
|
|
28527
|
+
if (typeof preview2 !== "string" || !preview2.trimStart().startsWith("{")) return preview2;
|
|
28528
|
+
let result;
|
|
28529
|
+
try {
|
|
28530
|
+
result = JSON.parse(preview2);
|
|
28531
|
+
} catch {
|
|
28532
|
+
return preview2;
|
|
28533
|
+
}
|
|
28534
|
+
if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
|
|
28535
|
+
const { output: output2, error: error62 } = result;
|
|
28536
|
+
const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
|
|
28537
|
+
return typeof said2 === "string" ? said2.trim() : "";
|
|
28538
|
+
}
|
|
28539
|
+
function lastLine(text) {
|
|
28540
|
+
const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line2) => line2.trim()).filter((line2) => line2 !== "");
|
|
28541
|
+
return lines2.at(-1) ?? "";
|
|
28542
|
+
}
|
|
28543
|
+
function lowerFirst(text) {
|
|
28544
|
+
return text.length === 0 ? text : `${text[0]?.toLowerCase() ?? ""}${text.slice(1)}`;
|
|
28066
28545
|
}
|
|
28067
28546
|
|
|
28068
28547
|
// src/keep.ts
|
|
28069
28548
|
import { homedir as homedir6 } from "node:os";
|
|
28070
|
-
import { basename as basename5, join as
|
|
28549
|
+
import { basename as basename5, join as join11, resolve as resolve4 } from "node:path";
|
|
28071
28550
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28072
28551
|
var flags = process.argv.slice(2);
|
|
28073
28552
|
var words3 = flags.filter((word) => !word.startsWith("--"));
|
|
28074
|
-
var whole = (word) => resolve4(word.startsWith("~/") ?
|
|
28553
|
+
var whole = (word) => resolve4(word.startsWith("~/") ? join11(homedir6(), word.slice(2)) : word);
|
|
28075
28554
|
var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
|
|
28076
28555
|
var self = fileURLToPath2(import.meta.url);
|
|
28077
28556
|
var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
|
|
@@ -28117,9 +28596,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
|
|
|
28117
28596
|
}
|
|
28118
28597
|
|
|
28119
28598
|
// src/setup.ts
|
|
28120
|
-
import { existsSync as
|
|
28599
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
28121
28600
|
import { homedir as homedir7 } from "node:os";
|
|
28122
|
-
import { join as
|
|
28601
|
+
import { join as join12, resolve as resolve5 } from "node:path";
|
|
28123
28602
|
var SetupError = class extends Error {
|
|
28124
28603
|
constructor(message, words4 = {}) {
|
|
28125
28604
|
super(message);
|
|
@@ -28144,16 +28623,16 @@ function readyHooks(config3, options) {
|
|
|
28144
28623
|
const lines2 = [];
|
|
28145
28624
|
let restart2 = false;
|
|
28146
28625
|
const done = /* @__PURE__ */ new Set();
|
|
28147
|
-
for (const
|
|
28148
|
-
const profile = profileName(
|
|
28626
|
+
for (const named of [...config3.agents.map((agent) => agent.address.profile), ...options.made ?? []]) {
|
|
28627
|
+
const profile = profileName(named);
|
|
28149
28628
|
if (done.has(profile)) continue;
|
|
28150
28629
|
done.add(profile);
|
|
28151
|
-
const home2 = profileHome(options.root,
|
|
28630
|
+
const home2 = profileHome(options.root, named);
|
|
28152
28631
|
let outcome;
|
|
28153
28632
|
try {
|
|
28154
28633
|
outcome = addHook(home2, options.port, profile, options.secret);
|
|
28155
28634
|
} catch (error62) {
|
|
28156
|
-
lines2.push(`Chips's webhook wasn't added to ${shown(
|
|
28635
|
+
lines2.push(`Chips's webhook wasn't added to ${shown(join12(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
|
|
28157
28636
|
continue;
|
|
28158
28637
|
}
|
|
28159
28638
|
if (outcome.state === "added") {
|
|
@@ -28179,8 +28658,8 @@ function hermesEnvPath(env = process.env) {
|
|
|
28179
28658
|
}
|
|
28180
28659
|
function fromHermes(name2, env) {
|
|
28181
28660
|
const path = hermesEnvPath(env);
|
|
28182
|
-
if (!
|
|
28183
|
-
for (const line2 of
|
|
28661
|
+
if (!existsSync10(path)) return void 0;
|
|
28662
|
+
for (const line2 of readFileSync10(path, "utf8").split("\n")) {
|
|
28184
28663
|
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line2);
|
|
28185
28664
|
if (match?.[1] === name2) return match[2]?.replace(/^(["'])(.*)\1$/, "$2");
|
|
28186
28665
|
}
|
|
@@ -28191,7 +28670,7 @@ function folderOf(said2) {
|
|
|
28191
28670
|
return resolve5(said2 === "~" || said2.startsWith("~/") ? homedir7() + said2.slice(1) : said2);
|
|
28192
28671
|
}
|
|
28193
28672
|
function readConfig(path, env = process.env) {
|
|
28194
|
-
const raw =
|
|
28673
|
+
const raw = existsSync10(path) ? JSON.parse(readFileSync10(path, "utf8")) : PLAIN;
|
|
28195
28674
|
const url2 = typeof raw.url === "string" ? raw.url : "http://127.0.0.1:8642";
|
|
28196
28675
|
const folder2 = folderOf(raw.folder);
|
|
28197
28676
|
if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
|
|
@@ -28261,15 +28740,19 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
|
|
|
28261
28740
|
}
|
|
28262
28741
|
}
|
|
28263
28742
|
|
|
28743
|
+
// src/version.ts
|
|
28744
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
28745
|
+
var VERSION = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
28746
|
+
|
|
28264
28747
|
// src/main.ts
|
|
28265
|
-
var home = (path) => path.startsWith("~/") ?
|
|
28748
|
+
var home = (path) => path.startsWith("~/") ? join13(homedir8(), path.slice(2)) : path;
|
|
28266
28749
|
var link = process.env["CHIPS_JOIN_LINK"] || void 0;
|
|
28267
28750
|
var LABEL = "chat.chips.hermes";
|
|
28268
28751
|
stopIfAsked(LABEL);
|
|
28269
28752
|
var configPath = resolve6(home(words3[0] ?? "~/.chips-hermes/config.json"));
|
|
28270
28753
|
var folder = dirname6(configPath);
|
|
28271
|
-
var statePath =
|
|
28272
|
-
var state =
|
|
28754
|
+
var statePath = join13(folder, "sessions.json");
|
|
28755
|
+
var state = existsSync11(statePath) ? JSON.parse(readFileSync12(statePath, "utf8")) : { sessions: {} };
|
|
28273
28756
|
var saveState = (next) => {
|
|
28274
28757
|
mkdirSync6(folder, { recursive: true, mode: 448 });
|
|
28275
28758
|
writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
|
|
@@ -28294,12 +28777,13 @@ try {
|
|
|
28294
28777
|
reportStep("checking", { detail: "Looking for its API server" });
|
|
28295
28778
|
await checkHermes(config2);
|
|
28296
28779
|
}
|
|
28297
|
-
fills.secret ??=
|
|
28780
|
+
fills.secret ??= randomBytes3(32).toString("hex");
|
|
28298
28781
|
fills.port = config2.hookPort ?? fills.port ?? hookPortIn(profileHome(root, config2.agents[0]?.address.profile)) ?? await freePort();
|
|
28299
28782
|
saveState(state);
|
|
28300
28783
|
const first = config2.agents[0];
|
|
28301
28784
|
const gatewayUp = link !== void 0 || first !== void 0 && await new HermesClient(first.address).healthy();
|
|
28302
|
-
const
|
|
28785
|
+
const made = Object.values(state.made ?? {});
|
|
28786
|
+
const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp, made });
|
|
28303
28787
|
for (const line2 of hooks.lines) log(line2);
|
|
28304
28788
|
if (link !== void 0) reportStep("checked", { detail: checkedWords(config2, hooks.restart) });
|
|
28305
28789
|
} catch (error62) {
|
|
@@ -28329,7 +28813,7 @@ var joined = await join3({
|
|
|
28329
28813
|
link,
|
|
28330
28814
|
url: process.env["CHIPS_URL"],
|
|
28331
28815
|
name,
|
|
28332
|
-
software:
|
|
28816
|
+
software: `chips-hermes ${VERSION}`,
|
|
28333
28817
|
wants: WANTS,
|
|
28334
28818
|
onCode: (code) => log(`Allow this harness in Chips: ${code}`)
|
|
28335
28819
|
}).catch(refused2);
|
|
@@ -28347,8 +28831,8 @@ var restart = restarter({
|
|
|
28347
28831
|
var chips = await connect({
|
|
28348
28832
|
url: joined.url,
|
|
28349
28833
|
token: joined.token,
|
|
28350
|
-
client: { name: "chips-hermes", version:
|
|
28351
|
-
store: fileStore(
|
|
28834
|
+
client: { name: "chips-hermes", version: VERSION },
|
|
28835
|
+
store: fileStore(join13(folder, "chips-state.json")),
|
|
28352
28836
|
onRestart: restart,
|
|
28353
28837
|
settings: settingsFor(() => bridge)
|
|
28354
28838
|
});
|
|
@@ -28364,7 +28848,10 @@ bridge = new Bridge({
|
|
|
28364
28848
|
saveState,
|
|
28365
28849
|
log,
|
|
28366
28850
|
hermesRoot: root,
|
|
28367
|
-
fills: { port: fills.port, secret: fills.secret }
|
|
28851
|
+
fills: { port: fills.port, secret: fills.secret },
|
|
28852
|
+
// Each agent made in Chips is a Hermes profile of its own (Chips ADR 0241).
|
|
28853
|
+
profiles: hermesProfiles({ root, env: process.env, system: thisMachine(), log: (line2) => log(`profiles: ${line2}`) }),
|
|
28854
|
+
restartGateway: () => restart({ askedBy: "chips-hermes, for a new agent's profile", askedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
28368
28855
|
});
|
|
28369
28856
|
await bridge.start();
|
|
28370
28857
|
log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through Hermes`);
|